Skip to main content

RDKit SMILES writer that orders atoms by their original atom index instead of canonical rank

Project description

idxsmiles

PyPI CI License: MIT

An RDKit SMILES writer that orders atoms by their original atom index instead of RDKit's canonical rank — deterministic, human-aligned output, with stereochemistry preserved.

Why

Chem.MolToSmiles(mol) (canonical=True) picks the next atom to visit using canonical invariant ranks (Canon::rankMolAtoms), which have no relationship to the atom's index in the Mol object. The resulting SMILES atom order can look arbitrary relative to how the molecule was built or numbered.

Chem.MolToSmiles(mol, canonical=False) looks like the fix — internally its ranks array is just [0, 1, ..., N-1] (the atom indices themselves). But Canon::dfsBuildStack (RDKit's C++ traversal code) doesn't sort candidates by that rank alone: if a candidate bond is part of a ring, a large bond-type-dependent term gets added to its sort key, so a lower-index ring neighbor can still be visited after a higher-index non-ring neighbor. This is reproducible: renumber C1=CC(C)C1 so the ring-closure atom gets index 3 and the exocyclic methyl gets index 4, and canonical=False still emits atom 4 before atom 3.

Because that bias is baked into RDKit's compiled writer and can't be disabled from Python, this library implements its own DFS-based SMILES writer: atoms are always visited in strict ascending atom-index order, with no exceptions for ring bonds — while still producing valid, stereo-correct SMILES that RDKit can re-parse into the identical structure.

Installation

pip install idxsmiles

Usage

from rdkit import Chem
from idxsmiles import mol_to_smiles

mol = Chem.MolFromSmiles("CC(=O)Oc1ccccc1C(=O)O")
smiles = mol_to_smiles(mol)

mol_to_smiles(mol: Chem.Mol) -> str takes an RDKit Mol and returns a SMILES string whose atom order follows the input Mol's atom indices as closely as the molecular graph allows (ring/branch structure still constrains what's possible — the result is not necessarily perfectly monotonic, but every branch point prefers the lowest available index). Re-parsing the output reproduces the same molecule, including stereochemistry.

atom_visit_order(mol: Chem.Mol) -> list[int] returns, for each atom index, its 0-based rank in that same traversal — i.e. the position it would appear at in mol_to_smiles's output — without serializing anything.

Reaction SMILES alignment

idxsmiles.reaction.align_reaction takes an atom-mapped reaction SMILES ("reactants>agents>products", agents may be empty) and returns the unmapped reaction with one side reordered so that atoms it shares with the other side are visited in the same relative order the other side visits them in — not just from a shared root atom, but across as much of the full atom order as connectivity allows.

from idxsmiles.reaction import align_reaction

mapped = ("[CH3:1][C:2](=[O:3])[OH:4].[CH3:5][CH2:6][OH:7]"
          ">>[CH3:1][C:2](=[O:3])[O:7][CH2:6][CH3:5]")
align_reaction(mapped)
# 'CC(=O)O.CCO>>CC(=O)OCC'
align_reaction(mapped, align_to="product")
# 'CC(=O)O.OCC>>CC(=O)OCC'

align_to picks which side stays fixed and which is reordered to match it:

  • "reactant" (default): the reactants are left as-is and the product is reordered to follow them — for forward reaction prediction (reactants are the fixed model input, the product is the generation target).
  • "product": the product is left as-is and the reactants are reordered to follow it — the R-SMILES/UAlign convention for retrosynthesis (product fixed, reactants are the generation target). Note how the reactants' ethanol fragment (OCC) comes out as a literal substring match of the product's tail (OCC) in the second example above — that's the alignment.

random=True fully shuffles the fixed side's atom indices (seeded by seed, if given) before aligning, so it's written from a random root with random branch-order tie-breaks each time and the other side follows that — useful for generating multiple distinct-but-equivalent training examples per reaction, as in R-SMILES's root-augmentation:

align_reaction(mapped, random=True, seed=0)
align_reaction(mapped, random=True, seed=1)  # a different, still-valid alignment

Other behavior:

  • Atoms on the moving side with no counterpart on the fixed side (leaving groups, unmapped atoms) are ordered after all mapped atoms, keeping their original relative order.
  • Agents, if present, are re-serialized in their original order with atom-map numbers stripped, but are not reordered.
  • Atom-map numbers are stripped from the returned SMILES.
  • A '>' inside a '->' dative bond is not mistaken for a reactants/agents/products separator.
  • Warns (rather than raising) instead of silently producing an unaligned result when the atom-map numbers can't drive an alignment: no atom-map numbers shared between reactants and product, or a duplicate atom-map number on one side.

