Skip to content

Python feature adapters and MCP

The optional modules smsd.adapters, smsd.mcp_server and smsd.highlight leave the core API unchanged. They are part of smsd 8.1.0 (python -m pip install "smsd==8.1.0"). NumPy, PyTorch, scikit-learn and the MCP SDK are loaded only by the functions that need them.

NumPy and PyTorch

The first Python block requires both NumPy and PyTorch, supplied by the torch extra. On macOS, use an OpenMP-free SMSD build or a compatible shared OpenMP runtime before running it; the source-build command below installs both extras. The adapters compute native molecular fingerprints and return a matrix with one molecule per row and one fingerprint position per column:

import smsd
from smsd.adapters import fingerprints_numpy, fingerprints_torch, to_numpy

molecules = ["CCO", "CCN", "c1ccccc1O"]
features = fingerprints_numpy(molecules, kind="ecfp", fp_size=2048)
assert features.shape == (3, 2048)
assert str(features.dtype) == "uint8"

tensor = fingerprints_torch(molecules, kind="ecfp", fp_size=2048)
assert tuple(tensor.shape) == (3, 2048)

options = smsd.FingerprintOptions(family="ecfp", size=512, counts=True)
counts = fingerprints_numpy(molecules, options=options)
assert counts.shape == (3, 512)
assert str(counts.dtype) == "int64"

sparse = smsd.fingerprints(molecules, kind="path", fp_size=128)
assert to_numpy(sparse, fp_size=128).shape == (3, 128)

to_numpy and to_torch consume sparse fingerprint rows: set-bit positions, or (position, count) pairs with counts=True. They do not consume dense bit vectors. Empty batches have shape (0, fp_size); an empty row is a zero vector. Repeated count bins are summed. Positions outside the declared size, negative counts and counts that overflow the chosen dtype raise ValueError. An explicitly selected floating dtype can reduce integer precision.

Binary output defaults to NumPy uint8 or PyTorch uint8; count output defaults to int64. PyTorch defaults to the CPU and accepts dtype and device, such as device="cuda" for an NVIDIA GPU or device="mps" on Apple silicon; on macOS, PyTorch may need the OpenMP-free SMSD build described below. Its adapter does not require NumPy. Native fingerprint options, including structural keys and count fingerprints, determine the output size and count mode when supplied.

Packaged macOS OpenMP wheels may require an OpenMP-free SMSD build with PyTorch. Different bundled OpenMP runtimes can abort the process; the adapters conservatively raise ImportError before importing PyTorch or running their native batch when they detect this combination. A shared runtime or an OpenMP-free SMSD build is required. To build the latter from the release source:

python -m pip install ".[numpy,torch]" --config-settings=cmake.define.SMSD_BUILD_OPENMP=OFF

This is a source build requiring the documented C++ build dependencies. Import order and a single requested thread do not resolve distinct OpenMP runtimes. No duplicate-runtime environment override is used.

scikit-learn

Install the sklearn extra. The transformer accepts an iterable of molecules or SMILES strings and participates in ordinary pipelines and parameter cloning:

from sklearn.neighbors import KNeighborsClassifier
from sklearn.pipeline import make_pipeline
from smsd.adapters import FingerprintTransformer

model = make_pipeline(
    FingerprintTransformer(kind="ecfp", fp_size=512),
    KNeighborsClassifier(n_neighbors=1),
)
model.fit(["CCO", "CCN", "c1ccccc1", "c1ccccc1O"], [0, 0, 1, 1])
assert model.predict(["CCO"]).tolist() == [0]

The transformer is stateless: fit records that it is fitted, and transform computes the fingerprints. Its counts=True mode supports ECFP, FCFP, path and torsion fingerprints. A scalar SMILES string must be wrapped in a list; the adapter will not treat its characters as separate molecules.

MCP over standard input/output

The optional smsd[mcp] extra uses the official Python SDK over standard input/output. Run smsd-mcp or python -m smsd.mcp_server in the environment that has the extra installed.

The eight tools are find_mcs, find_substructure, search_smarts, molecular_fingerprint, molecular_similarity, screen_molecules, standardise_molecule and depict_mcs. They accept bounded molecular text, not paths, commands or network requests. Native proof/status remains distinct from fingerprint scores and from whether a witness was found. Default standardisation changes representation only; chemical transformations require explicit options and the optional backend.

