code wiki / (root) / nx_peptide_ext.nx

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}