Accept 1-based atom maps and add featurizer keyword options#5
Conversation
parse_mol only honored atom map numbers that formed a complete 0-based permutation. Track the min and max map number instead, and accept the ordering when the numbers span a contiguous range starting at either 0 or 1, subtracting the minimum when inverting the order. Anything else (partial mapping, duplicates, non-contiguous numbers, or a range starting above 1) is still ignored, leaving the atoms in the order the SMILES parser produced them. Add tests/python/test_atom_map_order.py covering both numbering conventions and the rejection cases.
`parse_mol` could already reorder atoms according to their atom map numbers, but the featurizers always requested it and Python callers had no way to turn it off. Thread an `ordered` parameter through `mol_featurizer` and `batch_mol_featurizer` down to `read_graph`/`parse_mol`, and bind both functions with named arguments so `ordered` can be passed by keyword. It defaults to true, so existing callers are unaffected. Document the option in docs/USAGE.md and cover it in the atom-map ordering tests.
Greptile SummaryThis PR extends
Confidence Score: 5/5Safe to merge. All new parameters are optional with defaults that preserve prior behavior, and every existing positional and keyword call site is unaffected. The atom-map permutation check correctly handles 0-based and 1-based maps, partial maps, duplicates, and gaps. Short-circuit evaluation in the range comparison prevents unsigned-integer underflow on the degenerate path. The keep_h and ignore_stereo flags are threaded consistently through both the molecule and reaction pipelines, removeStereochemistry is placed after addHs in the correct order, and the _CIPCode concern is explicitly addressed. Two comprehensive new test files cover all new code paths including edge cases and backward-compatibility positional usage. No files require special attention. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A["Python: mol_featurizer / batch_mol_featurizer
(smiles, ..., ordered, keep_h, ignore_stereo)"] --> B["read_graph
(smiles, explicit_H, ordered, keep_h, ignore_stereo)"]
B --> C["parse_mol
(smiles, explicit_H, ordered, keep_h, ignore_stereo)"]
C --> D["SmilesParserParams
removeHs = !keep_h"]
D --> E["RDKit::SmilesToMol(smiles, params)"]
E -->|parse failed| Z["return nullptr"]
E -->|success| F{ordered?}
F -->|true| G["Scan atoms for molAtomMapNumber
track map_num_min, map_num_max
clear prop on all mapped atoms"]
G --> H{"Complete permutation?
min <= 1 AND max-min+1 == num_atoms?"}
H -->|No| I["ordered = false"]
H -->|Yes| J["Build inverse_order
check for duplicates"]
J -->|duplicate| I
J -->|clean| K["mol.renumberAtoms(inverse_order)"]
K --> L
I --> L
F -->|false| L
L{explicit_H?}
L -->|true| M["RDKit::MolOps::addHs"]
L -->|false| N
M --> N{ignore_stereo?}
N -->|true| O["RDKit::MolOps::removeStereochemistry"]
N -->|false| P["return mol"]
O --> P
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
A["Python: mol_featurizer / batch_mol_featurizer
(smiles, ..., ordered, keep_h, ignore_stereo)"] --> B["read_graph
(smiles, explicit_H, ordered, keep_h, ignore_stereo)"]
B --> C["parse_mol
(smiles, explicit_H, ordered, keep_h, ignore_stereo)"]
C --> D["SmilesParserParams
removeHs = !keep_h"]
D --> E["RDKit::SmilesToMol(smiles, params)"]
E -->|parse failed| Z["return nullptr"]
E -->|success| F{ordered?}
F -->|true| G["Scan atoms for molAtomMapNumber
track map_num_min, map_num_max
clear prop on all mapped atoms"]
G --> H{"Complete permutation?
min <= 1 AND max-min+1 == num_atoms?"}
H -->|No| I["ordered = false"]
H -->|Yes| J["Build inverse_order
check for duplicates"]
J -->|duplicate| I
J -->|clean| K["mol.renumberAtoms(inverse_order)"]
K --> L
I --> L
F -->|false| L
L{explicit_H?}
L -->|true| M["RDKit::MolOps::addHs"]
L -->|false| N
M --> N{ignore_stereo?}
N -->|true| O["RDKit::MolOps::removeStereochemistry"]
N -->|false| P["return mol"]
O --> P
Reviews (1): Last reviewed commit: "Add ignore_stereo to batch_reaction_feat..." | Re-trigger Greptile |
Accept 1-based atom maps and add featurizer keyword options
Why
Chemprop's
make_molsupportskeep_h,ignore_stereo, and atom-map-based reordering. This PR is a step towards makingcuik-molmakerthe default featurizer in Chemprop's CLI.Summary
parse_molnow accepts atom map numbers that form a complete 1-based ordering ([C:1][O:2]), in addition to the existing 0-based support — a strict permutation check replaces the old range check.orderedC++ parameter is now exposed as a Python keyword argument onmol_featurizer/batch_mol_featurizer(previously unreachable from Python since the pybind binding had no namedpy::args).keep_hkeyword argument onmol_featurizer/batch_mol_featurizer(molecule path): retains explicit hydrogens already written in a SMILES string, without adding any (explicit_Hstill controls that).ignore_stereokeyword argument onmol_featurizer/batch_mol_featurizerandbatch_reaction_featurizer(CGR path): clears R/S and cis/trans stereochemistry via a singleRDKit::MolOps::removeStereochemistrycall. This was a deliberate choice over a manualSetChiralTag/SetStereoport (as Chemprop'smake_moldoes it) — cuik-molmaker's floatchiralityfeature reads RDKit's_CIPCodeproperty, which survivesSetChiralTag(CHI_UNSPECIFIED)but notremoveStereochemistry; a manual port would silently leave that feature un-cleared.false/existing behavior and are appended at the end of their function's parameter list, so every existing call site (C++ and Python, positional or keyword) is unaffected.Backward compatibility
No existing call site changes behavior or stops working. Every new parameter (
ordered,keep_h,ignore_stereo) defaults to the prior behavior and is appended at the end of its function's signature, so:explicit_Hwas deliberately left as-is (not renamed toadd_h, despite Chemprop naming its equivalent flag that way) to avoid churn on an already-released name.test_reaction_features.py,test_features.py), which call these functions positionally without the new keywords and still pass.Test plan
tests/python/test_atom_map_order.py— 1-based/0-based atom map ordering, rejection cases,ordered=Falsetests/python/test_parse_options.py—keep_hatom counts,ignore_stereoon one-hot + float chirality and bond stereo (molecule path),ignore_stereoon CGR atom features (reaction path)PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 python -m pytest tests/python -q→ 72 passed, 2 pre-existing failures unrelated to this change (test_rdkit2D[fast/best], ascipy.statsversion issue)test_reaction_features.py,test_features.py)