amrpredict 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. amrpredict/__init__.py +259 -0
  2. amrpredict/_paths.py +11 -0
  3. amrpredict/amr_constants.py +202 -0
  4. amrpredict/cli.py +125 -0
  5. amrpredict/genome.py +496 -0
  6. amrpredict/lgbm.py +561 -0
  7. amrpredict/models/ab_rate_full.joblib +0 -0
  8. amrpredict/models/amr_lgbm_final_model.txt +10634 -0
  9. amrpredict/models/genome/feature_meta.json +536 -0
  10. amrpredict/models/genome/genome_metrics.json +52 -0
  11. amrpredict/models/genome/model.txt +10704 -0
  12. amrpredict/models/genome_genes/feature_meta.json +811 -0
  13. amrpredict/models/genome_genes/genome_metrics.json +52 -0
  14. amrpredict/models/genome_genes/model.txt +10961 -0
  15. amrpredict/models/genome_genes/species_profiles.npz +0 -0
  16. amrpredict/models/genus_ab_rate_full.joblib +0 -0
  17. amrpredict/models/lgbm_meta.joblib +0 -0
  18. amrpredict/models/lgbm_metrics.json +97 -0
  19. amrpredict/models/rl_agent/policy.npz +0 -0
  20. amrpredict/models/rl_agent/rl_agent.json +22 -0
  21. amrpredict/models/taxon_ab_rate_full.joblib +0 -0
  22. amrpredict/models/taxon_species.csv +13059 -0
  23. amrpredict/models/timeline_calibration.json +21 -0
  24. amrpredict/models/timeline_ml/model.txt +1683 -0
  25. amrpredict/models/timeline_ml/timeline_metrics.json +145 -0
  26. amrpredict/models/timeline_ml/timeline_ml.json +1 -0
  27. amrpredict/registry.py +67 -0
  28. amrpredict/rl_agent.py +177 -0
  29. amrpredict/timeline.py +275 -0
  30. amrpredict/timeline_ml.py +174 -0
  31. amrpredict-0.3.0.dist-info/METADATA +304 -0
  32. amrpredict-0.3.0.dist-info/RECORD +35 -0
  33. amrpredict-0.3.0.dist-info/WHEEL +4 -0
  34. amrpredict-0.3.0.dist-info/entry_points.txt +2 -0
  35. amrpredict-0.3.0.dist-info/licenses/LICENSE +21 -0
