Skip to content

SMSD Pro Python examples

Runnable examples for the smsd Python package beyond molecule search. Each block is self-contained, and blocks that write files create them in the current directory. Searches, options and the full API reference are in the Python guide. Interpreter and platform requirements are in the supported-platforms table.

Fingerprints and similarity

Circular and torsion fingerprints

import smsd

mol = smsd.parse_smiles("c1ccc(O)cc1")
fp = smsd.circular_fingerprint(mol, radius=2, fp_size=2048)  # ECFP4
counts = smsd.circular_fingerprint_counts(mol, radius=2, fp_size=2048)
fcfp = smsd.circular_fingerprint(mol, radius=2, fp_size=2048, mode="fcfp")
torsion = smsd.topological_torsion(mol, fp_size=2048)
assert fp and counts and fcfp and torsion

ECFP describes structural environments and FCFP functional classes (Rogers and Hahn, 2010). Binary fingerprints hold set bit positions; count fingerprints hold (position, count) pairs. Choose the fingerprint and threshold on your own data.

Binary and count metrics

import smsd

mol1 = smsd.parse_smiles("CCO")
mol2 = smsd.parse_smiles("CCCO")
fp1 = smsd.circular_fingerprint(mol1, radius=2)
fp2 = smsd.circular_fingerprint(mol2, radius=2)
scores = [smsd.tanimoto_coefficient(fp1, fp2), smsd.dice(fp1, fp2),
          smsd.cosine(fp1, fp2)]
assert all(0.0 <= score <= 1.0 for score in scores)

c1 = smsd.circular_fingerprint_counts(mol1, radius=2)
c2 = smsd.circular_fingerprint_counts(mol2, radius=2)
count_tanimoto = smsd.count_tanimoto_coefficient(c1, c2)
dense1 = smsd.counts_to_array(c1, 2048)
dense2 = smsd.counts_to_array(c2, 2048)
count_dice = smsd.count_dice(dense1, dense2)
assert 0.0 <= count_tanimoto <= 1.0 and 0.0 <= count_dice <= 1.0

# Hexadecimal storage for a binary fingerprint.
hex_string = smsd.to_hex(fp1, fp_size=2048)
assert set(smsd.from_hex(hex_string)) == set(fp1)

count_dice() and count_cosine() accept dense vectors, (position, count) pairs or dictionaries; count_tanimoto_coefficient() and count_overlap_coefficient() take pairs. Binary tanimoto_coefficient() compares set positions only, even for count input. Two empty fingerprints score 1.0 with tanimoto_coefficient() and overlap_coefficient().

Configurable fingerprints

import smsd

aspirin = "CC(=O)Oc1ccccc1C(=O)O"
# ECFP4 with ring-size and stereo terms; the default ECFP options give the
# same bits as circular_fingerprint().
options = smsd.FingerprintOptions(family="ecfp", ring_size=True, cip_atoms=True, cip_bonds=True)
fp = smsd.fingerprint(aspirin, options=options)
keys = smsd.fingerprint(aspirin, options=smsd.FingerprintOptions.maccs())  # 166 MACCS keys
assert fp and keys

The same FingerprintOptions give the same bits in C++ and Java.

Screening

Rank a small set by similarity

import smsd

query = "CC(=O)Oc1ccccc1C(=O)O"                    # aspirin
library = ["OC(=O)c1ccccc1O", "CC(=O)Nc1ccc(O)cc1", "c1ccccc1", "CC(=O)Oc1ccccc1"]
ranked = smsd.batch_similarity(query, library)    # (index, Tanimoto), most similar first
assert library[ranked[0][0]] == "CC(=O)Oc1ccccc1"
for index, score in ranked:
    print(f"{score:.2f}  {library[index]}")

Screen a large library, then confirm by substructure

import itertools
import smsd

groups = ["C", "CC", "OC", "N", "O", "F", "Cl", "Br", "C#N", "C(F)(F)F",
          "C(=O)O", "C(=O)N", "S(=O)(=O)N", "OCC"]
library = [f"c1c({a})cc({b})cc1{c}" for a, b, c in itertools.product(groups, repeat=3)]
assert len(library) == 2744

corpus = smsd.TargetCorpus.from_smiles(library)   # parse once, search many times
corpus.prewarm()

query = "c1c(C)cc(O)cc1C(=O)O"
nearest = smsd.batch_similarity(query, library)[:10]

acid = smsd.Substructure.smarts("C(=O)[OX2H1]")
results = acid.match_many(corpus, num_threads=0)  # every processor core
hits = [library[i] for i, result in enumerate(results) if result.found]
assert library[nearest[0][0]] == query and query in hits

With OpenMP, as in the published wheels, num_threads=0 uses every core unless OMP_NUM_THREADS sets fewer; results keep the order of the targets.

Screen by MCS similarity, then compute the MCS

