nx_chem_fingerprint.nx
buildroot/runtime/nx_chem_fingerprint.nx
about
nx_chem_fingerprint.nx -- C2.6 milestone: ECFP-style Morgan
circular-substructure fingerprint + Tanimoto similarity.
Algorithm: extended-connectivity hashing (Rogers & Hahn 2010).
1. Each atom gets an initial invariant from chemistry-relevant
features: (heavy-neighbor count, valence-sum, atomic number,
formal charge, implicit-H count, in-ring flag, aromaticity).
2. For each refinement iteration r in 1..radius:
new_inv[i] = poly_hash(old_inv[i], [(bond_order, neighbor_old_inv)] sorted)
The sorted-list-of-(bond, neighbor) construction makes the
hash atom-order-invariant; the polynomial multiplier 31 is
pinned by source identity ("ECFP4_v1_mul_31").
3. After radius iterations, accumulate ALL invariants seen
across all atoms at all radii into the fingerprint bit-set.
4. Fold into a bit vector of length N_BITS by setting
bit[h & (N_BITS-1)] for each accumulated invariant h.
EXCEED axis hit (landscape doc):
E5 -- hash-pinned fingerprint definitions. Algorithm identity is
the source bytes of this file. RDKit's Morgan/ECFP doesn't
match the ECFP-paper Figure 8 (RDKit Issue #2018); the nx
substrate ships content-addressed semantics: ECFP4_v1
computed in 2026 matches ECFP4_v1 computed in 2030 because
the function bytes are bit-identical.
Honest gaps (deferred):
- Stereo-aware ECFP (folds Atom.stereo + Bond.stereo into invariants):
C2.6.1 future iteration; current MVP is stereo-blind.
- FCFP (functional-class equivalence rather than atomic-equivalence):
C2.7 future primitive.
- Variable bit length (current 1024 only; 512 / 2048 / 4096 hash-pinned
variants are simple parameter changes when needed).
nx_safety_envelope:
intended_use: chemoinformatic similarity + substructure
hashing; foundation for nx_chem_similarity_search
and nx_chem_chemometrics descriptor matrix
sil_target: SIL1
evidence: [C2.6 KAT in nx_chem_fingerprint_test.nx;
algorithm hash-pinned by source bytes;
dependencies 2 imports · 1 importers
imports: nx_chem.nxnx_chem_molecule.nx
imported by: nx_chem_fingerprint_test.nx
structs
| none |
consts
| 46 | const NX_MAGIC_10000: i64 = 10000 |
| 49 | const NX_CHEM_ECFP_V1_MUL: nx_int = 31 // polynomial-hash multiplier |
| 50 | const NX_CHEM_ECFP_V1_INIT_SEED: nx_int = 5381 // djb2-style seed |
| 51 | const NX_CHEM_ECFP_DEFAULT_BITS: nx_int = 1024 |
| 52 | const NX_CHEM_ECFP_DEFAULT_RADIUS: nx_int = 2 // ECFP4 standard |
| 55 | const NX_CHEM_FP_BITS_PER_SLOT: nx_int = 64 |
functions
| 76 | func nx_chem_fp_initial_invariant(m: *MolGraph, atom_idx: nx_int) -> nx_int called by 1: nx_chem_compute_ecfp_fingerprint |
| 125 | func nx_chem_fp_sort_asc(arr: *nx_int, n: nx_int) -> nx_int called by 1: nx_chem_fp_refine_step |
| 156 | func nx_chem_fp_hash_neighborhood( called by 1: nx_chem_fp_refine_step |
| 177 | func nx_chem_fp_refine_step(m: *MolGraph, old_inv: *nx_int, new_inv: *nx_int) -> nx_int called by 1: nx_chem_compute_ecfp_fingerprint calls 2: nx_chem_fp_sort_ascnx_chem_fp_hash_neighborhood |
| 218 | func nx_chem_fp_set_bit(slots: *nx_int, bit_idx: nx_int) -> nx_int |
| 228 | func nx_chem_fp_get_bit(slots: *nx_int, bit_idx: nx_int) -> nx_int called by 1: i_set_get_bit |
| 241 | func nx_chem_fp_popcount_u64(v: nx_int) -> nx_int |
| 259 | func nx_chem_compute_ecfp_fingerprint( |
| 302 | func nx_chem_fp_tanimoto_q4(fp_a: *nx_int, fp_b: *nx_int, n_bits: nx_int) -> nx_int |
| 322 | func nx_chem_fp_popcount(fp: *nx_int, n_bits: nx_int) -> nx_int called by 4: a_empty_molb_methaneh_popcount_scalesj_popcount_all_set calls 1: nx_chem_fp_popcount_u64 |
| 336 | func nx_chem_fp_equal(fp_a: *nx_int, fp_b: *nx_int, n_bits: nx_int) -> nx_int |