nx_peptide_ext.nx source
↩ module page · 217 lines · 8496 B
1// nx_peptide_ext.nx -- CHEMISTRY SUITE / PEPTIDE EXTENSION rung. Closes the
2// three gaps nx_peptide and nx_supplement_screen declared in their own
3// headers: non-proteinogenic residues, disulfide bridges, and lactam
4// cyclisation. Together those cover the peptides that actually turn up in
5// supplement and research-peptide channels -- which are overwhelmingly
6// modified, cyclic, or both.
7//
8// WHY A BUILDER AND NOT A LONGER ALPHABET. A one-letter code has 26 slots
9// and 20 are spoken for; overloading B/J/O/U/X/Z with Aib/Nle/Orn would be
10// arbitrary, unreadable, and would silently reinterpret any sequence that
11// legitimately used them as ambiguity codes. So a peptide is COMPOSED:
12// runs of standard sequence, explicit non-standard residues by id, then
13// terminal modifications and bridges. Nothing is guessed from a letter.
14//
15// THE VALIDATION IS THE POINT. Oxytocin and vasopressin each land on their
16// published monoisotopic mass ONLY if the C-terminal amide AND the
17// disulfide are both applied -- miss the amide and you are 0.98 Da out,
18// miss the bridge and you are 2.02 Da out. Both are checked against
19// literature (1006.4367 and 1083.4380), and the gate ALSO runs the
20// no-bridge and no-amide variants as negative controls, so the test cannot
21// pass by accident.
22//
23// D-amino acids need no entry here: a stereoisomer has identical mass.
24// GHRP-6's D-Trp and D-Phe, ipamorelin's D-2-Nal -- all already correct in
25// nx_peptide. Chirality matters to the receptor, not to the balance.
26//
27// All INTEGER, _q4 = x10^4 Da, matching nx_peptide.
28//
29// Grounding (cited; researcher-groundable):
30// iupac_nonproteinogenic_residue_formulas (extended residue masses)
31// disulfide_bond_two_hydrogen_loss (-2.01565 Da per S-S)
32// lactam_cyclisation_water_loss (-18.01056 Da per bridge)
33// published_monoisotopic_masses_oxytocin_vasopressin (validation anchors)
34//
35// genealogy_id: peptide_chemistry + nishi_chem_suite
36
37import "nx_syscalls.nx"
38import "nx_peptide.nx"
39
40// ===== Terminal modifications (canonical home for these deltas) =======
41//
42// C-terminal amide : -OH -> -NH2 = -0.98402 Da
43// N-terminal acetyl: -H -> -COCH3 = +42.01056 Da
44
45const PEX_AMIDE_DELTA_Q4: i64 = 0 - 9840
46const PEX_ACETYL_DELTA_Q4: i64 = 420106
47
48// ===== Bridges ========================================================
49//
50// A disulfide oxidises two free thiols: 2 x -SH -> -S-S- loses 2 H.
51// A lactam joins a side-chain acid to a side-chain amine, losing water.
52
53const PEX_SS_DELTA_Q4: i64 = 0 - 20157 // -2.01565 Da per bridge
54const PEX_LACTAM_DELTA_Q4: i64 = 0 - 180106 // -18.01056 Da per bridge
55
56// ===== Non-proteinogenic residues =====================================
57//
58// Ids continue past nx_peptide's 0..19 so the two tables never collide.
59
60const PEX_AIB: i64 = 20 // 2-aminoisobutyric acid (ipamorelin)
61const PEX_NLE: i64 = 21 // norleucine (melanotan II)
62const PEX_NVA: i64 = 22 // norvaline
63const PEX_ORN: i64 = 23 // ornithine
64const PEX_CIT: i64 = 24 // citrulline
65const PEX_HYP: i64 = 25 // 4-hydroxyproline (collagen marker)
66const PEX_SAR: i64 = 26 // sarcosine (N-methylglycine)
67const PEX_2NAL: i64 = 27 // 3-(2-naphthyl)alanine (ipamorelin)
68const PEX_PGLU: i64 = 28 // pyroglutamate
69const PEX_N_EXT: i64 = 29
70
71// Monoisotopic residue mass, Q4. Values are DERIVED relationships, noted
72// so they can be re-checked rather than taken on faith:
73// Aib = Ala + CH2 Nle = Leu isomer (identical formula)
74// Nva = Val isomer Orn = Lys - CH2
75// Cit = Arg - NH + O Hyp = Pro + O
76// Sar = Gly + CH2 pGlu = Gln - NH3
77func pex_residue_mono_q4(ext_id: i64) -> i64 {
78 if ext_id == PEX_AIB { return 850528 }
79 if ext_id == PEX_NLE { return 1130841 }
80 if ext_id == PEX_NVA { return 990684 }
81 if ext_id == PEX_ORN { return 1140793 }
82 if ext_id == PEX_CIT { return 1570851 }
83 if ext_id == PEX_HYP { return 1130477 }
84 if ext_id == PEX_SAR { return 710371 }
85 if ext_id == PEX_2NAL { return 1970841 }
86 if ext_id == PEX_PGLU { return 1110320 }
87 return 0
88}
89
90func pex_is_known(ext_id: i64) -> i64 {
91 if ext_id < PEX_AIB { return 0 }
92 if ext_id >= PEX_N_EXT { return 0 }
93 return 1
94}
95
96// ===== The builder ====================================================
97
98struct NxPepBuild {
99 res_sum_q4: i64,
100 n_res: i64,
101 n_ss: i64,
102 n_lactam: i64,
103 has_amide: i64,
104 has_acetyl: i64,
105 valid: i64,
106}
107
108func nx_pep_build_new() -> *NxPepBuild {
109 let b: *NxPepBuild = (sys_mmap(64)) as *NxPepBuild
110 b.res_sum_q4 = 0
111 b.n_res = 0
112 b.n_ss = 0
113 b.n_lactam = 0
114 b.has_amide = 0
115 b.has_acetyl = 0
116 b.valid = 1
117 return b
118}
119
120// Append a run of STANDARD residues. Any unrecognised character poisons
121// the build permanently -- it is never skipped, and the mass can never
122// come back plausible-looking.
123func pex_add_seq(b: *NxPepBuild, seq: *u8) -> i64 {
124 var i: i64 = 0
125 let n: i64 = pep_len(seq)
126 if n <= 0 { b.valid = 0; return 0 }
127 while i < n {
128 let c: i64 = seq[i] as i64
129 let aa: i64 = pep_aa_from_char(c)
130 if aa == PEP_INVALID { b.valid = 0 }
131 if aa != PEP_INVALID {
132 let m: i64 = pep_residue_mono_q4(aa)
133 b.res_sum_q4 = b.res_sum_q4 + m
134 b.n_res = b.n_res + 1
135 }
136 i = i + 1
137 }
138 return b.valid
139}
140
141// Append ONE non-proteinogenic residue by id.
142func pex_add_ext(b: *NxPepBuild, ext_id: i64) -> i64 {
143 let known: i64 = pex_is_known(ext_id)
144 if known != 1 { b.valid = 0; return 0 }
145 let m: i64 = pex_residue_mono_q4(ext_id)
146 b.res_sum_q4 = b.res_sum_q4 + m
147 b.n_res = b.n_res + 1
148 return b.valid
149}
150
151func pex_set_amide(b: *NxPepBuild) -> i64 { b.has_amide = 1; return 0 }
152func pex_set_acetyl(b: *NxPepBuild) -> i64 { b.has_acetyl = 1; return 0 }
153func pex_add_disulfide(b: *NxPepBuild) -> i64 { b.n_ss = b.n_ss + 1; return 0 }
154func pex_add_lactam(b: *NxPepBuild) -> i64 { b.n_lactam = b.n_lactam + 1; return 0 }
155
156// A disulfide needs two cysteines to exist. This is a STRUCTURAL check,
157// not a mass check: a build claiming more bridges than its cysteine count
158// allows is chemically impossible and must be refused, because the mass it
159// would produce looks perfectly reasonable.
160func pex_bridges_possible(b: *NxPepBuild, n_cys: i64) -> i64 {
161 let need: i64 = b.n_ss * 2
162 if need > n_cys { return 0 }
163 return 1
164}
165
166// ===== Final mass =====================================================
167
168func pex_mass_q4(b: *NxPepBuild) -> i64 {
169 if b.valid != 1 { return PEP_INVALID }
170 if b.n_res <= 0 { return PEP_INVALID }
171 var total: i64 = b.res_sum_q4 + PEP_WATER_MONO_Q4
172 if b.has_amide == 1 { total = total + PEX_AMIDE_DELTA_Q4 }
173 if b.has_acetyl == 1 { total = total + PEX_ACETYL_DELTA_Q4 }
174 let ss: i64 = b.n_ss * PEX_SS_DELTA_Q4
175 let lac: i64 = b.n_lactam * PEX_LACTAM_DELTA_Q4
176 total = total + ss
177 total = total + lac
178 return total
179}
180
181// Ion m/z for a built peptide, reusing nx_peptide's proton convention.
182func pex_mz_q4(b: *NxPepBuild, z: i64) -> i64 {
183 let m: i64 = pex_mass_q4(b)
184 if m == PEP_INVALID { return PEP_INVALID }
185 return pep_mz_q4(m, z)
186}
187
188// ===== Near-isobar warning ============================================
189//
190// Hyp/Leu and Orn/Asn each differ by CH4 vs O = 0.03639 Da. At unit mass
191// resolution they are the SAME peak. A collagen assay that assumes Leu
192// where Hyp sits, or reads Orn as Asn, is not a rounding error -- it is a
193// different molecule with the same nominal mass. Expose it as a predicate
194// so callers can be forced to think about their instrument's resolution.
195
196const PEX_NEAR_ISOBAR_Q4: i64 = 500 // within 0.05 Da = same nominal peak
197
198func pex_near_isobaric(mass_a_q4: i64, mass_b_q4: i64) -> i64 {
199 if mass_a_q4 <= 0 { return 0 }
200 if mass_b_q4 <= 0 { return 0 }
201 var d: i64 = mass_a_q4 - mass_b_q4
202 if d < 0 { d = 0 - d }
203 if d == 0 { return 0 }
204 if d <= PEX_NEAR_ISOBAR_Q4 { return 1 }
205 return 0
206}
207
208// Resolving power (m/dm) needed to separate two masses at a given m/z.
209// Integer, and 0 when they are exactly isobaric -- no resolution separates
210// identical masses, and saying "infinite" would invite a divide-by-zero.
211func pex_resolving_power_needed(mass_a_q4: i64, mass_b_q4: i64) -> i64 {
212 var d: i64 = mass_a_q4 - mass_b_q4
213 if d < 0 { d = 0 - d }
214 if d == 0 { return 0 }
215 let lo: i64 = mass_a_q4
216 return lo / d
217}