import itertools
import smsd

groups = ["C", "CC", "OC", "N", "O", "F", "Cl", "C(=O)O"]
library = [f"c1c({a})cc({b})cc1{c}" for a, b, c in itertools.product(groups, repeat=3)]

corpus = smsd.TargetCorpus.from_smiles(library)   # prepared once for repeated screens

query = "c1c(C)cc(O)cc1C(=O)O"
candidates = smsd.screen_targets(query, corpus, 0.8)   # targets that can reach 0.8
results = smsd.MCS.find(query).with_timeout(1000).match_many([library[i] for i in candidates])
assert candidates and all(result.found for result in results)

screen_targets bounds the atom-count Tanimoto similarity of the MCS from above, so under the same chemistry options it never rejects a target able to reach the threshold, and the MCS runs only where it can matter. For repeated screens of the same library, a prepared TargetCorpus such as this one saves converting the targets on every call; a list of molecules or SMILES gives the same indices.

This threshold applies to atom-count Tanimoto, not bond Tanimoto or weighted MCS scores. MCS.match_many itself searches every supplied target without a similarity threshold.

With smsd-cuda and an NVIDIA GPU, gpu_screening.py runs packed and corpus screens on the device and checks every result against the CPU; without them it runs on the CPU.

SVG drawing

The native SVG renderer needs no RDKit. Check each drawing before publishing it, as fonts depend on the viewer.

import xml.etree.ElementTree as ET
import smsd

svg = smsd.depict_svg("CC(=O)Oc1ccccc1C(=O)O")
assert ET.fromstring(svg).tag.endswith("svg")
smsd.save_svg(svg, "aspirin.svg")

query = smsd.parse_smiles("CC(=O)NC")   # N-methylacetamide
target = smsd.parse_smiles("CCC(=O)N")  # propanamide
result = smsd.MCS.find(query).with_timeout(1000).match(target)
mapping = dict(result.best.atom_mapping)
assert mapping == {0: 1, 1: 2, 2: 3, 3: 4}
smsd.save_svg(smsd.depict_pair(query, target, mapping), "mcs_pair.svg")

# Highlight target atoms: keys must be indices in the rendered molecule.
target_mapping = {b: a for a, b in mapping.items()}
smsd.save_svg(smsd.depict_mapping(target, target_mapping), "target.svg")

Styling

import smsd

options = smsd.DepictOptions()
options.bond_length = 35
options.show_map_numbers = False
options.font_family = "Arial, sans-serif"
for index, smiles in enumerate(["CCO", "c1ccccc1", "CC(=O)O"]):
    smsd.save_svg(smsd.depict_svg(smiles, opts=options), f"molecule_{index}.svg")

svg = smsd.depict_svg("c1ccc2c(c1)cc1ccccc1c2", bond_length=50,
                      width=800, height=400, padding=40, show_atom_indices=True)
smsd.save_svg(svg, "large.svg")

Keyword arguments such as bond_length=50 set DepictOptions attributes, on opts when it is passed.

Optional conversion to PNG

Convert aspirin.svg from above with Inkscape or CairoSVG, installed separately:

inkscape aspirin.svg --export-type=png --export-filename=aspirin.png --export-dpi=600
python -c "import cairosvg; cairosvg.svg2png(url='aspirin.svg', write_to='aspirin.png', dpi=600)"

Coordinates and layout

Two and three dimensions

import smsd

mol = smsd.parse_smiles("c1ccccc1")
coords = smsd.generate_coords_2d(mol, target_bond_length=1.5)
assert len(coords) == len(mol) and all(len(point) == 2 for point in coords)
quality = smsd.layout_quality(mol, coords)
assert quality >= 0.0
coords_3d = smsd.generate_coords_3d(mol, target_bond_length=1.5)
assert len(coords_3d) == len(mol) and all(len(point) == 3 for point in coords_3d)

A layout_quality() score of 0.0 is ideal. Three-dimensional coordinates are a starting geometry, not a validated conformer or energy minimum.

Refinement

import smsd

mol = smsd.parse_smiles("c1ccccc1")
coords = smsd.generate_coords_2d(mol)
_, coords = smsd.force_directed_layout(mol, coords, max_iter=100)
_, coords = smsd.stress_majorisation(mol, coords, max_iter=100)
crossings, coords = smsd.reduce_crossings(mol, coords, max_iter=100)
assert len(coords) == len(mol)

Refinement does not improve every layout; compare layout_quality() and inspect the drawing.

Transforms

import math
import smsd

