Skip to content

Reach RDKit parity across every scalar descriptor - #14

Open
nkwork9999 wants to merge 1 commit into
mainfrom
feature/rdkit-parity
Open

Reach RDKit parity across every scalar descriptor#14
nkwork9999 wants to merge 1 commit into
mainfrom
feature/rdkit-parity

Conversation

@nkwork9999

Copy link
Copy Markdown
Owner

Cross-checked duckSMILES against RDKit 2026.03.5 over 214 molecules and 1188 equivalent spellings. Every scalar descriptor now agrees, and the suite that pins it down ships with the change.

What was wrong

Area Defect
Aromaticity Fused rings written in Kekulé form were misperceived (Kekulé naphthalene reported one aromatic ring). 10 of 30 molecules answered differently depending on notation.
Canonical SMILES Not canonical: 16 of 45 molecules produced different output for different atom orderings. Stereocentres inverted when the output order changed.
Ring perception Bridged polycycles came up short of RDKit's symmetrised SSSR.
H-bond acceptors Matched none of RDKit's definitions (27% mismatch).
Murcko scaffolds Exocyclic double-bonded atoms dropped; some outputs were not valid SMILES.
Crippen logP Aromatic N-H hydrogen typed as H2 instead of H3.
Rotatable bonds Ester and trifluoromethyl bonds over-counted.
SMARTS R<n> treated as "in a ring"; dot-separated patterns unsupported, disabling two QED alerts.
String FFI ds_canonical_smiles returned -1 on a small buffer; ds_mol_formula and ds_add_hydrogens truncated silently.
Morgan Not spelling-invariant; pairwise Tanimoto tracked RDKit at rho = 0.845.

What changed

  • Aromaticity — Hückel 4n+2 electron count per smallest ring and per fused system, judged against a snapshot of the original bond orders. Implicit hydrogens are now filled in before perception.
  • Canonicalisationcanonical_ranks refines graph-only atom invariants with deterministic tie-breaking; the writer orders neighbours by rank, recomputes tetrahedral parity against the emitted order, and normalises cis/trans marks into a canonical frame.
  • Rings — keep the relevant cycles, those not expressible as a GF(2) sum of strictly shorter cycles. Bicyclo[2.2.2]octane has three rings, adamantane four.
  • Definitions — acceptors, donors and rotatable bonds now follow RDKit's own SMARTS exactly.
  • Scaffolds — exocyclic double-bonded atoms retained, severed bonds replaced by hydrogens, stereo dropped only when the neighbour set changed.
  • FFI — all string functions follow the sizing protocol, and the fixed-size C++ buffers retry on the heap.
  • Morgan — shared ring perception, canonical dedup order, and an avalanche step before folding. rho > 0.95.

Tests

scripts/gen_rdkit_fixtures.py regenerates every fixture. 47 integration tests cover descriptor parity, the canonical-form contract (order invariance, idempotence, round-trip, distinctness, stereo), Kekulé/aromatic equivalence, scaffold rules, fingerprints and buffer contracts.

Canonical and scaffold outputs are compared structurally rather than byte-wise: two canonicalisers may legitimately choose different spellings of the same molecule, so RDKit's spelling is fed back through ds_canonical_smiles and required to land on ours.

546 unit tests plus 47 integration tests pass, with zero compiler warnings.

🤖 Generated with Claude Code

Cross-checking duckSMILES against RDKit 2026.03.5 over 214 molecules
turned up defects in nearly every layer of the library. This fixes all of
them and adds the parity suite that pins the behaviour down.

Aromaticity
- Replace the 6-ring single/double counting with a Hueckel 4n+2 electron
  count over each smallest ring, then over each fused ring system. Every
  ring is judged against a snapshot of the original bond orders, so a
  fused neighbour that was rewritten first can no longer change the
  answer -- Kekule naphthalene used to report one aromatic ring.
- Fill implicit hydrogens before perceiving aromaticity, otherwise a
  Kekule pyrrole nitrogen loses its hydrogen to the aromatic valence.

Canonical SMILES
- Add canonical_ranks: refinement of graph-only atom invariants with
  deterministic tie-breaking. The writer now orders neighbours by rank,
  so re-spelling a molecule cannot change the output. 16 of 45 test
  molecules used to canonicalise differently depending on atom order.
- Record the written neighbour order per atom and recompute tetrahedral
  parity on output, so @/@@ follows the molecule and not the spelling.
- Carry cis/trans marks on bonds and normalise them into a canonical
  frame anchored on the lowest-ranked substituent.
- Emit ring-opening digits at the atom itself (c1ccccc1), and bracket an
  atom whenever the bare form would imply the wrong hydrogen count.

Ring perception
- Keep the relevant cycles -- those not expressible as a GF(2) sum of
  strictly shorter cycles -- instead of a deduplicated per-bond scan.
  This matches RDKit's symmetrised SSSR: bicyclo[2.2.2]octane has three
  rings, adamantane four.

Descriptor definitions, matched to RDKit's own SMARTS
- H-bond acceptors: aromatic nitrogen only counts with two connections
  and no hydrogen; acid hydroxyls and amide nitrogens are excluded.
- H-bond donors: O/S need exactly one hydrogen and no charge.
- Rotatable bonds: full Strict pattern, including the trivial-rotor and
  conjugated-linkage exclusions.
- Crippen: test H3 before H2 so an aromatic N-H scores +0.2142.
- Murcko: retain exocyclic double-bonded atoms, replace severed bonds
  with hydrogens, and drop stereo only when the neighbour set changed.
- Formula gains its charge suffix; Li/Be/He gain monoisotopic masses.

SMARTS engine
- R<n> is ring-membership count, not "is in a ring".
- Support dot-separated multi-component patterns, which brings QED's
  last two structural alerts into play.

FFI
- ds_canonical_smiles returned -1 for a small buffer, ds_mol_formula and
  ds_add_hydrogens silently truncated. All three follow the sizing
  protocol now, and the fixed-size C++ buffers retry on the heap.

Morgan
- Share the common ring perception, break dedup ties by canonical rank,
  and avalanche the invariant before folding. Fingerprints are now
  spelling-invariant and pairwise Tanimoto tracks RDKit at rho > 0.95.

Tests
- scripts/gen_rdkit_fixtures.py regenerates the fixtures; 214 molecules
  and 1188 equivalent spellings under crates/smiles/tests/.
- 47 integration tests over descriptor parity, the canonical-form
  contract, Kekule/aromatic equivalence, scaffold rules, fingerprints
  and buffer contracts. Canonical and scaffold output is compared
  structurally rather than byte-wise, since two canonicalisers may
  legitimately choose different spellings of the same molecule.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant