RDKit SMILES writer that orders atoms by their original atom index instead of canonical rank
Project description
idxsmiles
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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9ff5a4edb6e38dcecca0bcbdf32af0a7f4d5cf224a61231c2aee39cb9468e982
|
|
| MD5 |
c7fbbe16ee7dd2633edaa59c9f1d6f10
|
|
| BLAKE2b-256 |
8392dbb04d471bfea18952f5c4a1e2d8d8861d24b18b3412bc6b5d318a7a76ae
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
22e0cb813636bd471c5bc25ba8f15367bb0618cf14e5cf29d3b39921fd9369c5
|
|
| MD5 |
7efc487025e3f3209481ac39f1115283
|
|
| BLAKE2b-256 |
2e3ed7ef2c4480291a857adb907861cdbd12984cf4eec2be67478f8a87145928
|