Adding a new fingerprint or descriptor set
Note
This page is written for LLM agents contributing to MATCHA. It trades prose for a step-by-step recipe with concrete file paths and code snippets.
All descriptor / fingerprint calculators live in src/matcha/datamodules/classic/rdkit_engine.py inside the Engine class. Adding a new one is a single-file change plus tests — do not scatter feature code across datamodules.
A new feature set touches four locations in Engine, plus the test file:
A
get_<name>method onEngine.An entry in
Engine._mapping(alias → method).An entry in
Engine._defaults(alias → parameter dict), if the calculator has tunable knobs.An entry in
Engine._dims(alias → output dimensionality).Coverage in
tests/datamodules/test_rdkit_engine.py.
1. Add the calculator method
Follow the shape of get_ECFP — parallelize over the mol list with parallelize, read tunables from self.defaults[<alias>], return a np.ndarray of shape (N, dim) with dtype=np.float32.
# src/matcha/datamodules/classic/rdkit_engine.py
from rdkit.Chem import AllChem
def get_dummy_embedding(
self, mols: list[Mol], n_jobs: int | None = None
) -> np.ndarray:
"""Dummy example: mean of Morgan bit vector per molecule, then tiled to `dim`.
Purpose here is to show the shape, not to be a useful descriptor.
"""
def batch_wrapper(batch):
params = self.defaults["dummy_embedding"]
out = []
for mol in batch:
fp = AllChem.GetMorganFingerprintAsBitVect(
mol, radius=params["radius"], nBits=params["n_source_bits"]
)
arr = np.zeros(params["n_source_bits"], dtype=np.float32)
from rdkit import DataStructs
DataStructs.ConvertToNumpyArray(fp, arr)
out.append(np.tile(arr.mean(keepdims=True), params["dim"]))
return out
feats = parallelize(
batch_wrapper, mols, n_jobs=n_jobs if n_jobs is not None else self.n_jobs
)
return np.array(feats, dtype=np.float32)
Use
parallelize(batch_wrapper, mols, n_jobs=...)— RDKit objects are non-pickleable at module scope, so the batch closure pattern is load-bearing.If the underlying RDKit function is not picklable at all (e.g.
GetErGFingerprint), wrap it withWrapper(name, module)at module top-level — see_wrapped_GetErGFingerprint.Return
float32for consistency with the rest ofEngine.
2. Register the alias in _mapping, _defaults, _dims
All three dicts are populated in Engine.__init__. Add matching entries:
# src/matcha/datamodules/classic/rdkit_engine.py, inside Engine.__init__
self._mapping = {
...,
"dummy_embedding": self.get_dummy_embedding,
}
self._defaults = {
...,
"dummy_embedding": {"radius": 2, "n_source_bits": 1024, "dim": 64},
}
self._dims = {
...,
"dummy_embedding": 64, # must match self._defaults["dummy_embedding"]["dim"]
}
The alias is load-bearing — it’s what
TabularFeaturizer/get_features/calculate_feature_dimlook up. Lowercase, snake_case, no renaming later without a migration.If
dimdepends on a default (e.g.nBits), reference it:self._dims["dummy_embedding"] = self._defaults["dummy_embedding"]["dim"].If the feature has no tunables, still register the empty dict in
_defaultsonly ifget_featuresneeds a lookup — otherwise skip it (seepubchem_fpandestatefor the mixed pattern).
3. Tests — tests/datamodules/test_rdkit_engine.py
The suite has one TestGet<Name> class per feature. Mirror the smallest existing one:
# tests/datamodules/test_rdkit_engine.py
class TestGetDummyEmbedding:
def test_shape(self, engine, mols):
out = engine.get_dummy_embedding(mols)
assert out.shape == (len(mols), engine.defaults["dummy_embedding"]["dim"])
assert out.dtype == np.float32
def test_dim_matches_registry(self, engine):
assert engine._dims["dummy_embedding"] == engine.defaults["dummy_embedding"]["dim"]
Also add
"dummy_embedding"to any parametrizedget_features/calculate_feature_dimtest that iterates overEngine._mapping.keys().
Checklist
get_<name>method onEngine, usesparallelize, returnsnp.ndarrayoffloat32.Alias added to
Engine._mapping,Engine._defaults(if tunable),Engine._dims._dims[alias]matches actual output width.Test class in
tests/datamodules/test_rdkit_engine.pycovering shape + dtype.uv run pytest tests/datamodules/test_rdkit_engine.pypasses.Codecov PR check stays green — new lines are covered and total coverage doesn’t drop (see
CONTRIBUTING.md→ Testing).