Skip to content

molforge.chem

chem

Cheminformatics operations on :class:~molforge.core.Molecule.

Where :mod:molforge.core holds the small-molecule type and :mod:molforge.io reads molecules from files, this package holds the chemistry operations — standardization (cleaning) for consistent, deduplicable structures, descriptors, quality/dedup, and Bemis-Murcko scaffold extraction for series- and diversity-level analysis. Everything here is RDKit-backed and lazy: importing :mod:molforge.chem never pulls RDKit in, and an operation without RDKit raises :class:~molforge.core.RDKitNotInstalledError.

MoleculeDataset

MoleculeDataset(molecules: Iterable[Molecule])

A lazy, immutable pipeline over a stream of molecules.

Wrap any iterable of :class:~molforge.core.Molecule; the combinators (:meth:map, :meth:take) return new datasets and nothing runs until the dataset is iterated or :meth:collect-ed.

Attributes are intentionally hidden: a dataset is defined only by what it yields when iterated.

Wrap an iterable of molecules (not consumed until iterated).

map

map(fn: Callable[[Molecule], Molecule]) -> MoleculeDataset

Apply fn to every molecule, lazily.

Parameters:

Name Type Description Default
fn Callable[[Molecule], Molecule]

A per-molecule transform, e.g. :func:molforge.chem.standardize.

required

Returns:

Type Description
MoleculeDataset

A new dataset yielding fn(m) for each molecule m.

take

take(n: int) -> MoleculeDataset

Keep only the first n molecules.

Parameters:

Name Type Description Default
n int

How many molecules to keep; take short-circuits, so an unbounded source is fine.

required

Returns:

Type Description
MoleculeDataset

A new dataset yielding at most n molecules.

Raises:

Type Description
ValueError

If n is negative.

filter

filter(criterion: Criterion) -> MoleculeDataset

Keep molecules whose descriptors satisfy criterion.

criterion is a :class:~molforge.validation.Criterion over molecule descriptors — see :func:molforge.chem.molecule_descriptors for the vocabulary (molecular_weight, formal_charge, n_atoms, n_heavy_atoms). Its referenced names are validated up front, and only those descriptors are computed per molecule::

from molforge.validation import Criterion
ds.filter(Criterion.lt("molecular_weight", 500) & Criterion.le("formal_charge", 0))

Parameters:

Name Type Description Default
criterion Criterion

A criterion over descriptor names.

required

Returns:

Type Description
MoleculeDataset

A new dataset yielding only the molecules that satisfy

MoleculeDataset

criterion.

Raises:

Type Description
ValueError

If the criterion references an unknown descriptor.

RDKitNotInstalledError

If RDKit isn't installed (on consumption).

valid

valid() -> MoleculeDataset

Keep only molecules that pass RDKit sanitization.

A lazy filter over :func:molforge.chem.is_valid — structures RDKit rejects are dropped rather than raising.

Returns:

Type Description
MoleculeDataset

A new dataset yielding only the valid molecules.

dedup

dedup(*, key: str = 'inchikey') -> MoleculeDataset

Drop duplicate molecules by structural identity, keeping the first.

Streams with a running set of seen identities, so only the identities (not the molecules) are held in memory.

Parameters:

Name Type Description Default
key str

Identity to compare on — "inchikey" (default), "smiles", or "scaffold" (Bemis-Murcko scaffold SMILES, which keeps one representative per chemical series rather than per exact structure). See :func:molforge.chem.unique for the full semantics.

'inchikey'

Returns:

Type Description
MoleculeDataset

A new dataset yielding the first molecule of each identity, in

MoleculeDataset

order.

Raises:

Type Description
ValueError

If key isn't one of the supported identities.

group_by_scaffold

group_by_scaffold(
    *, generic: bool = False
) -> dict[str, list[Molecule]]

Group the molecules by Bemis-Murcko scaffold.

A terminal operation, like :meth:collect: grouping can't know a scaffold is complete until the stream ends, so this consumes the dataset and materializes the groups. Use it to size a library's chemical series, measure scaffold diversity, or pick representatives — where :meth:dedup with key="scaffold" keeps only the first molecule of each series and stays lazy.

Parameters:

Name Type Description Default
generic bool

Group on the generic framework (every atom a carbon, every bond single), so scaffolds differing only in their heteroatoms — benzene and pyridine — land in one group.

False

Returns:

Type Description
dict[str, list[Molecule]]

{scaffold_smiles: [molecules]}, keys in first-seen order and

dict[str, list[Molecule]]

each list in dataset order. Acyclic molecules have no scaffold

dict[str, list[Molecule]]

and share the "" group.

Raises:

Type Description
RDKitNotInstalledError

If RDKit isn't installed.

Example

Size a library's chemical series, then inspect one of them::