See MCP tools and function definitions for signatures, finite budgets, original-JSON validation, result pages, timing boundaries, SVG scope, function-definition formats, lazy framework adapters and the local-wheel container/demo recipes. Exporting definitions makes no model request.

MCS SVG highlight

The native SVG helper renders an existing result without running another search or importing an optional drawing toolkit:

import smsd
from smsd.highlight import mcs_svg

query, target = "c1ccccc1O", "c1ccccc1N"
result = smsd.MCS.find(query).with_timeout(1000).match(target)
svg = mcs_svg(query, target, result, width=600, height=300)
assert "<svg " in svg

Use the same molecules and numbering as the search. The SVG title and data-search-status, data-optimal and data-complete attributes retain the result's proof status. The pair renderer highlights mapped atoms and bonds joining them. If an MCES result omits a bond between mapped atoms, the helper raises rather than highlighting that extra bond; render smsd.depict_svg(result.best.to_graph(query)) to draw the selected common graph in that case.

MCP fingerprint limits

MCP inputs are limited to 16,384 text characters. The fingerprint tool accepts at most 1,024 parsed atoms, 65,536 fingerprint positions, radius 8 and path length 8. Path and torsion requests are also checked against a conservative 2,000,000-walk expansion bound before enumeration. Larger requests raise ValueError; these service limits do not change the native fingerprint APIs. Fingerprinting has no hard wall-clock deadline. Search budgets apply after parsing, as described above.

SciPy sparse features

Install smsd[scipy]. to_scipy accepts the same sparse positions or position/count pairs as to_numpy, and fingerprints_scipy computes native fingerprints first. Both return a canonical CSR matrix with sorted columns, combined duplicates and no stored zero counts. They never allocate the full dense feature matrix. Binary values default to uint8; counts default to int64. Only boolean/integer dtypes are accepted. Counts are combined as Python integers before checking the selected dtype; overflow raises rather than wrapping or rounding.

from smsd.adapters import to_scipy, fingerprints_scipy

counts = to_scipy([[], [(1, 2), (1, 3)]], fp_size=4, counts=True)
assert counts.toarray().tolist() == [[0, 0, 0, 0], [0, 5, 0, 0]]
assert fingerprints_scipy(["CCO", "CCN"], fp_size=64).shape == (2, 64)

pandas and Polars molecule columns

Install smsd[pandas] or smsd[polars]. molecules_pandas and molecules_polars return a DataFrame with molecule and fingerprint columns, in input order. Inputs must be native MolGraph objects or explicit SMILES strings. SMILES are parsed; native molecule references are retained. Other objects, missing values and scalar strings supplied as a batch raise. These functions do not stringify arbitrary objects or convert external molecule types implicitly.

pandas uses object columns with immutable fingerprint tuples. Polars uses an explicit Object column for molecules and typed lists of uint64 positions. With count options, its fingerprint rows are lists of [position, count] pairs, each value checked against uint64. Neither frame is a copy of the native molecules. Polars Object molecules cannot be exported as Arrow molecular data; export the fingerprint features separately.

from smsd.adapters import molecules_pandas, molecules_polars

pandas_frame = molecules_pandas(["CCO", "CCN"], fp_size=64)
polars_frame = molecules_polars(["CCO", "CCN"], fp_size=64)
assert pandas_frame["molecule"].iloc[0].n == 3
assert polars_frame["molecule"][0].n == 3

Both functions accept the ordinary native fingerprint arguments, including options for count mode, and num_threads. Empty batches retain both columns and their declared types.

Arrow fingerprint buffers

Install smsd[arrow]. to_arrow exports one fingerprint column as an Arrow fixed-size list of integer values. Sparse inputs are expanded once into an owned dense NumPy matrix. A native-endian, C-contiguous two-dimensional integer NumPy matrix can instead share its value buffer without copying. Boolean NumPy storage needs conversion to byte values, since Arrow boolean storage is bit-packed. Other layouts and byte orders also need a copy. Floating values are rejected, including in count mode.

import numpy as np
from smsd.adapters import to_arrow

features = np.array([[0, 1, 0, 1]], dtype=np.uint8)
table = to_arrow(features, fp_size=4, zero_copy_only=True)
assert table.schema.metadata[b"smsd.zero_copy"] == b"true"
assert table.column("fingerprint").to_pylist() == [[0, 1, 0, 1]]

