code wiki / (root) / nx_chem_adulterant_db.nx

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}