nx_chem_molecule.nx source
↩ module page · 330 lines · 13177 B
1// nx_chem_molecule.nx -- C2.0 milestone: molecule graph primitive.
2//
3// MolGraph is the typed atom + bond graph used by cheminformatics
4// primitives (SMILES parser, fingerprints, substructure search, etc).
5// Composes nx_chem.nx Element / Molecule (stoichiometric) and
6// nx_chem_periodic.nx AtomicData (per-Z lookups) without modifying
7// either.
8//
9// Field layout chosen to be forward-compatible with EXCEED axes:
10// - aromaticity stored as 6-model bitfield per atom + per bond
11// (E3: 6-aromaticity tuple returned, never silently picked)
12// - stereo stored per-atom (sp3 @ / @@ + atropisomer slots) and
13// per-bond (sp2 E/Z / cis / trans) (E4: stereo round-trip)
14// - isotope + charge + h_count + radical + map_num all explicit
15// (E7: first-class radicals + organometallic-friendly)
16//
17// nx_safety_envelope:
18// intended_use: cheminformatics graph backbone for nx_chem_smiles + downstream
19// sil_target: SIL1
20// evidence: [C2.0 KAT in nx_chem_molecule_test.nx]
21// verdict: BENCH-PENDING
22
23import "nx_kernel_v2.nx"
24
25// Sym IDs (chem family 415xxx, picking up after nx_chem.nx 415001..415003)
26const NX_CHEM_SYM_MOL_GRAPH: nx_int = 415010
27const NX_CHEM_SYM_ATOM: nx_int = 415011
28const NX_CHEM_SYM_BOND: nx_int = 415012
29
30// ---- structured error codes for MolGraph operations ----
31const NX_MOL_OK: nx_int = 0
32const NX_MOL_ERR_ATOM_OVERFLOW: nx_int = 1
33const NX_MOL_ERR_BOND_OVERFLOW: nx_int = 2
34const NX_MOL_ERR_BAD_ATOM_INDEX: nx_int = 3
35const NX_MOL_ERR_BAD_BOND_ORDER: nx_int = 4
36const NX_MOL_ERR_SMILES_BAD_CHAR: nx_int = 5
37const NX_MOL_ERR_SMILES_UNCLOSED_BR: nx_int = 6
38const NX_MOL_ERR_SMILES_UNCLOSED_PAREN: nx_int = 7
39const NX_MOL_ERR_SMILES_ORPHAN_RING: nx_int = 8
40const NX_MOL_ERR_SMILES_EMPTY: nx_int = 9
41const NX_MOL_ERR_SMILES_INVALID_BR: nx_int = 10
42const NX_MOL_ERR_SMILES_BAD_BOND: nx_int = 11
43
44// ---- bond order encoding ----
45const NX_BOND_ZERO: nx_int = 0 // zero-order (dative / virtual)
46const NX_BOND_SINGLE: nx_int = 1
47const NX_BOND_DOUBLE: nx_int = 2
48const NX_BOND_TRIPLE: nx_int = 3
49const NX_BOND_AROMATIC: nx_int = 4
50const NX_BOND_QUAD: nx_int = 5 // quadruple (e.g. Re2Cl8 2-)
51
52// ---- aromaticity model bits (6-tuple per E3 EXCEED axis) ----
53const NX_AROM_DAYLIGHT: nx_int = 1 // bit 0
54const NX_AROM_OPENEYE: nx_int = 2 // bit 1
55const NX_AROM_RDKIT: nx_int = 4 // bit 2
56const NX_AROM_MDL: nx_int = 8 // bit 3
57const NX_AROM_TRIPOS: nx_int = 16 // bit 4
58const NX_AROM_MMFF: nx_int = 32 // bit 5
59
60// ---- stereo encoding (sp3 chirality on atom) ----
61const NX_STEREO_NONE: nx_int = 0
62const NX_STEREO_CCW: nx_int = 1 // @ (counter-clockwise looking from first neighbor)
63const NX_STEREO_CW: nx_int = 2 // @@ (clockwise)
64// Higher values reserved for atropisomer / enhanced-stereo (C2.2+)
65
66// ---- bond stereo (sp2 double bond) ----
67const NX_BSTEREO_NONE: nx_int = 0
68const NX_BSTEREO_UP: nx_int = 1 // /
69const NX_BSTEREO_DOWN: nx_int = 2 // \
70const NX_BSTEREO_CIS: nx_int = 3
71const NX_BSTEREO_TRANS: nx_int = 4
72
73// ---- Atom struct ----
74struct Atom {
75 z: nx_int, // atomic number (0 = wildcard *)
76 isotope: nx_int, // mass number, 0 = natural-abundance default
77 charge: nx_int, // formal charge
78 h_count: nx_int, // explicit H count; -1 = implicit (compute later via valence)
79 radical: nx_int, // 0 = not a radical; >=1 = radical degree
80 aromaticity: nx_int, // 6-bit tuple per aromaticity model (see NX_AROM_*)
81 stereo: nx_int, // 0=none, 1=@, 2=@@, higher reserved
82 map_num: nx_int, // SMIRKS atom mapping number (0 = unmapped)
83 in_ring: nx_int, // 0/1 flag; set after ring perception
84 // C2.3f stereo neighbor permutation (input parse order for chirality reference)
85 // -1 = unfilled, -2 = implicit H sentinel, otherwise atom index
86 stereo_n0: nx_int,
87 stereo_n1: nx_int,
88 stereo_n2: nx_int,
89 stereo_n3: nx_int,
90}
91const NX_ATOM_BYTES: nx_int = 104
92
93// ---- Bond struct ----
94struct Bond {
95 a: nx_int, // atom index (first endpoint)
96 b: nx_int, // atom index (second endpoint)
97 order: nx_int, // 1=single, 2=double, 3=triple, 4=aromatic, 0=zero, 5=quad
98 stereo: nx_int, // 0=none, 1=/, 2=\, 3=cis, 4=trans
99 aromaticity: nx_int, // 6-bit tuple per aromaticity model
100 in_ring: nx_int, // 0/1 flag
101}
102const NX_BOND_BYTES: nx_int = 48
103
104// ---- MolGraph struct ----
105struct MolGraph {
106 atoms: *Atom,
107 n_atoms: nx_int,
108 capacity_atoms: nx_int,
109 bonds: *Bond,
110 n_bonds: nx_int,
111 capacity_bonds: nx_int,
112 is_valid: nx_int, // 1 = healthy graph; 0 = parse / build failure
113 err_code: nx_int, // structured error code (NX_MOL_ERR_*)
114 err_pos: nx_int, // position in source (SMILES char index) when applicable
115}
116const NX_MOL_GRAPH_BYTES: nx_int = 72
117
118// ---- constructor ----
119// Pre-allocate atom + bond buffers. Pages are zeroed by sys_mmap so
120// fields default to 0 (which is the "absent" value for every field).
121func nx_chem_mol_new(cap_atoms: nx_int, cap_bonds: nx_int) -> *MolGraph {
122 let m: *MolGraph = (sys_mmap(NX_MOL_GRAPH_BYTES as i64)) as *MolGraph
123 m.atoms = (sys_mmap((cap_atoms * NX_ATOM_BYTES) as i64)) as *Atom
124 m.n_atoms = 0
125 m.capacity_atoms = cap_atoms
126 m.bonds = (sys_mmap((cap_bonds * NX_BOND_BYTES) as i64)) as *Bond
127 m.n_bonds = 0
128 m.capacity_bonds = cap_bonds
129 m.is_valid = 1
130 m.err_code = NX_MOL_OK
131 m.err_pos = 0
132 return m
133}
134
135// ---- bounded atom lookup ----
136// Returns null if i out of range.
137func nx_chem_mol_atom(m: *MolGraph, i: nx_int) -> *Atom {
138 if i < 0 { return 0 as *Atom }
139 if i >= m.n_atoms { return 0 as *Atom }
140 return ((m.atoms as nx_int) + (i * NX_ATOM_BYTES)) as *Atom
141}
142
143// ---- bounded bond lookup ----
144func nx_chem_mol_bond(m: *MolGraph, i: nx_int) -> *Bond {
145 if i < 0 { return 0 as *Bond }
146 if i >= m.n_bonds { return 0 as *Bond }
147 return ((m.bonds as nx_int) + (i * NX_BOND_BYTES)) as *Bond
148}
149
150// ---- append atom by Z; defaults all other fields to 0 / sentinel ----
151// Returns new atom index >=0 on success; -1 on capacity overflow.
152// On overflow, sets is_valid=0 + err_code=NX_MOL_ERR_ATOM_OVERFLOW.
153func nx_chem_mol_atom_add(m: *MolGraph, z: nx_int) -> nx_int {
154 if m.n_atoms >= m.capacity_atoms {
155 m.is_valid = 0
156 m.err_code = NX_MOL_ERR_ATOM_OVERFLOW
157 return -1
158 }
159 let idx: nx_int = m.n_atoms
160 let a: *Atom = ((m.atoms as nx_int) + (idx * NX_ATOM_BYTES)) as *Atom
161 a.z = z
162 a.isotope = 0
163 a.charge = 0
164 a.h_count = -1 // implicit by default; bracket atoms override
165 a.radical = 0
166 a.aromaticity = 0
167 a.stereo = NX_STEREO_NONE
168 a.map_num = 0
169 a.in_ring = 0
170 a.stereo_n0 = -1
171 a.stereo_n1 = -1
172 a.stereo_n2 = -1
173 a.stereo_n3 = -1
174 m.n_atoms = idx + 1
175 return idx
176}
177
178// ---- append bond between two atom indices ----
179// Returns new bond index >=0 on success; -1 on capacity overflow or
180// invalid endpoint.
181func nx_chem_mol_bond_add(m: *MolGraph, a: nx_int, b: nx_int, order: nx_int) -> nx_int {
182 if m.n_bonds >= m.capacity_bonds {
183 m.is_valid = 0
184 m.err_code = NX_MOL_ERR_BOND_OVERFLOW
185 return -1
186 }
187 if a < 0 {
188 m.is_valid = 0
189 m.err_code = NX_MOL_ERR_BAD_ATOM_INDEX
190 return -1
191 }
192 if a >= m.n_atoms {
193 m.is_valid = 0
194 m.err_code = NX_MOL_ERR_BAD_ATOM_INDEX
195 return -1
196 }
197 if b < 0 {
198 m.is_valid = 0
199 m.err_code = NX_MOL_ERR_BAD_ATOM_INDEX
200 return -1
201 }
202 if b >= m.n_atoms {
203 m.is_valid = 0
204 m.err_code = NX_MOL_ERR_BAD_ATOM_INDEX
205 return -1
206 }
207 if order < 0 {
208 m.is_valid = 0
209 m.err_code = NX_MOL_ERR_BAD_BOND_ORDER
210 return -1
211 }
212 if order > 5 {
213 m.is_valid = 0
214 m.err_code = NX_MOL_ERR_BAD_BOND_ORDER
215 return -1
216 }
217 let idx: nx_int = m.n_bonds
218 let bd: *Bond = ((m.bonds as nx_int) + (idx * NX_BOND_BYTES)) as *Bond
219 bd.a = a
220 bd.b = b
221 bd.order = order
222 bd.stereo = NX_BSTEREO_NONE
223 bd.aromaticity = 0
224 bd.in_ring = 0
225 m.n_bonds = idx + 1
226 return idx
227}
228
229// ---- count atoms with a given atomic number (analog of nx_chem_count_atom) ----
230func nx_chem_mol_count_z(m: *MolGraph, target_z: nx_int) -> nx_int {
231 var total: nx_int = 0
232 var i: nx_int = 0
233 while i < m.n_atoms {
234 let a: *Atom = ((m.atoms as nx_int) + (i * NX_ATOM_BYTES)) as *Atom
235 if a.z == target_z { total = total + 1 }
236 i = i + 1
237 }
238 return total
239}
240
241// =================================================================
242// Convenience builders -- hand-constructed reference molecules for
243// KAT before the SMILES parser ships. These verify the struct layout
244// + builder semantics in isolation.
245// =================================================================
246
247// Water: O with 2 explicit H (h_count = 2 on oxygen).
248// Represented as the single-atom O (H implicit by valence). In a
249// hydrogen-suppressed graph (standard SMILES convention) water is
250// just one atom with z=8, no bonds.
251func nx_chem_mol_water() -> *MolGraph {
252 let m: *MolGraph = nx_chem_mol_new(4, 4)
253 let _o: nx_int = nx_chem_mol_atom_add(m, 8)
254 return m
255}
256
257// Methane: single C with implicit Hs.
258func nx_chem_mol_methane() -> *MolGraph {
259 let m: *MolGraph = nx_chem_mol_new(4, 4)
260 let _c: nx_int = nx_chem_mol_atom_add(m, 6)
261 return m
262}
263
264// Ethanol: C-C-O (CCO in SMILES). 3 atoms, 2 single bonds.
265func nx_chem_mol_ethanol() -> *MolGraph {
266 let m: *MolGraph = nx_chem_mol_new(8, 8)
267 let c1: nx_int = nx_chem_mol_atom_add(m, 6)
268 let c2: nx_int = nx_chem_mol_atom_add(m, 6)
269 let o: nx_int = nx_chem_mol_atom_add(m, 8)
270 let _b1: nx_int = nx_chem_mol_bond_add(m, c1, c2, NX_BOND_SINGLE)
271 let _b2: nx_int = nx_chem_mol_bond_add(m, c2, o, NX_BOND_SINGLE)
272 return m
273}
274
275// Ethene (ethylene): C=C. 2 atoms, 1 double bond.
276func nx_chem_mol_ethene() -> *MolGraph {
277 let m: *MolGraph = nx_chem_mol_new(4, 4)
278 let c1: nx_int = nx_chem_mol_atom_add(m, 6)
279 let c2: nx_int = nx_chem_mol_atom_add(m, 6)
280 let _b: nx_int = nx_chem_mol_bond_add(m, c1, c2, NX_BOND_DOUBLE)
281 return m
282}
283
284// Ethyne (acetylene): C#C. 2 atoms, 1 triple bond.
285func nx_chem_mol_ethyne() -> *MolGraph {
286 let m: *MolGraph = nx_chem_mol_new(4, 4)
287 let c1: nx_int = nx_chem_mol_atom_add(m, 6)
288 let c2: nx_int = nx_chem_mol_atom_add(m, 6)
289 let _b: nx_int = nx_chem_mol_bond_add(m, c1, c2, NX_BOND_TRIPLE)
290 return m
291}
292
293// Benzene: 6 aromatic carbons in a ring. Ships with bond.order =
294// NX_BOND_AROMATIC and aromaticity bit set for all 6 supported models
295// (the canonical Kekule alternative is bonds alternating single/double
296// at the kekulization layer; the aromatic representation is the
297// Daylight + OpenEye + RDKit + MMFF default).
298func nx_chem_mol_benzene() -> *MolGraph {
299 let m: *MolGraph = nx_chem_mol_new(8, 8)
300 let all_arom: nx_int = NX_AROM_DAYLIGHT | NX_AROM_OPENEYE | NX_AROM_RDKIT | NX_AROM_MDL | NX_AROM_TRIPOS | NX_AROM_MMFF
301 // atoms
302 let c0: nx_int = nx_chem_mol_atom_add(m, 6)
303 let c1: nx_int = nx_chem_mol_atom_add(m, 6)
304 let c2: nx_int = nx_chem_mol_atom_add(m, 6)
305 let c3: nx_int = nx_chem_mol_atom_add(m, 6)
306 let c4: nx_int = nx_chem_mol_atom_add(m, 6)
307 let c5: nx_int = nx_chem_mol_atom_add(m, 6)
308 // mark aromatic on all atoms
309 let a0: *Atom = nx_chem_mol_atom(m, c0); a0.aromaticity = all_arom; a0.in_ring = 1
310 let a1: *Atom = nx_chem_mol_atom(m, c1); a1.aromaticity = all_arom; a1.in_ring = 1
311 let a2: *Atom = nx_chem_mol_atom(m, c2); a2.aromaticity = all_arom; a2.in_ring = 1
312 let a3: *Atom = nx_chem_mol_atom(m, c3); a3.aromaticity = all_arom; a3.in_ring = 1
313 let a4: *Atom = nx_chem_mol_atom(m, c4); a4.aromaticity = all_arom; a4.in_ring = 1
314 let a5: *Atom = nx_chem_mol_atom(m, c5); a5.aromaticity = all_arom; a5.in_ring = 1
315 // 6 aromatic bonds forming a ring
316 let b0: nx_int = nx_chem_mol_bond_add(m, c0, c1, NX_BOND_AROMATIC)
317 let b1: nx_int = nx_chem_mol_bond_add(m, c1, c2, NX_BOND_AROMATIC)
318 let b2: nx_int = nx_chem_mol_bond_add(m, c2, c3, NX_BOND_AROMATIC)
319 let b3: nx_int = nx_chem_mol_bond_add(m, c3, c4, NX_BOND_AROMATIC)
320 let b4: nx_int = nx_chem_mol_bond_add(m, c4, c5, NX_BOND_AROMATIC)
321 let b5: nx_int = nx_chem_mol_bond_add(m, c5, c0, NX_BOND_AROMATIC)
322 // mark in-ring + aromaticity on all bonds
323 let bd0: *Bond = nx_chem_mol_bond(m, b0); bd0.aromaticity = all_arom; bd0.in_ring = 1
324 let bd1: *Bond = nx_chem_mol_bond(m, b1); bd1.aromaticity = all_arom; bd1.in_ring = 1
325 let bd2: *Bond = nx_chem_mol_bond(m, b2); bd2.aromaticity = all_arom; bd2.in_ring = 1
326 let bd3: *Bond = nx_chem_mol_bond(m, b3); bd3.aromaticity = all_arom; bd3.in_ring = 1
327 let bd4: *Bond = nx_chem_mol_bond(m, b4); bd4.aromaticity = all_arom; bd4.in_ring = 1
328 let bd5: *Bond = nx_chem_mol_bond(m, b5); bd5.aromaticity = all_arom; bd5.in_ring = 1
329 return m
330}