Skip to content

Typifier

Graph typification and force-field parameter assignment.

The contract

A typifier is MolGraph -> MolGraph: it takes a molecular graph and returns a new one whose elements carry force-field types and parameters. Every typifier runs the same flow — copy, match, write the annotations back — so typify() is written once, on the base class, and match() is the single abstract step.

class Typifier[G: MolGraph](ABC):
    def typify(self, graph: G) -> G: ...      # concrete: copy, match, write back
    @abstractmethod
    def match(self, graph: G) -> Match: ...   # the only thing that differs

The pipeline is generic over the graph. An Atomistic and a CoarseGrain are both molrs graph leaves, and a concrete typifier specialises G to the one it understands. Nothing in the contract mentions bonds, angles or dihedrals: that decomposition belongs to a force field, not to typification.

Typifiers are named after the force field or the tool that decides the types. There is no "typifier" that merely spends a type it was given — that is a component, ForceFieldParams.

Quick reference

Symbol Summary Preferred for
Typifier The contract: one abstract match Writing your own
OPLSAATypifier Full OPLS-AA typing pipeline re-exported from molrs.typifier OPLS-AA all-atom force fields
MMFFTypifier Full MMFF94 typing pipeline re-exported from molrs.typifier MMFF all-atom force fields
ClpTypifier CL&P ionic-liquid overlay: molrs SMARTS types + MolPy parameters Ionic-liquid force fields
AmberToolsTypifier GAFF atom types via antechamber; accumulates the force field it discovers GAFF / AmberTools
ForceFieldParams Not a typifier. Annotates pair and bonded terms from node types A graph whose types are already known

UFF lives in molrs (Rust / WASM UFFTypifier); MolPy re-exports it when the published molrs minor exposes a Python binding.

Canonical example

import molpy as mp
from molpy.typifier import OPLSAATypifier

typifier = OPLSAATypifier(strict=True)
typed_mol = typifier.typify(mol)  # returns a new Atomistic
frame = typed_mol.to_frame()

Key behavior

  • typify() returns a new graph — the original is not modified
  • A typifier never asks whether its graph is a fragment. Truncation is a fact about provenance, not something readable off a graph's valences: a radical is a perfectly good molecule. The party that cut the graph completes it — see RegionTypes.of, which caps every region it types because every region is a cut
  • A term the force field does not parameterise is left undecided, never stamped with None
  • SMARTS matching is implemented in molrs; MolPy no longer carries a matcher

Writing a typifier

Implement match and stop. It returns the annotations each node and each link should receive, positional against graph.nodes and graph.links.bucket(cls).

from molpy.typifier import ForceFieldParams, Match, Typifier

class MyTypifier(Typifier[mp.Atomistic]):
    def __init__(self, forcefield):
        self._params = ForceFieldParams(forcefield)

    def match(self, graph):
        node_types = [{"type": decide(atom)} for atom in graph.atoms]
        return self._params.match(graph, node_types)

ForceFieldParams is the tail every force-field typifier ends with. It is also the one place in MolPy that knows a Bond is parameterised by a BondType — arity cannot decide that, since a dihedral and an improper both span four atoms.


Full API

Contract

base

What a typifier is: MolGraph -> MolGraph.

One pipeline, one hook. Every typifier copies the graph, matches it, and writes the annotations back — and the only step that differs between an OPLS typifier, an antechamber typifier and a coarse-grained typifier is the match. So :meth:Typifier.typify is concrete and shared, and :meth:Typifier.match is the single abstract method.

A typifier does not ask whether the graph it was handed is a fragment. Whether a graph was cut out of a larger one is a fact about its provenance, not something readable off its valences: a radical is a perfectly good molecule, and a connectivity graph with no bond orders looks under-coordinated everywhere. Guessing it would be guessing an identity. The party that cut the graph knows, and it is the one that completes it — see :meth:~molpy.typifier.region.RegionTypes.of, which caps every region it types because every region is, by construction, a cut.

The pipeline is generic over the graph: an :class:~molpy.core.atomistic.Atomistic and a :class:~molpy.core.cg.CoarseGrain are both molrs.Graph leaves, and a concrete typifier specialises G to the one it types. Nothing here knows what a bond, an angle or a dihedral is — that is a fact about a force field, and it lives in :mod:molpy.typifier.forcefield.

Typifiers are named after the force field or the tool that decides the types: :class:~molpy.typifier.clp.ClpTypifier, :class:~molpy.typifier.ambertools.AmberToolsTypifier, OPLSAATypifier, MMFFTypifier. "Assign parameters to a graph whose types are already known" is not a typifier — it is the second half of one, and it is :class:~molpy.typifier.forcefield.ForceFieldParams.

Match dataclass

Match(nodes, links=dict())

The annotations matching a graph produced, ready to be written back.

nodes is positional against graph.nodes; links maps each link class to a tuple positional against graph.links.bucket(cls). A match taken from a valence-completed graph may be written onto the graph it completed: the completion keeps the original elements first and appends the caps, so the write is a prefix copy and the caps fall off the end.

An empty mapping in either sequence means "this element got nothing", which is how an unparameterised term stays untyped instead of being stamped with None.

write_onto
write_onto(graph)

Write this match onto graph, whose elements are a prefix of the matched graph's.

Raises:

Type Description
ValueError

if the match is shorter than the graph it is written onto — the two came from different graphs, and a silent truncation would leave elements untyped for no stated reason.

Typifier

Bases: Typifier, ABC

Assign force-field types and parameters to a molecular graph.

Subclasses implement :meth:match and nothing else. Specialise G to the graph kind the typifier understands.

match abstractmethod
match(graph)