amrpredict/__init__.py ADDED
@@ -0,0 +1,259 @@
1
+ """amrpredict — antimicrobial resistance prediction from genomic data.
2
+
3
+ The models the project's web app serves, behind one flat API:
4
+
5
+ >>> import amrpredict
6
+ >>> amrpredict.forecast('ciprofloxacin', taxon_id=562, mic_value=4)['prediction']
7
+ 'Susceptible'
8
+
9
+ Trained models:
10
+
11
+ * ``forecast`` — resistance from metadata and an MIC value (LightGBM).
12
+ * ``predict_fasta`` — resistance from a complete genome: the gene model
13
+ (LightGBM on AMRFinderPlus genes) when AMRFinderPlus is installed, the k-mer
14
+ model (LightGBM on 4-mer frequencies) otherwise.
15
+ * ``forecast_trend`` — the yearly % resistant of a genus and antibiotic, and its
16
+ forecast for the coming years (LightGBM on BV-BRC laboratory results).
17
+ * ``plan_treatment`` — which drug to give each week, from an RL agent (PPO)
18
+ trained on the simulation, against fixed schedules.
19
+
20
+ ``simulate_timeline`` projects week-by-week resistance under treatment and is
21
+ a biological simulation, not a trained model.
22
+ """
23
+ from . import registry
24
+ from ._paths import default_model_dir
25
+
26
+ __version__ = '0.3.0'
27
+
28
+ __all__ = [
29
+ 'forecast',
30
+ 'predict_fasta',
31
+ 'identify_species',
32
+ 'forecast_trend',
33
+ 'trend_series',
34
+ 'plan_treatment',
35
+ 'simulate_timeline',
36
+ 'antibiotics',
37
+ 'status',
38
+ 'registry',
39
+ 'default_model_dir',
40
+ 'LGBMResistancePredictor',
41
+ 'GenomeModelPredictor',
42
+ 'TimelineMLForecaster',
43
+ 'RLAgent',
44
+ 'MutationTimelinePredictor',
45
+ '__version__',
46
+ ]
47
+
48
+
49
+ def forecast(antibiotic, *, taxon_id=None, mic_value=None, mic_sign=None,
50
+ genus='unknown', species='unknown', threshold=None, model_dir=None):
51
+ """Forecast resistance from genome metadata and (optionally) an MIC value.
52
+
53
+ Args:
54
+ antibiotic: Antibiotic name, case-insensitive (e.g. ``'ciprofloxacin'``).
55
+ taxon_id: NCBI taxonomy ID of the organism, e.g. ``562`` for *E. coli*.
56
+ mic_value: Minimum inhibitory concentration in mg/L.
57
+ mic_sign: Comparator recorded with the MIC — ``'='``, ``'>'``, ``'<='`` …
58
+ genus: Genus name, e.g. ``'Escherichia'``.
59
+ species: Species name, e.g. ``'coli'``.
60
+ threshold: Probability at or above which the call is ``'Resistant'``.
61
+ Default: the threshold the bundled model was promoted with
62
+ (``status()['lgbm_forecasting']['default_threshold']``).
63
+ model_dir: Override the bundled artifacts.
64
+
65
+ Returns:
66
+ dict with ``prediction``, ``probability`` (calibrated), ``confidence``,
67
+ ``drug_class``, ``model_run``, ``evidence`` (which inputs the model
68
+ recognised) and related fields.
69
+ """
70
+ return registry.lgbm(model_dir).predict(
71
+ antibiotic, taxon_id=taxon_id, mic_value=mic_value, mic_sign=mic_sign,
72
+ genus=genus, species=species, threshold=threshold,
73
+ )
74
+
75
+
76
+ def predict_fasta(fasta_text, antibiotic, *, threshold=None, genus=None, species=None,
77
+ use_genes=None, model_dir=None):
78
+ """Predict resistance from a complete genome assembly (at least 100,000 bp).
79
+
80
+ Args:
81
+ fasta_text: FASTA contents as a string; every contig is read.
82
+ antibiotic: Antibiotic name, case-insensitive.
83
+ threshold: Probability at or above which the call is ``'Resistant'``.
84
+ Default: the model's own threshold, chosen on validation genomes.
85
+ genus, species: The organism, if known (the k-mer model uses them).
86
+ use_genes: ``None`` (default) uses the gene model when AMRFinderPlus is
87
+ installed and the k-mer model otherwise; ``False`` always uses the
88
+ k-mer model; ``True`` requires the gene model.
89
+ model_dir: Override the bundled artifacts.
90
+
91
+ Returns:
92
+ dict with ``prediction``, ``probability``, ``confidence``,
93
+ ``gc_content``, ``sequence_length``, ``model_used``; ``top_kmers`` from
94
+ the k-mer model; ``genes_found`` and ``species_detected`` from the gene
95
+ model. Bad input gives a dict with ``error`` instead of raising.
96
+ """
97
+ if use_genes is False:
98
+ model = registry.genome_kmers(model_dir)
99
+ else:
100
+ model = registry.genome(model_dir)
101
+ if use_genes and not model.uses_genes:
102
+ return {'error': 'the gene model needs AMRFinderPlus (the `amrfinder` program), '
103
+ 'which was not found; pass use_genes=None to use the k-mer model'}
104
+ return model.predict(fasta_text, antibiotic, threshold, genus, species)
105
+
106
+
107
+ def identify_species(fasta_text, *, model_dir=None):
108
+ """Species and genus of a complete genome from its 6-mer profile.
109
+
110
+ Returns:
111
+ dict with ``species``, ``genus`` (``None`` when no reference is close)
112
+ and ``distance``; or a dict with ``error``.
113
+ """
114
+ from .genome import GENE_FOLDER, GenomeModelPredictor, SpeciesProfiles, genome_counts
115
+ import os
116
+ path = os.path.join(model_dir or default_model_dir(), GENE_FOLDER, 'species_profiles.npz')
117
+ counts, _, length = genome_counts(fasta_text)
118
+ if length < GenomeModelPredictor.MIN_BP:
119
+ return {'error': f'genome too short ({length:,} bp): upload a complete assembly',
120
+ 'sequence_length': length}
121
+ found = SpeciesProfiles(path).identify(counts) or {'species': None, 'genus': None, 'distance': None}
122
+ return {k: (str(v) if isinstance(v, str) else v) for k, v in found.items()}
123
+
124
+
125
+ def forecast_trend(genus, antibiotic, *, years_ahead=5, model_dir=None):
126
+ """Yearly % resistant of a genus and antibiotic, and the timeline ML model's forecast.
127
+
128
+ Args:
129
+ genus: Genus name, case-insensitive (e.g. ``'Salmonella'``);
130
+ ``trend_series()`` lists those with enough history.
131
+ antibiotic: Antibiotic name, case-insensitive.
132
+ years_ahead: Years to forecast, 1 to 10. The 80% band widens with each.
133
+ model_dir: Override the bundled artifacts.
134
+
135
+ Returns:
136
+ dict with ``history`` (year, resistant_pct, n lab results),
137
+ ``forecast`` (year, resistant_pct, lo, hi), the model's held-out
138
+ ``metrics`` against the baselines and a ``label``; or a dict with
139
+ ``error`` when the pair has too little history.
140
+ """
141
+ years_ahead = int(years_ahead)
142
+ if not 1 <= years_ahead <= 10:
143
+ return {'error': 'years_ahead must be from 1 to 10'}
144
+ model = registry.trend(model_dir)
145
+ if not model.is_trained:
146
+ return {'error': 'the timeline ML model is not bundled'}
147
+ result = model.forecast(genus, antibiotic, years_ahead=years_ahead)
148
+ return result or {'error': f'{genus} has too little yearly lab history for {antibiotic} to forecast'}
149
+
150
+
151
+ def trend_series(model_dir=None):
152
+ """``{genus: [antibiotic, ...]}`` that ``forecast_trend`` can forecast."""
153
+ return registry.trend(model_dir).series
154
+
155
+
156
+ def plan_treatment(antibiotic, *, n_weeks=52, seed=42, model_dir=None):
157
+ """Which drug to give each week: the RL agent against fixed schedules.
158
+
159
+ The agent (PPO) was trained on the cycling simulation for ciprofloxacin,
160
+ gentamicin and imipenem; ``antibiotic`` is the first drug, the other two
161
+ are the alternatives. It learned from simulated episodes, not patients.
162
+
163
+ Args:
164
+ antibiotic: One of the agent's drugs, case-insensitive.
165
+ n_weeks: Weeks to plan, 1 to 104 (its training horizon).
166
+ seed: Seed of the simulated episode.
167
+ model_dir: Override the bundled artifacts.
168
+
169
+ Returns:
170
+ dict with ``drugs``, ``policies`` (the agent and each baseline: the drug
171
+ per week, resistance curves, ``failure_week``, ``total_reward``) and
172
+ ``best``; or a dict with ``error``.
173
+ """
174
+ from .amr_constants import normalize_antibiotic
175
+ agent = registry.rl(model_dir)
176
+ if agent is None:
177
+ return {'error': 'the RL agent is not bundled'}
178
+ n_weeks = int(n_weeks)
179
+ if not 1 <= n_weeks <= agent.train_weeks:
180
+ return {'error': f'n_weeks must be from 1 to {agent.train_weeks}'}
181
+ block = agent.block(normalize_antibiotic(antibiotic), n_weeks, seed=seed)
182
+ return block or {'error': f'the RL agent was trained on {", ".join(agent.drugs)} only'}
183
+
184
+
185
+ def simulate_timeline(fasta_text, antibiotic, *, n_weeks=8, seed=42, model_dir=None):
186
+ """Simulate week-by-week resistance evolution under antibiotic pressure.
187
+
188
+ This is a biological simulation, not a trained model — it carries no AUC
189
+ or accuracy score. Treat its output as illustrative. The same inputs and
190
+ seed give the same output, and the three shares sum to 100 every week.
191
+ For a trained forecast of yearly resistance, use ``forecast_trend``.
192
+
193
+ Args:
194
+ fasta_text: FASTA contents as a string.
195
+ antibiotic: Antibiotic name, case-insensitive.
196
+ n_weeks: Number of weeks to project.
197
+ seed: Seed for the random parts (mutation counts and types).
198
+ model_dir: Override the bundled artifacts.
199
+
200
+ Returns:
201
+ dict with a ``weeks`` series, mutation events and gene activations.
202
+ """
203
+ return registry.timeline(model_dir).predict(fasta_text, antibiotic, n_weeks, seed=seed)
204
+
205
+
206
+ def antibiotics(model='genome', model_dir=None):
207
+ """Sorted antibiotic names a model was trained on.
208
+
209
+ Args:
210
+ model: ``'genome'`` (``predict_fasta``), ``'forecast'`` (``forecast``),
211
+ ``'trend'`` (``forecast_trend``) or ``'rl'`` (``plan_treatment``).
212
+ """
213
+ if model == 'genome':
214
+ return registry.genome_kmers(model_dir).ab_list
215
+ if model == 'forecast':
216
+ return sorted(registry.lgbm(model_dir).vocabulary.get('antibiotics') or [])
217
+ if model == 'trend':
218
+ return sorted({ab for drugs in trend_series(model_dir).values() for ab in drugs})
219
+ if model == 'rl':
220
+ agent = registry.rl(model_dir)
221
+ return sorted(agent.drugs) if agent else []
222
+ raise ValueError("model must be 'genome', 'forecast', 'trend' or 'rl'")
223
+
224
+
225
+ def status(model_dir=None):
226
+ """Report which models loaded, keyed by model name, with their measured
227
+ test metrics where they have them."""
228
+ agent = registry.rl(model_dir)
229
+ return {
230
+ 'version': __version__,
231
+ 'lgbm_forecasting': registry.lgbm(model_dir).status,
232
+ 'genome_resistance': registry.genome(model_dir).status,
233
+ 'trend_forecast': registry.trend(model_dir).status,
234
+ 'rl_agent': {'trained': agent is not None,
235
+ 'model_type': agent.label if agent else None,
236
+ 'drugs': agent.drugs if agent else [],
237
+ 'description': 'Plans which drug to give each week; trained on the simulation'},
238
+ 'mutation_timeline': registry.timeline(model_dir).status,
239
+ }
240
+
241
+
242
+ def __getattr__(name):
243
+ # Re-export the classes without importing numpy/pandas/lightgbm at import time.
244
+ if name == 'LGBMResistancePredictor':
245
+ from .lgbm import LGBMResistancePredictor
246
+ return LGBMResistancePredictor
247
+ if name == 'GenomeModelPredictor':
248
+ from .genome import GenomeModelPredictor
249
+ return GenomeModelPredictor
250
+ if name == 'TimelineMLForecaster':
251
+ from .timeline_ml import TimelineMLForecaster
252
+ return TimelineMLForecaster
253
+ if name == 'RLAgent':
254
+ from .rl_agent import RLAgent
255
+ return RLAgent
256
+ if name == 'MutationTimelinePredictor':
257
+ from .timeline import MutationTimelinePredictor
258
+ return MutationTimelinePredictor
259
+ raise AttributeError(f'module {__name__!r} has no attribute {name!r}')
amrpredict/_paths.py ADDED
@@ -0,0 +1,11 @@
1
+ """Locating the model artifacts bundled inside the installed package."""
2
+ from importlib.resources import files
3
+
4
+
5
+ def default_model_dir():
6
+ """Absolute path to the models shipped with this package.
7
+
8
+ Used whenever a predictor is constructed without an explicit ``model_dir``,
9
+ so that ``LGBMForecaster()`` works straight after ``pip install``.
10
+ """
11
+ return str(files(__package__) / 'models')
@@ -0,0 +1,202 @@
1
+ """
2
+ Antibiotic names and label maps: the library's copy of backend/amr_constants.py.
3
+
4
+ The package installs without backend/, so it carries this copy. Do not edit it
5
+ here: edit backend/amr_constants.py, then copy the data over (everything above
6
+ FRONTEND_COPY); backend/tests/test_amr_constants.py fails while they differ.
7
+ """
8
+
9
+ # Drug class per canonical antibiotic name. Anything missing is 'other'.
10
+ DRUG_CLASS_MAP = {
11
+ 'ampicillin': 'beta_lactam', 'amoxicillin': 'beta_lactam',
12
+ 'amoxicillin/clavulanic acid': 'beta_lactam', 'piperacillin': 'beta_lactam',
13
+ 'piperacillin/tazobactam': 'beta_lactam', 'oxacillin': 'beta_lactam',
14
+ 'ampicillin/sulbactam': 'beta_lactam', 'penicillin': 'beta_lactam',
15
+ 'methicillin': 'beta_lactam', 'carbenicillin': 'beta_lactam',
16
+ 'cefazolin': 'beta_lactam', 'cefoxitin': 'beta_lactam', 'cefotaxime': 'beta_lactam',
17
+ 'ceftazidime': 'beta_lactam', 'ceftriaxone': 'beta_lactam', 'cefepime': 'beta_lactam',
18
+ 'cefuroxime': 'beta_lactam', 'cephalothin': 'beta_lactam', 'cefalothin': 'beta_lactam',
19
+ 'ceftazidime/avibactam': 'beta_lactam', 'ceftolozane/tazobactam': 'beta_lactam',
20
+ 'ceftiofur': 'beta_lactam', 'cefpodoxime': 'beta_lactam',
21
+ 'imipenem': 'carbapenem', 'meropenem': 'carbapenem', 'ertapenem': 'carbapenem',
22
+ 'doripenem': 'carbapenem', 'aztreonam': 'monobactam',
23
+ 'ciprofloxacin': 'fluoroquinolone', 'levofloxacin': 'fluoroquinolone',
24
+ 'norfloxacin': 'fluoroquinolone', 'nalidixic acid': 'fluoroquinolone',
25
+ 'ofloxacin': 'fluoroquinolone', 'moxifloxacin': 'fluoroquinolone',
26
+ 'pefloxacin': 'fluoroquinolone', 'trovafloxacin': 'fluoroquinolone',
27
+ 'gentamicin': 'aminoglycoside', 'tobramycin': 'aminoglycoside',
28
+ 'amikacin': 'aminoglycoside', 'streptomycin': 'aminoglycoside',
29
+ 'neomycin': 'aminoglycoside', 'kanamycin': 'aminoglycoside',
30
+ 'spectinomycin': 'aminoglycoside', 'capreomycin': 'aminoglycoside',
31
+ 'tetracycline': 'tetracycline', 'doxycycline': 'tetracycline',
32
+ 'minocycline': 'tetracycline', 'tigecycline': 'tetracycline',
33
+ 'sulfamethoxazole': 'sulfonamide', 'trimethoprim': 'sulfonamide',
34
+ 'trimethoprim/sulfamethoxazole': 'sulfonamide', 'sulfisoxazole': 'sulfonamide',
35
+ 'chloramphenicol': 'phenicol',
36
+ 'azithromycin': 'macrolide', 'erythromycin': 'macrolide',
37
+ 'telithromycin': 'macrolide', 'clarithromycin': 'macrolide',
38
+ 'colistin': 'polymyxin', 'polymyxin b': 'polymyxin',
39
+ 'vancomycin': 'glycopeptide', 'teicoplanin': 'glycopeptide',
40
+ 'clindamycin': 'lincosamide', 'nitrofurantoin': 'nitrofuran',
41
+ 'rifampicin': 'rifamycin', 'rifampin': 'rifamycin', 'rifabutin': 'rifamycin',
42
+ 'linezolid': 'oxazolidinone', 'daptomycin': 'lipopeptide',
43
+ 'fusidic acid': 'fusidane', 'quinupristin/dalfopristin': 'streptogramin',
44
+ 'pristinamycin': 'streptogramin', 'cephalexin': 'beta_lactam',
45
+ 'cefpodoxime/clavulanic acid': 'beta_lactam', 'ticarcillin/clavulanic acid': 'beta_lactam',
46
+ 'isoniazid': 'antitubercular', 'ethambutol': 'antitubercular',
47
+ 'pyrazinamide': 'antitubercular', 'ethionamide': 'antitubercular',
48
+ 'prothionamide': 'antitubercular', 'cycloserine': 'antitubercular',
49
+ 'para-aminosalicylic acid': 'antitubercular', 'clofazimine': 'antitubercular',
50
+ 'delamanid': 'antitubercular', 'bedaquiline': 'antitubercular',
51
+ # v4: drugs in the export (or the UI list) that fell into 'other'.
52
+ # Only azidothymidine (an antiviral) is left there on purpose.
53
+ 'cefixime': 'beta_lactam', 'cefpirome': 'beta_lactam', 'cefoperazone': 'beta_lactam',
54
+ 'cefotetan': 'beta_lactam', 'cefiderocol': 'beta_lactam', 'ceftaroline': 'beta_lactam',
55
+ 'cefovecin': 'beta_lactam', 'cefamandole': 'beta_lactam', 'cefaclor': 'beta_lactam',
56
+ 'cefmetazole': 'beta_lactam', 'ceftizoxime': 'beta_lactam', 'cefdinir': 'beta_lactam',
57
+ 'cefozopran': 'beta_lactam', 'ceftobiprole': 'beta_lactam', 'ceftibuten': 'beta_lactam',
58
+ 'ceftriaxone/cefpodoxime': 'beta_lactam', 'cefotaxime/clavulanic acid': 'beta_lactam',
59
+ 'ceftazidime/clavulanic acid': 'beta_lactam', 'cefoperazone/sulbactam': 'beta_lactam',
60
+ 'cefepime/taniborbactam': 'beta_lactam', 'temocillin': 'beta_lactam',
61
+ 'ticarcillin': 'beta_lactam', 'mecillinam': 'beta_lactam', 'sulbactam': 'beta_lactam',
62
+ 'imipenem/relebactam': 'carbapenem',
63
+ 'netilmicin': 'aminoglycoside', 'plazomicin': 'aminoglycoside', 'apramycin': 'aminoglycoside',
64
+ 'gatifloxacin': 'fluoroquinolone', 'enrofloxacin': 'fluoroquinolone',
65
+ 'danofloxacin': 'fluoroquinolone', 'sparfloxacin': 'fluoroquinolone',
66
+ 'pradofloxacin': 'fluoroquinolone', 'delafloxacin': 'fluoroquinolone',
67
+ 'chlortetracycline': 'tetracycline', 'oxytetracycline': 'tetracycline',
68
+ 'eravacycline': 'tetracycline', 'omadacycline': 'tetracycline',
69
+ 'sulfathiazole': 'sulfonamide', 'sulfamethazine': 'sulfonamide',
70
+ 'trimethoprim/sulfobactam': 'sulfonamide',
71
+ 'tylosin': 'macrolide', 'tulathromycin': 'macrolide', 'spiramycin': 'macrolide',
72
+ 'florfenicol': 'phenicol', 'lincomycin': 'lincosamide', 'virginiamycin': 'streptogramin',
73
+ 'furazolidone': 'nitrofuran', 'metronidazole': 'nitroimidazole',
74
+ 'fosfomycin': 'phosphonic_acid', 'mupirocin': 'pseudomonic_acid',
75
+ 'zoliflodacin': 'spiropyrimidinetrione', 'avilamycin': 'orthosomycin',
76
+ 'nicotinamide': 'antitubercular', # all 229 rows are Mycobacterium
77
+ }
78
+
79
+ # Spelling variants, typos and non-drugs found in the raw Antibiotic column.
80
+ # Left-hand side is what appears in the export; right-hand side is canonical.
81
+ # None means "drop these rows", a drug class is not a drug.
82
+ ANTIBIOTIC_ALIASES = {
83
+ 'ampicillin-sulbactam': 'ampicillin/sulbactam',
84
+ 'ampicillin_clavulanic_acid': 'amoxicillin/clavulanic acid',
85
+ 'amoxicillin-clavulanic acid': 'amoxicillin/clavulanic acid',
86
+ 'piperacillin-tazobactam': 'piperacillin/tazobactam',
87
+ 'trimethoprim-sulfamethoxazole': 'trimethoprim/sulfamethoxazole',
88
+ 'sulfamethoxazole/trimethoprim': 'trimethoprim/sulfamethoxazole',
89
+ 'co_trimoxazole': 'trimethoprim/sulfamethoxazole',
90
+ 'co-trimoxazole': 'trimethoprim/sulfamethoxazole',
91
+ 'geamycin': 'gentamicin',
92
+ 'trimotheprim': 'trimethoprim',
93
+ 'cefalothin': 'cephalothin',
94
+ 'rifampin': 'rifampicin',
95
+ # Separator variants of combination drugs
96
+ 'tazobactam_piperacillin': 'piperacillin/tazobactam',
97
+ 'ceftazidime_avibactam': 'ceftazidime/avibactam',
98
+ 'ceftolozane_tazobactam': 'ceftolozane/tazobactam',
99
+ 'ticarcillin_clavulanate': 'ticarcillin/clavulanic acid',
100
+ 'cefpodoxime_clavulanic_acid': 'cefpodoxime/clavulanic acid',
101
+ 'trimethoprim_sulfobactam': 'trimethoprim/sulfobactam',
102
+ 'para_aminosalicylic_acid': 'para-aminosalicylic acid',
103
+ 'cefepime_taniborbactam': 'cefepime/taniborbactam',
104
+ 'amoxicillin_clavulanat': 'amoxicillin/clavulanic acid',
105
+ 'polymyxin_b': 'polymyxin b',
106
+ 'ceftazidime-avibactam': 'ceftazidime/avibactam',
107
+ 'ceftolozane-tazobactam': 'ceftolozane/tazobactam',
108
+ 'imipenem-relebactam': 'imipenem/relebactam',
109
+ # Typos and a broken character encoding
110
+ 'amipicillin_sulbactam': 'ampicillin/sulbactam',
111
+ 'tgecycline': 'tigecycline',
112
+ 'cefuroxim\u00e2': 'cefuroxime',
113
+ 'pristimycin': 'pristinamycin',
114
+ 'cefotaxime/clavulanic acid\u00e2': 'cefotaxime/clavulanic acid',
115
+ # Other-language spellings, found by Suleman in BVBRC_genome_amr.csv
116
+ 'tigecyklin': 'tigecycline',
117
+ 'tetracyklin': 'tetracycline',
118
+ 'cefpirom': 'cefpirome',
119
+ # Shigella panel; never on the same genome as nitrofurantoin
120
+ 'strofurantoin': 'nitrofurantoin',
121
+ # Alternative names for the same drug
122
+ 'cefuroxime_sodium': 'cefuroxime',
123
+ 'cefalotin': 'cephalothin',
124
+ 'cefalexin': 'cephalexin',
125
+ 'synercid': 'quinupristin/dalfopristin',
126
+ 'phosphomycin': 'fosfomycin',
127
+ 'benzylpenicillin': 'penicillin',
128
+ # Not a single drug: drug classes, a phenotype, and a lost drug name
129
+ # ('instrument' is 593 C. difficile lab rows with the drug name missing)
130
+ 'carbapenem': None,
131
+ 'beta-lactam': None,
132
+ 'cephalosporin': None,
133
+ 'fluoroquinolones': None,
134
+ 'aminogycosides': None,
135
+ 'macrolides': None,
136
+ 'sulfonamides': None,
137
+ 'sulfa': None,
138
+ 'extended spectrum beta lactamase': None,
139
+ 'instrument': None,
140
+ }
141
+
142
+ # Resistant Phenotype -> label. 0 = susceptible, 1 = resistant.
143
+ PHENOTYPE_MAP = {
144
+ 'Susceptible': 0,
145
+ 'Susceptible-dose dependent': 0,
146
+ 'Resistant': 1,
147
+ 'Intermediate': 1,
148
+ 'Nonsusceptible': 1,
149
+ 'Reduced Susceptibility': 1,
150
+ }
151
+
152
+ # Phenotypes each trainer keeps; the rest of its rows are dropped. They
153
+ # differ on purpose, to keep the served models' training data as it was.
154
+ LGBM_TRAIN_PHENOTYPES = ('Susceptible', 'Resistant', 'Intermediate', 'Nonsusceptible')
155
+ KMER_TRAIN_PHENOTYPES = ('Susceptible', 'Resistant', 'Intermediate')
156
+
157
+ # MIC comparators a user may type that the forecaster has no category for,
158
+ # mapped onto the nearest sign it learned.
159
+ MIC_SIGN_ALIASES = {'>=': '>', '\u2265': '>', '\u2264': '<=', '=<': '<=', '=>': '>'}
160
+
161
+ # Antibiotics offered in the dropdowns, grouped by class. The pages show only
162
+ # the names the loaded model knows; this list is the fallback when the backend
163
+ # is down, and what GET /api/antibiotics/ returns.
164
+ UI_ANTIBIOTICS = [
165
+ 'penicillin', 'ampicillin', 'ampicillin/sulbactam', 'amoxicillin',
166
+ 'amoxicillin/clavulanic acid', 'piperacillin', 'piperacillin/tazobactam',
167
+ 'oxacillin', 'temocillin', 'carbenicillin', 'ticarcillin/clavulanic acid',
168
+ 'cefazolin', 'cefoxitin', 'cefotetan', 'cefmetazole', 'cefotaxime',
169
+ 'cefotaxime/clavulanic acid', 'ceftazidime', 'ceftazidime/avibactam',
170
+ 'ceftazidime/clavulanic acid', 'ceftolozane/tazobactam', 'ceftriaxone',
171
+ 'cefepime', 'cefepime/taniborbactam', 'cefuroxime', 'cephalothin',
172
+ 'cefixime', 'cefpodoxime', 'cefpodoxime/clavulanic acid', 'ceftibuten',
173
+ 'cefoperazone/sulbactam', 'ceftiofur', 'cefpirome', 'cefozopran',
174
+ 'ceftaroline', 'ceftobiprole',
175
+ 'imipenem', 'imipenem/relebactam', 'meropenem', 'ertapenem', 'doripenem',
176
+ 'aztreonam', 'ciprofloxacin', 'levofloxacin', 'norfloxacin',
177
+ 'nalidixic acid', 'ofloxacin', 'moxifloxacin', 'pefloxacin',
178
+ 'delafloxacin',
179
+ 'gentamicin', 'tobramycin', 'amikacin', 'streptomycin', 'neomycin',
180
+ 'kanamycin', 'spectinomycin', 'apramycin',
181
+ 'tetracycline', 'oxytetracycline', 'doxycycline', 'minocycline',
182
+ 'tigecycline',
183
+ 'sulfamethoxazole', 'sulfisoxazole', 'trimethoprim',
184
+ 'trimethoprim/sulfamethoxazole',
185
+ 'chloramphenicol', 'florfenicol',
186
+ 'azithromycin', 'erythromycin', 'clarithromycin', 'telithromycin',
187
+ 'colistin', 'polymyxin b', 'vancomycin', 'teicoplanin',
188
+ 'clindamycin', 'lincomycin', 'nitrofurantoin', 'fosfomycin', 'rifampicin',
189
+ ]
190
+
191
+
192
+ def normalize_antibiotic(name):
193
+ """Lower-case, strip, and map a spelling variant to its canonical name.
194
+
195
+ Names the alias table drops from training (drug classes such as
196
+ 'carbapenem') are returned unchanged; a model then reports them as
197
+ unrecognised instead of silently answering for a different drug.
198
+ """
199
+ if name in (None, ''):
200
+ return name
201
+ ab = ' '.join(str(name).split()).lower()
202
+ return ANTIBIOTIC_ALIASES.get(ab) or ab
amrpredict/cli.py ADDED
@@ -0,0 +1,125 @@
1
+ """Command-line interface: ``amrpredict <command> [options]``."""
2
+ import argparse
3
+ import json
4
+ import sys
5
+
6
+
7
+ def _read_fasta(path):
8
+ if path == '-':
9
+ return sys.stdin.read()
10
+ with open(path, 'r', encoding='utf-8', errors='replace') as fh:
11
+ return fh.read()
12
+
13
+
14
+ def _emit(payload, pretty):
15
+ print(json.dumps(payload, indent=2 if pretty else None, default=str))
16
+
17
+
18
+ def build_parser():
19
+ from . import __version__
20
+
21
+ parser = argparse.ArgumentParser(
22
+ prog='amrpredict',
23
+ description='Predict antimicrobial resistance from genomic data.',
24
+ )
25
+ parser.add_argument('--version', action='version', version=f'amrpredict {__version__}')
26
+ parser.add_argument('--compact', action='store_true', help='emit single-line JSON')
27
+ parser.add_argument('--model-dir', default=None, help='use artifacts from this directory')
28
+ sub = parser.add_subparsers(dest='command', required=True)
29
+
30
+ p = sub.add_parser('forecast', help='predict from metadata + MIC value')
31
+ p.add_argument('antibiotic')
32
+ p.add_argument('--taxon-id', type=int, default=None, help='NCBI taxonomy ID, e.g. 562')
33
+ p.add_argument('--mic-value', type=float, default=None, help='MIC in mg/L')
34
+ p.add_argument('--mic-sign', default=None, help="MIC comparator, e.g. '=' or '>'")
35
+ p.add_argument('--genus', default='unknown')
36
+ p.add_argument('--species', default='unknown')
37
+ p.add_argument('--threshold', type=float, default=None,
38
+ help="default: the bundled model's own threshold")
39
+
40
+ p = sub.add_parser('predict', help='predict from a complete genome FASTA')
41
+ p.add_argument('fasta', help="path to a FASTA file, or '-' for stdin")
42
+ p.add_argument('antibiotic')
43
+ p.add_argument('--threshold', type=float, default=None,
44
+ help="default: the model's own threshold")
45
+ p.add_argument('--genus', default=None)
46
+ p.add_argument('--species', default=None)
47
+ genes = p.add_mutually_exclusive_group()
48
+ genes.add_argument('--kmers', action='store_true', help='always use the k-mer model')
49
+ genes.add_argument('--genes', action='store_true',
50
+ help='require the gene model (needs AMRFinderPlus)')
51
+
52
+ p = sub.add_parser('species', help='identify the species of a complete genome')
53
+ p.add_argument('fasta', help="path to a FASTA file, or '-' for stdin")
54
+
55
+ p = sub.add_parser('trend', help='yearly resistance of a genus and drug, and its forecast')
56
+ p.add_argument('genus')
57
+ p.add_argument('antibiotic')
58
+ p.add_argument('--years', type=int, default=5, help='years ahead, 1 to 10')
59
+
60
+ p = sub.add_parser('plan', help='drug per week: the RL agent against fixed schedules')
61
+ p.add_argument('antibiotic', help='ciprofloxacin, gentamicin or imipenem')
62
+ p.add_argument('--weeks', type=int, default=52, help='1 to 104')
63
+ p.add_argument('--seed', type=int, default=42)
64
+
65
+ p = sub.add_parser('timeline', help='simulate resistance evolution (not a trained model)')
66
+ p.add_argument('fasta', help="path to a FASTA file, or '-' for stdin")
67
+ p.add_argument('antibiotic')
68
+ p.add_argument('--weeks', type=int, default=8)
69
+
70
+ p = sub.add_parser('antibiotics', help='list the antibiotics a model knows')
71
+ p.add_argument('--model', default='genome', choices=['genome', 'forecast', 'trend', 'rl'])
72
+ sub.add_parser('status', help='report which models loaded')
73
+ return parser
74
+
75
+
76
+ def main(argv=None):
77
+ import amrpredict
78
+
79
+ args = build_parser().parse_args(argv)
80
+ pretty = not args.compact
81
+ md = args.model_dir
82
+
83
+ try:
84
+ if args.command == 'forecast':
85
+ result = amrpredict.forecast(
86
+ args.antibiotic, taxon_id=args.taxon_id, mic_value=args.mic_value,
87
+ mic_sign=args.mic_sign, genus=args.genus, species=args.species,
88
+ threshold=args.threshold, model_dir=md,
89
+ )
90
+ elif args.command == 'predict':
91
+ result = amrpredict.predict_fasta(
92
+ _read_fasta(args.fasta), args.antibiotic, threshold=args.threshold,
93
+ genus=args.genus, species=args.species,
94
+ use_genes=False if args.kmers else (True if args.genes else None), model_dir=md,
95
+ )
96
+ elif args.command == 'species':
97
+ result = amrpredict.identify_species(_read_fasta(args.fasta), model_dir=md)
98
+ elif args.command == 'trend':
99
+ result = amrpredict.forecast_trend(args.genus, args.antibiotic,
100
+ years_ahead=args.years, model_dir=md)
101
+ elif args.command == 'plan':
102
+ result = amrpredict.plan_treatment(args.antibiotic, n_weeks=args.weeks,
103
+ seed=args.seed, model_dir=md)
104
+ elif args.command == 'timeline':
105
+ result = amrpredict.simulate_timeline(
106
+ _read_fasta(args.fasta), args.antibiotic,
107
+ n_weeks=args.weeks, model_dir=md,
108
+ )
109
+ elif args.command == 'antibiotics':
110
+ result = amrpredict.antibiotics(args.model, model_dir=md)
111
+ else:
112
+ result = amrpredict.status(model_dir=md)
113
+ except FileNotFoundError as exc:
114
+ print(f'amrpredict: {exc}', file=sys.stderr)
115
+ return 2
116
+ except ValueError as exc:
117
+ print(f'amrpredict: {exc}', file=sys.stderr)
118
+ return 1
119
+
120
+ _emit(result, pretty)
121
+ return 0
122
+
123
+
124
+ if __name__ == '__main__':
125
+ raise SystemExit(main())