Skill Tracer
Skills › Scientific Agent Skills › rdkit › workflows_and_best_practices.md

workflows_and_best_practices.md

223 lines · skills/rdkit/references/workflows_and_best_practices.md at 5f2bc03

Open on GitHub
1# RDKit Workflows and Best Practices
2
3Worked workflows (drug-likeness analysis, similarity screening, substructure filtering)
4followed by error handling, performance optimization, version-sensitive behavior, thread
5safety, and memory management.
6

RDKit Workflows and Best Practices

Worked workflows (drug-likeness analysis, similarity screening, substructure filtering) followed by error handling, performance optimization, version-sensitive behavior, thread safety, and memory management.

Raw lines 1 to 6
7## Common Workflows
8

Common Workflows

Raw lines 7 to 8
9### Drug-likeness Analysis
11```python
12from rdkit import Chem
13from rdkit.Chem import Descriptors
15def analyze_druglikeness(smiles):
16 mol = Chem.MolFromSmiles(smiles)
17 if mol is None:
18 return None
20 # Calculate Lipinski descriptors
21 results = {
22 'MW': Descriptors.MolWt(mol),
23 'LogP': Descriptors.MolLogP(mol),
24 'HBD': Descriptors.NumHDonors(mol),
25 'HBA': Descriptors.NumHAcceptors(mol),
26 'TPSA': Descriptors.TPSA(mol),
27 'RotBonds': Descriptors.NumRotatableBonds(mol)
28 }
30 # Check Lipinski's Rule of Five
31 results['Lipinski'] = (
32 results['MW'] <= 500 and
33 results['LogP'] <= 5 and
34 results['HBD'] <= 5 and
35 results['HBA'] <= 10
36 )
38 return results
39```

Drug-likeness Analysis

from rdkit import Chem
from rdkit.Chem import Descriptors

def analyze_druglikeness(smiles):
    mol = Chem.MolFromSmiles(smiles)
    if mol is None:
        return None

    # Calculate Lipinski descriptors
    results = {
        'MW': Descriptors.MolWt(mol),
        'LogP': Descriptors.MolLogP(mol),
        'HBD': Descriptors.NumHDonors(mol),
        'HBA': Descriptors.NumHAcceptors(mol),
        'TPSA': Descriptors.TPSA(mol),
        'RotBonds': Descriptors.NumRotatableBonds(mol)
    }

    # Check Lipinski's Rule of Five
    results['Lipinski'] = (
        results['MW'] <= 500 and
        results['LogP'] <= 5 and
        results['HBD'] <= 5 and
        results['HBA'] <= 10
    )

    return results
Raw lines 9 to 40
41### Similarity Screening
43```python
44from rdkit import Chem
45from rdkit.Chem import rdFingerprintGenerator
46from rdkit import DataStructs
48def similarity_screen(query_smiles, database_smiles, threshold=0.7):
49 query_mol = Chem.MolFromSmiles(query_smiles)
50 if query_mol is None:
51 return []
53 morgan_gen = rdFingerprintGenerator.GetMorganGenerator(radius=2, fpSize=2048)
54 query_fp = morgan_gen.GetFingerprint(query_mol)
56 hits = []
57 for idx, smiles in enumerate(database_smiles):
58 mol = Chem.MolFromSmiles(smiles)
59 if mol:
60 fp = morgan_gen.GetFingerprint(mol)
61 sim = DataStructs.TanimotoSimilarity(query_fp, fp)
62 if sim >= threshold:
63 hits.append((idx, smiles, sim))
65 return sorted(hits, key=lambda x: x[2], reverse=True)
66```

Similarity Screening

from rdkit import Chem
from rdkit.Chem import rdFingerprintGenerator
from rdkit import DataStructs

def similarity_screen(query_smiles, database_smiles, threshold=0.7):
    query_mol = Chem.MolFromSmiles(query_smiles)
    if query_mol is None:
        return []

    morgan_gen = rdFingerprintGenerator.GetMorganGenerator(radius=2, fpSize=2048)
    query_fp = morgan_gen.GetFingerprint(query_mol)

    hits = []
    for idx, smiles in enumerate(database_smiles):
        mol = Chem.MolFromSmiles(smiles)
        if mol:
            fp = morgan_gen.GetFingerprint(mol)
            sim = DataStructs.TanimotoSimilarity(query_fp, fp)
            if sim >= threshold:
                hits.append((idx, smiles, sim))

    return sorted(hits, key=lambda x: x[2], reverse=True)
