code wiki / (root) / nx_supplement_screen.nx

nx_supplement_screen.nx source

↩ module page · 275 lines · 10335 B

1// nx_supplement_screen.nx -- SUPPLEMENT ARC / PEPTIDE ADULTERANT rung. 2// Screens an observed LC-MS peak against a catalog of peptide actives sold 3// in supplement and "research peptide" channels, ACROSS CHARGE STATES. 4// 5// WHY THIS EXISTS -- a measured hole, not a hypothetical one. 6// nx_chem_adulterant_db screens small molecules well: it parses SMILES, 7// computes a monoisotopic mass, and matches an observed peak against 8// [M+H]+ within a ppm window. That design silently cannot see peptides: 9// 10// 1. A peptide is a SEQUENCE, not a SMILES the parser will take. 11// 2. Electrospray puts peptides on a CHARGE LADDER. BPC-157 is 12// 1418.70 Da, so [M+H]+ sits at m/z 1419.7 -- outside the m/z 13// 100-1000 window a small-molecule method typically scans. Its real 14// signals are [M+2H]2+ at 710.4 and [M+3H]3+ at 473.9. A screen that 15// computes only [M+H]+ therefore reports CLEAN on a spiked sample. 16// 17// sup_mh_plus_in_range and sup_detectable_charges_in_range make that 18// failure MECHANICAL rather than rhetorical: they compute, per compound, 19// whether the single-charge assumption would have missed it. 20// 21// MASSES ARE DERIVED, NOT TABULATED. Every catalog mass comes from 22// nx_peptide walking the actual sequence, plus an explicit terminal 23// modification delta. Nothing here is a copied number that can drift out 24// of agreement with the residue table. 25// 26// FAIL-CLOSED ON WHAT IT CANNOT DO. Peptides carrying non-proteinogenic 27// residues (ipamorelin's Aib and 2-Nal, melanotan-II's Nle) are REFUSED by 28// nx_peptide rather than approximated, so they screen as "unknown", never 29// as "absent". Extending to them needs a non-standard residue table and is 30// an honest open gap, listed below -- not quietly rounded away. 31// 32// All INTEGER. _q4 = x10^4 (mass/mz in Da), _q1 = x10 (ppm). 33// 34// Grounding (cited; researcher-groundable): 35// fda_tainted_supplements_database (adulteration channel) 36// wada_prohibited_list_s2_peptide_hormones (status flags) 37// fenn_1989_electrospray_multiple_charging (the charge ladder) 38// usp_dietary_supplement_label_claim_bands (dose acceptance) 39// 40// Open gaps (stated, not hidden): 41// - Non-proteinogenic residues (Aib, Nle, 2-Nal, D-amino acids are mass- 42// identical so those are fine): needs a residue-table extension. 43// - Disulfide-bridged peptides (oxytocin, insulin): -2H per bridge is not 44// yet modelled, so cyclic actives are out of catalog. 45// - Lipidated / PEGylated actives (semaglutide class): out of scope. 46// 47// genealogy_id: supplement_adulterant_arc + peptide_chemistry + nishi_chem_suite 48 49import "nx_syscalls.nx" 50import "nx_peptide.nx" 51import "nx_peptide_ext.nx" 52 53// ===== Terminal modifications ========================================= 54// 55// Both are ubiquitous in real peptide drugs and both MOVE the mass, so a 56// screen that ignores them looks for the wrong m/z entirely. 57// The DELTAS themselves live in nx_peptide_ext (single source of truth for 58// a physical constant); this file only names the combinations. 59// C-terminal amide : -OH -> -NH2 = -0.98402 Da 60// N-terminal acetyl: -H -> -COCH3 = +42.01056 Da 61 62const SUP_MOD_NONE: i64 = 0 63const SUP_MOD_AMIDE: i64 = 1 64const SUP_MOD_ACETYL: i64 = 2 65const SUP_MOD_BOTH: i64 = 3 66 67// ===== Regulatory status ============================================== 68 69const SUP_REG_UNKNOWN: i64 = 0 70const SUP_REG_APPROVED: i64 = 1 // approved drug / reference standard 71const SUP_REG_ENDOGENOUS: i64 = 2 // naturally occurring, control 72const SUP_REG_NOT_APPROVED: i64 = 3 // no marketing authorisation 73const SUP_REG_WADA_PROHIBITED: i64 = 4 // WADA prohibited list 74 75// ===== Catalog ======================================================== 76 77const SUP_BPC157: i64 = 0 78const SUP_TB500: i64 = 1 79const SUP_GHRP6: i64 = 2 80const SUP_ANGII: i64 = 3 81const SUP_BRADYK: i64 = 4 82const SUP_N_ENTRY: i64 = 5 83 84const SUP_NO_MATCH: i64 = 0 - 1 85 86// Typical small-molecule adulterant screen window, m/z 100-1000 (Q4). 87// Data-driven so a lab running a different window can re-derive its own 88// blind spots rather than inherit ours. 89const SUP_SCAN_LO_Q4: i64 = 1000000 90const SUP_SCAN_HI_Q4: i64 = 10000000 91 92// Charge states electrospray realistically produces for these sizes. 93const SUP_MAX_CHARGE: i64 = 4 94 95// USP-style label-claim acceptance band for a stated-potency active. 96const SUP_LABEL_MIN_PCT: i64 = 90 97const SUP_LABEL_MAX_PCT: i64 = 110 98 99// ---- catalog accessors ----------------------------------------------- 100 101func sup_seq(id: i64) -> *u8 { 102 if id == SUP_BPC157 { return "GEPPPGKPADDAGLV" as *u8 } 103 if id == SUP_TB500 { return "LKKTETQ" as *u8 } 104 if id == SUP_GHRP6 { return "HWAWFK" as *u8 } 105 if id == SUP_ANGII { return "DRVYIHPF" as *u8 } 106 if id == SUP_BRADYK { return "RPPGFSPFR" as *u8 } 107 return "\x00" as *u8 108} 109 110func sup_mod(id: i64) -> i64 { 111 if id == SUP_GHRP6 { return SUP_MOD_AMIDE } 112 return SUP_MOD_NONE 113} 114 115func sup_status(id: i64) -> i64 { 116 if id == SUP_BPC157 { return SUP_REG_WADA_PROHIBITED } 117 if id == SUP_TB500 { return SUP_REG_WADA_PROHIBITED } 118 if id == SUP_GHRP6 { return SUP_REG_WADA_PROHIBITED } 119 if id == SUP_ANGII { return SUP_REG_APPROVED } 120 if id == SUP_BRADYK { return SUP_REG_ENDOGENOUS } 121 return SUP_REG_UNKNOWN 122} 123 124func sup_is_prohibited(id: i64) -> i64 { 125 let s: i64 = sup_status(id) 126 if s == SUP_REG_WADA_PROHIBITED { return 1 } 127 return 0 128} 129 130// ===== Derived masses ================================================= 131 132func sup_mod_delta_q4(mod: i64) -> i64 { 133 if mod == SUP_MOD_AMIDE { return PEX_AMIDE_DELTA_Q4 } 134 if mod == SUP_MOD_ACETYL { return PEX_ACETYL_DELTA_Q4 } 135 if mod == SUP_MOD_BOTH { return PEX_AMIDE_DELTA_Q4 + PEX_ACETYL_DELTA_Q4 } 136 return 0 137} 138 139// Neutral monoisotopic mass of the catalog entry AS SOLD (backbone from 140// the sequence, plus its terminal modification). Refuses if the sequence 141// carries a residue nx_peptide does not know. 142func sup_mass_q4(id: i64) -> i64 { 143 let seq: *u8 = sup_seq(id) 144 let base: i64 = pep_mass_mono_q4(seq) 145 if base == PEP_INVALID { return PEP_INVALID } 146 let mod: i64 = sup_mod(id) 147 let delta: i64 = sup_mod_delta_q4(mod) 148 return base + delta 149} 150 151// Observed m/z for the [M+zH]z+ ion. 152func sup_mz_q4(id: i64, z: i64) -> i64 { 153 let mass: i64 = sup_mass_q4(id) 154 if mass == PEP_INVALID { return PEP_INVALID } 155 return pep_mz_q4(mass, z) 156} 157 158// ===== ppm matching =================================================== 159 160// Mass error in ppm x10, always non-negative. The Q4 scaling cancels in 161// the ratio, so observed and reference may both be raw Q4. 162func sup_ppm_q1(observed_q4: i64, reference_q4: i64) -> i64 { 163 if reference_q4 <= 0 { return 0 - 1 } 164 var d: i64 = observed_q4 - reference_q4 165 if d < 0 { d = 0 - d } 166 let num: i64 = d * 10000000 167 return num / reference_q4 168} 169 170func sup_within_ppm(observed_q4: i64, reference_q4: i64, tol_ppm_q1: i64) -> i64 { 171 let p: i64 = sup_ppm_q1(observed_q4, reference_q4) 172 if p < 0 { return 0 } 173 if p <= tol_ppm_q1 { return 1 } 174 return 0 175} 176 177// ===== Charge-state screening ========================================= 178// 179// The core of the rung: try EVERY charge state, not just z=1. Returns the 180// charge that matched, or 0 for no match at any charge. 181 182func sup_match_charge(id: i64, observed_mz_q4: i64, tol_ppm_q1: i64) -> i64 { 183 var z: i64 = 1 184 var hit: i64 = 0 185 while z <= SUP_MAX_CHARGE { 186 let ref_mz: i64 = sup_mz_q4(id, z) 187 if ref_mz > 0 { 188 let ok: i64 = sup_within_ppm(observed_mz_q4, ref_mz, tol_ppm_q1) 189 if ok == 1 { if hit == 0 { hit = z } } 190 } 191 z = z + 1 192 } 193 return hit 194} 195 196// Scan the whole catalog. Returns the matching entry id, or SUP_NO_MATCH. 197func sup_screen(observed_mz_q4: i64, tol_ppm_q1: i64) -> i64 { 198 var i: i64 = 0 199 var found: i64 = SUP_NO_MATCH 200 while i < SUP_N_ENTRY { 201 let z: i64 = sup_match_charge(i, observed_mz_q4, tol_ppm_q1) 202 if z > 0 { if found == SUP_NO_MATCH { found = i } } 203 i = i + 1 204 } 205 return found 206} 207 208// ===== The blind-spot calculation ===================================== 209// 210// Would a single-charge screen over a small-molecule scan window have seen 211// this compound at all? 212 213func sup_mz_in_range(mz_q4: i64) -> i64 { 214 if mz_q4 <= 0 { return 0 } 215 if mz_q4 < SUP_SCAN_LO_Q4 { return 0 } 216 if mz_q4 > SUP_SCAN_HI_Q4 { return 0 } 217 return 1 218} 219 220func sup_mh_plus_in_range(id: i64) -> i64 { 221 let mz: i64 = sup_mz_q4(id, 1) 222 return sup_mz_in_range(mz) 223} 224 225// How many charge states of this compound fall inside the scan window. 226func sup_detectable_charges_in_range(id: i64) -> i64 { 227 var z: i64 = 1 228 var n: i64 = 0 229 while z <= SUP_MAX_CHARGE { 230 let mz: i64 = sup_mz_q4(id, z) 231 let ok: i64 = sup_mz_in_range(mz) 232 n = n + ok 233 z = z + 1 234 } 235 return n 236} 237 238// 1 iff a [M+H]+-only method MISSES a compound that multi-charge screening 239// would catch: the single-charge ion is out of window, but some higher 240// charge state is in window. This is the defect, stated as a predicate. 241func sup_single_charge_blind_spot(id: i64) -> i64 { 242 let mh: i64 = sup_mh_plus_in_range(id) 243 if mh == 1 { return 0 } 244 let n: i64 = sup_detectable_charges_in_range(id) 245 if n > 0 { return 1 } 246 return 0 247} 248 249// ===== Label-claim dose verification ================================== 250// 251// Recovery of the declared amount, in percent. Both arguments in the same 252// unit (micrograms is the practical one for peptides). 253 254func sup_recovery_pct(measured_ug: i64, declared_ug: i64) -> i64 { 255 if declared_ug <= 0 { return 0 - 1 } 256 return measured_ug * 100 / declared_ug 257} 258 259func sup_label_compliant(measured_ug: i64, declared_ug: i64) -> i64 { 260 let r: i64 = sup_recovery_pct(measured_ug, declared_ug) 261 if r < 0 { return 0 } 262 if r < SUP_LABEL_MIN_PCT { return 0 } 263 if r > SUP_LABEL_MAX_PCT { return 0 } 264 return 1 265} 266 267// Underfilled, overfilled, or on-claim: -1 / +1 / 0. A superpotent product 268// is a finding too, not a pass. 269func sup_dose_verdict(measured_ug: i64, declared_ug: i64) -> i64 { 270 let r: i64 = sup_recovery_pct(measured_ug, declared_ug) 271 if r < 0 { return 0 - 1 } 272 if r < SUP_LABEL_MIN_PCT { return 0 - 1 } 273 if r > SUP_LABEL_MAX_PCT { return 1 } 274 return 0 275}