nx_chem_morgan.nx
buildroot/runtime/nx_chem_morgan.nx
about
nx_chem_morgan.nx -- C2.3c milestone: Morgan extended-connectivity
canonical atom ranking.
Implements Weisfeiler-Lehman (1-WL) partition refinement to compute
a canonical rank per atom that is invariant under graph isomorphism.
Two MolGraphs representing the same molecule produce the same set
of canonical ranks (modulo atom-index relabeling), so canonical
SMILES emit using these ranks yields byte-identical output for
graph-equivalent inputs.
Algorithm (after Weininger 1989 + modern WL formulation):
1. Initial invariant per atom: packed (z, degree, charge,
isotope, h_count, aromaticity) into i64.
2. Sort by initial invariant, assign DENSE class indices 0..k-1.
All atoms with same packed invariant get same class.
3. Refinement step: for each atom, signature =
(current_class, sorted_neighbor_classes). Sort by signature,
assign new dense class indices.
4. Iterate refinement until class count stable (or MAX_ITER).
5. Final rank = position in sorted (final_class, atom_index) order.
EXCEED axis hit (landscape doc):
E1 -- bit-reproducible canonical form across versions/platforms.
The Morgan algorithm here is content-addressed via versioned
initial-invariant packing + deterministic signature sort.
RDKit's BackwardsIncompatibleChanges.html documents
cross-release canonical drift; this substrate's canonical is
pinned to nx_chem_morgan_canonical_rank_v1 semantics
(hash-pinned at the function level by source identity).
Honest gaps (deferred):
- Bond orders not yet used in invariants (so cyclohexane vs
cyclohexene atoms could collide on initial invariant; one WL
refinement step resolves it via neighbor distinctness)
- Stereo not yet folded into canonicalization (C2.3d)
- Tie-breaking for true graph automorphisms uses (final_class,
atom_idx); not a true automorphism canonicalizer but adequate
for graph-equivalent input alternatives
dependencies 2 imports · 2 importers
imports: nx_chem.nxnx_chem_molecule.nx
imported by: nx_chem_morgan_test.nxnx_chem_smiles_emit.nx
structs
| none |
consts
| 43 | const NX_MORGAN_MAX_DEG: nx_int = 7 // max neighbors per atom (organic typical <= 4) |
| 44 | const NX_MORGAN_MAX_ITER: nx_int = 32 // safety cap for refinement loop |
| 45 | const NX_MORGAN_SIG_LEN: nx_int = 8 // 1 (own class) + 7 (neighbor classes) = 8 nx_int per row |
functions
| 57 | func nx_chem_morgan_initial_invariant(m: *MolGraph, atom_idx: nx_int) -> nx_int |
| 93 | func nx_chem_morgan_get_neighbors(m: *MolGraph, atom_idx: nx_int, out: *nx_int) -> nx_int called by 1: nx_chem_morgan_signature |
| 118 | func nx_chem_morgan_sort_asc(arr: *nx_int, n: nx_int) -> nx_int called by 1: nx_chem_morgan_signature |
| 144 | func nx_chem_morgan_signature( |
| 174 | func nx_chem_morgan_sig_cmp(sig_a: *nx_int, sig_b: *nx_int) -> nx_int |
| 189 | func nx_chem_morgan_sort_indices_by_sig(indices: *nx_int, n: nx_int, sigs: *nx_int) -> nx_int |
| 219 | func nx_chem_morgan_refine( |
| 255 | func nx_chem_morgan_densify_initial( called by 1: nx_chem_morgan_canonical_rank |
| 303 | func nx_chem_morgan_canonical_rank(m: *MolGraph, rank_out: *nx_int) -> nx_int |