groups = MoleculeDataset(library).group_by_scaffold()
len(groups)                             # 41 distinct scaffolds
[m.name for m in groups["c1ccccc1"]]    # ['aspirin', 'paracetamol']

collect

collect() -> list[Molecule]

Materialize the dataset into a list, running the whole pipeline.

molecule_descriptors

molecule_descriptors(
    molecule: Molecule,
    *,
    names: Iterable[str] | None = None,
) -> dict[str, Any]

Compute filterable descriptors for a molecule.

Parameters:

Name Type Description Default
molecule Molecule

The molecule to describe.

required
names Iterable[str] | None

Which descriptors to compute; defaults to all of :data:DESCRIPTOR_NAMES. Restricting to the names a filter actually references avoids unnecessary RDKit work.

None

Returns:

Type Description
dict[str, Any]

A flat {name: value} dict, ready for

dict[str, Any]

meth:molforge.validation.Criterion.evaluate.

Raises:

Type Description
ValueError

If a requested name isn't a known descriptor.

RDKitNotInstalledError

If RDKit isn't installed.

is_valid

is_valid(molecule: Molecule) -> bool

Whether molecule passes RDKit sanitization.

Sanitization (valence, aromaticity, kekulization) runs on a copy, so the molecule is never mutated. A structure RDKit rejects — a pentavalent carbon, an unkekulizable ring — is reported as invalid rather than raising, so this reads as a predicate you can filter a set on.

Parameters:

Name Type Description Default
molecule Molecule

The molecule to check.

required

Returns:

Type Description
bool

True if the molecule sanitizes cleanly, False otherwise.

Raises:

Type Description
RDKitNotInstalledError

If RDKit isn't installed.

unique

unique(
    molecules: Iterable[Molecule], *, key: str = "inchikey"
) -> list[Molecule]

Deduplicate molecules by structural identity, keeping the first seen.

Parameters:

Name Type Description Default
molecules Iterable[Molecule]

The molecules to deduplicate.

required
key str

Which identity to compare on — "inchikey" (the default, a stable structural hash), "smiles" (canonical isomeric SMILES), or "scaffold" (Bemis-Murcko scaffold SMILES). InChIKey is the safer default; SMILES is there for when InChI generation is unavailable or undesirable. "scaffold" dedups at the level of the chemical series rather than the exact structure — one representative per scaffold — and treats all acyclic molecules as a single (empty) scaffold.

'inchikey'

Returns:

Type Description
list[Molecule]

A new list with duplicates removed, preserving input order and

list[Molecule]

keeping the first molecule of each identity.

Raises:

Type Description
ValueError

If key isn't one of the supported identities.

RDKitNotInstalledError

If RDKit isn't installed.

murcko_scaffold

murcko_scaffold(
    molecule: Molecule, *, generic: bool = False
) -> Molecule

Extract a molecule's Bemis-Murcko scaffold.

Returns a new :class:~molforge.core.Molecule, leaving the input untouched, and preserves its name while noting the extraction in metadata["scaffold"] ("murcko", or "murcko_generic" when generic) — the same convention the standardization ops follow.

An acyclic molecule has no scaffold, so the result is an empty molecule (n_atoms == 0, smiles == ""). That is Bemis-Murcko's own convention, not an error; a set of acyclic molecules therefore shares a single empty scaffold.

Parameters:

Name Type Description Default
molecule Molecule

The molecule to reduce (left unmodified).

required
generic bool

Return the generic framework instead — every atom becomes a carbon and every bond a single bond, so scaffolds that differ only in their heteroatoms or bond orders (benzene vs. pyridine) come out identical. Useful for coarser, topology-only grouping.

False

Returns:

Type Description
Molecule

The scaffold as a new :class:~molforge.core.Molecule.

Raises:

Type Description
RDKitNotInstalledError

If RDKit isn't installed.

Example

from molforge.core import Molecule from molforge.chem import murcko_scaffold aspirin = Molecule.from_smiles("CC(=O)OC1=CC=CC=C1C(=O)O") murcko_scaffold(aspirin).smiles 'c1ccccc1' nicotinamide = Molecule.from_smiles("c1ccncc1C(N)=O") murcko_scaffold(nicotinamide).smiles # pyridine, amide stripped 'c1ccncc1' murcko_scaffold(nicotinamide, generic=True).smiles # same frame as benzene 'C1CCCCC1'

canonical_tautomer

canonical_tautomer(molecule: Molecule) -> Molecule

Convert to RDKit's canonical tautomer.

cleanup

cleanup(molecule: Molecule) -> Molecule

Sanitize, normalize functional groups, and reionize.

largest_fragment

largest_fragment(molecule: Molecule) -> Molecule

Keep the largest organic fragment — strips salts and solvents.

neutralize

neutralize(molecule: Molecule) -> Molecule

Remove formal charges where chemically reasonable.