code wiki / (root) / nx_peptide.nx

nx_peptide.nx source

↩ module page · 565 lines · 20914 B

1// nx_peptide.nx -- CHEMISTRY SUITE / PEPTIDE PRIMARY-STRUCTURE rung. Turns 2// an amino-acid SEQUENCE into real chemistry: exact mass, charge state, 3// isoelectric point, UV absorbance, hydropathy, and the b/y fragment-ion 4// ladder an MS/MS run actually observes. 5// 6// This is the missing rung between the ecosystem's molecule stack 7// (nx_chem_molecule / nx_chem_mass / nx_chem_isotope_pattern / nx_chem_peak_list) 8// and peptide work: the mass stack could weigh a MolGraph but nothing could 9// go from "DRVYIHPF" to 1045.5345 Da, so the NIST Peptide Tandem Mass 10// Spectral Libraries (SRD 1c) in the corpus had no local counterpart to 11// match against. This closes that. 12// 13// ALL INTEGER, no float: 14// *_q4 = value x 10^4 (mass in Da -> micro-AMU; matches nx_chem_mass) 15// *_q3 = value x 10^3 (charge, hydropathy, decade-log intermediates) 16// *_milli = value x 10^3 (pH and pKa) 17// 18// THE exceed: a sequence viewer prints letters. This computes the mass an 19// instrument will SEE (monoisotopic, not average -- they differ by ~0.66 Da 20// at 1 kDa and that is the difference between a hit and a miss), the m/z of 21// every charge state, and the b/y ladder for identification -- and it 22// REFUSES a sequence containing a residue it does not know rather than 23// silently scoring the unknown as zero mass. 24// 25// PRECISION, stated honestly (not asserted): 26// - Residue masses are rounded to Q4, so an n-mer accumulates up to n/2 27// Q4 units of rounding. Measured against literature: angiotensin II 28// (8-mer) is EXACT (1045.5345); bradykinin (9-mer) lands 1 Q4 unit 29// (0.0001 Da) high. pep_mass_agrees() makes that tolerance explicit. 30// - pI comes from bisection over pep_pow10_q3, whose decade table is 31// 0.1-log granular, so pI is good to about +/-0.05 pH -- adequate to 32// choose a buffer, NOT a substitute for a titration. 33// 34// Grounding (cited; researcher-groundable): 35// iupac_amino_acid_residue_monoisotopic_masses (residue table) 36// emboss_pepstats_pka_set (pKa set for pI/charge) 37// kyte_doolittle_hydropathy_1982 (GRAVY) 38// pace_1995_extinction_coefficient_280nm (Trp 5500 / Tyr 1490 / cystine 125) 39// biemann_1990_peptide_fragment_ion_nomenclature (b/y ions) 40// 41// genealogy_id: peptide_chemistry + nishi_chem_suite + nishi_food_science 42 43import "nx_syscalls.nx" 44import "nx_pow10.nx" 45const PEP_MAGIC_710371: i64 = 710371 46const PEP_MAGIC_1030092: i64 = 1030092 47const PEP_MAGIC_1150269: i64 = 1150269 48const PEP_MAGIC_1290426: i64 = 1290426 49const PEP_MAGIC_1470684: i64 = 1470684 50const PEP_MAGIC_570215: i64 = 570215 51const PEP_MAGIC_1370589: i64 = 1370589 52const PEP_MAGIC_1130841: i64 = 1130841 53const PEP_MAGIC_1280950: i64 = 1280950 54const PEP_MAGIC_1310405: i64 = 1310405 55const PEP_MAGIC_1140429: i64 = 1140429 56const PEP_MAGIC_970528: i64 = 970528 57const PEP_MAGIC_1280586: i64 = 1280586 58const PEP_MAGIC_1561011: i64 = 1561011 59const PEP_MAGIC_870320: i64 = 870320 60const PEP_MAGIC_1010477: i64 = 1010477 61const PEP_MAGIC_990684: i64 = 990684 62const PEP_MAGIC_1860793: i64 = 1860793 63const PEP_MAGIC_1630633: i64 = 1630633 64const PEP_MAGIC_710788: i64 = 710788 65const PEP_MAGIC_1031388: i64 = 1031388 66const PEP_MAGIC_1150886: i64 = 1150886 67const PEP_MAGIC_1291155: i64 = 1291155 68const PEP_MAGIC_1471766: i64 = 1471766 69const PEP_MAGIC_570519: i64 = 570519 70const PEP_MAGIC_1371411: i64 = 1371411 71const PEP_MAGIC_1131594: i64 = 1131594 72const PEP_MAGIC_1281741: i64 = 1281741 73const PEP_MAGIC_1311926: i64 = 1311926 74const PEP_MAGIC_1141038: i64 = 1141038 75const PEP_MAGIC_971167: i64 = 971167 76const PEP_MAGIC_1281307: i64 = 1281307 77const PEP_MAGIC_1561875: i64 = 1561875 78const PEP_MAGIC_870782: i64 = 870782 79const PEP_MAGIC_1011051: i64 = 1011051 80const PEP_MAGIC_991326: i64 = 991326 81const PEP_MAGIC_1862132: i64 = 1862132 82const PEP_MAGIC_1631760: i64 = 1631760 83const PEP_MAGIC_1800: i64 = 1800 84const PEP_MAGIC_2500: i64 = 2500 85const PEP_MAGIC_3500: i64 = 3500 86const PEP_MAGIC_2800: i64 = 2800 87const PEP_MAGIC_3200: i64 = 3200 88const PEP_MAGIC_4500: i64 = 4500 89const PEP_MAGIC_3900: i64 = 3900 90const PEP_MAGIC_3800: i64 = 3800 91const PEP_MAGIC_1900: i64 = 1900 92const PEP_MAGIC_1600: i64 = 1600 93const PEP_MAGIC_4200: i64 = 4200 94const PEP_MAGIC_1300: i64 = 1300 95const PEP_MAGIC_1000000: i64 = 1000000 96 97// ===== Sealed enum: the 20 proteinogenic residues (1-letter order) ===== 98 99const PEP_A: i64 = 0 100const PEP_C: i64 = 1 101const PEP_D: i64 = 2 102const PEP_E: i64 = 3 103const PEP_F: i64 = 4 104const PEP_G: i64 = 5 105const PEP_H: i64 = 6 106const PEP_I: i64 = 7 107const PEP_K: i64 = 8 108const PEP_L: i64 = 9 109const PEP_M: i64 = 10 110const PEP_N: i64 = 11 111const PEP_P: i64 = 12 112const PEP_Q: i64 = 13 113const PEP_R: i64 = 14 114const PEP_S: i64 = 15 115const PEP_T: i64 = 16 116const PEP_V: i64 = 17 117const PEP_W: i64 = 18 118const PEP_Y: i64 = 19 119const PEP_N_AA: i64 = 20 120 121const PEP_INVALID: i64 = 0 - 1 122 123// ===== Physical constants (Q4 micro-AMU) ============================== 124// Water is added once per chain (the residue table is already 125// free-amino-acid MINUS water, so a chain of n residues + 1 water = peptide). 126 127const PEP_WATER_MONO_Q4: i64 = 180106 // H2O monoisotopic 18.010565 Da 128const PEP_WATER_AVG_Q4: i64 = 180153 // H2O average 18.0153 Da 129// Proton mass; same value nx_chem_mass.nx uses for [M+H]+ so ion m/z from 130// a sequence and from a MolGraph are directly comparable. 131const PEP_PROTON_Q4: i64 = 10073 // 1.00728 Da 132 133// ===== Molar extinction at 280 nm (M^-1 cm^-1, Pace 1995) ============= 134 135const PEP_EXT_TRP: i64 = 5500 136const PEP_EXT_TYR: i64 = 1490 137const PEP_EXT_CYSTINE: i64 = 125 // per DISULFIDE, not per free Cys 138 139// ===== pKa set (EMBOSS pepstats), pH x 1000 =========================== 140 141const PEP_PKA_CTERM: i64 = 3600 142const PEP_PKA_NTERM: i64 = 8600 143const PEP_PKA_C: i64 = 8500 144const PEP_PKA_D: i64 = 3900 145const PEP_PKA_E: i64 = 4100 146const PEP_PKA_H: i64 = 6500 147const PEP_PKA_K: i64 = 10800 148const PEP_PKA_R: i64 = 12500 149const PEP_PKA_Y: i64 = 10100 150 151// pI bisection bounds + depth. 20 halvings of a 14-pH span lands far below 152// the 0.1-log granularity of the decade table, so the table is the limit. 153const PEP_PI_LO_MILLI: i64 = 0 154const PEP_PI_HI_MILLI: i64 = 14000 155const PEP_PI_ITERS: i64 = 20 156 157// Beyond +/-6 log units the ionisation fraction is under 1e-6 -- saturate 158// rather than overflow. 159const PEP_POW10_CLAMP: i64 = 6000 160 161// ===== Residue masses ================================================= 162// 163// Monoisotopic residue mass (Q4). Leu and Ile are ISOBARIC (identical 164// mass) -- that is real chemistry, not a copy-paste error: MS cannot 165// distinguish them without fragmentation of the side chain. 166 167func pep_residue_mono_q4(aa: i64) -> i64 { 168 if aa == PEP_A { return PEP_MAGIC_710371 } 169 if aa == PEP_C { return PEP_MAGIC_1030092 } 170 if aa == PEP_D { return PEP_MAGIC_1150269 } 171 if aa == PEP_E { return PEP_MAGIC_1290426 } 172 if aa == PEP_F { return PEP_MAGIC_1470684 } 173 if aa == PEP_G { return PEP_MAGIC_570215 } 174 if aa == PEP_H { return PEP_MAGIC_1370589 } 175 if aa == PEP_I { return PEP_MAGIC_1130841 } 176 if aa == PEP_K { return PEP_MAGIC_1280950 } 177 if aa == PEP_L { return PEP_MAGIC_1130841 } 178 if aa == PEP_M { return PEP_MAGIC_1310405 } 179 if aa == PEP_N { return PEP_MAGIC_1140429 } 180 if aa == PEP_P { return PEP_MAGIC_970528 } 181 if aa == PEP_Q { return PEP_MAGIC_1280586 } 182 if aa == PEP_R { return PEP_MAGIC_1561011 } 183 if aa == PEP_S { return PEP_MAGIC_870320 } 184 if aa == PEP_T { return PEP_MAGIC_1010477 } 185 if aa == PEP_V { return PEP_MAGIC_990684 } 186 if aa == PEP_W { return PEP_MAGIC_1860793 } 187 if aa == PEP_Y { return PEP_MAGIC_1630633 } 188 return 0 189} 190 191// Average (isotope-weighted) residue mass (Q4) -- what a low-resolution 192// instrument or a gravimetric calculation sees. 193func pep_residue_avg_q4(aa: i64) -> i64 { 194 if aa == PEP_A { return PEP_MAGIC_710788 } 195 if aa == PEP_C { return PEP_MAGIC_1031388 } 196 if aa == PEP_D { return PEP_MAGIC_1150886 } 197 if aa == PEP_E { return PEP_MAGIC_1291155 } 198 if aa == PEP_F { return PEP_MAGIC_1471766 } 199 if aa == PEP_G { return PEP_MAGIC_570519 } 200 if aa == PEP_H { return PEP_MAGIC_1371411 } 201 if aa == PEP_I { return PEP_MAGIC_1131594 } 202 if aa == PEP_K { return PEP_MAGIC_1281741 } 203 if aa == PEP_L { return PEP_MAGIC_1131594 } 204 if aa == PEP_M { return PEP_MAGIC_1311926 } 205 if aa == PEP_N { return PEP_MAGIC_1141038 } 206 if aa == PEP_P { return PEP_MAGIC_971167 } 207 if aa == PEP_Q { return PEP_MAGIC_1281307 } 208 if aa == PEP_R { return PEP_MAGIC_1561875 } 209 if aa == PEP_S { return PEP_MAGIC_870782 } 210 if aa == PEP_T { return PEP_MAGIC_1011051 } 211 if aa == PEP_V { return PEP_MAGIC_991326 } 212 if aa == PEP_W { return PEP_MAGIC_1862132 } 213 if aa == PEP_Y { return PEP_MAGIC_1631760 } 214 return 0 215} 216 217// Kyte-Doolittle hydropathy index x 1000 (positive = hydrophobic). 218func pep_hydropathy_q3(aa: i64) -> i64 { 219 if aa == PEP_A { return PEP_MAGIC_1800 } 220 if aa == PEP_C { return PEP_MAGIC_2500 } 221 if aa == PEP_D { return 0 - PEP_MAGIC_3500 } 222 if aa == PEP_E { return 0 - PEP_MAGIC_3500 } 223 if aa == PEP_F { return PEP_MAGIC_2800 } 224 if aa == PEP_G { return 0 - 400 } 225 if aa == PEP_H { return 0 - PEP_MAGIC_3200 } 226 if aa == PEP_I { return PEP_MAGIC_4500 } 227 if aa == PEP_K { return 0 - PEP_MAGIC_3900 } 228 if aa == PEP_L { return PEP_MAGIC_3800 } 229 if aa == PEP_M { return PEP_MAGIC_1900 } 230 if aa == PEP_N { return 0 - PEP_MAGIC_3500 } 231 if aa == PEP_P { return 0 - PEP_MAGIC_1600 } 232 if aa == PEP_Q { return 0 - PEP_MAGIC_3500 } 233 if aa == PEP_R { return 0 - PEP_MAGIC_4500 } 234 if aa == PEP_S { return 0 - 800 } 235 if aa == PEP_T { return 0 - 700 } 236 if aa == PEP_V { return PEP_MAGIC_4200 } 237 if aa == PEP_W { return 0 - 900 } 238 if aa == PEP_Y { return 0 - PEP_MAGIC_1300 } 239 return 0 240} 241 242// ===== Sequence parsing (fail-closed) ================================= 243// 244// An unrecognised letter returns PEP_INVALID rather than mapping to a 245// zero-mass residue -- so a typo or a non-standard residue (B/J/O/U/X/Z, 246// or lowercase) can never fabricate a plausible-looking mass. 247// ASCII codes are used directly (65='A' .. 89='Y'). 248 249func pep_aa_from_char(c: i64) -> i64 { 250 if c == 65 { return PEP_A } 251 if c == 67 { return PEP_C } 252 if c == 68 { return PEP_D } 253 if c == 69 { return PEP_E } 254 if c == 70 { return PEP_F } 255 if c == 71 { return PEP_G } 256 if c == 72 { return PEP_H } 257 if c == 73 { return PEP_I } 258 if c == 75 { return PEP_K } 259 if c == 76 { return PEP_L } 260 if c == 77 { return PEP_M } 261 if c == 78 { return PEP_N } 262 if c == 80 { return PEP_P } 263 if c == 81 { return PEP_Q } 264 if c == 82 { return PEP_R } 265 if c == 83 { return PEP_S } 266 if c == 84 { return PEP_T } 267 if c == 86 { return PEP_V } 268 if c == 87 { return PEP_W } 269 if c == 89 { return PEP_Y } 270 return PEP_INVALID 271} 272 273func pep_len(seq: *u8) -> i64 { 274 var i: i64 = 0 275 while seq[i] != 0 { i = i + 1 } 276 return i 277} 278 279// 1 iff every character maps to a known residue AND the chain is non-empty. 280func pep_seq_valid(seq: *u8) -> i64 { 281 var i: i64 = 0 282 var ok: i64 = 1 283 let n: i64 = pep_len(seq) 284 if n <= 0 { return 0 } 285 while i < n { 286 let c: i64 = seq[i] as i64 287 let aa: i64 = pep_aa_from_char(c) 288 if aa == PEP_INVALID { ok = 0 } 289 i = i + 1 290 } 291 return ok 292} 293 294func pep_count_aa(seq: *u8, aa_want: i64) -> i64 { 295 var i: i64 = 0 296 var k: i64 = 0 297 let n: i64 = pep_len(seq) 298 while i < n { 299 let c: i64 = seq[i] as i64 300 let aa: i64 = pep_aa_from_char(c) 301 if aa == aa_want { k = k + 1 } 302 i = i + 1 303 } 304 return k 305} 306 307// ===== Molecular mass ================================================= 308// 309// Peptide mass = sum(residue masses) + one water (the residues are already 310// water-subtracted, so this is mass conservation over n-1 peptide bonds). 311// REFUSES (returns PEP_INVALID) on an unknown residue. 312 313func pep_mass_mono_q4(seq: *u8) -> i64 { 314 var i: i64 = 0 315 var total: i64 = PEP_WATER_MONO_Q4 316 if pep_seq_valid(seq) != 1 { return PEP_INVALID } 317 let n: i64 = pep_len(seq) 318 while i < n { 319 let c: i64 = seq[i] as i64 320 let aa: i64 = pep_aa_from_char(c) 321 let m: i64 = pep_residue_mono_q4(aa) 322 total = total + m 323 i = i + 1 324 } 325 return total 326} 327 328func pep_mass_avg_q4(seq: *u8) -> i64 { 329 var i: i64 = 0 330 var total: i64 = PEP_WATER_AVG_Q4 331 if pep_seq_valid(seq) != 1 { return PEP_INVALID } 332 let n: i64 = pep_len(seq) 333 while i < n { 334 let c: i64 = seq[i] as i64 335 let aa: i64 = pep_aa_from_char(c) 336 let m: i64 = pep_residue_avg_q4(aa) 337 total = total + m 338 i = i + 1 339 } 340 return total 341} 342 343// Explicit tolerance check against a reference mass -- makes the Q4 344// accumulation budget a TESTABLE claim instead of a promise. 345func pep_mass_agrees(computed_q4: i64, reference_q4: i64, tol_q4: i64) -> i64 { 346 if computed_q4 == PEP_INVALID { return 0 } 347 var d: i64 = computed_q4 - reference_q4 348 if d < 0 { d = 0 - d } 349 if d <= tol_q4 { return 1 } 350 return 0 351} 352 353// ===== Ion m/z ======================================================== 354// 355// [M+nH]n+ : (M + n*proton)/n. Electrospray of a peptide gives a charge 356// LADDER, so the observed m/z depends on n -- this returns the value for 357// any n, which is what deconvoluting a real spectrum needs. 358 359func pep_mz_q4(mass_q4: i64, n_charge: i64) -> i64 { 360 if n_charge <= 0 { return 0 } 361 if mass_q4 == PEP_INVALID { return PEP_INVALID } 362 let protons: i64 = n_charge * PEP_PROTON_Q4 363 let total: i64 = mass_q4 + protons 364 return total / n_charge 365} 366 367// ===== b / y fragment ions (Biemann nomenclature) ===================== 368// 369// Backbone cleavage at the amide bond gives complementary series: 370// b_i = sum of the FIRST i residues + proton (N-terminal) 371// y_i = sum of the LAST i residues + water + proton (C-terminal) 372// Matching an observed peak list against these two ladders IS peptide 373// sequencing; b_i + y_(n-i) = M + 2*proton, which pep_by_complement_ok 374// checks as a self-consistency invariant. 375 376func pep_b_ion_q4(seq: *u8, i_frag: i64) -> i64 { 377 var i: i64 = 0 378 var total: i64 = PEP_PROTON_Q4 379 if pep_seq_valid(seq) != 1 { return PEP_INVALID } 380 let n: i64 = pep_len(seq) 381 if i_frag <= 0 { return PEP_INVALID } 382 if i_frag >= n { return PEP_INVALID } 383 while i < i_frag { 384 let c: i64 = seq[i] as i64 385 let aa: i64 = pep_aa_from_char(c) 386 let m: i64 = pep_residue_mono_q4(aa) 387 total = total + m 388 i = i + 1 389 } 390 return total 391} 392 393func pep_y_ion_q4(seq: *u8, i_frag: i64) -> i64 { 394 var total: i64 = PEP_PROTON_Q4 + PEP_WATER_MONO_Q4 395 if pep_seq_valid(seq) != 1 { return PEP_INVALID } 396 let n: i64 = pep_len(seq) 397 if i_frag <= 0 { return PEP_INVALID } 398 if i_frag >= n { return PEP_INVALID } 399 let start: i64 = n - i_frag 400 var i: i64 = start 401 while i < n { 402 let c: i64 = seq[i] as i64 403 let aa: i64 = pep_aa_from_char(c) 404 let m: i64 = pep_residue_mono_q4(aa) 405 total = total + m 406 i = i + 1 407 } 408 return total 409} 410 411// Invariant: b_i + y_(n-i) == M + 2 protons, for every interior i. 412func pep_by_complement_ok(seq: *u8, i_frag: i64) -> i64 { 413 let n: i64 = pep_len(seq) 414 let b: i64 = pep_b_ion_q4(seq, i_frag) 415 let y: i64 = pep_y_ion_q4(seq, n - i_frag) 416 if b == PEP_INVALID { return 0 } 417 if y == PEP_INVALID { return 0 } 418 let m: i64 = pep_mass_mono_q4(seq) 419 if m == PEP_INVALID { return 0 } 420 let want: i64 = m + 2 * PEP_PROTON_Q4 421 if b + y == want { return 1 } 422 return 0 423} 424 425// ===== Integer decade exponential ===================================== 426// 427// Delegated to nx_pow10 (shared with the thermal-kinetics lane, which needs 428// the same decade-log primitive). The name is kept so callers and the gate 429// are untouched by the extraction. nx_pow10 INTERPOLATES between table 430// entries, so this is now better than the 0.1-log-granular original -- the 431// pI figure is correspondingly tighter than the +/-0.05 pH stated above. 432 433func pep_pow10_q3(d_milli: i64) -> i64 { 434 return ipow10_q3(d_milli) 435} 436 437// ===== Ionisation fractions (Henderson-Hasselbalch) =================== 438// 439// Basic group, fraction PROTONATED (carries +1): 1/(1 + 10^(pH-pKa)) 440// Acidic group, fraction DEPROTONATED (carries -1): 1/(1 + 10^(pKa-pH)) 441// Both returned x1000. At pH == pKa each is exactly 500 (half-ionised), 442// which is the cheapest correctness check there is. 443 444func pep_frac_basic_q3(ph_milli: i64, pka_milli: i64) -> i64 { 445 let p: i64 = pep_pow10_q3(ph_milli - pka_milli) 446 let denom: i64 = 1000 + p 447 if denom <= 0 { return 0 } 448 return PEP_MAGIC_1000000 / denom 449} 450 451func pep_frac_acidic_q3(ph_milli: i64, pka_milli: i64) -> i64 { 452 let p: i64 = pep_pow10_q3(pka_milli - ph_milli) 453 let denom: i64 = 1000 + p 454 if denom <= 0 { return 0 } 455 return PEP_MAGIC_1000000 / denom 456} 457 458// ===== Net charge at a given pH ======================================= 459// 460// Sums the free alpha-amino terminus, the free alpha-carboxyl terminus, 461// and every ionisable side chain. Returned x1000 (so +2500 = +2.5 e). 462 463func pep_charge_q3(seq: *u8, ph_milli: i64) -> i64 { 464 if pep_seq_valid(seq) != 1 { return 0 } 465 let n_k: i64 = pep_count_aa(seq, PEP_K) 466 let n_r: i64 = pep_count_aa(seq, PEP_R) 467 let n_h: i64 = pep_count_aa(seq, PEP_H) 468 let n_d: i64 = pep_count_aa(seq, PEP_D) 469 let n_e: i64 = pep_count_aa(seq, PEP_E) 470 let n_c: i64 = pep_count_aa(seq, PEP_C) 471 let n_y: i64 = pep_count_aa(seq, PEP_Y) 472 var pos: i64 = pep_frac_basic_q3(ph_milli, PEP_PKA_NTERM) 473 let f_k: i64 = pep_frac_basic_q3(ph_milli, PEP_PKA_K) 474 let f_r: i64 = pep_frac_basic_q3(ph_milli, PEP_PKA_R) 475 let f_h: i64 = pep_frac_basic_q3(ph_milli, PEP_PKA_H) 476 pos = pos + n_k * f_k 477 pos = pos + n_r * f_r 478 pos = pos + n_h * f_h 479 var neg: i64 = pep_frac_acidic_q3(ph_milli, PEP_PKA_CTERM) 480 let f_d: i64 = pep_frac_acidic_q3(ph_milli, PEP_PKA_D) 481 let f_e: i64 = pep_frac_acidic_q3(ph_milli, PEP_PKA_E) 482 let f_c: i64 = pep_frac_acidic_q3(ph_milli, PEP_PKA_C) 483 let f_y: i64 = pep_frac_acidic_q3(ph_milli, PEP_PKA_Y) 484 neg = neg + n_d * f_d 485 neg = neg + n_e * f_e 486 neg = neg + n_c * f_c 487 neg = neg + n_y * f_y 488 return pos - neg 489} 490 491// ===== Isoelectric point ============================================== 492// 493// pI = the pH where net charge crosses zero. Charge is monotonically 494// DECREASING in pH (every group deprotonates as pH rises), so plain 495// bisection is sound -- no derivative, no float, no iteration limit risk. 496 497func pep_pi_milli(seq: *u8) -> i64 { 498 if pep_seq_valid(seq) != 1 { return PEP_INVALID } 499 var lo: i64 = PEP_PI_LO_MILLI 500 var hi: i64 = PEP_PI_HI_MILLI 501 var i: i64 = 0 502 while i < PEP_PI_ITERS { 503 let mid: i64 = (lo + hi) / 2 504 let c: i64 = pep_charge_q3(seq, mid) 505 if c > 0 { lo = mid } 506 if c <= 0 { hi = mid } 507 i = i + 1 508 } 509 return (lo + hi) / 2 510} 511 512// ===== UV absorbance at 280 nm ======================================== 513// 514// Molar extinction is EXACT integer arithmetic (Pace 1995): only Trp, Tyr 515// and disulfide-bonded cystine absorb. n_cystine is a DISULFIDE count and 516// must be supplied by the caller -- it is not derivable from sequence 517// alone, and guessing it would be fabrication. A peptide with no W and no 518// Y has extinction 0, meaning A280 CANNOT be used to quantify it: a real 519// and frequently-missed laboratory constraint. 520 521func pep_ext_coeff_280(seq: *u8, n_cystine: i64) -> i64 { 522 if pep_seq_valid(seq) != 1 { return PEP_INVALID } 523 let n_w: i64 = pep_count_aa(seq, PEP_W) 524 let n_y: i64 = pep_count_aa(seq, PEP_Y) 525 var e: i64 = n_w * PEP_EXT_TRP 526 e = e + n_y * PEP_EXT_TYR 527 if n_cystine > 0 { e = e + n_cystine * PEP_EXT_CYSTINE } 528 return e 529} 530 531// 1 iff A280 is a usable quantitation method for this peptide. 532func pep_a280_quantifiable(seq: *u8, n_cystine: i64) -> i64 { 533 let e: i64 = pep_ext_coeff_280(seq, n_cystine) 534 if e == PEP_INVALID { return 0 } 535 if e > 0 { return 1 } 536 return 0 537} 538 539// ===== GRAVY (grand average of hydropathy) ============================ 540// 541// Mean Kyte-Doolittle index over the chain, x1000. Positive = hydrophobic 542// (aggregation / membrane-association risk); negative = soluble. 543 544func pep_gravy_q3(seq: *u8) -> i64 { 545 var i: i64 = 0 546 var sum: i64 = 0 547 if pep_seq_valid(seq) != 1 { return PEP_INVALID } 548 let n: i64 = pep_len(seq) 549 if n <= 0 { return PEP_INVALID } 550 while i < n { 551 let c: i64 = seq[i] as i64 552 let aa: i64 = pep_aa_from_char(c) 553 let h: i64 = pep_hydropathy_q3(aa) 554 sum = sum + h 555 i = i + 1 556 } 557 return sum / n 558} 559 560func pep_is_hydrophobic(seq: *u8) -> i64 { 561 let g: i64 = pep_gravy_q3(seq) 562 if g == PEP_INVALID { return 0 } 563 if g > 0 { return 1 } 564 return 0 565}