Arrow retains the NumPy buffer owner after the caller drops its reference. Shared values still alias the original array: do not mutate that array while using the exported table. Use copy=True for isolation. zero_copy_only=True rejects any required copy and cannot be combined with copy=True.

Schema metadata records smsd.fp_size, smsd.counts, smsd.zero_copy, smsd.copy_reason and smsd.ownership. A false zero-copy flag means copying from the original input was necessary, even though Arrow subsequently retains the prepared NumPy buffer. dtype is an optional integer dtype for sparse expansion only; an existing matrix retains its dtype. This is a feature-table export, not molecular graph serialisation or a GPU transfer.

Similarity features in scikit-learn pipelines

The sklearn extra also supplies SimilarityTransformer. fit snapshots binary native fingerprints for its reference molecules, retaining order and duplicates. transform returns a dense float64 matrix of Tanimoto values, one column per fitted reference. It computes intersection/union of native set-bit positions; two empty fingerprints have similarity one. It does not fit an approximate index, execute MCS, or infer molecular identity from a fingerprint. The output matrix requires memory proportional to queries times references.

from sklearn.neighbors import KNeighborsClassifier
from sklearn.pipeline import make_pipeline
from smsd.adapters import SimilarityTransformer

model = make_pipeline(
    SimilarityTransformer(kind="ecfp", fp_size=128),
    KNeighborsClassifier(n_neighbors=1),
)
model.fit(["CCO", "c1ccccc1"], [0, 1])
assert model.predict(["CCO"]).tolist() == [0]

fit_transform consumes an input iterator once. Empty query batches produce zero rows; an empty reference fit raises. Changing fingerprint parameters after fitting requires a refit. Native molecule objects and SMILES are the accepted input types, as for the DataFrame adapters.

PyTorch Geometric molecular graphs

Install smsd[pyg]. to_pyg returns a PyTorch Geometric Data object, using the same macOS OpenMP guard as the existing PyTorch adapter. It defaults to CPU; an explicitly selected tensor device does not move molecular parsing or fingerprint generation to a GPU.

from smsd.adapters import to_pyg

graph = to_pyg("CCO")
assert tuple(graph.x.shape) == (3, 7)
assert tuple(graph.edge_index.shape) == (2, 4)
assert graph.atom_id.tolist() == [0, 1, 2]

All feature tensors are int64. Atom rows preserve native atom numbering; atom_id records it explicitly, and num_nodes retains isolated atoms. The seven x columns are atomic number, formal charge, mass number, recorded hydrogen count, degree, aromatic flag and ring flag. These are the native graph's recorded values, not newly inferred descriptors.

Every unique bond produces both directed edges, ordered by its native endpoint pair and then forward/reverse. The three edge_attr columns are recorded bond order, aromatic flag and ring flag. Empty graphs have shapes (0, 7), (2, 0) and (0, 3). No coordinates, stereo labels, training targets or one-hot encodings are invented. Stereo and coordinates are not encoded by this feature schema, so it is not a lossless molecular file format. Add task-specific features explicitly before model training.

The external data structures follow the official SciPy CSR interface, Arrow fixed-size-list interface and PyTorch Geometric Data interface.

Jupyter and Colab notebook

The feature-search notebook combines MCS-to-SMARTS search, SVG highlighting, native fingerprint features, a small scikit-learn pipeline and a temporary packed corpus. Its example labels are illustrative, not a predictive-performance evaluation.

Use a local kernel with SMSD 8.1.0, NumPy and scikit-learn already installed. The setup cell detects an actual Colab shell; ordinary local execution performs no upload or installation. Upload the notebook file to Colab and select a fresh Linux x86_64 runtime. There, the cell prompts for one user-uploaded SMSD 8.1.0 core wheel. The distribution name, version, CPython ABI and compatible Linux x86_64 platform tags are checked before installation with sys.executable -m pip --no-index --no-deps --force-reinstall. The imported version is then checked. No CUDA package is needed.

Download the matching trusted wheel beforehand from PyPI or the GitHub releases page; the notebook does not download packages. NumPy and scikit-learn must already be available; they are checked but are not installed by this cell. Run the upload setup before importing SMSD, and restart the Colab runtime before repeating it. A native wheel executes code when loaded.