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}