mol = smsd.parse_smiles("c1ccccc1")
coords = smsd.generate_coords_2d(mol)
coords = smsd.translate_2d(coords, dx=10.0, dy=5.0)
coords = smsd.rotate_2d(coords, angle=math.radians(45))
coords = smsd.scale_2d(coords, factor=2.0)
coords = smsd.mirror_x(coords)
coords = smsd.mirror_y(coords)
coords = smsd.center_2d(coords)
coords = smsd.normalise_bond_length(mol, coords, target=1.5)
coords = smsd.canonical_orientation(mol, coords)
reference = smsd.generate_coords_2d(mol)
rmsd, aligned = smsd.align_2d(coords, reference)
box = smsd.bounding_box_2d(aligned)
assert len(aligned) == len(mol) and len(box) == 4 and rmsd >= 0.0

Angles are in radians. Save coordinates when exact reproducibility matters.

Stereo assignment

import smsd

alanine = smsd.parse_smiles("N[C@@H](C)C(=O)O")
assert smsd.assign_rs(alanine) == {1: "S"}
butene = smsd.parse_smiles("C/C=C/C")
assert smsd.assign_ez(butene) == {(1, 2): "E"}
descriptors = smsd.assign_cip(alanine)
assert descriptors.rs_labels[1] == smsd.RSLabel.S

Write explicit stereo markers in the SMILES; unspecified stereo is not assigned.

MOL and SDF files

Each example creates its own input files.

import smsd

mol = smsd.parse_smiles("CCO")
smsd.write_molfile(mol, "molecule.mol")
loaded = smsd.read_mol_file("molecule.mol")
assert len(loaded) == 3
block = smsd.write_mol_block(loaded)
block_v3000 = smsd.write_mol_block_v3000(loaded)
assert len(smsd.read_mol_block(block)) == 3
assert len(smsd.read_mol_block(block_v3000)) == 3
import smsd

molecules = [smsd.parse_smiles(s) for s in ["CCO", "c1ccccc1"]]
smsd.write_sdf(molecules, "compounds.sdf")
loaded = smsd.read_sdf("compounds.sdf")
assert [len(mol) for mol in loaded] == [3, 6]
smsd.write_sdf(loaded, "output.sdf")

read_sdf() loads the whole file and turns malformed records into empty graphs, so check each record. For large files, pass records one at a time to read_mol_block(). write_mol_block() and the SDF writers switch to V3000 above 999 atoms or bonds.

R-group decomposition

import smsd

options = smsd.Substructure.Options()
options.timeout_ms = 1000
results = smsd.Substructure.decompose_r_groups("c1ccccc1", ["Oc1ccccc1", "Nc1ccccc1", "CCO"], options)
assert [result["search"].found for result in results] == [True, True, False]
assert results[0]["core"].n == 6 and results[0]["rgroups"]["R1"].n == 1

Each entry holds the core graph, the substituent graphs and the core's search result, which shows whether a missing core was proved absent.

Scaffolds

import smsd

parent = smsd.parse_smiles("Cc1ccccc1")
scaffold = smsd.murcko_scaffold(parent)
assert scaffold.n == 6
assert smsd.Substructure.find(scaffold).match(parent).found
result = smsd.MCS.find(scaffold).match("Oc1ccccc1")
assert result.best.atom_count == 6

The Bemis–Murcko scaffold keeps ring systems, their linkers and atoms double- or triple-bonded to them, such as carbonyl oxygens. An acyclic input is returned unchanged.

Graph utilities

import smsd

mol = smsd.parse_smiles("c1ccccc1.CCO")
assert smsd.count_components(mol) == 2
components = smsd.split_components(mol)
assert sorted(len(part) for part in components) == [3, 6]
assert smsd.same_canonical_graph(smsd.parse_smiles("c1ccccc1"),
                                 smsd.parse_smiles("C1=CC=CC=C1"))

To compare stereoisomers in a search, enable the stereo chemistry options.

Atom maps

import smsd

mapped = "[CH3:1][C:2](=[O:3])[OH:4]"
clean = smsd.strip_atom_maps(mapped)
assert clean == "[CH3][C](=[O])[OH]"
assert len(smsd.parse_smiles(clean)) == 4

Use strip_atom_maps() rather than a regular expression: outside brackets, a colon is an aromatic bond, so text replacement can alter the structure. parse_mapped_smiles() strips and parses in one call.

AI assistants

An AI assistant can call the molecular tools through the local MCP server (smsd-mcp) or through tool definitions in your own Python loop. The tools run on your machine. Each search result reports its status and whether it is optimal and complete. See Connecting AI assistants for set-up steps and worked examples. agent_workflow.py chains screening, MCS and depiction through call_tool without a model, and mcp_stdio_client.py checks the MCP server from Python.

Further reading

See the user guide, the Python guide, the installation guide, the C++ guide and the Java guide. Five notebooks work through the guide's topics: substructure and SMARTS search, maximum common substructure, fingerprints and similarity, library screening and CPU and GPU screening.

A C++ program that screens packed fingerprints on a Metal GPU, with the CPU screen as the fallback, is the Metal screening example.