0
+ − 1 #!/usr/bin/env python
+ − 2
+ − 3 import argparse
+ − 4 import csv
+ − 5 import os
4
+ − 6 import subprocess
+ − 7 import sys
+ − 8 import tempfile
0
+ − 9
4
+ − 10 import Bio.SeqIO
0
+ − 11 import numpy
+ − 12 import pandas
+ − 13 import matplotlib.pyplot as pyplot
+ − 14
+ − 15
1
+ − 16 def get_amr_in_feature_hits(amr_feature_hits):
+ − 17 for k in amr_feature_hits.keys():
0
+ − 18 if k.lower().find('amr') >= 0:
1
+ − 19 return amr_feature_hits[k]
0
+ − 20 return None
+ − 21
+ − 22
4
+ − 23 def load_fasta(fasta_file):
+ − 24 sequence = pandas.Series(dtype=object)
+ − 25 for contig in Bio.SeqIO.parse(fasta_file, 'fasta'):
+ − 26 sequence[contig.id] = contig
+ − 27 return sequence
+ − 28
+ − 29
+ − 30 def run_command(cmd):
+ − 31 try:
+ − 32 tmp_name = tempfile.NamedTemporaryFile(dir=".").name
+ − 33 tmp_stderr = open(tmp_name, 'wb')
+ − 34 proc = subprocess.Popen(args=cmd, shell=True, stderr=tmp_stderr.fileno())
+ − 35 returncode = proc.wait()
+ − 36 tmp_stderr.close()
+ − 37 if returncode != 0:
+ − 38 # Get stderr, allowing for case where it's very large.
+ − 39 tmp_stderr = open(tmp_name, 'rb')
+ − 40 stderr = ''
+ − 41 buffsize = 1048576
+ − 42 try:
+ − 43 while True:
+ − 44 stderr += tmp_stderr.read(buffsize)
+ − 45 if not stderr or len(stderr) % buffsize != 0:
+ − 46 break
+ − 47 except OverflowError:
+ − 48 pass
+ − 49 tmp_stderr.close()
+ − 50 os.remove(tmp_name)
+ − 51 stop_err(stderr)
+ − 52 except Exception as e:
+ − 53 stop_err('Command:\n%s\n\nended with error:\n%s\n\n' % (cmd, str(e)))
+ − 54
+ − 55
+ − 56 def stop_err(msg):
+ − 57 sys.stderr.write(msg)
+ − 58 sys.exit(1)
+ − 59
+ − 60
9
+ − 61 def draw_amr_matrix(amr_feature_hits_files, amr_deletions_file, varscan_vcf_file, amr_mutation_regions_bed_file, amr_gene_drug_file, reference, reference_size, mutation_regions_dir, amr_matrix_png_dir, errors):
+ − 62 efh = open(errors, 'w')
0
+ − 63 ofh = open('process_log', 'w')
+ − 64
1
+ − 65 # Read amr_feature_hits_files.
+ − 66 amr_feature_hits = pandas.Series(dtype=object)
+ − 67 for amr_feature_hits_file in amr_feature_hits_files:
+ − 68 feature_name = os.path.basename(amr_feature_hits_file)
0
+ − 69 # Make sure the file is not empty.
1
+ − 70 if os.path.isfile(amr_feature_hits_file) and os.path.getsize(amr_feature_hits_file) > 0:
+ − 71 best_hits = pandas.read_csv(filepath_or_buffer=amr_feature_hits_file, sep='\t', header=None)
+ − 72 ofh.write("\nFeature file %s will be processed\n" % os.path.basename(amr_feature_hits_file))
0
+ − 73 else:
1
+ − 74 ofh.write("\nEmpty feature file %s will NOT be processed\n" % os.path.basename(amr_feature_hits_file))
0
+ − 75 best_hits = None
1
+ − 76 amr_feature_hits[feature_name] = best_hits
0
+ − 77
1
+ − 78 amr_hits = get_amr_in_feature_hits(amr_feature_hits)
0
+ − 79 ofh.write("\namr_hits:\n%s\n" % str(amr_hits))
+ − 80 if amr_hits is not None:
+ − 81 amr_to_draw = pandas.DataFrame(columns=['gene', 'drug'])
+ − 82 ofh.write("\namr_to_draw:\n%s\n" % str(amr_to_draw))
+ − 83 # Read amr_drug_gene_file.
+ − 84 amr_gene_drug = pandas.read_csv(amr_gene_drug_file, index_col=None, sep='\t', quoting=csv.QUOTE_NONE, header=None)
+ − 85 ofh.write("\namr_gene_drug:\n%s\n" % str(amr_gene_drug))
+ − 86
+ − 87 # Roll up AMR gene hits.
4
+ − 88 ofh.write("\namr_hits.shape[0]: %s\n" % str(amr_hits.shape[0]))
0
+ − 89 if amr_hits.shape[0] > 0:
+ − 90 for gene_idx, gene in amr_hits.iterrows():
4
+ − 91 ofh.write("gene_idx: %s\n" % str(gene_idx))
+ − 92 ofh.write("gene: %s\n" % str(gene))
0
+ − 93 gene_name = gene[3]
+ − 94 ofh.write("gene_name: %s\n" % str(gene_name))
+ − 95 ofh.write("amr_gene_drug[0]: %s\n" % str(amr_gene_drug[0]))
+ − 96 drugs = amr_gene_drug.loc[amr_gene_drug[0] == gene_name, :][1]
4
+ − 97 ofh.write("drugs: %s\n" % str(drugs))
0
+ − 98 for drug in drugs:
+ − 99 amr_to_draw = amr_to_draw.append(pandas.Series([gene_name, drug], name=amr_to_draw.shape[0], index=amr_to_draw.columns))
4
+ − 100 ofh.write("\amr_to_draw: %s\n" % str(amr_to_draw))
0
+ − 101
9
+ − 102 ofh.write("\nvarscan_vcf_file is None: %s\n" % str(varscan_vcf_file == 'None'))
7
+ − 103 if varscan_vcf_file not in [None, 'None'] and os.path.getsize(varscan_vcf_file) > 0:
4
+ − 104 amr_mutations = pandas.Series(dtype=object)
7
+ − 105 if amr_mutation_regions_bed_file is not None:
+ − 106 mutation_regions = pandas.read_csv(amr_mutation_regions_bed_file, header=0, sep='\t', index_col=False)
9
+ − 107 # Validate mutation regions.
4
+ − 108 if mutation_regions.shape[1] != 7:
9
+ − 109 efh.write("\nThe selected mutations regions BED file is invalid, it should be a six column file.\n")
4
+ − 110 elif mutation_regions.shape[0] == 0:
9
+ − 111 efh.write("\nThere are no rows in the selected mutation regions file.\n")
4
+ − 112 else:
+ − 113 for region_i in range(mutation_regions.shape[0]):
+ − 114 region = mutation_regions.iloc[region_i, :]
9
+ − 115 if region[0] not in reference:
+ − 116 efh.write("\nMutation region '%s' not found in reference genome.\n" % str(region))
+ − 117 break
+ − 118 if not isinstance(region[1], numpy.int64):
+ − 119 efh.write("\nNon-integer found in mutation region start (column 2): %s.\n" % str(region[1]))
+ − 120 break
+ − 121 if not isinstance(region[2], numpy.int64):
+ − 122 efh.write("\nNon-integer found in mutation region start (column 3): %s.\n" % str(region[2]))
+ − 123 break
+ − 124 if region[1] <= 0 or region[2] <= 0:
+ − 125 efh.write("\nMutation region '%s' starts before the reference sequence.\n" % str(region))
+ − 126 if region[1] > len(reference[region[0]].seq) or region[2] > len(reference[region[0]].seq):
+ − 127 efh.write("\nMutation region '%s' ends after the reference sequence.\n" % str(region))
4
+ − 128 if not region.get('type', default='No Type') in ['snp', 'small-indel', 'any']:
9
+ − 129 ofh.write("\n\nSkipping mutation region '%s' with invalid type '%s', valid types are 'snp', 'small-indel', 'any'.\n\n" % (str(region), str(region.get('type', default='No Type'))))
4
+ − 130 continue
+ − 131 ofh.write("\nFinding AMR mutations for %s.\n" % str(region['name']))
7
+ − 132 region_bed = 'region_%s.bed' % region_i
4
+ − 133 mutation_regions.loc[[region_i], ].to_csv(path_or_buf=region_bed, sep='\t', header=False, index=False)
7
+ − 134 region_mutations_tsv = os.path.join(mutation_regions_dir, 'region_%s_mutations.tsv' % region_i)
6
+ − 135 cmd = ' '.join(['bedtools intersect -nonamecheck -wb -a',
+ − 136 region_bed,
+ − 137 '-b',
7
+ − 138 varscan_vcf_file,
+ − 139 ' | awk \'BEGIN{getline < "' + amr_mutation_regions_bed_file + '";printf $0"\\t";',
+ − 140 'getline < "' + varscan_vcf_file + '"; getline < "' + varscan_vcf_file + '";print $0}{print}\'',
+ − 141 '1>' + region_mutations_tsv])
6
+ − 142 ofh.write("\ncmd:\n%s\n" % cmd)
4
+ − 143 run_command(cmd)
+ − 144 else:
+ − 145 ofh.write("\nMutation region BED not received.\n")
0
+ − 146 # Roll up potentially resistance conferring mutations.
+ − 147 for mutation_region, mutation_hits in amr_mutations.iteritems():
+ − 148 for mutation_idx, mutation_hit in mutation_hits.iterrows():
+ − 149 mutation_name = mutation_region + ' ' + mutation_hit['REF'] + '->' + mutation_hit['ALT']
+ − 150 amr_to_draw = amr_to_draw.append(pandas.Series([mutation_name, mutation_hit['DRUG']], name=amr_to_draw.shape[0], index=amr_to_draw.columns))
+ − 151
4
+ − 152 if amr_deletions_file not in [None, 'None'] and os.path.getsize(amr_deletions_file) > 0:
0
+ − 153 # Roll up deletions that might confer resistance.
3
+ − 154 try:
+ − 155 amr_deletions = pandas.read_csv(filepath_or_buffer=amr_deletions_file, sep='\t', header=None)
+ − 156 except Exception:
+ − 157 amr_deletions = pandas.DataFrame()
1
+ − 158 if amr_deletions.shape[0] > 0:
+ − 159 amr_deletions.columns = ['contig', 'start', 'stop', 'name', 'type', 'drug', 'note']
+ − 160 amr_deletions = amr_deletions.loc[amr_deletions['type'].isin(['large-deletion', 'any']), :]
+ − 161 for deletion_idx, deleted_gene in amr_deletions.iterrows():
+ − 162 amr_to_draw = amr_to_draw.append(pandas.Series(['\u0394' + deleted_gene[3], deleted_gene[5]], name=amr_to_draw.shape[0], index=amr_to_draw.columns))
0
+ − 163
+ − 164 if amr_to_draw.shape[0] > 1:
+ − 165 ofh.write("\nDrawing AMR matrix...\n")
+ − 166 present_genes = amr_to_draw['gene'].unique()
+ − 167 present_drugs = amr_to_draw['drug'].unique()
+ − 168 amr_matrix = pandas.DataFrame(0, index=present_genes, columns=present_drugs)
+ − 169 for hit_idx, hit in amr_to_draw.iterrows():
+ − 170 amr_matrix.loc[hit[0], hit[1]] = 1
7
+ − 171 amr_matrix_png = os.path.join(amr_matrix_png_dir, 'amr_matrix.png')
0
+ − 172 int_matrix = amr_matrix[amr_matrix.columns].astype(int)
+ − 173 figure, axis = pyplot.subplots()
6
+ − 174 heatmap = axis.pcolor(int_matrix, cmap=pyplot.cm.Blues, linewidth=0)
0
+ − 175 axis.invert_yaxis()
+ − 176 axis.set_yticks(numpy.arange(0.5, len(amr_matrix.index)), minor=False)
+ − 177 axis.set_yticklabels(int_matrix.index.values)
+ − 178 axis.set_xticks(numpy.arange(0.5, len(amr_matrix.columns)), minor=False)
+ − 179 axis.set_xticklabels(amr_matrix.columns.values, rotation=90)
+ − 180 axis.xaxis.tick_top()
+ − 181 axis.xaxis.set_label_position('top')
+ − 182 pyplot.tight_layout()
+ − 183 pyplot.savefig(amr_matrix_png, dpi=300)
+ − 184 else:
+ − 185 ofh.write("\nEmpty AMR matrix, nothing to draw...\n")
9
+ − 186 efh.close()
0
+ − 187 ofh.close()
+ − 188
+ − 189
+ − 190 if __name__ == '__main__':
+ − 191 parser = argparse.ArgumentParser()
+ − 192
1
+ − 193 parser.add_argument('--amr_feature_hits_dir', action='store', dest='amr_feature_hits_dir', help='Directory of tabular files containing feature hits')
+ − 194 parser.add_argument('--amr_deletions_file', action='store', dest='amr_deletions_file', default=None, help='AMR deletions BED file')
7
+ − 195 parser.add_argument('--varscan_vcf_file', action='store', dest='varscan_vcf_file', default=None, help='Varscan VCF file produced by the call_amr_mutations tool')
+ − 196 parser.add_argument('--amr_mutation_regions_bed_file', action='store', dest='amr_mutation_regions_bed_file', default=None, help='AMR mutation regions BED file')
0
+ − 197 parser.add_argument('--amr_gene_drug_file', action='store', dest='amr_gene_drug_file', help='AMR_gene_drugs tsv file')
4
+ − 198 parser.add_argument('--reference_genome', action='store', dest='reference_genome', help='Reference genome fasta file')
7
+ − 199 parser.add_argument('--mutation_regions_dir', action='store', dest='mutation_regions_dir', help='Directory for mutation regions TSV files produced by this tool')
+ − 200 parser.add_argument('--amr_matrix_png_dir', action='store', dest='amr_matrix_png_dir', help='Directory for PNG files produced by this tool')
9
+ − 201 parser.add_argument('--errors', action='store', dest='errors', help='Output file containing errors')
0
+ − 202
+ − 203 args = parser.parse_args()
+ − 204
4
+ − 205 # Get the collection of feature hits files. The collection
0
+ − 206 # will be sorted alphabetically and will contain 2 files
+ − 207 # named something like AMR_CDS_311_2022_12_20.fasta and
+ − 208 # Incompatibility_Groups_2023_01_01.fasta.
1
+ − 209 amr_feature_hits_files = []
+ − 210 for file_name in sorted(os.listdir(args.amr_feature_hits_dir)):
+ − 211 file_path = os.path.abspath(os.path.join(args.amr_feature_hits_dir, file_name))
+ − 212 amr_feature_hits_files.append(file_path)
0
+ − 213
4
+ − 214 # Load the reference genome into memory.
+ − 215 reference = load_fasta(args.reference_genome)
+ − 216 reference_size = 0
+ − 217 for i in reference:
+ − 218 reference_size += len(i.seq)
+ − 219
9
+ − 220 draw_amr_matrix(amr_feature_hits_files, args.amr_deletions_file, args.varscan_vcf_file, args.amr_mutation_regions_bed_file, args.amr_gene_drug_file, reference, reference_size, args.mutation_regions_dir, args.amr_matrix_png_dir, args.errors)