Decide what every element of graph should be annotated with.

graph arrives valence-completed and is a private copy, so an implementation may write intermediate results onto it — writing the node types it just decided, for instance, so that the bonded terms can be matched against them.

typify
typify(graph)

Return a new graph carrying types and parameters; graph is untouched.

graph is taken at face value: whatever it is, that is the molecule being typed. A caller holding a fragment completes its valences first — a matcher shown a raw slice sees radicals where the cut fell, and types the interior against them.

Force-field parameters

forcefield

Turn node types into force-field parameters.

:class:ForceFieldParams is not a typifier — it decides nothing about what an atom is. It is the second half of every force-field typifier: given a graph whose nodes already carry a type, it looks each pair and bonded term up in a force field and annotates it. ClpTypifier and AmberToolsTypifier differ only in how they obtain those node types; both hand the result to this class.

It is also the one place in molpy that knows a :class:~molpy.core.atomistic.Bond is parameterised by a :class:~molpy.core.forcefield.BondType. That mapping cannot be derived from arity — a dihedral and an improper both span four atoms — so it is written down once, in :data:_FF_TYPE_OF. When a coarse-grained force field grows a CGBondType, one line is added there and nothing else changes.

ForceFieldParams

ForceFieldParams(forcefield, *, strict=True)

Annotate a graph's pair and bonded terms from its node types.

Not a :class:~molpy.typifier.base.Typifier: it never decides a node's type, it only spends one. Use it as the tail of a typifier's match (via :meth:match), or on its own when the node types are already on the graph (via :meth:assign).

A kind the force field declares no types for is not parameterised by it, and its terms are left alone — that is a fact about the force field, not a failure. A term the force field should cover but does not is an error when strict is on.

Parameters:

Name Type Description Default
forcefield ForceField

The force field to look types and parameters up in.

required
strict bool

Raise on a term this force field ought to parameterise but does not, rather than leaving it untyped.

True
assign
assign(graph)

Return a copy of graph with its pair and bonded terms parameterised.

For a graph whose node types are already known — the output of an AmberTools run, say, whose topology was then regenerated. Replaces the old Atomistic.assign_bonded_types, which matched force-field type names by splitting them on "-" (no wildcards, no classes, no overlay layers) and left a term it could not match silently unlabelled.

match
match(graph, node_types=None)

Annotate graph's nodes and links.

Parameters:

Name Type Description Default
graph Any

The graph to annotate. It is written to: the node types are stamped on so the bonded terms can be matched against them. Callers pass the private valence-completed copy a typifier owns.

required
node_types Sequence[Mapping[str, Annotation]] | None

Per-node annotations the caller's matcher decided ({"type": ...}, possibly with class). None means the types are already on graph.

None

Raises:

Type Description
TypeError

if graph carries a link kind no force field in molpy knows how to parameterise. Adding one is a line in :data:_FF_TYPE_OF, never a silent skip.

CL&P Typifier

clp

CL&P ionic-liquid force-field typifier.

CL&P stays in molpy (OPLS-AA moved to molrs). It is an OPLS-AA overlay: the built-in force field is oplsaa.xml with clp.xml layered on top (layer 1), so CL&P atom types (imidazolium ring, alkyl chain, and the BF4/PF6/NTf2/FSI/dca anions) override the OPLS base while OPLS remains the fallback.

Atom typing itself is SMARTS-based and that matcher is owned by molrs, so the only thing this typifier's match does is ask molrs for the atom types and hand them to :class:~molpy.typifier.forcefield.ForceFieldParams — exactly the "SMARTS owned by molrs, CL&P parameters stay a molpy overlay" split.

ClpTypifier

ClpTypifier(forcefield=None, *, strict=True)

Bases: Typifier[Atomistic]

CL&P ionic-liquid typifier — molrs SMARTS atom typing + molpy parameters.

Parameters:

Name Type Description Default
forcefield ForceField | None

The CL&P-over-OPLS overlay; the built-in one by default.

None
strict bool

Raise on an atom no SMARTS pattern matches, or a bonded term the force field does not parameterise.

True
load_forcefield staticmethod
load_forcefield()

Load the built-in CL&P force field as an OPLS-AA overlay.

AmberTools Typifier

ambertools

GAFF typifier backed by AmberTools.

Decoupled from :class:~molpy.builder.ambertools.AmberTools (the raw antechamber/parmchk2/tleap wrapper): this is the typifier. Its match drives antechamber over the graph, reads back the GAFF atom types, and hands them to :class:~molpy.typifier.forcefield.ForceFieldParams like every other force-field typifier does.

Types only, not charges. Atom types + bonded params are charge-method independent, so the graph is typed with gas charges — no sqm/AM1-BCC solve. Recomputing charges from a capped fragment would be both non-local and biased (adding H to a cut ether-O makes antechamber see a hydroxyl); charge is instead conserved by construction, by folding each cap's charge onto its site atom on the template so a deleted atom carries exactly zero. This typifier never writes a charge back.

Valence completion is not this class's business either: antechamber cannot parameterise a sliced fragment, but whether a graph is a fragment is known to whoever cut it. :meth:~molpy.typifier.region.RegionTypes.of completes every region before typing it; a caller typing a whole molecule has nothing to complete.

AmberToolsTypifier

AmberToolsTypifier(amber)

Bases: Typifier[Atomistic]

Type a graph's atoms with GAFF via antechamber; accumulate its force field.

Parameters:

Name Type Description Default
amber AmberTools

The antechamber/parmchk2/tleap wrapper.

required
forcefield property
forcefield

The accumulated force field of everything typed so far (None until one).

match
match(graph)

Run antechamber over graph and annotate it from the GAFF it yields.