# In Google Colab, install SMSD Pro first; elsewhere this cell does nothing.
import importlib.util, subprocess, sys
if importlib.util.find_spec("smsd") is None:
subprocess.run([sys.executable, "-m", "pip", "install", "-q", "smsd==8.1.0", "rdkit<2027"], check=True)
MCS to a small library search¶
This notebook uses SMSD 8.1.0 to turn a common graph into a SMARTS query, search a small molecular library, and explore fingerprint neighbours and clustering. Search results retain their completion and proof status. NumPy and scikit-learn are optional; the cells that need them report when they are missing.
Setup: local Jupyter or Colab¶
In local Jupyter, select a kernel with SMSD 8.1.0, NumPy and scikit-learn already installed. The setup cell uses that package and performs no upload or installation.
Upload this notebook file to Colab and start a fresh Linux x86_64 runtime. Run the setup cell first and upload exactly one trusted SMSD 8.1.0 core wheel matching its CPython version and ABI. Download the wheel beforehand from PyPI or the GitHub releases page; this notebook does not download packages. The filename and compatible Linux x86_64 tags are checked before installation, followed by an exact imported-version check. The optional CUDA wheel is not needed.
Colab normally supplies NumPy and scikit-learn. The setup cell checks their availability but does not install dependencies. It installs only the uploaded core wheel with this kernel's sys.executable, --no-index, --no-deps and --force-reinstall. Restart the runtime before repeating the upload setup after importing SMSD. Uploaded wheels contain native code; use a trusted matching artefact.
# Local notebooks use the preinstalled package; only Colab offers a wheel upload.
import importlib.util
import platform
import subprocess
import sys
from pathlib import Path
from tempfile import TemporaryDirectory
try:
_shell = get_ipython()
except NameError:
_shell = None
_in_colab = _shell is not None and type(_shell).__module__.startswith("google.colab.")
if _in_colab:
if sys.platform != "linux" or platform.machine().lower() != "x86_64":
raise RuntimeError("This upload route requires Linux x86_64 Colab")
if "smsd" in sys.modules:
raise RuntimeError("Restart the Colab runtime, then run this setup cell first")
from google.colab import files
try:
from packaging.tags import sys_tags
from packaging.utils import parse_wheel_filename
except ImportError:
from pip._vendor.packaging.tags import sys_tags
from pip._vendor.packaging.utils import parse_wheel_filename
_uploaded = files.upload()
if len(_uploaded) != 1:
raise ValueError("Upload exactly one matching SMSD 8.1.0 core wheel")
_filename, _payload = next(iter(_uploaded.items()))
if Path(_filename).name != _filename:
raise ValueError("The wheel filename must be a basename")
_name, _version, _, _tags = parse_wheel_filename(_filename)
_python_tag = f"cp{sys.version_info.major}{sys.version_info.minor}"
_compatible = set(sys_tags())
_matching_tag = any(
tag in _compatible and tag.interpreter == _python_tag and tag.abi == _python_tag
and (tag.platform == "linux_x86_64"
or (tag.platform.startswith("manylinux_") and tag.platform.endswith("_x86_64")))
for tag in _tags
)
if str(_name) != "smsd" or str(_version) != "8.1.0" or not _matching_tag:
raise ValueError("Expected an SMSD 8.1.0 Linux x86_64 wheel for this CPython ABI")
with TemporaryDirectory() as _directory:
_wheel = Path(_directory) / _filename
_wheel.write_bytes(_payload)
subprocess.check_call([
sys.executable, "-m", "pip", "install", "--no-index", "--no-deps",
"--force-reinstall", "--disable-pip-version-check", str(_wheel),
])
import smsd
if smsd.__version__ != "8.1.0":
raise RuntimeError("Use SMSD 8.1.0 in this notebook's kernel")
_missing = [label for name, label in (("numpy", "NumPy"), ("sklearn", "scikit-learn"))
if importlib.util.find_spec(name) is None]
if _missing:
print("Warning: optional dependencies not installed:", ", ".join(_missing) + ".",
"The cells that need them report this instead of failing.")
else:
print("Using SMSD", smsd.__version__, "with NumPy and scikit-learn available")
Warning: optional dependencies not installed: scikit-learn. The cells that need them report this instead of failing.
import smsd
print("SMSD", smsd.__version__)
library = [
"c1ccccc1O", "c1ccccc1N", "Cc1ccccc1", "CCO",
"CCN", "CC(=O)O", "CCOc1ccccc1", "C1CCCCC1",
]
query_smiles, target_smiles = library[:2]
SMSD 8.1.0
Find the common graph and export SMARTS¶
The search below maximises common induced atoms under the default chemistry rules. Both the time and node limits are explicit. A found candidate is usable as a pattern even if stopped, but it is described as a maximum only when optimal is true. SMARTS export raises for matching rules that cannot be represented, rather than dropping those rules.
options = smsd.MCS.Options()
options.timeout_ms = 1000
options.node_limit = 100000
common = smsd.MCS.find(query_smiles).with_options(options).match_with_smarts(target_smiles)
result = common.result
print({
"found": result.found, "status": result.status.name,
"optimal": result.optimal, "complete": result.complete,
"nodes_visited": result.nodes_visited, "elapsed_ms": result.elapsed_ms,
})
if not result.found:
raise RuntimeError("No common graph was found within the search budget")
pattern = common.smarts
print("Proved maximum pattern:" if result.optimal else "Candidate pattern; maximum unproved:", pattern)
{'found': True, 'status': 'COMPLETED', 'optimal': True, 'complete': True, 'nodes_visited': 7, 'elapsed_ms': 0}
Proved maximum pattern: [#6]-,=,:1-,=,:[#6]-,=,:[#6]-,=,:[#6]-,=,:[#6]-,=,:[#6]1
search_options = smsd.Substructure.Options()
search_options.timeout_ms = 1000
search_options.node_limit = 100000
search = smsd.Substructure.smarts(pattern).with_options(search_options)
search_results = search.match_many(library, num_threads=1)
for smiles, match in zip(library, search_results):
print({
"smiles": smiles, "found": match.found,
"status": match.status.name, "optimal": match.optimal,
"complete": match.complete,
})
# An unfinished result with found=False does not prove absence.
{'smiles': 'c1ccccc1O', 'found': True, 'status': 'COMPLETED', 'optimal': True, 'complete': True}
{'smiles': 'c1ccccc1N', 'found': True, 'status': 'COMPLETED', 'optimal': True, 'complete': True}
{'smiles': 'Cc1ccccc1', 'found': True, 'status': 'COMPLETED', 'optimal': True, 'complete': True}
{'smiles': 'CCO', 'found': False, 'status': 'COMPLETED', 'optimal': True, 'complete': True}
{'smiles': 'CCN', 'found': False, 'status': 'COMPLETED', 'optimal': True, 'complete': True}
{'smiles': 'CC(=O)O', 'found': False, 'status': 'COMPLETED', 'optimal': True, 'complete': True}
{'smiles': 'CCOc1ccccc1', 'found': True, 'status': 'COMPLETED', 'optimal': True, 'complete': True}
{'smiles': 'C1CCCCC1', 'found': True, 'status': 'COMPLETED', 'optimal': True, 'complete': True}
from smsd.highlight import mcs_svg
svg_text = mcs_svg(query_smiles, target_smiles, result, width=600, height=300)
try:
from IPython.display import SVG, display
except ImportError:
print("SVG generated as svg_text; inline display requires IPython")
else:
display(SVG(svg_text))
Optional fingerprint features¶
The feature adapter returns one native fingerprint per row. Binary features default to uint8; count features default to int64. The classifier below is a small pipeline demonstration with example labels, not an estimate of predictive performance.
try:
from smsd.adapters import fingerprints_numpy
features = fingerprints_numpy(library, kind="ecfp", fp_size=256, num_threads=1)
except ImportError as error:
print("NumPy features unavailable:", error)
else:
print("Feature shape:", features.shape, "dtype:", features.dtype)
print("Set bits per molecule:", features.sum(axis=1).tolist())
Feature shape: (8, 256) dtype: uint8 Set bits per molecule: [12, 12, 12, 9, 9, 12, 20, 3]
try:
from sklearn.neighbors import KNeighborsClassifier
from sklearn.pipeline import make_pipeline
from smsd.adapters import FingerprintTransformer
except ImportError as error:
print("Optional scikit-learn pipeline unavailable:", error)
else:
example_labels = [1, 1, 1, 0, 0, 0, 1, 0]
model = make_pipeline(
FingerprintTransformer(kind="ecfp", fp_size=256, num_threads=1),
KNeighborsClassifier(n_neighbors=1),
)
model.fit(library, example_labels)
print("Example predictions:", model.predict(["COc1ccccc1", "CCC"]).tolist())
Optional scikit-learn pipeline unavailable: No module named 'sklearn'
Packed corpus, top-k neighbours and clustering¶
The file stores packed binary fingerprints and is opened read-only through a memory map. Pair scans are exact and quadratic; thresholds or top-k limit the returned data. Butina groups molecules near each cluster centre, while MaxMin selects diverse rows. This example uses a temporary file that is removed on completion.
from pathlib import Path
from tempfile import TemporaryDirectory
from smsd.corpus import FingerprintCorpus
from smsd.clustering import butina, maxmin
try:
import numpy
except ImportError:
print("Install the NumPy extra to run the mapped-corpus example")
else:
with TemporaryDirectory() as directory:
with FingerprintCorpus.from_smiles(
Path(directory) / "library.fp", library,
kind="ecfp", fp_size=256, chunk_size=3, num_threads=1,
) as corpus:
neighbours = list(corpus.top_k_pairs(2, threshold=0.2, chunk_size=3))
clusters = butina(corpus, threshold=0.5, reordering=True, chunk_size=3)
diverse = maxmin(corpus, count=4, seed=0, chunk_size=3)
print("Directed (query index, neighbour index, Tanimoto):", neighbours)
print("Butina clusters (centre first):", clusters)
print("MaxMin selection:", [(index, library[index]) for index in diverse])
Directed (query index, neighbour index, Tanimoto): [(0, 6, 0.3333333333333333), (1, 2, 0.4117647058823529), (2, 1, 0.4117647058823529), (3, 4, 0.2), (4, 3, 0.2), (6, 0, 0.3333333333333333)] Butina clusters (centre first): [(0,), (1,), (2,), (3,), (4,), (5,), (6,), (7,)] MaxMin selection: [(0, 'c1ccccc1O'), (3, 'CCO'), (7, 'C1CCCCC1'), (1, 'c1ccccc1N')]
All reported search statuses come from the engine. A fingerprint similarity or cluster assignment does not prove a chemical MCS. The temporary corpus stores bit width and input order; reuse the same fingerprint specification when supplying queries.