Order agreement between the two sides is only guaranteed within each moving-side connected component: a fragment that only bonds to the rest of the molecule on the other side (e.g. a second reagent joining mid-chain) still has to be written as one contiguous block, even where the fixed side interleaves it via a branch — and more generally, a DFS can never visit a child before its own parent, even where the fixed side's order would ask for that. Both are structural limitations of SMILES itself, not bugs (see test_disconnected_fragment_limitation and test_dfs_parent_before_child_limitation in tests/test_reaction.py).

What's handled

  • Ring closures (including multi-digit %(NN) closures)
  • Multi-fragment SMILES (.-separated)
  • Aromaticity, charges, isotopes, atom maps
  • Standard tetrahedral chirality (@/@@), including 2-real-neighbor, lone-pair-bearing stereocentres (e.g. chiral P, S)
  • Double-bond cis/trans stereo (/, \)
  • Dative bonds (->, <-)

Out of scope

Non-tetrahedral chirality (square planar, trigonal bipyramidal, octahedral) and cumulated/shared stereo double bonds (one single bond flanking two stereo double bonds at once, e.g. conjugated diazo systems) are not supported. These raise NotImplementedError rather than silently emitting incorrect stereochemistry.

Testing

pip install rdkit pytest
pytest

tests/test_idxsmiles.py checks round-trip structural correctness under random atom renumbering across a curated set of molecules (rings, chirality, cis/trans, dative bonds, multi-fragment), a dedicated regression test for the ring-bond bias described above, and an independent check that atoms are actually written in ascending-index order (not just that the round trip happens to produce an equivalent structure) — verified by tagging atoms with their index as an atom-map number and confirming the re-parsed output reads them back in the same order as a separately implemented reference traversal.

tests/test_reaction.py checks align_reaction the same way, in both align_to directions: structural round-trip correctness across a curated set of reactions, and — since align_reaction strips atom-map numbers from its output — an isotope-tag trick to independently confirm the ordering itself (that shared atoms really do come out in the fixed side's relative order within each moving-side fragment), plus random/seed, the atom-map warnings, the dative-bond-separator, and malformed-input edge cases.

How it works

RDKit is still used for the low-level pieces:

  • Atom.GetSmarts() / Bond.GetSmarts() for atom/bond text (these happen to call straight into RDKit's own SMILES atom/bond writer, SmilesWrite::GetAtomSmiles / GetBondSmiles)
  • The chirality- and double-bond-stereo bookkeeping is a Python port of the logic in Canon::canonicalizeFragment / Canon::canonicalizeDoubleBond, adapted to run against this library's own traversal order instead of RDKit's canonical one.

Everything else — the DFS traversal order, ring-closure digit assignment, and branch/parenthesis structure — is implemented independently in idxsmiles.py.

License

MIT — see LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

idxsmiles-0.2.0.tar.gz (23.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

idxsmiles-0.2.0-py3-none-any.whl (15.7 kB view details)

Uploaded Python 3

File details

Details for the file idxsmiles-0.2.0.tar.gz.

File metadata

  • Download URL: idxsmiles-0.2.0.tar.gz
  • Upload date:
  • Size: 23.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.9.6

File hashes

Hashes for idxsmiles-0.2.0.tar.gz
Algorithm Hash digest
SHA256 9ff5a4edb6e38dcecca0bcbdf32af0a7f4d5cf224a61231c2aee39cb9468e982
MD5 c7fbbe16ee7dd2633edaa59c9f1d6f10
BLAKE2b-256 8392dbb04d471bfea18952f5c4a1e2d8d8861d24b18b3412bc6b5d318a7a76ae

See more details on using hashes here.

File details

Details for the file idxsmiles-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: idxsmiles-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 15.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.9.6

File hashes

Hashes for idxsmiles-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 22e0cb813636bd471c5bc25ba8f15367bb0618cf14e5cf29d3b39921fd9369c5
MD5 7efc487025e3f3209481ac39f1115283
BLAKE2b-256 2e3ed7ef2c4480291a857adb907861cdbd12984cf4eec2be67478f8a87145928

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page