Raw lines 41 to 67
68### Substructure Filtering
70```python
71from rdkit import Chem
73def filter_by_substructure(smiles_list, pattern_smarts):
74 query = Chem.MolFromSmarts(pattern_smarts)
75 if query is None or query.GetNumAtoms() == 0:
76 raise ValueError("Invalid or empty SMARTS")
78 hits = []
79 for smiles in smiles_list:
80 mol = Chem.MolFromSmiles(smiles)
81 if mol and mol.HasSubstructMatch(query):
82 hits.append(smiles)
84 return hits
85```

Substructure Filtering

from rdkit import Chem

def filter_by_substructure(smiles_list, pattern_smarts):
    query = Chem.MolFromSmarts(pattern_smarts)
    if query is None or query.GetNumAtoms() == 0:
        raise ValueError("Invalid or empty SMARTS")

    hits = []
    for smiles in smiles_list:
        mol = Chem.MolFromSmiles(smiles)
        if mol and mol.HasSubstructMatch(query):
            hits.append(smiles)

    return hits
Raw lines 68 to 86
87## Best Practices

Best Practices

Raw lines 87 to 88
89### Error Handling
91Always check for `None` when parsing molecules:
93```python
94mol = Chem.MolFromSmiles(smiles)
95if mol is None:
96 print(f"Failed to parse: {smiles}")
97 continue
98```

Error Handling

Always check for None when parsing molecules:

mol = Chem.MolFromSmiles(smiles)
if mol is None:
    print(f"Failed to parse: {smiles}")
    continue
Raw lines 89 to 99
100### Performance Optimization
102**Use safe storage formats:**
104```python
105import base64
106import json
107from pathlib import Path
108from rdkit import Chem
110# Portable exchange formats such as SMILES and SDF are safest for shared data.
111# For local caches, RDKit's binary molecule representation avoids generic pickle.
112payload = [base64.b64encode(mol.ToBinary()).decode("ascii") for mol in mols]
113Path("molecules.rdmol.json").write_text(json.dumps(payload))
115cached = json.loads(Path("molecules.rdmol.json").read_text())
116mols = [Chem.Mol(base64.b64decode(item)) for item in cached]
117```
119Do not load Python pickle files from untrusted sources. Pickle deserialization can execute arbitrary code; prefer SMILES/SDF for interchange and RDKit binary payloads for trusted local caches.
121**Use bulk operations:**
123```python
124from rdkit import DataStructs
125from rdkit.Chem import rdFingerprintGenerator
127# Calculate fingerprints for all molecules at once
128morgan_gen = rdFingerprintGenerator.GetMorganGenerator(radius=2, fpSize=2048)
129fps = [morgan_gen.GetFingerprint(mol) for mol in mols]
131# Use bulk similarity calculations
132similarities = DataStructs.BulkTanimotoSimilarity(fps[0], fps[1:])
133```

Performance Optimization

Use safe storage formats:

import base64
import json
from pathlib import Path
from rdkit import Chem

# Portable exchange formats such as SMILES and SDF are safest for shared data.
# For local caches, RDKit's binary molecule representation avoids generic pickle.
payload = [base64.b64encode(mol.ToBinary()).decode("ascii") for mol in mols]
Path("molecules.rdmol.json").write_text(json.dumps(payload))

cached = json.loads(Path("molecules.rdmol.json").read_text())
mols = [Chem.Mol(base64.b64decode(item)) for item in cached]

Do not load Python pickle files from untrusted sources. Pickle deserialization can execute arbitrary code; prefer SMILES/SDF for interchange and RDKit binary payloads for trusted local caches.

Use bulk operations:

from rdkit import DataStructs
from rdkit.Chem import rdFingerprintGenerator

# Calculate fingerprints for all molecules at once
morgan_gen = rdFingerprintGenerator.GetMorganGenerator(radius=2, fpSize=2048)
fps = [morgan_gen.GetFingerprint(mol) for mol in mols]

# Use bulk similarity calculations
similarities = DataStructs.BulkTanimotoSimilarity(fps[0], fps[1:])
Raw lines 100 to 134
135### Version-Sensitive Behavior
137Pin RDKit versions when exact molecular identifiers or numeric features are part of a persisted dataset, model feature pipeline, or regulated report. Recent releases changed or documented behavior in several Python-facing areas:
139- **Canonical SMILES and stereo:** 2026.03 changed canonical double-bond handling to avoid stereo corruption, so some stereo-containing SMILES may differ from older releases.
140- **Descriptors and hashes:** preserve exact feature names and representation rules with the installed version; a successful rerun does not prove cross-version feature equivalence.
141- **Drawing:** legacy `rdkit.Chem.Draw` canvas modules and functions such as `MolToImageFile`, `MolToMPL`, and `MolToQPixmap` were removed; use `Draw.MolToFile`, `Draw.MolToImage`, or `rdMolDraw2D`.
142- **MolStandardize:** use `rdkit.Chem.MolStandardize.rdMolStandardize`; the older Python MolStandardize implementation was removed.
143- **Similarity maps:** `GetSimilarityMapFromWeights()`, `GetSimilarityMapForFingerprint()`, and `GetSimilarityMapForModel()` now require an `rdMolDraw2D` drawing object.

