code wiki / (root) / nx_chem_valence.nx

nx_chem_valence.nx source

↩ module page · 204 lines · 8747 B

1// nx_chem_valence.nx -- C2.3a milestone: implicit-H valence inference 2// + molecular weight computation. 3// 4// Provides: 5// nx_chem_default_valence(z) -- Daylight 1988 default valence (organic subset) 6// nx_chem_compute_implicit_h(m) -- walks atoms; sets h_count for -1 sentinels per 7// (valence - bond_order_sum) for unambiguous cases 8// nx_chem_mol_weight_q3(m, table) -- sum atomic weights * 1000 (milli-AMU) including 9// implicit Hs; matches NIST WebBook standard molecular weight 10// 11// Honest gaps (preserved for future milestones): 12// - Aromatic non-carbon atoms (n / o / s / p in rings) leave h_count = -1 because pyrrole- 13// vs-pyridine disambiguation requires C2.5 ring-electron-counting; substrate refuses 14// to fabricate H counts rather than guess wrong. 15// - Hypervalent forms (N+5, S+4/+6, P+5, etc.) require explicit bracket specification. 16// - Non-organic-subset atoms (Si, B with non-standard, transition metals, etc.) leave 17// h_count = -1 unless specified in brackets. 18// 19// nx_safety_envelope: 20// intended_use: implicit-H + molecular-weight for cheminformatics + MS downstream 21// sil_target: SIL1 22// evidence: [C2.3a KAT in nx_chem_valence_test.nx; NIST WebBook reference values] 23// verdict: BENCH-PENDING 24 25import "nx_chem.nx" 26import "nx_chem_molecule.nx" 27 28// ================================================================= 29// Default valence per Daylight 1988 organic-subset rules. 30// Returns -1 for elements outside the organic subset (signals 31// "implicit-H not inferred for this element; bracket required"). 32// ================================================================= 33func nx_chem_default_valence(z: nx_int) -> nx_int { 34 if z == 1 { return 1 } // H 35 if z == 5 { return 3 } // B 36 if z == 6 { return 4 } // C 37 if z == 7 { return 3 } // N (default; +5 hypervalent needs bracket) 38 if z == 8 { return 2 } // O 39 if z == 9 { return 1 } // F 40 if z == 15 { return 3 } // P (default; +5 needs bracket) 41 if z == 16 { return 2 } // S (default; +4/+6 needs bracket) 42 if z == 17 { return 1 } // Cl 43 if z == 35 { return 1 } // Br 44 if z == 53 { return 1 } // I 45 return -1 // outside organic subset 46} 47 48// ================================================================= 49// Bond-order summary for one atom. Writes via output pointers: 50// sum_orders_out -- sum where single=1, double=2, triple=3, quad=5, 51// aromatic=1 (the aromatic-bond contribution beyond 52// 1.0 is accounted for via aromatic_count_out) 53// aromatic_count_out -- count of aromatic bonds incident to atom 54// 55// Bond direction: a bond connects atom.a to atom.b; we count the 56// bond for whichever endpoint matches atom_idx. 57// ================================================================= 58func nx_chem_atom_bond_summary( 59 m: *MolGraph, atom_idx: nx_int, 60 sum_orders_out: *nx_int, aromatic_count_out: *nx_int 61) -> nx_int { 62 var sum_orders: nx_int = 0 63 var arom: nx_int = 0 64 var i: nx_int = 0 65 while i < m.n_bonds { 66 let b: *Bond = ((m.bonds as nx_int) + (i * NX_BOND_BYTES)) as *Bond 67 var touches: nx_int = 0 68 if b.a == atom_idx { touches = 1 } 69 if b.b == atom_idx { touches = 1 } 70 if touches == 1 { 71 if b.order == NX_BOND_SINGLE { sum_orders = sum_orders + 1 } 72 if b.order == NX_BOND_DOUBLE { sum_orders = sum_orders + 2 } 73 if b.order == NX_BOND_TRIPLE { sum_orders = sum_orders + 3 } 74 if b.order == NX_BOND_QUAD { sum_orders = sum_orders + 4 } 75 if b.order == NX_BOND_AROMATIC { sum_orders = sum_orders + 1; arom = arom + 1 } 76 } 77 i = i + 1 78 } 79 sum_orders_out[0] = sum_orders 80 aromatic_count_out[0] = arom 81 return 0 82} 83 84// ================================================================= 85// Compute implicit-H for every atom with h_count == -1 (sentinel 86// "implicit, please infer"). Atoms with h_count >= 0 (explicit 87// from brackets) are LEFT ALONE per the substrate's 88// no-overwrite-explicit-data discipline. 89// 90// Rules: 91// - organic-subset aliphatic atom (Atom.aromaticity == 0): 92// h = max(0, default_valence(z) - bond_order_sum) 93// - aromatic carbon (z=6 + aromaticity != 0): 94// h = max(0, 4 - bond_order_sum - 1) [the -1 accounts for the 95// half-order aromatic-electron-pair contribution; benzene C with 96// 2 aromatic bonds gets 4 - 2 - 1 = 1 H] 97// - aromatic non-carbon (n / o / s / p with aromaticity != 0): 98// h_count LEFT at -1 (honest gap; C2.5 ring-electron counting 99// disambiguates pyrrole-N vs pyridine-N) 100// - non-organic-subset atoms (z not in {1,5,6,7,8,9,15,16,17,35,53}): 101// h_count LEFT at -1 (bracket-required) 102// 103// Returns number of atoms whose h_count was filled in. 104// ================================================================= 105func nx_chem_compute_implicit_h(m: *MolGraph) -> nx_int { 106 let sum_io: *nx_int = (sys_mmap(8)) as *nx_int 107 let arom_io: *nx_int = (sys_mmap(8)) as *nx_int 108 var filled: nx_int = 0 109 var i: nx_int = 0 110 while i < m.n_atoms { 111 let a: *Atom = ((m.atoms as nx_int) + (i * NX_ATOM_BYTES)) as *Atom 112 if a.h_count == -1 { 113 let v: nx_int = nx_chem_default_valence(a.z) 114 if v >= 0 { 115 let _r: nx_int = nx_chem_atom_bond_summary(m, i, sum_io, arom_io) 116 let sum_orders: nx_int = sum_io[0] 117 let arom: nx_int = arom_io[0] 118 if a.aromaticity == 0 { 119 // aliphatic 120 var h: nx_int = v - sum_orders 121 if h < 0 { h = 0 } 122 a.h_count = h 123 filled = filled + 1 124 } 125 else { 126 // aromatic 127 if a.z == 6 { 128 // aromatic carbon: subtract 1 for aromatic-electron contribution 129 var h: nx_int = v - sum_orders - 1 130 if h < 0 { h = 0 } 131 a.h_count = h 132 filled = filled + 1 133 } 134 // aromatic non-carbon: leave h_count = -1 (honest gap; C2.5) 135 let _u: nx_int = arom 136 } 137 } 138 } 139 i = i + 1 140 } 141 return filled 142} 143 144// ================================================================= 145// Molecular weight in Q3 milli-AMU. Sums atomic weights from the 146// supplied Element table plus implicit H counts. 147// 148// Caller MUST run nx_chem_compute_implicit_h(m) FIRST to fill in 149// inferable Hs; otherwise h_count = -1 atoms contribute only their 150// heavy-atom mass. Atoms with h_count > 0 contribute h_count * H_mass. 151// 152// Standard atomic weight of H is taken from the table (Z=1). 153// ================================================================= 154func nx_chem_mol_weight_q3(m: *MolGraph, table: *Element) -> nx_int { 155 let h_elem: *Element = ((table as nx_int) + (0 * NX_ELEM_BYTES)) as *Element // Z=1 at index 0 156 let h_mass: nx_int = h_elem.mass_q3 157 var total: nx_int = 0 158 var i: nx_int = 0 159 while i < m.n_atoms { 160 let a: *Atom = ((m.atoms as nx_int) + (i * NX_ATOM_BYTES)) as *Atom 161 // heavy atom mass (skip wildcard z=0) 162 if a.z >= 1 { 163 if a.z <= 118 { 164 let e: *Element = ((table as nx_int) + ((a.z - 1) * NX_ELEM_BYTES)) as *Element 165 total = total + e.mass_q3 166 } 167 } 168 // implicit Hs 169 if a.h_count > 0 { 170 total = total + (a.h_count * h_mass) 171 } 172 i = i + 1 173 } 174 return total 175} 176 177// ================================================================= 178// Heavy-atom count (Z != 1, Z != 0). 179// ================================================================= 180func nx_chem_mol_heavy_atom_count(m: *MolGraph) -> nx_int { 181 var c: nx_int = 0 182 var i: nx_int = 0 183 while i < m.n_atoms { 184 let a: *Atom = ((m.atoms as nx_int) + (i * NX_ATOM_BYTES)) as *Atom 185 if a.z >= 2 { c = c + 1 } 186 i = i + 1 187 } 188 return c 189} 190 191// ================================================================= 192// Total H count (explicit + inferred implicit). 193// ================================================================= 194func nx_chem_mol_h_count_total(m: *MolGraph) -> nx_int { 195 var c: nx_int = 0 196 var i: nx_int = 0 197 while i < m.n_atoms { 198 let a: *Atom = ((m.atoms as nx_int) + (i * NX_ATOM_BYTES)) as *Atom 199 if a.z == 1 { c = c + 1 } 200 if a.h_count > 0 { c = c + a.h_count } 201 i = i + 1 202 } 203 return c 204}