code wiki / (root) / nx_chem_smiles_emit.nx

nx_chem_smiles_emit.nx source

↩ module page · 945 lines · 44337 B

1// nx_chem_smiles_emit.nx -- C2.3b milestone: SMILES emit from MolGraph. 2// 3// DFS-based emit with: 4// - Organic-subset atoms emitted without brackets when possible 5// (no charge, no isotope, no stereo, no map_num, no h_count override) 6// - Bracket atoms for everything else 7// - Bond markers emitted only when non-default 8// (single between aliphatic = default; aromatic between aromatics = default) 9// - Ring closures via single digits 1..9 then %10..%99 10// - Branches via parentheses 11// - Multi-component graphs via '.' separator 12// 13// Round-trip property: parse -> emit -> parse -> emit reaches a fixed 14// point. Two different input SMILES of the same molecule do NOT yet 15// produce byte-identical output -- that requires Morgan canonical 16// ranking (C2.3c). 17// 18// Honest gaps (deferred): 19// - Morgan canonical atom ranking (C2.3c) -- unlocks bit-reproducible 20// canonical SMILES across versions/platforms (landscape EXCEED axis E1) 21// - Stereo emit (@ / @@ for atoms, / \\ for bonds) (C2.3c after canonical) 22// - Bond stereo E/Z resolution (C2.3c CIP rules) 23// 24// nx_safety_envelope: 25// intended_use: MolGraph -> SMILES string emit; round-trip clean 26// sil_target: SIL1 27// evidence: [C2.3b KAT in nx_chem_smiles_emit_test.nx; bounded buffer writes; round-trip via parse] 28// verdict: BENCH-PENDING 29 30import "nx_chem.nx" 31import "nx_chem_molecule.nx" 32import "nx_chem_smiles.nx" 33import "nx_chem_morgan.nx" 34 35const NX_MAX_BACK_PER_ATOM: nx_int = 8 // generous cap; molecules rarely > 4 ring closures per atom 36const NX_MAX_TREE_PER_ATOM: nx_int = 8 // organic atoms have <= 4 bonds typically 37 38// ================================================================= 39// EmitState -- scratch storage for the DFS emit. 40// Per-atom tree-child list + per-atom back-edge digit list. 41// ================================================================= 42struct EmitState { 43 n_atoms: nx_int, 44 visited: *nx_int, // size n_atoms (0/1) 45 in_dfs: *nx_int, // size n_atoms (currently on DFS stack) 46 parent: *nx_int, // size n_atoms; -1 for roots 47 // Back-edge digit registry: digit value + bond-order per (atom, slot) 48 back_digit: *nx_int, // size n_atoms * NX_MAX_BACK_PER_ATOM 49 back_bond: *nx_int, 50 back_count: *nx_int, // size n_atoms 51 next_digit: nx_int, // next free ring digit (1-99) 52 // Tree-child lists: child atom idx + bond order + bond stereo per (atom, slot) 53 tree_child: *nx_int, // size n_atoms * NX_MAX_TREE_PER_ATOM 54 tree_bond: *nx_int, 55 tree_stereo: *nx_int, // size n_atoms * NX_MAX_TREE_PER_ATOM (C2.3d) 56 tree_count: *nx_int, // size n_atoms 57} 58const NX_EMIT_STATE_BYTES: nx_int = 96 59 60// ================================================================= 61// Allocate + initialize EmitState. 62// ================================================================= 63func nx_chem_emit_state_new(n_atoms: nx_int) -> *EmitState { 64 let s: *EmitState = (sys_mmap(NX_EMIT_STATE_BYTES as i64)) as *EmitState 65 s.n_atoms = n_atoms 66 s.visited = (sys_mmap((n_atoms * 8) as i64)) as *nx_int 67 s.in_dfs = (sys_mmap((n_atoms * 8) as i64)) as *nx_int 68 s.parent = (sys_mmap((n_atoms * 8) as i64)) as *nx_int 69 s.back_digit = (sys_mmap((n_atoms * NX_MAX_BACK_PER_ATOM * 8) as i64)) as *nx_int 70 s.back_bond = (sys_mmap((n_atoms * NX_MAX_BACK_PER_ATOM * 8) as i64)) as *nx_int 71 s.back_count = (sys_mmap((n_atoms * 8) as i64)) as *nx_int 72 s.next_digit = 1 73 s.tree_child = (sys_mmap((n_atoms * NX_MAX_TREE_PER_ATOM * 8) as i64)) as *nx_int 74 s.tree_bond = (sys_mmap((n_atoms * NX_MAX_TREE_PER_ATOM * 8) as i64)) as *nx_int 75 s.tree_stereo = (sys_mmap((n_atoms * NX_MAX_TREE_PER_ATOM * 8) as i64)) as *nx_int 76 s.tree_count = (sys_mmap((n_atoms * 8) as i64)) as *nx_int 77 // mmap pages are zero -- parent[i] = 0 will need explicit -1 init 78 var i: nx_int = 0 79 while i < n_atoms { 80 s.parent[i] = -1 81 i = i + 1 82 } 83 return s 84} 85 86// ================================================================= 87// Bounded byte append. Returns 0 on success, -1 on buffer overflow. 88// ================================================================= 89func nx_chem_emit_byte(buf: *u8, len_io: *nx_int, cap: nx_int, b: nx_int) -> nx_int { 90 if len_io[0] >= cap { return -1 } 91 buf[len_io[0]] = b & 0xff 92 len_io[0] = len_io[0] + 1 93 return 0 94} 95 96// ================================================================= 97// Emit unsigned integer in decimal. 98// ================================================================= 99func nx_chem_emit_uint(buf: *u8, len_io: *nx_int, cap: nx_int, v: nx_int) -> nx_int { 100 if v == 0 { return nx_chem_emit_byte(buf, len_io, cap, 0x30) } 101 // up to 10 digits for 32-bit-range; in practice ring digits + isotopes 102 // + charges are <= 3 digits 103 let tmp: *u8 = (sys_mmap(16)) as *u8 104 var n: nx_int = 0 105 var x: nx_int = v 106 while x > 0 { 107 tmp[n] = ((x - ((x / 10) * 10)) + 0x30) & 0xff 108 x = x / 10 109 n = n + 1 110 } 111 var i: nx_int = n - 1 112 while i >= 0 { 113 let r: nx_int = nx_chem_emit_byte(buf, len_io, cap, tmp[i] & 0xff) 114 if r != 0 { return r } 115 i = i - 1 116 } 117 return 0 118} 119 120// ================================================================= 121// Emit a ring-closure digit. Uses 1..9 directly, %10..%99 with the 122// % prefix for two-digit closures. 123// ================================================================= 124func nx_chem_emit_ring_digit(buf: *u8, len_io: *nx_int, cap: nx_int, d: nx_int) -> nx_int { 125 if d < 10 { 126 return nx_chem_emit_byte(buf, len_io, cap, 0x30 + d) 127 } 128 let r1: nx_int = nx_chem_emit_byte(buf, len_io, cap, NX_C_PCT) 129 if r1 != 0 { return r1 } 130 return nx_chem_emit_uint(buf, len_io, cap, d) 131} 132 133// ================================================================= 134// Emit a bond marker. Omits the marker when default (single between 135// non-aromatic atoms, aromatic between two aromatic atoms). 136// 137// pa_arom + nb_arom non-zero = both endpoints aromatic. 138// ================================================================= 139func nx_chem_emit_bond_marker(buf: *u8, len_io: *nx_int, cap: nx_int, order: nx_int, both_aromatic: nx_int, stereo: nx_int) -> nx_int { 140 // Bond stereo (/ or \) for single bonds adjacent to a double bond. 141 // C2.3d: emit stored stereo verbatim; parity correction for cases 142 // where canonical DFS reverses traversal vs input is deferred to C2.3e. 143 if stereo == NX_BSTEREO_UP { 144 return nx_chem_emit_byte(buf, len_io, cap, NX_C_SLASH) 145 } 146 if stereo == NX_BSTEREO_DOWN { 147 return nx_chem_emit_byte(buf, len_io, cap, NX_C_BSLASH) 148 } 149 if order == NX_BOND_SINGLE { 150 // single bond is the default for aliphatic atoms; omit. 151 return 0 152 } 153 if order == NX_BOND_AROMATIC { 154 // aromatic bond by definition connects two aromatic atoms; omit 155 return 0 156 } 157 if order == NX_BOND_DOUBLE { return nx_chem_emit_byte(buf, len_io, cap, NX_C_EQ) } 158 if order == NX_BOND_TRIPLE { return nx_chem_emit_byte(buf, len_io, cap, NX_C_HASH) } 159 if order == NX_BOND_ZERO { return nx_chem_emit_byte(buf, len_io, cap, NX_C_MINUS) } 160 return 0 161} 162 163// ================================================================= 164// Z -> organic-subset uppercase symbol (1 or 2 chars). 165// Returns symbol length on match (1 or 2), 0 if z not in organic subset. 166// ================================================================= 167func nx_chem_z_to_organic_aliphatic(z: nx_int, c1_out: *nx_int, c2_out: *nx_int) -> nx_int { 168 if z == 5 { c1_out[0] = 0x42; c2_out[0] = 0; return 1 } // B 169 if z == 6 { c1_out[0] = 0x43; c2_out[0] = 0; return 1 } // C 170 if z == 7 { c1_out[0] = 0x4E; c2_out[0] = 0; return 1 } // N 171 if z == 8 { c1_out[0] = 0x4F; c2_out[0] = 0; return 1 } // O 172 if z == 9 { c1_out[0] = 0x46; c2_out[0] = 0; return 1 } // F 173 if z == 15 { c1_out[0] = 0x50; c2_out[0] = 0; return 1 } // P 174 if z == 16 { c1_out[0] = 0x53; c2_out[0] = 0; return 1 } // S 175 if z == 17 { c1_out[0] = 0x43; c2_out[0] = 0x6C; return 2 } // Cl 176 if z == 35 { c1_out[0] = 0x42; c2_out[0] = 0x72; return 2 } // Br 177 if z == 53 { c1_out[0] = 0x49; c2_out[0] = 0; return 1 } // I 178 return 0 179} 180 181// ================================================================= 182// Z -> aromatic lowercase symbol (single char). 183// ================================================================= 184func nx_chem_z_to_aromatic_lower(z: nx_int) -> nx_int { 185 if z == 5 { return 0x62 } // b 186 if z == 6 { return 0x63 } // c 187 if z == 7 { return 0x6E } // n 188 if z == 8 { return 0x6F } // o 189 if z == 15 { return 0x70 } // p 190 if z == 16 { return 0x73 } // s 191 return 0 192} 193 194// ================================================================= 195// Z -> any-element symbol for bracket atoms (1 or 2 chars). 196// Covers all 118 elements. Returns symbol length. 197// ================================================================= 198func nx_chem_z_to_bracket_symbol(z: nx_int, c1_out: *nx_int, c2_out: *nx_int) -> nx_int { 199 // Single-char first 200 if z == 1 { c1_out[0] = 0x48; c2_out[0] = 0; return 1 } // H 201 if z == 5 { c1_out[0] = 0x42; c2_out[0] = 0; return 1 } // B 202 if z == 6 { c1_out[0] = 0x43; c2_out[0] = 0; return 1 } // C 203 if z == 7 { c1_out[0] = 0x4E; c2_out[0] = 0; return 1 } // N 204 if z == 8 { c1_out[0] = 0x4F; c2_out[0] = 0; return 1 } // O 205 if z == 9 { c1_out[0] = 0x46; c2_out[0] = 0; return 1 } // F 206 if z == 15 { c1_out[0] = 0x50; c2_out[0] = 0; return 1 } // P 207 if z == 16 { c1_out[0] = 0x53; c2_out[0] = 0; return 1 } // S 208 if z == 19 { c1_out[0] = 0x4B; c2_out[0] = 0; return 1 } // K 209 if z == 23 { c1_out[0] = 0x56; c2_out[0] = 0; return 1 } // V 210 if z == 39 { c1_out[0] = 0x59; c2_out[0] = 0; return 1 } // Y 211 if z == 53 { c1_out[0] = 0x49; c2_out[0] = 0; return 1 } // I 212 if z == 74 { c1_out[0] = 0x57; c2_out[0] = 0; return 1 } // W 213 if z == 92 { c1_out[0] = 0x55; c2_out[0] = 0; return 1 } // U 214 // Two-char (large switch keyed by z) 215 if z == 2 { c1_out[0] = 0x48; c2_out[0] = 0x65; return 2 } // He 216 if z == 3 { c1_out[0] = 0x4C; c2_out[0] = 0x69; return 2 } // Li 217 if z == 4 { c1_out[0] = 0x42; c2_out[0] = 0x65; return 2 } // Be 218 if z == 10 { c1_out[0] = 0x4E; c2_out[0] = 0x65; return 2 } // Ne 219 if z == 11 { c1_out[0] = 0x4E; c2_out[0] = 0x61; return 2 } // Na 220 if z == 12 { c1_out[0] = 0x4D; c2_out[0] = 0x67; return 2 } // Mg 221 if z == 13 { c1_out[0] = 0x41; c2_out[0] = 0x6C; return 2 } // Al 222 if z == 14 { c1_out[0] = 0x53; c2_out[0] = 0x69; return 2 } // Si 223 if z == 17 { c1_out[0] = 0x43; c2_out[0] = 0x6C; return 2 } // Cl 224 if z == 18 { c1_out[0] = 0x41; c2_out[0] = 0x72; return 2 } // Ar 225 if z == 20 { c1_out[0] = 0x43; c2_out[0] = 0x61; return 2 } // Ca 226 if z == 21 { c1_out[0] = 0x53; c2_out[0] = 0x63; return 2 } // Sc 227 if z == 22 { c1_out[0] = 0x54; c2_out[0] = 0x69; return 2 } // Ti 228 if z == 24 { c1_out[0] = 0x43; c2_out[0] = 0x72; return 2 } // Cr 229 if z == 25 { c1_out[0] = 0x4D; c2_out[0] = 0x6E; return 2 } // Mn 230 if z == 26 { c1_out[0] = 0x46; c2_out[0] = 0x65; return 2 } // Fe 231 if z == 27 { c1_out[0] = 0x43; c2_out[0] = 0x6F; return 2 } // Co 232 if z == 28 { c1_out[0] = 0x4E; c2_out[0] = 0x69; return 2 } // Ni 233 if z == 29 { c1_out[0] = 0x43; c2_out[0] = 0x75; return 2 } // Cu 234 if z == 30 { c1_out[0] = 0x5A; c2_out[0] = 0x6E; return 2 } // Zn 235 if z == 31 { c1_out[0] = 0x47; c2_out[0] = 0x61; return 2 } // Ga 236 if z == 32 { c1_out[0] = 0x47; c2_out[0] = 0x65; return 2 } // Ge 237 if z == 33 { c1_out[0] = 0x41; c2_out[0] = 0x73; return 2 } // As 238 if z == 34 { c1_out[0] = 0x53; c2_out[0] = 0x65; return 2 } // Se 239 if z == 35 { c1_out[0] = 0x42; c2_out[0] = 0x72; return 2 } // Br 240 if z == 36 { c1_out[0] = 0x4B; c2_out[0] = 0x72; return 2 } // Kr 241 if z == 37 { c1_out[0] = 0x52; c2_out[0] = 0x62; return 2 } // Rb 242 if z == 38 { c1_out[0] = 0x53; c2_out[0] = 0x72; return 2 } // Sr 243 if z == 40 { c1_out[0] = 0x5A; c2_out[0] = 0x72; return 2 } // Zr 244 if z == 41 { c1_out[0] = 0x4E; c2_out[0] = 0x62; return 2 } // Nb 245 if z == 42 { c1_out[0] = 0x4D; c2_out[0] = 0x6F; return 2 } // Mo 246 if z == 43 { c1_out[0] = 0x54; c2_out[0] = 0x63; return 2 } // Tc 247 if z == 44 { c1_out[0] = 0x52; c2_out[0] = 0x75; return 2 } // Ru 248 if z == 45 { c1_out[0] = 0x52; c2_out[0] = 0x68; return 2 } // Rh 249 if z == 46 { c1_out[0] = 0x50; c2_out[0] = 0x64; return 2 } // Pd 250 if z == 47 { c1_out[0] = 0x41; c2_out[0] = 0x67; return 2 } // Ag 251 if z == 48 { c1_out[0] = 0x43; c2_out[0] = 0x64; return 2 } // Cd 252 if z == 49 { c1_out[0] = 0x49; c2_out[0] = 0x6E; return 2 } // In 253 if z == 50 { c1_out[0] = 0x53; c2_out[0] = 0x6E; return 2 } // Sn 254 if z == 51 { c1_out[0] = 0x53; c2_out[0] = 0x62; return 2 } // Sb 255 if z == 52 { c1_out[0] = 0x54; c2_out[0] = 0x65; return 2 } // Te 256 if z == 54 { c1_out[0] = 0x58; c2_out[0] = 0x65; return 2 } // Xe 257 if z == 55 { c1_out[0] = 0x43; c2_out[0] = 0x73; return 2 } // Cs 258 if z == 56 { c1_out[0] = 0x42; c2_out[0] = 0x61; return 2 } // Ba 259 if z == 57 { c1_out[0] = 0x4C; c2_out[0] = 0x61; return 2 } // La 260 if z == 58 { c1_out[0] = 0x43; c2_out[0] = 0x65; return 2 } // Ce 261 if z == 59 { c1_out[0] = 0x50; c2_out[0] = 0x72; return 2 } // Pr 262 if z == 60 { c1_out[0] = 0x4E; c2_out[0] = 0x64; return 2 } // Nd 263 if z == 61 { c1_out[0] = 0x50; c2_out[0] = 0x6D; return 2 } // Pm 264 if z == 62 { c1_out[0] = 0x53; c2_out[0] = 0x6D; return 2 } // Sm 265 if z == 63 { c1_out[0] = 0x45; c2_out[0] = 0x75; return 2 } // Eu 266 if z == 64 { c1_out[0] = 0x47; c2_out[0] = 0x64; return 2 } // Gd 267 if z == 65 { c1_out[0] = 0x54; c2_out[0] = 0x62; return 2 } // Tb 268 if z == 66 { c1_out[0] = 0x44; c2_out[0] = 0x79; return 2 } // Dy 269 if z == 67 { c1_out[0] = 0x48; c2_out[0] = 0x6F; return 2 } // Ho 270 if z == 68 { c1_out[0] = 0x45; c2_out[0] = 0x72; return 2 } // Er 271 if z == 69 { c1_out[0] = 0x54; c2_out[0] = 0x6D; return 2 } // Tm 272 if z == 70 { c1_out[0] = 0x59; c2_out[0] = 0x62; return 2 } // Yb 273 if z == 71 { c1_out[0] = 0x4C; c2_out[0] = 0x75; return 2 } // Lu 274 if z == 72 { c1_out[0] = 0x48; c2_out[0] = 0x66; return 2 } // Hf 275 if z == 73 { c1_out[0] = 0x54; c2_out[0] = 0x61; return 2 } // Ta 276 if z == 75 { c1_out[0] = 0x52; c2_out[0] = 0x65; return 2 } // Re 277 if z == 76 { c1_out[0] = 0x4F; c2_out[0] = 0x73; return 2 } // Os 278 if z == 77 { c1_out[0] = 0x49; c2_out[0] = 0x72; return 2 } // Ir 279 if z == 78 { c1_out[0] = 0x50; c2_out[0] = 0x74; return 2 } // Pt 280 if z == 79 { c1_out[0] = 0x41; c2_out[0] = 0x75; return 2 } // Au 281 if z == 80 { c1_out[0] = 0x48; c2_out[0] = 0x67; return 2 } // Hg 282 if z == 81 { c1_out[0] = 0x54; c2_out[0] = 0x6C; return 2 } // Tl 283 if z == 82 { c1_out[0] = 0x50; c2_out[0] = 0x62; return 2 } // Pb 284 if z == 83 { c1_out[0] = 0x42; c2_out[0] = 0x69; return 2 } // Bi 285 if z == 84 { c1_out[0] = 0x50; c2_out[0] = 0x6F; return 2 } // Po 286 if z == 85 { c1_out[0] = 0x41; c2_out[0] = 0x74; return 2 } // At 287 if z == 86 { c1_out[0] = 0x52; c2_out[0] = 0x6E; return 2 } // Rn 288 if z == 87 { c1_out[0] = 0x46; c2_out[0] = 0x72; return 2 } // Fr 289 if z == 88 { c1_out[0] = 0x52; c2_out[0] = 0x61; return 2 } // Ra 290 if z == 89 { c1_out[0] = 0x41; c2_out[0] = 0x63; return 2 } // Ac 291 if z == 90 { c1_out[0] = 0x54; c2_out[0] = 0x68; return 2 } // Th 292 if z == 91 { c1_out[0] = 0x50; c2_out[0] = 0x61; return 2 } // Pa 293 if z == 93 { c1_out[0] = 0x4E; c2_out[0] = 0x70; return 2 } // Np 294 if z == 94 { c1_out[0] = 0x50; c2_out[0] = 0x75; return 2 } // Pu 295 if z == 95 { c1_out[0] = 0x41; c2_out[0] = 0x6D; return 2 } // Am 296 if z == 96 { c1_out[0] = 0x43; c2_out[0] = 0x6D; return 2 } // Cm 297 if z == 97 { c1_out[0] = 0x42; c2_out[0] = 0x6B; return 2 } // Bk 298 if z == 98 { c1_out[0] = 0x43; c2_out[0] = 0x66; return 2 } // Cf 299 if z == 99 { c1_out[0] = 0x45; c2_out[0] = 0x73; return 2 } // Es 300 if z == 100 { c1_out[0] = 0x46; c2_out[0] = 0x6D; return 2 } // Fm 301 if z == 101 { c1_out[0] = 0x4D; c2_out[0] = 0x64; return 2 } // Md 302 if z == 102 { c1_out[0] = 0x4E; c2_out[0] = 0x6F; return 2 } // No 303 if z == 103 { c1_out[0] = 0x4C; c2_out[0] = 0x72; return 2 } // Lr 304 if z == 104 { c1_out[0] = 0x52; c2_out[0] = 0x66; return 2 } // Rf 305 if z == 105 { c1_out[0] = 0x44; c2_out[0] = 0x62; return 2 } // Db 306 if z == 106 { c1_out[0] = 0x53; c2_out[0] = 0x67; return 2 } // Sg 307 if z == 107 { c1_out[0] = 0x42; c2_out[0] = 0x68; return 2 } // Bh 308 if z == 108 { c1_out[0] = 0x48; c2_out[0] = 0x73; return 2 } // Hs 309 if z == 109 { c1_out[0] = 0x4D; c2_out[0] = 0x74; return 2 } // Mt 310 if z == 110 { c1_out[0] = 0x44; c2_out[0] = 0x73; return 2 } // Ds 311 if z == 111 { c1_out[0] = 0x52; c2_out[0] = 0x67; return 2 } // Rg 312 if z == 112 { c1_out[0] = 0x43; c2_out[0] = 0x6E; return 2 } // Cn 313 if z == 113 { c1_out[0] = 0x4E; c2_out[0] = 0x68; return 2 } // Nh 314 if z == 114 { c1_out[0] = 0x46; c2_out[0] = 0x6C; return 2 } // Fl 315 if z == 115 { c1_out[0] = 0x4D; c2_out[0] = 0x63; return 2 } // Mc 316 if z == 116 { c1_out[0] = 0x4C; c2_out[0] = 0x76; return 2 } // Lv 317 if z == 117 { c1_out[0] = 0x54; c2_out[0] = 0x73; return 2 } // Ts 318 if z == 118 { c1_out[0] = 0x4F; c2_out[0] = 0x67; return 2 } // Og 319 return 0 320} 321 322// ================================================================= 323// Decide whether an atom needs brackets. Returns 1 if brackets 324// required, 0 if can be emitted without brackets. 325// 326// Rules: 327// - z = 0 (wildcard *) -> no brackets, emit "*" 328// - charge != 0 -> brackets 329// - isotope != 0 -> brackets 330// - stereo != 0 -> brackets (so the @/@@ emit has a frame) 331// - map_num != 0 -> brackets 332// - z not in organic subset (B/C/N/O/P/S/F/Cl/Br/I) -> brackets 333// - z in organic subset, aromaticity != 0 -> no brackets (lowercase) 334// - z in organic subset, aliphatic -> no brackets (uppercase) 335// ================================================================= 336func nx_chem_atom_needs_brackets(a: *Atom) -> nx_int { 337 if a.z == 0 { return 0 } 338 if a.charge != 0 { return 1 } 339 if a.isotope != 0 { return 1 } 340 if a.stereo != 0 { return 1 } 341 if a.map_num != 0 { return 1 } 342 // organic subset check 343 if a.z == 5 { return 0 } 344 if a.z == 6 { return 0 } 345 if a.z == 7 { return 0 } 346 if a.z == 8 { return 0 } 347 if a.z == 9 { return 0 } 348 if a.z == 15 { return 0 } 349 if a.z == 16 { return 0 } 350 if a.z == 17 { return 0 } 351 if a.z == 35 { return 0 } 352 if a.z == 53 { return 0 } 353 return 1 354} 355 356// ================================================================= 357// Emit a single atom. Brackets when needed; bare symbol otherwise. 358// ================================================================= 359// C2.3f: emit_atom with parity-corrected stereo override. 360// stereo_override: -1 means use a.stereo verbatim; >=0 means use this value instead. 361func nx_chem_emit_atom_with_stereo(buf: *u8, len_io: *nx_int, cap: nx_int, a: *Atom, stereo_override: nx_int) -> nx_int { 362 var effective_stereo: nx_int = a.stereo 363 if stereo_override >= 0 { effective_stereo = stereo_override } 364 // Wildcard 365 if a.z == 0 { 366 return nx_chem_emit_byte(buf, len_io, cap, NX_C_STAR) 367 } 368 let c1_io: *nx_int = (sys_mmap(8)) as *nx_int 369 let c2_io: *nx_int = (sys_mmap(8)) as *nx_int 370 let needs_br: nx_int = nx_chem_atom_needs_brackets(a) 371 if needs_br == 0 { 372 // Aromatic lowercase if aromatic, else aliphatic uppercase 373 if a.aromaticity != 0 { 374 let alc: nx_int = nx_chem_z_to_aromatic_lower(a.z) 375 if alc > 0 { 376 return nx_chem_emit_byte(buf, len_io, cap, alc) 377 } 378 // aromatic but not lowercase-representable (e.g. Cl in aromatic context — rare); fall through to brackets 379 } 380 let alen: nx_int = nx_chem_z_to_organic_aliphatic(a.z, c1_io, c2_io) 381 if alen >= 1 { 382 let r1: nx_int = nx_chem_emit_byte(buf, len_io, cap, c1_io[0]) 383 if r1 != 0 { return r1 } 384 if alen == 2 { 385 let r2: nx_int = nx_chem_emit_byte(buf, len_io, cap, c2_io[0]) 386 if r2 != 0 { return r2 } 387 } 388 return 0 389 } 390 } 391 // Bracket emit 392 let r_l: nx_int = nx_chem_emit_byte(buf, len_io, cap, NX_C_LBRACKET) 393 if r_l != 0 { return r_l } 394 if a.isotope != 0 { 395 let r_i: nx_int = nx_chem_emit_uint(buf, len_io, cap, a.isotope) 396 if r_i != 0 { return r_i } 397 } 398 let slen: nx_int = nx_chem_z_to_bracket_symbol(a.z, c1_io, c2_io) 399 if slen == 0 { 400 // Unknown element; emit "*" as fallback inside brackets 401 let r_star: nx_int = nx_chem_emit_byte(buf, len_io, cap, NX_C_STAR) 402 if r_star != 0 { return r_star } 403 } 404 else { 405 let r_c1: nx_int = nx_chem_emit_byte(buf, len_io, cap, c1_io[0]) 406 if r_c1 != 0 { return r_c1 } 407 if slen == 2 { 408 let r_c2: nx_int = nx_chem_emit_byte(buf, len_io, cap, c2_io[0]) 409 if r_c2 != 0 { return r_c2 } 410 } 411 } 412 // stereo @ (C2.3f: parity-corrected via effective_stereo) 413 if effective_stereo == NX_STEREO_CCW { 414 let r_at: nx_int = nx_chem_emit_byte(buf, len_io, cap, NX_C_AT) 415 if r_at != 0 { return r_at } 416 } 417 if effective_stereo == NX_STEREO_CW { 418 let r_at1: nx_int = nx_chem_emit_byte(buf, len_io, cap, NX_C_AT) 419 if r_at1 != 0 { return r_at1 } 420 let r_at2: nx_int = nx_chem_emit_byte(buf, len_io, cap, NX_C_AT) 421 if r_at2 != 0 { return r_at2 } 422 } 423 // H count 424 if a.h_count > 0 { 425 let r_h: nx_int = nx_chem_emit_byte(buf, len_io, cap, NX_C_H_UP) 426 if r_h != 0 { return r_h } 427 if a.h_count > 1 { 428 let r_hn: nx_int = nx_chem_emit_uint(buf, len_io, cap, a.h_count) 429 if r_hn != 0 { return r_hn } 430 } 431 } 432 // charge 433 if a.charge > 0 { 434 let r_pl: nx_int = nx_chem_emit_byte(buf, len_io, cap, NX_C_PLUS) 435 if r_pl != 0 { return r_pl } 436 if a.charge > 1 { 437 let r_pn: nx_int = nx_chem_emit_uint(buf, len_io, cap, a.charge) 438 if r_pn != 0 { return r_pn } 439 } 440 } 441 if a.charge < 0 { 442 let r_mi: nx_int = nx_chem_emit_byte(buf, len_io, cap, NX_C_MINUS) 443 if r_mi != 0 { return r_mi } 444 let mag: nx_int = 0 - a.charge 445 if mag > 1 { 446 let r_mn: nx_int = nx_chem_emit_uint(buf, len_io, cap, mag) 447 if r_mn != 0 { return r_mn } 448 } 449 } 450 // atom map 451 if a.map_num != 0 { 452 let r_co: nx_int = nx_chem_emit_byte(buf, len_io, cap, NX_C_COLON) 453 if r_co != 0 { return r_co } 454 let r_mp: nx_int = nx_chem_emit_uint(buf, len_io, cap, a.map_num) 455 if r_mp != 0 { return r_mp } 456 } 457 return nx_chem_emit_byte(buf, len_io, cap, NX_C_RBRACKET) 458} 459 460// ================================================================= 461// Wrapper for back-compat: emit atom with default (no override). 462// ================================================================= 463func nx_chem_emit_atom(buf: *u8, len_io: *nx_int, cap: nx_int, a: *Atom) -> nx_int { 464 return nx_chem_emit_atom_with_stereo(buf, len_io, cap, a, -1) 465} 466 467// ================================================================= 468// Pre-scan DFS: identify tree edges + back-edges + assign ring digits. 469// 470// Uses an explicit stack to avoid recursion depth issues. For each 471// atom processed: 472// - mark visited 473// - enumerate incident edges 474// - for each neighbor: 475// if neighbor visited && neighbor != parent && not already recorded: 476// allocate next ring digit; record on BOTH endpoints 477// else if neighbor not visited: 478// add as tree child + push onto DFS stack 479// ================================================================= 480func nx_chem_emit_prescan(m: *MolGraph, s: *EmitState, root: nx_int) -> nx_int { 481 // explicit DFS stack of atom indices 482 let stack: *nx_int = (sys_mmap((s.n_atoms * 8) as i64)) as *nx_int 483 var stack_n: nx_int = 0 484 stack[stack_n] = root 485 stack_n = stack_n + 1 486 s.visited[root] = 1 487 while stack_n > 0 { 488 stack_n = stack_n - 1 489 let u: nx_int = stack[stack_n] 490 // enumerate incident edges 491 var ei: nx_int = 0 492 while ei < m.n_bonds { 493 let b: *Bond = ((m.bonds as nx_int) + (ei * NX_BOND_BYTES)) as *Bond 494 var v: nx_int = -1 495 if b.a == u { v = b.b } 496 if b.b == u { v = b.a } 497 if v >= 0 { 498 if v != s.parent[u] { 499 if s.visited[v] == 1 { 500 // back-edge u-v; record only once (when u > v) 501 if u > v { 502 // assign next digit 503 let d: nx_int = s.next_digit 504 s.next_digit = s.next_digit + 1 505 // record on u 506 let cu: nx_int = s.back_count[u] 507 if cu < NX_MAX_BACK_PER_ATOM { 508 s.back_digit[(u * NX_MAX_BACK_PER_ATOM) + cu] = d 509 s.back_bond[(u * NX_MAX_BACK_PER_ATOM) + cu] = b.order 510 s.back_count[u] = cu + 1 511 } 512 // record on v 513 let cv: nx_int = s.back_count[v] 514 if cv < NX_MAX_BACK_PER_ATOM { 515 s.back_digit[(v * NX_MAX_BACK_PER_ATOM) + cv] = d 516 s.back_bond[(v * NX_MAX_BACK_PER_ATOM) + cv] = b.order 517 s.back_count[v] = cv + 1 518 } 519 } 520 } 521 else { 522 // tree edge u -> v 523 // C2.3e: flip bond stereo if DFS direction (u->v) is reversed 524 // relative to bond's stored (a->b) direction. UP <-> DOWN keeps 525 // cis/trans semantics intact. 526 var bond_stereo: nx_int = b.stereo 527 if u == b.b { 528 if bond_stereo == NX_BSTEREO_UP { bond_stereo = NX_BSTEREO_DOWN } 529 else { 530 if bond_stereo == NX_BSTEREO_DOWN { bond_stereo = NX_BSTEREO_UP } 531 } 532 } 533 let ct: nx_int = s.tree_count[u] 534 if ct < NX_MAX_TREE_PER_ATOM { 535 s.tree_child[(u * NX_MAX_TREE_PER_ATOM) + ct] = v 536 s.tree_bond[(u * NX_MAX_TREE_PER_ATOM) + ct] = b.order 537 s.tree_stereo[(u * NX_MAX_TREE_PER_ATOM) + ct] = bond_stereo 538 s.tree_count[u] = ct + 1 539 } 540 s.visited[v] = 1 541 s.parent[v] = u 542 // push v on stack (LIFO: depth-first; but for stable emit-order 543 // we want children visited in bond-index order, so we push in REVERSE order) 544 // For now, simple push; this affects branch ordering but not correctness. 545 stack[stack_n] = v 546 stack_n = stack_n + 1 547 } 548 } 549 } 550 ei = ei + 1 551 } 552 } 553 return 0 554} 555 556// ================================================================= 557// Emit a single DFS subtree rooted at atom_idx. bond_in is the bond 558// order of the edge from parent to atom_idx (or NX_BOND_SINGLE for 559// the root since that bond marker is omitted by default rules anyway). 560// 561// from_aromatic = 1 if the parent atom was aromatic (used to decide 562// whether to omit aromatic bond marker). 563// ================================================================= 564 565// ================================================================= 566// C2.3f: compute parity-corrected stereo for a chiral atom at emit time. 567// 568// Returns: 569// -1 if atom has no stereo or correction can't be determined (passes 570// through emit_atom which uses a.stereo verbatim) 571// NX_STEREO_CCW or NX_STEREO_CW: the corrected marker for canonical emit 572// 573// Algorithm: 574// 1. Build input order from a.stereo_n0..n3 (set at parse time). 575// 2. Build emit order: parent (if any) + implicit H (if h_count > 0) + 576// ring closures + tree children. For MVP we skip atoms with 577// ring closures (back_count > 0). 578// 3. Compute permutation parity (count inversions) between input 579// order and emit order. 580// 4. If parity is even, return a.stereo unchanged. 581// If parity is odd, return flipped (CCW <-> CW). 582// ================================================================= 583func nx_chem_compute_stereo_parity(s: *EmitState, atom_idx: nx_int, a: *Atom) -> nx_int { 584 if a.stereo == NX_STEREO_NONE { return -1 } 585 // Verify stereo_n* is fully populated (parse-time may leave -1 if 586 // atom had fewer than expected neighbors). 587 if a.stereo_n3 == -1 { return -1 } 588 // MVP: skip atoms with ring closures (back-edges). They need their 589 // own neighbor-order computation (back-edges interleave with tree 590 // children at the bracket). 591 if s.back_count[atom_idx] > 0 { return -1 } 592 // Build emit order 593 let emit_order: *nx_int = (sys_mmap(64)) as *nx_int 594 emit_order[0] = -1 595 emit_order[1] = -1 596 emit_order[2] = -1 597 emit_order[3] = -1 598 var pos: nx_int = 0 599 let parent: nx_int = s.parent[atom_idx] 600 if parent >= 0 { 601 emit_order[pos] = parent 602 pos = pos + 1 603 } 604 if a.h_count > 0 { 605 if pos < 4 { 606 emit_order[pos] = -2 607 pos = pos + 1 608 } 609 } 610 // tree children in tree_child order (rank-sorted by canonical prescan) 611 var ti: nx_int = 0 612 while ti < s.tree_count[atom_idx] { 613 if pos < 4 { 614 emit_order[pos] = s.tree_child[(atom_idx * NX_MAX_TREE_PER_ATOM) + ti] 615 pos = pos + 1 616 } 617 ti = ti + 1 618 } 619 if pos != 4 { return -1 } 620 // Compute permutation map: for each emit[i], find position in input 621 let input_order: *nx_int = (sys_mmap(64)) as *nx_int 622 input_order[0] = a.stereo_n0 623 input_order[1] = a.stereo_n1 624 input_order[2] = a.stereo_n2 625 input_order[3] = a.stereo_n3 626 let map: *nx_int = (sys_mmap(64)) as *nx_int 627 var mi: nx_int = 0 628 while mi < 4 { 629 var mj: nx_int = 0 630 var found: nx_int = -1 631 while mj < 4 { 632 if input_order[mj] == emit_order[mi] { found = mj } 633 mj = mj + 1 634 } 635 if found < 0 { return -1 } // emit element not found in input -> can't determine parity 636 map[mi] = found 637 mi = mi + 1 638 } 639 // Count inversions in map 640 var inv: nx_int = 0 641 var pi: nx_int = 0 642 while pi < 4 { 643 var pj: nx_int = pi + 1 644 while pj < 4 { 645 if map[pi] > map[pj] { inv = inv + 1 } 646 pj = pj + 1 647 } 648 pi = pi + 1 649 } 650 // Parity: even -> keep, odd -> flip 651 let odd: nx_int = inv & 1 652 if odd == 0 { return a.stereo } 653 if a.stereo == NX_STEREO_CCW { return NX_STEREO_CW } 654 if a.stereo == NX_STEREO_CW { return NX_STEREO_CCW } 655 return a.stereo 656} 657 658func nx_chem_emit_subtree( 659 buf: *u8, len_io: *nx_int, cap: nx_int, 660 m: *MolGraph, s: *EmitState, 661 atom_idx: nx_int, bond_in: nx_int, bond_in_stereo: nx_int, from_aromatic: nx_int 662) -> nx_int { 663 let a: *Atom = ((m.atoms as nx_int) + (atom_idx * NX_ATOM_BYTES)) as *Atom 664 var both_arom: nx_int = 0 665 if from_aromatic != 0 { 666 if a.aromaticity != 0 { both_arom = 1 } 667 } 668 let r_bm: nx_int = nx_chem_emit_bond_marker(buf, len_io, cap, bond_in, both_arom, bond_in_stereo) 669 if r_bm != 0 { return r_bm } 670 // emit atom (C2.3f: compute parity-corrected stereo for chiral centers) 671 let corrected_stereo: nx_int = nx_chem_compute_stereo_parity(s, atom_idx, a) 672 let r_at: nx_int = nx_chem_emit_atom_with_stereo(buf, len_io, cap, a, corrected_stereo) 673 if r_at != 0 { return r_at } 674 // emit ring closures attached to this atom (back-edge bonds have no stereo in C2.3d) 675 var bi: nx_int = 0 676 while bi < s.back_count[atom_idx] { 677 let d: nx_int = s.back_digit[(atom_idx * NX_MAX_BACK_PER_ATOM) + bi] 678 let bo: nx_int = s.back_bond[(atom_idx * NX_MAX_BACK_PER_ATOM) + bi] 679 let r_bbm: nx_int = nx_chem_emit_bond_marker(buf, len_io, cap, bo, both_arom, NX_BSTEREO_NONE) 680 if r_bbm != 0 { return r_bbm } 681 let r_rd: nx_int = nx_chem_emit_ring_digit(buf, len_io, cap, d) 682 if r_rd != 0 { return r_rd } 683 bi = bi + 1 684 } 685 // recurse into tree children; all but last are branches in parens 686 let n_children: nx_int = s.tree_count[atom_idx] 687 var ci: nx_int = 0 688 while ci < n_children { 689 let child: nx_int = s.tree_child[(atom_idx * NX_MAX_TREE_PER_ATOM) + ci] 690 let cbond: nx_int = s.tree_bond[(atom_idx * NX_MAX_TREE_PER_ATOM) + ci] 691 let cstereo: nx_int = s.tree_stereo[(atom_idx * NX_MAX_TREE_PER_ATOM) + ci] 692 let is_last: nx_int = ci == (n_children - 1) 693 if is_last == 1 { 694 let r_sub: nx_int = nx_chem_emit_subtree(buf, len_io, cap, m, s, child, cbond, cstereo, a.aromaticity) 695 if r_sub != 0 { return r_sub } 696 } 697 else { 698 let r_lp: nx_int = nx_chem_emit_byte(buf, len_io, cap, NX_C_LPAREN) 699 if r_lp != 0 { return r_lp } 700 let r_sub: nx_int = nx_chem_emit_subtree(buf, len_io, cap, m, s, child, cbond, cstereo, a.aromaticity) 701 if r_sub != 0 { return r_sub } 702 let r_rp: nx_int = nx_chem_emit_byte(buf, len_io, cap, NX_C_RPAREN) 703 if r_rp != 0 { return r_rp } 704 } 705 ci = ci + 1 706 } 707 return 0 708} 709 710// ================================================================= 711// Rank-aware pre-scan DFS. Like nx_chem_emit_prescan but: 712// - sorts tree children by Morgan rank ascending before appending 713// - sorts back-edge neighbors by rank ascending before assigning digits 714// - uses an explicit DFS stack with reverse-rank push for canonical 715// traversal order 716// 717// rank[i] = canonical rank of atom i (from nx_chem_morgan_canonical_rank). 718// ================================================================= 719func nx_chem_emit_prescan_canonical(m: *MolGraph, s: *EmitState, root: nx_int, rank: *nx_int) -> nx_int { 720 let stack: *nx_int = (sys_mmap((s.n_atoms * 8) as i64)) as *nx_int 721 var stack_n: nx_int = 0 722 stack[stack_n] = root 723 stack_n = stack_n + 1 724 s.visited[root] = 1 725 while stack_n > 0 { 726 stack_n = stack_n - 1 727 let u: nx_int = stack[stack_n] 728 // Collect tree-neighbors and back-edge-neighbors separately 729 let tree_v: *nx_int = (sys_mmap((NX_MAX_TREE_PER_ATOM * 8) as i64)) as *nx_int 730 let tree_o: *nx_int = (sys_mmap((NX_MAX_TREE_PER_ATOM * 8) as i64)) as *nx_int 731 let tree_s: *nx_int = (sys_mmap((NX_MAX_TREE_PER_ATOM * 8) as i64)) as *nx_int 732 var n_tree: nx_int = 0 733 let back_v: *nx_int = (sys_mmap((NX_MAX_BACK_PER_ATOM * 8) as i64)) as *nx_int 734 let back_o: *nx_int = (sys_mmap((NX_MAX_BACK_PER_ATOM * 8) as i64)) as *nx_int 735 var n_back: nx_int = 0 736 var ei: nx_int = 0 737 while ei < m.n_bonds { 738 let b: *Bond = ((m.bonds as nx_int) + (ei * NX_BOND_BYTES)) as *Bond 739 var v: nx_int = -1 740 if b.a == u { v = b.b } 741 if b.b == u { v = b.a } 742 if v >= 0 { 743 if v != s.parent[u] { 744 if s.visited[v] == 1 { 745 if u > v { 746 if n_back < NX_MAX_BACK_PER_ATOM { 747 back_v[n_back] = v 748 back_o[n_back] = b.order 749 n_back = n_back + 1 750 } 751 } 752 } 753 else { 754 // C2.3e: flip bond stereo if DFS direction (u->v) is reversed 755 // relative to bond's stored (a->b) direction. 756 var bond_stereo: nx_int = b.stereo 757 if u == b.b { 758 if bond_stereo == NX_BSTEREO_UP { bond_stereo = NX_BSTEREO_DOWN } 759 else { 760 if bond_stereo == NX_BSTEREO_DOWN { bond_stereo = NX_BSTEREO_UP } 761 } 762 } 763 if n_tree < NX_MAX_TREE_PER_ATOM { 764 tree_v[n_tree] = v 765 tree_o[n_tree] = b.order 766 tree_s[n_tree] = bond_stereo 767 n_tree = n_tree + 1 768 } 769 } 770 } 771 } 772 ei = ei + 1 773 } 774 // Insertion sort tree-neighbors by rank ascending 775 var i_t: nx_int = 1 776 while i_t < n_tree { 777 let kv: nx_int = tree_v[i_t] 778 let ko: nx_int = tree_o[i_t] 779 let ks: nx_int = tree_s[i_t] 780 let kr: nx_int = rank[kv] 781 var j_t: nx_int = i_t - 1 782 var done_t: nx_int = 0 783 while done_t == 0 { 784 if j_t < 0 { done_t = 1 } 785 else { 786 if rank[tree_v[j_t]] <= kr { done_t = 1 } 787 else { 788 tree_v[j_t + 1] = tree_v[j_t] 789 tree_o[j_t + 1] = tree_o[j_t] 790 tree_s[j_t + 1] = tree_s[j_t] 791 j_t = j_t - 1 792 } 793 } 794 } 795 tree_v[j_t + 1] = kv 796 tree_o[j_t + 1] = ko 797 tree_s[j_t + 1] = ks 798 i_t = i_t + 1 799 } 800 // Append tree children in rank-ascending order 801 var k: nx_int = 0 802 while k < n_tree { 803 let ct: nx_int = s.tree_count[u] 804 s.tree_child[(u * NX_MAX_TREE_PER_ATOM) + ct] = tree_v[k] 805 s.tree_bond[(u * NX_MAX_TREE_PER_ATOM) + ct] = tree_o[k] 806 s.tree_stereo[(u * NX_MAX_TREE_PER_ATOM) + ct] = tree_s[k] 807 s.tree_count[u] = ct + 1 808 s.visited[tree_v[k]] = 1 809 s.parent[tree_v[k]] = u 810 k = k + 1 811 } 812 // Push tree-neighbors onto stack in REVERSE rank order so the 813 // lowest-rank child is popped first (becomes the next emit 814 // target after u; canonical traversal walks the lowest-rank 815 // branch first). 816 var k2: nx_int = n_tree - 1 817 while k2 >= 0 { 818 stack[stack_n] = tree_v[k2] 819 stack_n = stack_n + 1 820 k2 = k2 - 1 821 } 822 // Sort back-edge neighbors by rank ascending 823 var i_b: nx_int = 1 824 while i_b < n_back { 825 let kv2: nx_int = back_v[i_b] 826 let ko2: nx_int = back_o[i_b] 827 let kr2: nx_int = rank[kv2] 828 var j_b: nx_int = i_b - 1 829 var done_b: nx_int = 0 830 while done_b == 0 { 831 if j_b < 0 { done_b = 1 } 832 else { 833 if rank[back_v[j_b]] <= kr2 { done_b = 1 } 834 else { 835 back_v[j_b + 1] = back_v[j_b] 836 back_o[j_b + 1] = back_o[j_b] 837 j_b = j_b - 1 838 } 839 } 840 } 841 back_v[j_b + 1] = kv2 842 back_o[j_b + 1] = ko2 843 i_b = i_b + 1 844 } 845 // Allocate ring digits in rank-sorted order 846 var kb: nx_int = 0 847 while kb < n_back { 848 let v2: nx_int = back_v[kb] 849 let bo: nx_int = back_o[kb] 850 let d: nx_int = s.next_digit 851 s.next_digit = s.next_digit + 1 852 let cu: nx_int = s.back_count[u] 853 if cu < NX_MAX_BACK_PER_ATOM { 854 s.back_digit[(u * NX_MAX_BACK_PER_ATOM) + cu] = d 855 s.back_bond[(u * NX_MAX_BACK_PER_ATOM) + cu] = bo 856 s.back_count[u] = cu + 1 857 } 858 let cv: nx_int = s.back_count[v2] 859 if cv < NX_MAX_BACK_PER_ATOM { 860 s.back_digit[(v2 * NX_MAX_BACK_PER_ATOM) + cv] = d 861 s.back_bond[(v2 * NX_MAX_BACK_PER_ATOM) + cv] = bo 862 s.back_count[v2] = cv + 1 863 } 864 kb = kb + 1 865 } 866 } 867 return 0 868} 869 870// ================================================================= 871// Canonical SMILES emit. Computes Morgan rank, then walks each 872// connected component starting from the lowest-rank unvisited atom, 873// ordering children by rank. Output is byte-identical for two 874// MolGraphs representing the same molecule. 875// ================================================================= 876func nx_chem_emit_canonical_smiles(m: *MolGraph, out_buf: *u8, cap: nx_int, out_len_io: *nx_int) -> nx_int { 877 out_len_io[0] = 0 878 if m.n_atoms == 0 { return 0 } 879 let rank: *nx_int = (sys_mmap((m.n_atoms * 8) as i64)) as *nx_int 880 let _r: nx_int = nx_chem_morgan_canonical_rank(m, rank) 881 let s: *EmitState = nx_chem_emit_state_new(m.n_atoms) 882 var first_component: nx_int = 1 883 var more: nx_int = 1 884 while more == 1 { 885 // find unvisited atom with lowest rank 886 var root: nx_int = -1 887 var best_rank: nx_int = m.n_atoms + 1 888 var ri: nx_int = 0 889 while ri < m.n_atoms { 890 if s.visited[ri] == 0 { 891 if rank[ri] < best_rank { 892 best_rank = rank[ri] 893 root = ri 894 } 895 } 896 ri = ri + 1 897 } 898 if root < 0 { more = 0 } 899 else { 900 if first_component == 0 { 901 let r_dot: nx_int = nx_chem_emit_byte(out_buf, out_len_io, cap, NX_C_DOT) 902 if r_dot != 0 { return r_dot } 903 } 904 first_component = 0 905 let _p: nx_int = nx_chem_emit_prescan_canonical(m, s, root, rank) 906 let r_sub: nx_int = nx_chem_emit_subtree(out_buf, out_len_io, cap, m, s, root, NX_BOND_SINGLE, NX_BSTEREO_NONE, 0) 907 if r_sub != 0 { return r_sub } 908 } 909 } 910 return 0 911} 912 913// ================================================================= 914// Main entry: MolGraph -> SMILES. Writes into out_buf (capacity 915// cap); out_len_io receives the byte count written. Returns 0 on 916// success or -1 on overflow. 917// 918// Multi-component graphs: each connected component is emitted in 919// turn, separated by '.'. 920// ================================================================= 921func nx_chem_emit_smiles(m: *MolGraph, out_buf: *u8, cap: nx_int, out_len_io: *nx_int) -> nx_int { 922 out_len_io[0] = 0 923 if m.n_atoms == 0 { return 0 } 924 let s: *EmitState = nx_chem_emit_state_new(m.n_atoms) 925 var first_component: nx_int = 1 926 var root: nx_int = 0 927 while root < m.n_atoms { 928 if s.visited[root] == 0 { 929 // emit '.' separator between components 930 if first_component == 0 { 931 let r_dot: nx_int = nx_chem_emit_byte(out_buf, out_len_io, cap, NX_C_DOT) 932 if r_dot != 0 { return r_dot } 933 } 934 first_component = 0 935 // pre-scan from this root 936 let _p: nx_int = nx_chem_emit_prescan(m, s, root) 937 // emit the subtree rooted at root (bond_in = NX_BOND_SINGLE is harmless 938 // since the bond_marker emit omits single bonds anyway) 939 let r_sub: nx_int = nx_chem_emit_subtree(out_buf, out_len_io, cap, m, s, root, NX_BOND_SINGLE, NX_BSTEREO_NONE, 0) 940 if r_sub != 0 { return r_sub } 941 } 942 root = root + 1 943 } 944 return 0 945}