Version-Sensitive Behavior

Pin RDKit versions when exact molecular identifiers or numeric features are part of a persisted dataset, model feature pipeline, or regulated report. Recent releases changed or documented behavior in several Python-facing areas:

  • Canonical SMILES and stereo: 2026.03 changed canonical double-bond handling to avoid stereo corruption, so some stereo-containing SMILES may differ from older releases.
  • Descriptors and hashes: preserve exact feature names and representation rules with the installed version; a successful rerun does not prove cross-version feature equivalence.
  • Drawing: legacy rdkit.Chem.Draw canvas modules and functions such as MolToImageFile, MolToMPL, and MolToQPixmap were removed; use Draw.MolToFile, Draw.MolToImage, or rdMolDraw2D.
  • MolStandardize: use rdkit.Chem.MolStandardize.rdMolStandardize; the older Python MolStandardize implementation was removed.
  • Similarity maps: GetSimilarityMapFromWeights(), GetSimilarityMapForFingerprint(), and GetSimilarityMapForModel() now require an rdMolDraw2D drawing object.
Raw lines 135 to 144
145### Thread Safety
147Use independent molecule/query objects per worker. Never share a supplier across
148threads; recursive SMARTS, calculated properties, and concurrent MolToSmiles on
149the same Mol have specific caveats. Most Python bindings do not gain parallelism
150from Python threads; prefer APIs exposing `numThreads` or separate processes.
151`MultithreadedSDMolSupplier` may reorder records: capture `GetLastRecordId()` at
152each iteration, not a filtered-list index. See the RDKit Book thread-safety section.

Thread Safety

Use independent molecule/query objects per worker. Never share a supplier across threads; recursive SMARTS, calculated properties, and concurrent MolToSmiles on the same Mol have specific caveats. Most Python bindings do not gain parallelism from Python threads; prefer APIs exposing numThreads or separate processes. MultithreadedSDMolSupplier may reorder records: capture GetLastRecordId() at each iteration, not a filtered-list index. See the RDKit Book thread-safety section.

Raw lines 145 to 153
154### Memory Management
156For large datasets:
158```python
159# Use ForwardSDMolSupplier to avoid loading entire file
160with open('large.sdf', 'rb') as f:
161 suppl = Chem.ForwardSDMolSupplier(f)
162 for mol in suppl:
163 # Process one molecule at a time
164 pass
166# Use MultithreadedSDMolSupplier for parallel processing
167suppl = Chem.MultithreadedSDMolSupplier('large.sdf', numWriterThreads=4)
168```

Memory Management

For large datasets:

# Use ForwardSDMolSupplier to avoid loading entire file
with open('large.sdf', 'rb') as f:
    suppl = Chem.ForwardSDMolSupplier(f)
    for mol in suppl:
        # Process one molecule at a time
        pass

