code wiki / (root) / nx_chem_molecule.nx

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}