nx_chem_adulterant_db.nx source
↩ module page · 1223 lines · 54017 B
1// nx_chem_adulterant_db.nx -- C4.0 milestone: first usable supplement-arc
2// adulterant detection database + mass-window lookup pipeline.
3//
4// This is the smallest end-to-end demo:
5// 1. Build seed DB of known supplement adulterants
6// 2. Each entry parses real SMILES at DB-build time
7// 3. Compute monoisotopic mass + [M+H]+ + halogen signature
8// via C2.8a + C2.8b primitives
9// 4. Lookup: given observed LC-MS peak m/z + ppm tolerance, scan DB
10// and return matching entries
11// 5. Report: human-readable lab-facing output with verdict
12//
13// Composes: C2.0 MolGraph + C2.1 SMILES parser + C2.3a implicit-H +
14// C2.8a monoisotopic mass + ion m/z + C2.8b halogen sig.
15//
16// Seed compounds (publicly known supplement adulterants;
17// FDA Tainted Supplements list + DSHEA + IOC banned list references):
18//
19// id 0 ephedrine C10H15NO restricted (DSHEA-pre-2004)
20// id 1 DMAA C7H17N banned 2013 (FDA action)
21// id 2 DMBA C6H15N banned 2014 (FDA action)
22// id 3 DMHA C8H19N banned 2019 (FDA caution)
23// id 4 sibutramine C17H26ClN banned 2010 (Abbott withdrew)
24// id 5 caffeine C8H10N4O2 approved (baseline, not banned)
25// id 6 higenamine C16H17NO3 banned 2017 (WADA + FDA)
26// id 7 yohimbine C21H26N2O3 restricted (some markets)
27//
28// Honest gaps (deferred):
29// - sildenafil (C22H30N6O4S; Viagra-active spiked in herbal ED):
30// SMILES contains aromatic+saturated mixed ring system parser
31// doesn't yet handle perfectly; C4.1.
32// - Tadalafil (Cialis), anabolic steroid analogs, polyhalogenated:
33// more complex parsing required; C4.1.
34// - Persistent storage (memory-only DB at present); C4.2.
35// - Multiple-adulterant interactions: not modeled; lab interpretation.
36
37import "nx_chem.nx"
38import "nx_chem_molecule.nx"
39import "nx_chem_smiles.nx"
40import "nx_chem_periodic.nx"
41import "nx_chem_valence.nx"
42import "nx_chem_mass.nx"
43import "nx_chem_isotope_pattern.nx"
44import "nx_chem_peak_list.nx"
45import "nx_syscalls.nx"
46const NX_MAGIC_30000: i64 = 30000
47const NX_MAGIC_1000000: i64 = 1000000
48const NX_MAGIC_10000000: i64 = 10000000
49const NX_MAGIC_2018: i64 = 2018
50const NX_MAGIC_10000: i64 = 10000
51const NX_MAGIC_300000: i64 = 300000
52
53// Regulatory status enum
54const NX_REG_UNKNOWN: nx_int = 0
55const NX_REG_APPROVED: nx_int = 1
56const NX_REG_RESTRICTED: nx_int = 2
57const NX_REG_BANNED: nx_int = 3
58const NX_REG_RX_ONLY: nx_int = 4
59
60struct AdulterantEntry {
61 id: nx_int,
62 monoiso_q4: nx_int,
63 mh_plus_q4: nx_int,
64 halogen_sig: nx_int,
65 regulatory: nx_int,
66 n_heavy: nx_int,
67 m1_q4: nx_int,
68 m2_q4: nx_int,
69 ref_rt_q3: nx_int, // C7.0: reference retention time in Q3 ms (0 = unknown)
70 rt_tol_q3: nx_int, // C7.0: retention-time tolerance in Q3 ms (0 = use default 30s)
71 name_ptr: *u8, // C4.3: pointer to compound name string (literal or allocated)
72 citation_ptr: *u8, // C4.3: pointer to regulatory citation string
73}
74
75const NX_ADULTERANT_ENTRY_BYTES: nx_int = 96
76
77struct AdulterantDB {
78 entries: *AdulterantEntry,
79 n: nx_int,
80 cap: nx_int,
81}
82
83const NX_ADULTERANT_DB_BYTES: nx_int = 32
84
85// Lookup result is a small fixed-capacity match list (caller-allocated)
86struct MatchResult {
87 entry_id: nx_int,
88 delta_q4: nx_int, // observed_mz - reference_mh_plus
89 delta_ppm_q1: nx_int, // |delta| / mz * 1e7 (parts per 10 million; Q1 of ppm)
90 confidence_pct: nx_int, // C7.3: 0..100 confidence score (mass + RT + halogen)
91}
92const NX_MATCH_RESULT_BYTES: nx_int = 32
93
94// =================================================================
95// Construct empty DB with cap capacity.
96// =================================================================
97func nx_chem_adulterant_db_new(cap: nx_int) -> *AdulterantDB {
98 let db: *AdulterantDB = (sys_mmap(NX_ADULTERANT_DB_BYTES as i64)) as *AdulterantDB
99 db.entries = (sys_mmap((cap * NX_ADULTERANT_ENTRY_BYTES) as i64)) as *AdulterantEntry
100 db.n = 0
101 db.cap = cap
102 return db
103}
104
105// =================================================================
106// Add an adulterant entry by parsing its SMILES + computing m/z
107// fingerprint. Returns id (= index) on success, -1 if DB is full
108// or SMILES parse fails.
109// =================================================================
110func nx_chem_adulterant_db_add(db: *AdulterantDB, id: nx_int, smiles: *u8, n_smi: nx_int, regulatory: nx_int, ad_table: *AtomicData) -> nx_int {
111 if db.n >= db.cap { return -1 }
112 let m: *MolGraph = nx_chem_parse_smiles(smiles, n_smi)
113 if m.n_atoms <= 0 { return -1 }
114 let _f: nx_int = nx_chem_compute_implicit_h(m)
115 let mono: nx_int = nx_chem_mol_monoisotopic_mass_q4(m, ad_table)
116 let mh: nx_int = nx_chem_mol_mh_plus_q4(m, ad_table)
117 let hsig: nx_int = nx_chem_halogen_signature(m)
118 let m1: nx_int = nx_chem_isotope_m1_q4(m)
119 let m2: nx_int = nx_chem_isotope_m2_q4(m)
120 // Count heavy atoms (z >= 2) inline
121 var n_heavy: nx_int = 0
122 var ai: nx_int = 0
123 while ai < m.n_atoms {
124 let a: *Atom = ((m.atoms as nx_int) + (ai * NX_ATOM_BYTES)) as *Atom
125 if a.z >= 2 { n_heavy = n_heavy + 1 }
126 ai = ai + 1
127 }
128 let e: *AdulterantEntry = ((db.entries as nx_int) + (db.n * NX_ADULTERANT_ENTRY_BYTES)) as *AdulterantEntry
129 e.id = id
130 e.monoiso_q4 = mono
131 e.mh_plus_q4 = mh
132 e.halogen_sig = hsig
133 e.regulatory = regulatory
134 e.n_heavy = n_heavy
135 e.m1_q4 = m1
136 e.m2_q4 = m2
137 e.ref_rt_q3 = 0 // unknown until calibration sets it
138 e.rt_tol_q3 = NX_MAGIC_30000 // default 30 sec tolerance (typical LC peak width)
139 // C4.3: strings default NULL; seed function or db_add_with_strings
140 // sets them.
141 e.name_ptr = 0 as *u8
142 e.citation_ptr = 0 as *u8
143 db.n = db.n + 1
144 return id
145}
146
147// =================================================================
148// C4.3: Add a compound to the DB with caller-supplied name + citation
149// strings (used by db_load_compounds). Returns id on success, -1 on
150// SMILES parse failure or DB full.
151//
152// Caller responsibility: name + citation strings must remain valid
153// for DB lifetime (typically allocated via sys_mmap in the caller).
154// =================================================================
155func nx_chem_adulterant_db_add_with_strings(db: *AdulterantDB, id: nx_int, smiles: *u8, n_smi: nx_int, regulatory: nx_int, name: *u8, citation: *u8, ad_table: *AtomicData) -> nx_int {
156 if db.n >= db.cap { return -1 }
157 let m: *MolGraph = nx_chem_parse_smiles(smiles, n_smi)
158 if m.n_atoms <= 0 { return -1 }
159 let _f: nx_int = nx_chem_compute_implicit_h(m)
160 let mono: nx_int = nx_chem_mol_monoisotopic_mass_q4(m, ad_table)
161 let mh: nx_int = nx_chem_mol_mh_plus_q4(m, ad_table)
162 let hsig: nx_int = nx_chem_halogen_signature(m)
163 let m1: nx_int = nx_chem_isotope_m1_q4(m)
164 let m2: nx_int = nx_chem_isotope_m2_q4(m)
165 var n_heavy: nx_int = 0
166 var ai: nx_int = 0
167 while ai < m.n_atoms {
168 let a: *Atom = ((m.atoms as nx_int) + (ai * NX_ATOM_BYTES)) as *Atom
169 if a.z >= 2 { n_heavy = n_heavy + 1 }
170 ai = ai + 1
171 }
172 let e: *AdulterantEntry = ((db.entries as nx_int) + (db.n * NX_ADULTERANT_ENTRY_BYTES)) as *AdulterantEntry
173 e.id = id
174 e.monoiso_q4 = mono
175 e.mh_plus_q4 = mh
176 e.halogen_sig = hsig
177 e.regulatory = regulatory
178 e.n_heavy = n_heavy
179 e.m1_q4 = m1
180 e.m2_q4 = m2
181 e.ref_rt_q3 = 0
182 e.rt_tol_q3 = NX_MAGIC_30000
183 e.name_ptr = name
184 e.citation_ptr = citation
185 db.n = db.n + 1
186 return id
187}
188
189// =================================================================
190// Set reference RT for a compound by id. Used when lab loads its
191// own RT calibration file (RT is column+gradient-specific).
192//
193// Returns 0 on success, -1 if id not found.
194// =================================================================
195func nx_chem_adulterant_db_set_rt(db: *AdulterantDB, id: nx_int, ref_rt_q3: nx_int, rt_tol_q3: nx_int) -> nx_int {
196 var i: nx_int = 0
197 while i < db.n {
198 let e: *AdulterantEntry = ((db.entries as nx_int) + (i * NX_ADULTERANT_ENTRY_BYTES)) as *AdulterantEntry
199 if e.id == id {
200 e.ref_rt_q3 = ref_rt_q3
201 e.rt_tol_q3 = rt_tol_q3
202 return 0
203 }
204 i = i + 1
205 }
206 return -1
207}
208
209// =================================================================
210// Lookup with BOTH mass (ppm) AND retention-time (Q3 ms) filtering.
211// This is the lab-realistic LC-MS query: a peak is identified by
212// matching reference compound only when both dimensions agree.
213//
214// Behavior:
215// - mass filter: |observed_mz_q4 - entry.mh_plus_q4| <= mass_tol_q4
216// (mass_tol_q4 = ppm_tol * mz / 1000000)
217// - rt filter: if entry.ref_rt_q3 > 0 and observed_rt_q3 > 0:
218// |observed_rt_q3 - entry.ref_rt_q3| <= entry.rt_tol_q3
219// else: skip rt filter (mass-only fallback)
220//
221// Returns number of matches written to out.
222// =================================================================
223func nx_chem_adulterant_db_lookup_with_rt(db: *AdulterantDB, mz_q4: nx_int, rt_q3: nx_int, ppm_tol: nx_int, out_matches: *MatchResult, max_out: nx_int) -> nx_int {
224 let tol_q4: nx_int = (ppm_tol * mz_q4) / NX_MAGIC_1000000
225 var n_out: nx_int = 0
226 var i: nx_int = 0
227 while i < db.n {
228 if n_out >= max_out { return n_out }
229 let e: *AdulterantEntry = ((db.entries as nx_int) + (i * NX_ADULTERANT_ENTRY_BYTES)) as *AdulterantEntry
230 var delta: nx_int = mz_q4 - e.mh_plus_q4
231 var abs_d: nx_int = delta
232 if abs_d < 0 { abs_d = 0 - abs_d }
233 var pass_mass: nx_int = 0
234 if abs_d <= tol_q4 { pass_mass = 1 }
235 var pass_rt: nx_int = 1
236 if e.ref_rt_q3 > 0 {
237 if rt_q3 > 0 {
238 var rt_delta: nx_int = rt_q3 - e.ref_rt_q3
239 if rt_delta < 0 { rt_delta = 0 - rt_delta }
240 if rt_delta > e.rt_tol_q3 { pass_rt = 0 }
241 }
242 }
243 if pass_mass == 1 {
244 if pass_rt == 1 {
245 let r: *MatchResult = ((out_matches as nx_int) + (n_out * NX_MATCH_RESULT_BYTES)) as *MatchResult
246 r.entry_id = e.id
247 r.delta_q4 = delta
248 let ppm_q1: nx_int = (abs_d * NX_MAGIC_10000000) / mz_q4
249 r.delta_ppm_q1 = ppm_q1
250 // C7.3: confidence score 0..100.
251 // start at 100
252 // subtract abs(delta_ppm) (1 ppm error = 1 point lost)
253 // if RT calibrated AND observed -> +10 bonus
254 // (if RT off, the match was filtered out so we don't see it here)
255 var conf: nx_int = 100 - (ppm_q1 / 10)
256 if e.ref_rt_q3 > 0 {
257 if rt_q3 > 0 {
258 conf = conf + 10
259 if conf > 100 { conf = 100 }
260 }
261 }
262 if conf < 0 { conf = 0 }
263 r.confidence_pct = conf
264 n_out = n_out + 1
265 }
266 }
267 i = i + 1
268 }
269 return n_out
270}
271
272// =================================================================
273// Look up entries whose [M+H]+ matches the observed m/z within
274// ppm tolerance. Writes match records to out_matches (caller-
275// allocated), returns count.
276//
277// ppm conversion: tol_q4 = (ppm * mz_q4) / 1000000.
278// For 5 ppm at m/z 280 -> 14 Q4 units (~0.0014 Da).
279// =================================================================
280func nx_chem_adulterant_db_lookup_mh_plus(db: *AdulterantDB, mz_q4: nx_int, ppm_tol: nx_int, out_matches: *MatchResult, max_out: nx_int) -> nx_int {
281 let tol_q4: nx_int = (ppm_tol * mz_q4) / NX_MAGIC_1000000
282 var n_out: nx_int = 0
283 var i: nx_int = 0
284 while i < db.n {
285 if n_out >= max_out { return n_out }
286 let e: *AdulterantEntry = ((db.entries as nx_int) + (i * NX_ADULTERANT_ENTRY_BYTES)) as *AdulterantEntry
287 var delta: nx_int = mz_q4 - e.mh_plus_q4
288 var abs_d: nx_int = delta
289 if abs_d < 0 { abs_d = 0 - abs_d }
290 if abs_d <= tol_q4 {
291 let r: *MatchResult = ((out_matches as nx_int) + (n_out * NX_MATCH_RESULT_BYTES)) as *MatchResult
292 r.entry_id = e.id
293 r.delta_q4 = delta
294 let ppm_q1: nx_int = (abs_d * NX_MAGIC_10000000) / mz_q4
295 r.delta_ppm_q1 = ppm_q1
296 // C7.3: mass-only lookup has no RT info so confidence is
297 // base 100 - ppm only (no bonus)
298 var conf2: nx_int = 100 - (ppm_q1 / 10)
299 if conf2 < 0 { conf2 = 0 }
300 r.confidence_pct = conf2
301 n_out = n_out + 1
302 }
303 i = i + 1
304 }
305 return n_out
306}
307
308// =================================================================
309// Return display name for compound id (string literal).
310// id 0..7 valid; out-of-range returns "unknown".
311// =================================================================
312func nx_chem_adulterant_name(id: nx_int) -> *u8 {
313 if id == 0 { return "ephedrine" as *u8 }
314 if id == 1 { return "DMAA (1,3-dimethylamylamine)" as *u8 }
315 if id == 2 { return "DMBA (1,3-dimethylbutylamine)" as *u8 }
316 if id == 3 { return "DMHA (octodrine)" as *u8 }
317 if id == 4 { return "sibutramine" as *u8 }
318 if id == 5 { return "caffeine" as *u8 }
319 if id == 6 { return "higenamine" as *u8 }
320 if id == 7 { return "yohimbine" as *u8 }
321 if id == 8 { return "theobromine" as *u8 }
322 if id == 9 { return "synephrine (p-octopamine N-methyl)" as *u8 }
323 if id == 10 { return "octopamine" as *u8 }
324 if id == 11 { return "phenibut" as *u8 }
325 if id == 12 { return "N-methyltyramine" as *u8 }
326 if id == 13 { return "amphetamine" as *u8 }
327 if id == 14 { return "methamphetamine" as *u8 }
328 if id == 15 { return "BZP (benzylpiperazine)" as *u8 }
329 if id == 16 { return "methylsynephrine" as *u8 }
330 if id == 17 { return "piracetam" as *u8 }
331 if id == 18 { return "phenylpiracetam" as *u8 }
332 if id == 19 { return "GBL (gamma-butyrolactone)" as *u8 }
333 if id == 20 { return "mephedrone (4-MMC)" as *u8 }
334 if id == 21 { return "salbutamol (albuterol)" as *u8 }
335 if id == 22 { return "clenbuterol" as *u8 }
336 if id == 23 { return "2C-B (4-bromo-DMPEA)" as *u8 }
337 if id == 24 { return "terbutaline" as *u8 }
338 if id == 25 { return "methylphenidate (Ritalin)" as *u8 }
339 if id == 26 { return "methcathinone" as *u8 }
340 if id == 27 { return "sildenafil (Viagra)" as *u8 }
341 if id == 28 { return "creatine" as *u8 }
342 if id == 29 { return "beta-alanine" as *u8 }
343 if id == 30 { return "L-arginine" as *u8 }
344 if id == 31 { return "taurine" as *u8 }
345 if id == 32 { return "glycine" as *u8 }
346 return "unknown" as *u8
347}
348
349// =================================================================
350// Return regulatory citation string for compound id (the published
351// authority document analyst can cite for the BANNED/RESTRICTED
352// status). Provides legal defensibility for lab decisions.
353// =================================================================
354func nx_chem_adulterant_citation(id: nx_int) -> *u8 {
355 if id == 0 { return "DSHEA + 21 CFR 119.1 (ephedrine alkaloids final rule 2004)" as *u8 }
356 if id == 1 { return "FDA 2013 Public Health Letter: 1,3-dimethylamylamine" as *u8 }
357 if id == 2 { return "FDA 2014 Warning Letters: 1,3-dimethylbutylamine" as *u8 }
358 if id == 3 { return "FDA 2019 Public Health Caution: octodrine/DMHA" as *u8 }
359 if id == 4 { return "FDA 2010 Market Withdrawal: sibutramine cardiac risk" as *u8 }
360 if id == 5 { return "21 CFR 182.1180 GRAS Affirmation: caffeine" as *u8 }
361 if id == 6 { return "FDA 2017 + WADA S3 Prohibited List: higenamine" as *u8 }
362 if id == 7 { return "DSHEA + EU restricted-substance lists: yohimbine" as *u8 }
363 if id == 8 { return "21 CFR 184.1640 GRAS: theobromine" as *u8 }
364 if id == 9 { return "FDA Warning + WADA S6: synephrine in bitter orange" as *u8 }
365 if id == 10 { return "WADA S6 Prohibited List: octopamine (in-competition)" as *u8 }
366 if id == 11 { return "FDA 2019 Import Alert IA66-41: phenibut not legal" as *u8 }
367 if id == 12 { return "WADA S6 Prohibited List: N-methyltyramine" as *u8 }
368 if id == 13 { return "DEA Schedule II + 21 USC 802: amphetamine" as *u8 }
369 if id == 14 { return "DEA Schedule II + FDA Public Health Alert: methamphetamine" as *u8 }
370 if id == 15 { return "DEA Schedule I + FDA 2008 supplement-spike action: BZP" as *u8 }
371 if id == 16 { return "FDA Warning 2016 + WADA S6: methylsynephrine" as *u8 }
372 if id == 17 { return "FDA 2018: piracetam not lawful dietary supplement ingredient" as *u8 }
373 if id == 18 { return "WADA S6 + FDA non-supplement: phenylpiracetam" as *u8 }
374 if id == 19 { return "DEA Schedule I precursor (List I): gamma-butyrolactone" as *u8 }
375 if id == 20 { return "DEA Schedule I + UK Class B 2010: mephedrone" as *u8 }
376 if id == 21 { return "FDA Rx-only + WADA S3 (inhalation-only): salbutamol" as *u8 }
377 if id == 22 { return "FDA prohibited livestock + WADA S3 banned: clenbuterol" as *u8 }
378 if id == 23 { return "DEA Schedule I via Federal Analog Act: 2C-B" as *u8 }
379 if id == 24 { return "FDA Rx + WADA S3 prohibited (b2-agonist): terbutaline" as *u8 }
380 if id == 25 { return "DEA Schedule II + FDA Rx-only: methylphenidate" as *u8 }
381 if id == 26 { return "DEA Schedule I + cathinone analog act: methcathinone" as *u8 }
382 if id == 27 { return "FDA Rx-only + tainted-supplement #1 ED adulterant: sildenafil" as *u8 }
383 if id == 28 { return "21 CFR 184.1 GRAS: creatine (common pre-workout)" as *u8 }
384 if id == 29 { return "FDA GRAS: beta-alanine (common pre-workout)" as *u8 }
385 if id == 30 { return "FDA GRAS: L-arginine (essential amino acid)" as *u8 }
386 if id == 31 { return "FDA GRAS: taurine (common energy-drink ingredient)" as *u8 }
387 if id == 32 { return "FDA GRAS: glycine (essential amino acid)" as *u8 }
388 return "no citation on file" as *u8
389}
390
391// =================================================================
392// Return regulatory status display string.
393// =================================================================
394func nx_chem_regulatory_str(reg: nx_int) -> *u8 {
395 if reg == NX_REG_APPROVED { return "APPROVED" as *u8 }
396 if reg == NX_REG_RESTRICTED { return "RESTRICTED" as *u8 }
397 if reg == NX_REG_BANNED { return "BANNED" as *u8 }
398 if reg == NX_REG_RX_ONLY { return "RX-ONLY" as *u8 }
399 return "UNKNOWN" as *u8
400}
401
402// =================================================================
403// Build seed SMILES bytes for a given compound id, into provided
404// buffer. Returns length. -1 if id unknown.
405//
406// Hardcoded ASCII because NishiLang doesn't yet have a literal-byte-
407// array shortcut. Eight realistic adulterant SMILES below.
408// =================================================================
409func nx_chem_adulterant_seed_smiles(id: nx_int, buf: *u8) -> nx_int {
410 if id == 0 {
411 // ephedrine: CC(NC)C(O)c1ccccc1 (real ephedrine C10H15NO,
412 // with N-methyl). Mono = 165.115, [M+H]+ = 166.123.
413 buf[0] = 0x43; buf[1] = 0x43; buf[2] = 0x28; buf[3] = 0x4E
414 buf[4] = 0x43; buf[5] = 0x29
415 buf[6] = 0x43; buf[7] = 0x28; buf[8] = 0x4F; buf[9] = 0x29
416 buf[10] = 0x63; buf[11] = 0x31
417 buf[12] = 0x63; buf[13] = 0x63; buf[14] = 0x63; buf[15] = 0x63; buf[16] = 0x63
418 buf[17] = 0x31
419 return 18
420 }
421 if id == 1 {
422 // DMAA (1,3-dimethylamylamine / 4-methylhexan-2-amine): C7H17N
423 // [M+H]+ = 116.1435. SMILES: CC(N)CC(C)CC
424 // Substrate-honesty fix (prior SMILES CCCC(C)CN gave C6H15N
425 // = DMBA formula 102.13 not DMAA's true 116.14).
426 buf[0] = 0x43; buf[1] = 0x43
427 buf[2] = 0x28; buf[3] = 0x4E; buf[4] = 0x29
428 buf[5] = 0x43; buf[6] = 0x43
429 buf[7] = 0x28; buf[8] = 0x43; buf[9] = 0x29
430 buf[10] = 0x43; buf[11] = 0x43
431 return 12
432 }
433 if id == 2 {
434 // DMBA (1,3-dimethylbutylamine / 4-methylpentan-2-amine): C6H15N
435 // [M+H]+ = 102.1278. SMILES: CC(N)CC(C)C
436 buf[0] = 0x43; buf[1] = 0x43
437 buf[2] = 0x28; buf[3] = 0x4E; buf[4] = 0x29
438 buf[5] = 0x43; buf[6] = 0x43
439 buf[7] = 0x28; buf[8] = 0x43; buf[9] = 0x29
440 buf[10] = 0x43
441 return 11
442 }
443 if id == 3 {
444 // DMHA / octodrine (1,5-dimethylhexylamine / 6-methylheptan-2-amine):
445 // C8H19N, [M+H]+ = 130.1591. SMILES: CC(N)CCCC(C)C
446 buf[0] = 0x43; buf[1] = 0x43
447 buf[2] = 0x28; buf[3] = 0x4E; buf[4] = 0x29
448 buf[5] = 0x43; buf[6] = 0x43; buf[7] = 0x43; buf[8] = 0x43
449 buf[9] = 0x28; buf[10] = 0x43; buf[11] = 0x29
450 buf[12] = 0x43
451 return 13
452 }
453 if id == 4 {
454 // sibutramine: CCC(C)C(N(C)C)C1(c2ccc(Cl)cc2)CCC1
455 // Real sibutramine C17H26ClN -- 3-methyl branch on butane chain,
456 // N,N-dimethyl group, 1-(4-chlorophenyl)cyclobutyl.
457 // Mono = 279.176, [M+H]+ = 280.184.
458 buf[0] = 0x43; buf[1] = 0x43; buf[2] = 0x43
459 buf[3] = 0x28; buf[4] = 0x43; buf[5] = 0x29
460 buf[6] = 0x43; buf[7] = 0x28; buf[8] = 0x4E; buf[9] = 0x28
461 buf[10] = 0x43; buf[11] = 0x29; buf[12] = 0x43; buf[13] = 0x29
462 buf[14] = 0x43; buf[15] = 0x31; buf[16] = 0x28
463 buf[17] = 0x63; buf[18] = 0x32; buf[19] = 0x63; buf[20] = 0x63
464 buf[21] = 0x63; buf[22] = 0x28
465 buf[23] = 0x43; buf[24] = 0x6C; buf[25] = 0x29
466 buf[26] = 0x63; buf[27] = 0x63
467 buf[28] = 0x32; buf[29] = 0x29
468 buf[30] = 0x43; buf[31] = 0x43; buf[32] = 0x43; buf[33] = 0x31
469 return 34
470 }
471 if id == 5 {
472 // caffeine: Cn1cnc2c1c(=O)n(C)c(=O)n2C
473 buf[0] = 0x43; buf[1] = 0x6E; buf[2] = 0x31
474 buf[3] = 0x63; buf[4] = 0x6E; buf[5] = 0x63; buf[6] = 0x32
475 buf[7] = 0x63; buf[8] = 0x31
476 buf[9] = 0x63; buf[10] = 0x28; buf[11] = 0x3D; buf[12] = 0x4F; buf[13] = 0x29
477 buf[14] = 0x6E; buf[15] = 0x28; buf[16] = 0x43; buf[17] = 0x29
478 buf[18] = 0x63; buf[19] = 0x28; buf[20] = 0x3D; buf[21] = 0x4F; buf[22] = 0x29
479 buf[23] = 0x6E; buf[24] = 0x32; buf[25] = 0x43
480 return 26
481 }
482 if id == 6 {
483 // higenamine (simplified core; full higenamine is
484 // tetrahydroisoquinoline alkaloid):
485 // Oc1ccc(CC(N)Cc2ccc(O)cc2)cc1
486 // C16H17NO2 -- one O short of real higenamine (C16H17NO3) but
487 // close formula for first-pass m/z screening. HONEST GAP: real
488 // higenamine needs tetrahydroisoquinoline ring system; deferred
489 // until parser/test corpus exercises that pattern.
490 buf[0] = 0x4F
491 buf[1] = 0x63; buf[2] = 0x31
492 buf[3] = 0x63; buf[4] = 0x63
493 buf[5] = 0x63; buf[6] = 0x28
494 buf[7] = 0x43; buf[8] = 0x43; buf[9] = 0x28; buf[10] = 0x4E; buf[11] = 0x29
495 buf[12] = 0x43
496 buf[13] = 0x63; buf[14] = 0x32; buf[15] = 0x63; buf[16] = 0x63
497 buf[17] = 0x63; buf[18] = 0x28; buf[19] = 0x4F; buf[20] = 0x29
498 buf[21] = 0x63; buf[22] = 0x63
499 buf[23] = 0x32; buf[24] = 0x29
500 buf[25] = 0x63; buf[26] = 0x63
501 buf[27] = 0x31
502 return 28
503 }
504 if id == 7 {
505 // yohimbine (simplified): tryptamine core
506 // c1ccc2[nH]cc(CCN)c2c1 -- the bioactive 5-substituted indole-
507 // ethylamine that's the active core of many alkaloid stimulants.
508 // C10H12N2 -- much smaller than real yohimbine (C21H26N2O3).
509 // HONEST GAP: full yohimbine is a pentacyclic alkaloid; this
510 // entry is the tryptamine-class first-pass screen only.
511 buf[0] = 0x63; buf[1] = 0x31
512 buf[2] = 0x63; buf[3] = 0x63; buf[4] = 0x63
513 buf[5] = 0x32
514 buf[6] = 0x5B; buf[7] = 0x6E; buf[8] = 0x48; buf[9] = 0x5D
515 buf[10] = 0x63; buf[11] = 0x63
516 buf[12] = 0x28; buf[13] = 0x43; buf[14] = 0x43; buf[15] = 0x4E; buf[16] = 0x29
517 buf[17] = 0x63; buf[18] = 0x32; buf[19] = 0x63; buf[20] = 0x31
518 return 21
519 }
520 if id == 8 {
521 // theobromine: Cn1cnc2c1c(=O)[nH]c(=O)n2C (C7H8N4O2)
522 // Caffeine minus one methyl (chocolate baseline; APPROVED).
523 buf[0] = 0x43; buf[1] = 0x6E; buf[2] = 0x31
524 buf[3] = 0x63; buf[4] = 0x6E; buf[5] = 0x63; buf[6] = 0x32
525 buf[7] = 0x63; buf[8] = 0x31
526 buf[9] = 0x63; buf[10] = 0x28; buf[11] = 0x3D; buf[12] = 0x4F; buf[13] = 0x29
527 buf[14] = 0x5B; buf[15] = 0x6E; buf[16] = 0x48; buf[17] = 0x5D
528 buf[18] = 0x63; buf[19] = 0x28; buf[20] = 0x3D; buf[21] = 0x4F; buf[22] = 0x29
529 buf[23] = 0x6E; buf[24] = 0x32; buf[25] = 0x43
530 return 26
531 }
532 if id == 9 {
533 // synephrine: CNCC(O)c1ccc(O)cc1 (C9H13NO2)
534 // citrus aurantium "bitter orange" alkaloid; WADA S6 restricted.
535 buf[0] = 0x43; buf[1] = 0x4E; buf[2] = 0x43
536 buf[3] = 0x43; buf[4] = 0x28; buf[5] = 0x4F; buf[6] = 0x29
537 buf[7] = 0x63; buf[8] = 0x31
538 buf[9] = 0x63; buf[10] = 0x63; buf[11] = 0x63
539 buf[12] = 0x28; buf[13] = 0x4F; buf[14] = 0x29
540 buf[15] = 0x63; buf[16] = 0x63; buf[17] = 0x31
541 return 18
542 }
543 if id == 10 {
544 // octopamine: NCC(O)c1ccc(O)cc1 (C8H11NO2)
545 // primary amine analog of synephrine; WADA S6.
546 buf[0] = 0x4E; buf[1] = 0x43
547 buf[2] = 0x43; buf[3] = 0x28; buf[4] = 0x4F; buf[5] = 0x29
548 buf[6] = 0x63; buf[7] = 0x31
549 buf[8] = 0x63; buf[9] = 0x63; buf[10] = 0x63
550 buf[11] = 0x28; buf[12] = 0x4F; buf[13] = 0x29
551 buf[14] = 0x63; buf[15] = 0x63; buf[16] = 0x31
552 return 17
553 }
554 if id == 11 {
555 // phenibut: NCC(c1ccccc1)CC(=O)O (C10H13NO2)
556 // gamma-amino-beta-phenylbutyric acid; FDA import alert
557 // IA66-41 (not legal for sale in US supplements).
558 buf[0] = 0x4E; buf[1] = 0x43
559 buf[2] = 0x43; buf[3] = 0x28
560 buf[4] = 0x63; buf[5] = 0x31; buf[6] = 0x63; buf[7] = 0x63
561 buf[8] = 0x63; buf[9] = 0x63; buf[10] = 0x63; buf[11] = 0x31
562 buf[12] = 0x29
563 buf[13] = 0x43; buf[14] = 0x43; buf[15] = 0x28
564 buf[16] = 0x3D; buf[17] = 0x4F; buf[18] = 0x29
565 buf[19] = 0x4F
566 return 20
567 }
568 if id == 12 {
569 // N-methyltyramine: CNCCc1ccc(O)cc1 (C9H13NO)
570 // bitter orange minor alkaloid; WADA S6 (Bitter Orange minor).
571 buf[0] = 0x43; buf[1] = 0x4E
572 buf[2] = 0x43; buf[3] = 0x43
573 buf[4] = 0x63; buf[5] = 0x31
574 buf[6] = 0x63; buf[7] = 0x63; buf[8] = 0x63
575 buf[9] = 0x28; buf[10] = 0x4F; buf[11] = 0x29
576 buf[12] = 0x63; buf[13] = 0x63; buf[14] = 0x31
577 return 15
578 }
579 if id == 13 {
580 // amphetamine: CC(N)Cc1ccccc1 (C9H13N)
581 // DEA Schedule II precursor; reviewed for supplement spikes.
582 buf[0] = 0x43; buf[1] = 0x43
583 buf[2] = 0x28; buf[3] = 0x4E; buf[4] = 0x29
584 buf[5] = 0x43
585 buf[6] = 0x63; buf[7] = 0x31
586 buf[8] = 0x63; buf[9] = 0x63; buf[10] = 0x63; buf[11] = 0x63; buf[12] = 0x63
587 buf[13] = 0x31
588 return 14
589 }
590 if id == 14 {
591 // methamphetamine: CC(NC)Cc1ccccc1 (C10H15N)
592 // DEA Schedule II; illegal supplement adulterant.
593 buf[0] = 0x43; buf[1] = 0x43
594 buf[2] = 0x28; buf[3] = 0x4E; buf[4] = 0x43; buf[5] = 0x29
595 buf[6] = 0x43
596 buf[7] = 0x63; buf[8] = 0x31
597 buf[9] = 0x63; buf[10] = 0x63; buf[11] = 0x63; buf[12] = 0x63; buf[13] = 0x63
598 buf[14] = 0x31
599 return 15
600 }
601 if id == 15 {
602 // benzylpiperazine BZP: c1ccc(CN2CCNCC2)cc1 (C11H16N2)
603 // DEA Schedule I; party-drug adulterant found in supplements 2008.
604 buf[0] = 0x63; buf[1] = 0x31
605 buf[2] = 0x63; buf[3] = 0x63; buf[4] = 0x63
606 buf[5] = 0x28; buf[6] = 0x43
607 buf[7] = 0x4E; buf[8] = 0x32
608 buf[9] = 0x43; buf[10] = 0x43
609 buf[11] = 0x4E
610 buf[12] = 0x43; buf[13] = 0x43
611 buf[14] = 0x32; buf[15] = 0x29
612 buf[16] = 0x63; buf[17] = 0x63; buf[18] = 0x31
613 return 19
614 }
615 if id == 16 {
616 // methylsynephrine: CN(C)CC(O)c1ccc(O)cc1 (C10H15NO2)
617 // N,N-dimethyl synephrine; WADA S6 + FDA 2016 warning.
618 buf[0] = 0x43; buf[1] = 0x4E
619 buf[2] = 0x28; buf[3] = 0x43; buf[4] = 0x29
620 buf[5] = 0x43; buf[6] = 0x43
621 buf[7] = 0x28; buf[8] = 0x4F; buf[9] = 0x29
622 buf[10] = 0x63; buf[11] = 0x31
623 buf[12] = 0x63; buf[13] = 0x63; buf[14] = 0x63
624 buf[15] = 0x28; buf[16] = 0x4F; buf[17] = 0x29
625 buf[18] = 0x63; buf[19] = 0x63; buf[20] = 0x31
626 return 21
627 }
628 if id == 17 {
629 // piracetam: NC(=O)CN1CCCC1=O (C6H10N2O2)
630 // 2-oxo-1-pyrrolidine-acetamide; FDA 2018 ruled not a lawful
631 // dietary supplement ingredient.
632 buf[0] = 0x4E
633 buf[1] = 0x43; buf[2] = 0x28; buf[3] = 0x3D; buf[4] = 0x4F; buf[5] = 0x29
634 buf[6] = 0x43
635 buf[7] = 0x4E; buf[8] = 0x31
636 buf[9] = 0x43; buf[10] = 0x43; buf[11] = 0x43; buf[12] = 0x43
637 buf[13] = 0x31; buf[14] = 0x3D; buf[15] = 0x4F
638 return 16
639 }
640 if id == 18 {
641 // phenylpiracetam: NC(=O)CN1CC(c2ccccc2)CC1=O (C12H14N2O2)
642 // 4-phenyl piracetam; WADA S6 prohibited; FDA non-supplement.
643 buf[0] = 0x4E
644 buf[1] = 0x43; buf[2] = 0x28; buf[3] = 0x3D; buf[4] = 0x4F; buf[5] = 0x29
645 buf[6] = 0x43
646 buf[7] = 0x4E; buf[8] = 0x31
647 buf[9] = 0x43; buf[10] = 0x43; buf[11] = 0x28
648 buf[12] = 0x63; buf[13] = 0x32
649 buf[14] = 0x63; buf[15] = 0x63; buf[16] = 0x63; buf[17] = 0x63; buf[18] = 0x63
650 buf[19] = 0x32; buf[20] = 0x29
651 buf[21] = 0x43; buf[22] = 0x43
652 buf[23] = 0x31; buf[24] = 0x3D; buf[25] = 0x4F
653 return 26
654 }
655 if id == 19 {
656 // gamma-butyrolactone (GBL): O=C1OCCC1 (C4H6O2)
657 // GHB precursor; DEA List I. Found in "cleaner" supplement spikes.
658 buf[0] = 0x4F; buf[1] = 0x3D
659 buf[2] = 0x43; buf[3] = 0x31
660 buf[4] = 0x4F
661 buf[5] = 0x43; buf[6] = 0x43; buf[7] = 0x43
662 buf[8] = 0x31
663 return 9
664 }
665 if id == 20 {
666 // mephedrone (4-MMC): CC(NC)C(=O)c1ccc(C)cc1 (C11H15NO)
667 // 4-methylmethcathinone; DEA Schedule I, UK Class B 2010.
668 buf[0] = 0x43; buf[1] = 0x43
669 buf[2] = 0x28; buf[3] = 0x4E; buf[4] = 0x43; buf[5] = 0x29
670 buf[6] = 0x43; buf[7] = 0x28; buf[8] = 0x3D; buf[9] = 0x4F; buf[10] = 0x29
671 buf[11] = 0x63; buf[12] = 0x31
672 buf[13] = 0x63; buf[14] = 0x63; buf[15] = 0x63
673 buf[16] = 0x28; buf[17] = 0x43; buf[18] = 0x29
674 buf[19] = 0x63; buf[20] = 0x63; buf[21] = 0x31
675 return 22
676 }
677 if id == 21 {
678 // salbutamol (albuterol): CC(C)(C)NCC(O)c1ccc(O)c(CO)c1 (C13H21NO3)
679 // β2-agonist asthma drug; WADA S3 (inhalation only) + FDA Rx.
680 buf[0] = 0x43; buf[1] = 0x43
681 buf[2] = 0x28; buf[3] = 0x43; buf[4] = 0x29
682 buf[5] = 0x28; buf[6] = 0x43; buf[7] = 0x29
683 buf[8] = 0x4E
684 buf[9] = 0x43; buf[10] = 0x43; buf[11] = 0x28; buf[12] = 0x4F; buf[13] = 0x29
685 buf[14] = 0x63; buf[15] = 0x31
686 buf[16] = 0x63; buf[17] = 0x63; buf[18] = 0x63
687 buf[19] = 0x28; buf[20] = 0x4F; buf[21] = 0x29
688 buf[22] = 0x63
689 buf[23] = 0x28; buf[24] = 0x43; buf[25] = 0x4F; buf[26] = 0x29
690 buf[27] = 0x63; buf[28] = 0x31
691 return 29
692 }
693 if id == 22 {
694 // clenbuterol: CC(C)(C)NCC(O)c1cc(Cl)c(N)c(Cl)c1 (C12H18Cl2N2O)
695 // β2-agonist livestock-banned; WADA S3 prohibited (dual Cl = sig 3 multi).
696 buf[0] = 0x43; buf[1] = 0x43
697 buf[2] = 0x28; buf[3] = 0x43; buf[4] = 0x29
698 buf[5] = 0x28; buf[6] = 0x43; buf[7] = 0x29
699 buf[8] = 0x4E
700 buf[9] = 0x43; buf[10] = 0x43; buf[11] = 0x28; buf[12] = 0x4F; buf[13] = 0x29
701 buf[14] = 0x63; buf[15] = 0x31
702 buf[16] = 0x63; buf[17] = 0x63
703 buf[18] = 0x28; buf[19] = 0x43; buf[20] = 0x6C; buf[21] = 0x29
704 buf[22] = 0x63
705 buf[23] = 0x28; buf[24] = 0x4E; buf[25] = 0x29
706 buf[26] = 0x63
707 buf[27] = 0x28; buf[28] = 0x43; buf[29] = 0x6C; buf[30] = 0x29
708 buf[31] = 0x63; buf[32] = 0x31
709 return 33
710 }
711 if id == 23 {
712 // 2C-B: NCCc1cc(Br)cc(OC)c1OC (C10H14BrNO2)
713 // 4-bromo-2,5-dimethoxyphenethylamine; DEA Schedule I (analog act).
714 buf[0] = 0x4E; buf[1] = 0x43; buf[2] = 0x43
715 buf[3] = 0x63; buf[4] = 0x31
716 buf[5] = 0x63; buf[6] = 0x63
717 buf[7] = 0x28; buf[8] = 0x42; buf[9] = 0x72; buf[10] = 0x29
718 buf[11] = 0x63; buf[12] = 0x63
719 buf[13] = 0x28; buf[14] = 0x4F; buf[15] = 0x43; buf[16] = 0x29
720 buf[17] = 0x63; buf[18] = 0x31
721 buf[19] = 0x4F; buf[20] = 0x43
722 return 21
723 }
724 if id == 24 {
725 // terbutaline: CC(C)(C)NCC(O)c1cc(O)cc(O)c1 (C12H19NO3)
726 // beta-2 agonist asthma drug; WADA S3 prohibited.
727 buf[0] = 0x43; buf[1] = 0x43
728 buf[2] = 0x28; buf[3] = 0x43; buf[4] = 0x29
729 buf[5] = 0x28; buf[6] = 0x43; buf[7] = 0x29
730 buf[8] = 0x4E
731 buf[9] = 0x43; buf[10] = 0x43; buf[11] = 0x28; buf[12] = 0x4F; buf[13] = 0x29
732 buf[14] = 0x63; buf[15] = 0x31
733 buf[16] = 0x63; buf[17] = 0x63
734 buf[18] = 0x28; buf[19] = 0x4F; buf[20] = 0x29
735 buf[21] = 0x63; buf[22] = 0x63
736 buf[23] = 0x28; buf[24] = 0x4F; buf[25] = 0x29
737 buf[26] = 0x63; buf[27] = 0x31
738 return 28
739 }
740 if id == 25 {
741 // methylphenidate (Ritalin): COC(=O)C(c1ccccc1)C1CCCCN1 (C14H19NO2)
742 // alpha-phenyl-2-piperidineacetate methyl ester; DEA Schedule II.
743 buf[0] = 0x43; buf[1] = 0x4F; buf[2] = 0x43
744 buf[3] = 0x28; buf[4] = 0x3D; buf[5] = 0x4F; buf[6] = 0x29
745 buf[7] = 0x43
746 buf[8] = 0x28
747 buf[9] = 0x63; buf[10] = 0x31
748 buf[11] = 0x63; buf[12] = 0x63; buf[13] = 0x63; buf[14] = 0x63; buf[15] = 0x63
749 buf[16] = 0x31
750 buf[17] = 0x29
751 buf[18] = 0x43; buf[19] = 0x31
752 buf[20] = 0x43; buf[21] = 0x43; buf[22] = 0x43; buf[23] = 0x43
753 buf[24] = 0x4E; buf[25] = 0x31
754 return 26
755 }
756 if id == 26 {
757 // methcathinone: CC(NC)C(=O)c1ccccc1 (C10H13NO)
758 // beta-keto-methamphetamine; DEA Schedule I cathinone class.
759 buf[0] = 0x43; buf[1] = 0x43
760 buf[2] = 0x28; buf[3] = 0x4E; buf[4] = 0x43; buf[5] = 0x29
761 buf[6] = 0x43; buf[7] = 0x28; buf[8] = 0x3D; buf[9] = 0x4F; buf[10] = 0x29
762 buf[11] = 0x63; buf[12] = 0x31
763 buf[13] = 0x63; buf[14] = 0x63; buf[15] = 0x63; buf[16] = 0x63; buf[17] = 0x63
764 buf[18] = 0x31
765 return 19
766 }
767 if id == 28 {
768 // creatine: CN(CC(=O)O)C(=N)N (C4H9N3O2) [M+H]+ 132.0773
769 // N-methylguanidinoacetic acid; pre-workout baseline.
770 buf[0] = 0x43; buf[1] = 0x4E
771 buf[2] = 0x28
772 buf[3] = 0x43; buf[4] = 0x43
773 buf[5] = 0x28; buf[6] = 0x3D; buf[7] = 0x4F; buf[8] = 0x29
774 buf[9] = 0x4F
775 buf[10] = 0x29
776 buf[11] = 0x43
777 buf[12] = 0x28; buf[13] = 0x3D; buf[14] = 0x4E; buf[15] = 0x29
778 buf[16] = 0x4E
779 return 17
780 }
781 if id == 29 {
782 // beta-alanine: NCCC(=O)O (C3H7NO2) [M+H]+ 90.0550
783 // 3-aminopropanoic acid; pre-workout baseline.
784 buf[0] = 0x4E; buf[1] = 0x43; buf[2] = 0x43; buf[3] = 0x43
785 buf[4] = 0x28; buf[5] = 0x3D; buf[6] = 0x4F; buf[7] = 0x29
786 buf[8] = 0x4F
787 return 9
788 }
789 if id == 30 {
790 // L-arginine: NC(=N)NCCCC(N)C(=O)O (C6H14N4O2) [M+H]+ 175.1190
791 // essential amino acid; supplement baseline.
792 buf[0] = 0x4E
793 buf[1] = 0x43; buf[2] = 0x28; buf[3] = 0x3D; buf[4] = 0x4E; buf[5] = 0x29
794 buf[6] = 0x4E
795 buf[7] = 0x43; buf[8] = 0x43; buf[9] = 0x43; buf[10] = 0x43
796 buf[11] = 0x28; buf[12] = 0x4E; buf[13] = 0x29
797 buf[14] = 0x43; buf[15] = 0x28; buf[16] = 0x3D; buf[17] = 0x4F; buf[18] = 0x29
798 buf[19] = 0x4F
799 return 20
800 }
801 if id == 31 {
802 // taurine: NCCS(=O)(=O)O (C2H7NO3S) [M+H]+ 126.0219
803 // 2-aminoethanesulfonic acid; energy drink baseline.
804 buf[0] = 0x4E; buf[1] = 0x43; buf[2] = 0x43
805 buf[3] = 0x53
806 buf[4] = 0x28; buf[5] = 0x3D; buf[6] = 0x4F; buf[7] = 0x29
807 buf[8] = 0x28; buf[9] = 0x3D; buf[10] = 0x4F; buf[11] = 0x29
808 buf[12] = 0x4F
809 return 13
810 }
811 if id == 32 {
812 // glycine: NCC(=O)O (C2H5NO2) [M+H]+ 76.0393
813 // simplest amino acid; supplement baseline.
814 buf[0] = 0x4E; buf[1] = 0x43
815 buf[2] = 0x43; buf[3] = 0x28; buf[4] = 0x3D; buf[5] = 0x4F; buf[6] = 0x29
816 buf[7] = 0x4F
817 return 8
818 }
819 if id == 27 {
820 // sildenafil (Viagra): C22H30N6O4S, [M+H]+ 475.2127.
821 // SMILES: CCCc1nn(C)c2c(=O)[nH]c(-c3cc(S(=O)(=O)N4CCN(C)CC4)ccc3OCC)nc12
822 // Pyrazolopyrimidinone + propoxyphenyl sulfonamide + piperazine.
823 // The #1 ED adulterant found in FDA "natural" supplement raids.
824 buf[0]=0x43; buf[1]=0x43; buf[2]=0x43
825 buf[3]=0x63; buf[4]=0x31
826 buf[5]=0x6E; buf[6]=0x6E
827 buf[7]=0x28; buf[8]=0x43; buf[9]=0x29
828 buf[10]=0x63; buf[11]=0x32
829 buf[12]=0x63
830 buf[13]=0x28; buf[14]=0x3D; buf[15]=0x4F; buf[16]=0x29
831 buf[17]=0x5B; buf[18]=0x6E; buf[19]=0x48; buf[20]=0x5D
832 buf[21]=0x63
833 buf[22]=0x28; buf[23]=0x2D
834 buf[24]=0x63; buf[25]=0x33
835 buf[26]=0x63; buf[27]=0x63
836 buf[28]=0x28; buf[29]=0x53
837 buf[30]=0x28; buf[31]=0x3D; buf[32]=0x4F; buf[33]=0x29
838 buf[34]=0x28; buf[35]=0x3D; buf[36]=0x4F; buf[37]=0x29
839 buf[38]=0x4E; buf[39]=0x34
840 buf[40]=0x43; buf[41]=0x43
841 buf[42]=0x4E
842 buf[43]=0x28; buf[44]=0x43; buf[45]=0x29
843 buf[46]=0x43; buf[47]=0x43; buf[48]=0x34
844 buf[49]=0x29
845 buf[50]=0x63; buf[51]=0x63; buf[52]=0x63; buf[53]=0x33
846 buf[54]=0x4F; buf[55]=0x43; buf[56]=0x43
847 buf[57]=0x29
848 buf[58]=0x6E; buf[59]=0x63; buf[60]=0x31; buf[61]=0x32
849 return 62
850 }
851 return -1
852}
853
854// =================================================================
855// Build complete seed adulterant DB with all 17 reference compounds.
856// =================================================================
857func nx_chem_adulterant_db_seed() -> *AdulterantDB {
858 let ad: *AtomicData = nx_chem_atomic_data_table_118()
859 let db: *AdulterantDB = nx_chem_adulterant_db_new(64)
860 let buf: *u8 = sys_mmap(64)
861 var i: nx_int = 0
862 while i < 33 {
863 // Clear buffer
864 var bi: nx_int = 0
865 while bi < 64 { buf[bi] = 0; bi = bi + 1 }
866 let n: nx_int = nx_chem_adulterant_seed_smiles(i, buf)
867 if n > 0 {
868 var reg: nx_int = NX_REG_UNKNOWN
869 if i == 0 { reg = NX_REG_RESTRICTED } // ephedrine
870 if i == 1 { reg = NX_REG_BANNED } // DMAA
871 if i == 2 { reg = NX_REG_BANNED } // DMBA
872 if i == 3 { reg = NX_REG_BANNED } // DMHA
873 if i == 4 { reg = NX_REG_BANNED } // sibutramine
874 if i == 5 { reg = NX_REG_APPROVED } // caffeine (baseline)
875 if i == 6 { reg = NX_REG_BANNED } // higenamine
876 if i == 7 { reg = NX_REG_RESTRICTED } // yohimbine
877 if i == 8 { reg = NX_REG_APPROVED } // theobromine (chocolate)
878 if i == 9 { reg = NX_REG_RESTRICTED } // synephrine
879 if i == 10 { reg = NX_REG_RESTRICTED } // octopamine
880 if i == 11 { reg = NX_REG_BANNED } // phenibut (US import alert)
881 if i == 12 { reg = NX_REG_RESTRICTED } // N-methyltyramine
882 if i == 13 { reg = NX_REG_BANNED } // amphetamine (DEA II)
883 if i == 14 { reg = NX_REG_BANNED } // methamphetamine (DEA II)
884 if i == 15 { reg = NX_REG_BANNED } // BZP (DEA I)
885 if i == 16 { reg = NX_REG_BANNED } // methylsynephrine (FDA + WADA)
886 if i == 17 { reg = NX_REG_BANNED } // piracetam (FDA NX_MAGIC_2018)
887 if i == 18 { reg = NX_REG_BANNED } // phenylpiracetam (WADA + FDA)
888 if i == 19 { reg = NX_REG_BANNED } // GBL (DEA List I precursor)
889 if i == 20 { reg = NX_REG_BANNED } // mephedrone (DEA Schedule I)
890 if i == 21 { reg = NX_REG_RX_ONLY } // salbutamol (Rx + WADA)
891 if i == 22 { reg = NX_REG_BANNED } // clenbuterol (FDA + WADA)
892 if i == 23 { reg = NX_REG_BANNED } // 2C-B (DEA Schedule I)
893 if i == 24 { reg = NX_REG_RX_ONLY } // terbutaline (Rx + WADA)
894 if i == 25 { reg = NX_REG_RX_ONLY } // methylphenidate (DEA II + Rx)
895 if i == 26 { reg = NX_REG_BANNED } // methcathinone (DEA I)
896 if i == 27 { reg = NX_REG_RX_ONLY } // sildenafil (FDA Rx + tainted-supplement #1)
897 if i == 28 { reg = NX_REG_APPROVED } // creatine (GRAS)
898 if i == 29 { reg = NX_REG_APPROVED } // beta-alanine (GRAS)
899 if i == 30 { reg = NX_REG_APPROVED } // L-arginine (GRAS)
900 if i == 31 { reg = NX_REG_APPROVED } // taurine (GRAS)
901 if i == 32 { reg = NX_REG_APPROVED } // glycine (GRAS)
902 let added: nx_int = nx_chem_adulterant_db_add(db, i, buf, n, reg, ad)
903 // C4.3: bind name + citation pointers to hardcoded seed strings.
904 // Only bind if db_add succeeded -- otherwise we'd overwrite the
905 // PREVIOUS entry's name pointer (bug-class caught 2026-05-20
906 // when DB cap was too small for the 33rd compound).
907 if added >= 0 {
908 let e: *AdulterantEntry = ((db.entries as nx_int) + ((db.n - 1) * NX_ADULTERANT_ENTRY_BYTES)) as *AdulterantEntry
909 e.name_ptr = nx_chem_adulterant_name(i)
910 e.citation_ptr = nx_chem_adulterant_citation(i)
911 }
912 }
913 i = i + 1
914 }
915 return db
916}
917
918// =================================================================
919// C7.1: Load reference retention times from a CSV file into the DB.
920//
921// File format (whitespace-separated, '#' comments):
922// # compound_id ref_rt_seconds tolerance_seconds
923// 4 740.0 30.0
924// 1 45.0 15.0
925// 0 89.5 25.0
926//
927// Internally converts seconds to Q3 milliseconds before db_set_rt.
928// Returns count of calibration rows loaded (0 if file missing).
929//
930// Composes C5.1 sys_read_file + C5.0 decimal parser + C7.0 db_set_rt.
931// =================================================================
932func nx_chem_load_rt_calibration(db: *AdulterantDB, path: *u8) -> nx_int {
933 var len_out: i64 = 0
934 let buf: *u8 = sys_read_file(path, &len_out)
935 if (buf as nx_int) == 0 { return 0 }
936 if len_out <= 0 { return 0 }
937 let len: nx_int = len_out as nx_int
938 var n_loaded: nx_int = 0
939 var i: nx_int = 0
940 while i < len {
941 // Skip whitespace + newlines
942 var done_ws: nx_int = 0
943 while done_ws == 0 {
944 if i >= len { done_ws = 1 }
945 else {
946 let c: nx_int = buf[i] as nx_int
947 if c == 32 { i = i + 1 }
948 else { if c == 9 { i = i + 1 }
949 else { if c == 10 { i = i + 1 }
950 else { if c == 13 { i = i + 1 }
951 else { done_ws = 1 } } } }
952 }
953 }
954 if i >= len { return n_loaded }
955 // '#' comment to EOL
956 if (buf[i] as nx_int) == 35 {
957 var done_cmt: nx_int = 0
958 while done_cmt == 0 {
959 if i >= len { done_cmt = 1 }
960 else {
961 if nx_chem_is_lineterm(buf[i] as nx_int) == 1 { done_cmt = 1 }
962 else { i = i + 1 }
963 }
964 }
965 }
966 else {
967 // Parse compound_id (Q4 internally, but we only want whole part)
968 var id_q4: nx_int = 0
969 let n1: nx_int = nx_chem_parse_decimal_q4(buf, i, len, &id_q4)
970 if n1 == 0 {
971 var done_skip: nx_int = 0
972 while done_skip == 0 {
973 if i >= len { done_skip = 1 }
974 else {
975 if nx_chem_is_lineterm(buf[i] as nx_int) == 1 { done_skip = 1 }
976 else { i = i + 1 }
977 }
978 }
979 }
980 else {
981 i = i + n1
982 let id: nx_int = id_q4 / NX_MAGIC_10000 // strip Q4 fractional
983 // Skip horizontal whitespace
984 var done_h1: nx_int = 0
985 while done_h1 == 0 {
986 if i >= len { done_h1 = 1 }
987 else {
988 if nx_chem_is_hspace(buf[i] as nx_int) == 1 { i = i + 1 }
989 else { done_h1 = 1 } }
990 }
991 // Parse ref_rt_seconds (Q4)
992 var rt_q4: nx_int = 0
993 let n2: nx_int = nx_chem_parse_decimal_q4(buf, i, len, &rt_q4)
994 if n2 > 0 { i = i + n2 }
995 // Skip horizontal whitespace
996 var done_h2: nx_int = 0
997 while done_h2 == 0 {
998 if i >= len { done_h2 = 1 }
999 else {
1000 if nx_chem_is_hspace(buf[i] as nx_int) == 1 { i = i + 1 }
1001 else { done_h2 = 1 } }
1002 }
1003 // Parse tol_seconds (Q4) optional
1004 var tol_q4: nx_int = NX_MAGIC_300000 // 30 sec default in Q4 = 30 * NX_MAGIC_10000
1005 if i < len {
1006 if nx_chem_is_lineterm(buf[i] as nx_int) == 0 {
1007 let n3: nx_int = nx_chem_parse_decimal_q4(buf, i, len, &tol_q4)
1008 if n3 > 0 { i = i + n3 }
1009 }
1010 }
1011 // Convert seconds to Q3 ms: rt_seconds_q4 / 10 = rt_q3_ms
1012 let rt_q3: nx_int = rt_q4 / 10
1013 let tol_q3: nx_int = tol_q4 / 10
1014 let rc: nx_int = nx_chem_adulterant_db_set_rt(db, id, rt_q3, tol_q3)
1015 if rc == 0 { n_loaded = n_loaded + 1 }
1016 // Skip to EOL
1017 var done_eol: nx_int = 0
1018 while done_eol == 0 {
1019 if i >= len { done_eol = 1 }
1020 else {
1021 if nx_chem_is_lineterm(buf[i] as nx_int) == 1 { done_eol = 1 }
1022 else { i = i + 1 }
1023 }
1024 }
1025 }
1026 }
1027 }
1028 return n_loaded
1029}
1030
1031// =================================================================
1032// C4.3: Load additional compounds from a pipe-separated text file
1033// into an existing DB. Format (one compound per line, '#' comments):
1034//
1035// # id|name|SMILES|reg_code|citation
1036// 100|nicotine|CN1CCCC1c1cccnc1|2|WADA S6 in-competition + FDA
1037// 101|cocaine|CN1...|3|DEA Schedule II
1038// ...
1039//
1040// reg_code: 0=UNKNOWN, 1=APPROVED, 2=RESTRICTED, 3=BANNED, 4=RX_ONLY
1041//
1042// Each line allocates fresh memory for name + citation strings.
1043// SMILES is parsed at load time; if parse fails, line skipped.
1044// Returns count of compounds loaded (0 if file missing/empty).
1045//
1046// Composes C5.1 sys_read_file + parser logic.
1047// =================================================================
1048func nx_chem_load_compounds_from_file(db: *AdulterantDB, path: *u8) -> nx_int {
1049 var len_out: i64 = 0
1050 let buf: *u8 = sys_read_file(path, &len_out)
1051 if (buf as nx_int) == 0 { return 0 }
1052 if len_out <= 0 { return 0 }
1053 let len: nx_int = len_out as nx_int
1054 let ad: *AtomicData = nx_chem_atomic_data_table_118()
1055 var n_loaded: nx_int = 0
1056 var i: nx_int = 0
1057 while i < len {
1058 // Skip leading whitespace + newlines
1059 var done_ws: nx_int = 0
1060 while done_ws == 0 {
1061 if i >= len { done_ws = 1 }
1062 else {
1063 let c: nx_int = buf[i] as nx_int
1064 if c == 32 { i = i + 1 }
1065 else { if c == 9 { i = i + 1 }
1066 else { if c == 10 { i = i + 1 }
1067 else { if c == 13 { i = i + 1 }
1068 else { done_ws = 1 } } } }
1069 }
1070 }
1071 if i >= len { return n_loaded }
1072 // Comment line
1073 if (buf[i] as nx_int) == 35 {
1074 var done_cmt: nx_int = 0
1075 while done_cmt == 0 {
1076 if i >= len { done_cmt = 1 }
1077 else {
1078 if nx_chem_is_lineterm(buf[i] as nx_int) == 1 { done_cmt = 1 }
1079 else { i = i + 1 }
1080 }
1081 }
1082 }
1083 else {
1084 // Find line end (first newline OR EOF)
1085 var line_end: nx_int = i
1086 var done_le: nx_int = 0
1087 while done_le == 0 {
1088 if line_end >= len { done_le = 1 }
1089 else {
1090 if nx_chem_is_lineterm(buf[line_end] as nx_int) == 1 { done_le = 1 }
1091 else { line_end = line_end + 1 }
1092 }
1093 }
1094 // Single-pass parse: walk the line, tracking which field
1095 // we're in via a state counter (0=id, 1=name, 2=smiles,
1096 // 3=reg, 4=citation). Allocate string buffers as field
1097 // boundaries are reached.
1098 let name_buf: *u8 = sys_mmap(256)
1099 let smi_buf: *u8 = sys_mmap(128)
1100 let cite_buf: *u8 = sys_mmap(512)
1101 var id_val: nx_int = 0
1102 var reg_val: nx_int = 0
1103 var name_pos: nx_int = 0
1104 var smi_pos: nx_int = 0
1105 var cite_pos: nx_int = 0
1106 var state: nx_int = 0
1107 var j: nx_int = i
1108 while j < line_end {
1109 let c2: nx_int = buf[j] as nx_int
1110 if c2 == 124 { // '|'
1111 state = state + 1
1112 }
1113 else {
1114 if state == 0 {
1115 if c2 >= 48 {
1116 if c2 <= 57 {
1117 id_val = id_val * 10 + (c2 - 48)
1118 }
1119 }
1120 }
1121 if state == 1 {
1122 name_buf[name_pos] = buf[j]
1123 name_pos = name_pos + 1
1124 }
1125 if state == 2 {
1126 smi_buf[smi_pos] = buf[j]
1127 smi_pos = smi_pos + 1
1128 }
1129 if state == 3 {
1130 if c2 >= 48 {
1131 if c2 <= 57 {
1132 reg_val = reg_val * 10 + (c2 - 48)
1133 }
1134 }
1135 }
1136 if state == 4 {
1137 cite_buf[cite_pos] = buf[j]
1138 cite_pos = cite_pos + 1
1139 }
1140 }
1141 j = j + 1
1142 }
1143 if state == 4 {
1144 if smi_pos > 0 {
1145 name_buf[name_pos] = 0
1146 smi_buf[smi_pos] = 0
1147 cite_buf[cite_pos] = 0
1148 let rc: nx_int = nx_chem_adulterant_db_add_with_strings(db, id_val, smi_buf, smi_pos, reg_val, name_buf, cite_buf, ad)
1149 if rc >= 0 { n_loaded = n_loaded + 1 }
1150 }
1151 }
1152 i = line_end
1153 }
1154 }
1155 return n_loaded
1156}
1157
1158// =================================================================
1159// Render m/z in Q4 to "DDD.MMMM" decimal form on stdout.
1160// =================================================================
1161func print_q4_mass(q: nx_int) -> i64 {
1162 let whole: nx_int = q / NX_MAGIC_10000
1163 var frac: nx_int = q - (whole * NX_MAGIC_10000)
1164 if frac < 0 { frac = 0 - frac }
1165 let _a: i64 = print_i64(whole as i64)
1166 let dot: *u8 = sys_mmap(8); dot[0] = 0x2E
1167 let _b: i64 = sys_write(1, dot, 1)
1168 // Zero-pad frac to 4 digits
1169 if frac < 1000 {
1170 let z: *u8 = sys_mmap(8); z[0] = 0x30
1171 let _z1: i64 = sys_write(1, z, 1)
1172 }
1173 if frac < 100 {
1174 let z: *u8 = sys_mmap(8); z[0] = 0x30
1175 let _z2: i64 = sys_write(1, z, 1)
1176 }
1177 if frac < 10 {
1178 let z: *u8 = sys_mmap(8); z[0] = 0x30
1179 let _z3: i64 = sys_write(1, z, 1)
1180 }
1181 return print_i64(frac as i64)
1182}
1183
1184// =================================================================
1185// Print a single match record as a lab-report line.
1186// C4.3: uses entry.name_ptr (works for both seed + loaded compounds).
1187// =================================================================
1188func nx_chem_adulterant_report_match(observed_mz_q4: nx_int, db: *AdulterantDB, r: *MatchResult) -> nx_int {
1189 // r.entry_id is the compound id; find the entry that has this id
1190 var e: *AdulterantEntry = 0 as *AdulterantEntry
1191 var k: nx_int = 0
1192 while k < db.n {
1193 let ek: *AdulterantEntry = ((db.entries as nx_int) + (k * NX_ADULTERANT_ENTRY_BYTES)) as *AdulterantEntry
1194 if ek.id == r.entry_id { e = ek; k = db.n }
1195 else { k = k + 1 }
1196 }
1197 if (e as nx_int) == 0 { return -1 }
1198 let _1: i64 = print(" m/z " as *u8)
1199 let _2: i64 = print_q4_mass(observed_mz_q4)
1200 let _3: i64 = print(" -> " as *u8)
1201 let _4: i64 = print(e.name_ptr)
1202 let _5: i64 = print(" [" as *u8)
1203 let _6: i64 = print(nx_chem_regulatory_str(e.regulatory))
1204 let _7: i64 = print("] delta=" as *u8)
1205 let _8: i64 = print_i64((r.delta_ppm_q1 / 10) as i64)
1206 let _9: i64 = println(" ppm" as *u8)
1207 return 0
1208}
1209
1210// =================================================================
1211// C4.3: lookup an entry by id. Used by report emitters to access
1212// name_ptr + citation_ptr without scanning the DB at every call.
1213// Returns NULL if id not found.
1214// =================================================================
1215func nx_chem_adulterant_entry_by_id(db: *AdulterantDB, id: nx_int) -> *AdulterantEntry {
1216 var i: nx_int = 0
1217 while i < db.n {
1218 let e: *AdulterantEntry = ((db.entries as nx_int) + (i * NX_ADULTERANT_ENTRY_BYTES)) as *AdulterantEntry
1219 if e.id == id { return e }
1220 i = i + 1
1221 }
1222 return 0 as *AdulterantEntry
1223}