# Use MultithreadedSDMolSupplier for parallel processing
suppl = Chem.MultithreadedSDMolSupplier('large.sdf', numWriterThreads=4)
Raw lines 154 to 169
170## Identity and deliberate standardization
172This small example is executed by the regression suite:
174```python
175from rdkit import Chem
176from rdkit.Chem.MolStandardize import rdMolStandardize as standardize
178def canonical(smiles):
179 mol = Chem.MolFromSmiles(smiles)
180 if mol is None or mol.GetNumAtoms() == 0:
181 raise ValueError("Invalid or empty molecule")
182 return Chem.MolToSmiles(mol, canonical=True, isomericSmiles=True)
184assert canonical('OCC') == canonical('CCO')
185assert canonical('C[C@H](O)F') != canonical('C[C@@H](O)F')
186assert canonical('CC(=O)O') != canonical('CC(=O)[O-]')
187assert canonical('CC=O') != canonical('C=CO')
189original = Chem.MolFromSmiles('CC(=O)[O-].[Na+]')
190cleaned = standardize.Cleanup(original)
191assert len(Chem.GetMolFrags(cleaned)) == 2 # Cleanup does NOT strip salts
192parent = standardize.FragmentParent(cleaned)
193neutral = standardize.Uncharger().uncharge(parent)
194assert Chem.MolToSmiles(neutral) == 'CC(=O)O'
195```
197Fragment selection, neutralization, and tautomer canonicalization are separate
198policy decisions; retain original IDs/structures and each transformation. They
199can erase experimental distinctions such as salt form, isotope labeling or stereo.
200`Reionize` is rule-based charge placement, not pKa/pH speciation. `Uncharger` cannot
201neutralize every ion (e.g. quaternary ammonium). `TautomerEnumerator().Enumerate`
202returns a result with `status`; ensure it is `TautomerEnumeratorStatus.Completed`
203before treating enumeration as complete. Its canonical tautomer is a scoring-rule
204representative, not a measured population maximum. Default tautomer transforms
205may remove stereo at affected centers/bonds.
207Record fingerprint family, radius (Morgan radius 2 is ECFP4-like), length,
208chirality, atom/bond invariants, bit versus count representation, count-simulation
209settings and RDKit version. Equal-length fingerprints from different pipelines
210are not interchangeable. Save `generator.GetInfoString()` where available.
211Specified stereo, unspecified stereo and a racemate are different data meanings;
212`useChirality=True` cannot recover information absent from the source.
214Even enabling chirality cannot make a folded fingerprint an identity key. In
2152026.03.6, `C[C@H](F)Cl` and `C[C@@H](F)Cl` have identical 2048-bit atom-pair
216fingerprints with `includeChirality=True`, despite different sparse count
217fingerprints. Increasing length changes collisions, not the identity guarantee.
219Plain isomeric SMILES also omits enhanced stereo-group relationships. Preserve
220these with CXSMILES or an appropriate SDF representation. The bundled text
221outputs include enhanced stereo groups, but omit other CX annotations and are
222not a lossless archive of every input property. Fingerprint chirality flags do
223not encode a complete sample-level interpretation of stereo groups.

Identity and deliberate standardization

This small example is executed by the regression suite:

from rdkit import Chem
from rdkit.Chem.MolStandardize import rdMolStandardize as standardize

def canonical(smiles):
    mol = Chem.MolFromSmiles(smiles)
    if mol is None or mol.GetNumAtoms() == 0:
        raise ValueError("Invalid or empty molecule")
    return Chem.MolToSmiles(mol, canonical=True, isomericSmiles=True)

assert canonical('OCC') == canonical('CCO')
assert canonical('C[C@H](O)F') != canonical('C[C@@H](O)F')
assert canonical('CC(=O)O') != canonical('CC(=O)[O-]')
assert canonical('CC=O') != canonical('C=CO')

original = Chem.MolFromSmiles('CC(=O)[O-].[Na+]')
cleaned = standardize.Cleanup(original)
assert len(Chem.GetMolFrags(cleaned)) == 2  # Cleanup does NOT strip salts
parent = standardize.FragmentParent(cleaned)
neutral = standardize.Uncharger().uncharge(parent)
assert Chem.MolToSmiles(neutral) == 'CC(=O)O'

Fragment selection, neutralization, and tautomer canonicalization are separate policy decisions; retain original IDs/structures and each transformation. They can erase experimental distinctions such as salt form, isotope labeling or stereo. Reionize is rule-based charge placement, not pKa/pH speciation. Uncharger cannot neutralize every ion (e.g. quaternary ammonium). TautomerEnumerator().Enumerate returns a result with status; ensure it is TautomerEnumeratorStatus.Completed before treating enumeration as complete. Its canonical tautomer is a scoring-rule representative, not a measured population maximum. Default tautomer transforms may remove stereo at affected centers/bonds.

Record fingerprint family, radius (Morgan radius 2 is ECFP4-like), length, chirality, atom/bond invariants, bit versus count representation, count-simulation settings and RDKit version. Equal-length fingerprints from different pipelines are not interchangeable. Save generator.GetInfoString() where available. Specified stereo, unspecified stereo and a racemate are different data meanings; useChirality=True cannot recover information absent from the source.

Even enabling chirality cannot make a folded fingerprint an identity key. In 2026.03.6, C[C@H](F)Cl and C[C@@H](F)Cl have identical 2048-bit atom-pair fingerprints with includeChirality=True, despite different sparse count fingerprints. Increasing length changes collisions, not the identity guarantee.

Plain isomeric SMILES also omits enhanced stereo-group relationships. Preserve these with CXSMILES or an appropriate SDF representation. The bundled text outputs include enhanced stereo groups, but omit other CX annotations and are not a lossless archive of every input property. Fingerprint chirality flags do not encode a complete sample-level interpretation of stereo groups.

Raw lines 170 to 223