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}