Extending Typifiers¶
Typifiers operate on molecular graphs. The core contract is the molrs base
mp.typifier.Typifier:
import molpy as mp
class MyTypifier(mp.typifier.Typifier):
def match(self, graph: mp.Atomistic) -> mp.typifier.Match: ...
typify(mol) belongs to the base and is final: it copies mol, calls match
on the copy, writes the returned Match onto the copy and defines its types in
the output force field, forcefield(). The returned object is a typed
Atomistic; the input is never touched. A typifier must not return a Frame;
call .to_frame() after typification when a writer or potential compiler needs
columnar data.
Required API Shape¶
- Implement
match(graph) -> Matchand, optionally,library(). Definingtypifyon a subclass raisesTypeErrorat class creation. library()returns the force field the typifier matches against. The output starts as its empty likeness — its name, declared units and special_bonds — so declare them there (the AmberTools typifiers declarerealunits and the AMBER 1-4 scaling this way).matchmay write intermediate results (generated angles and dihedrals, perceived bond types) onto the graph it is given:typifyalways hands it a private copy.- Do not add
from_forcefield(ff). A typifier constructor loads or builds its own parameter tables and keeps what it needs internally. forcefield()is the union of whattypifyassigned. Do not add a second way to build it.
The match must cover every topology class the force field supports. For OPLS-AA that means atoms, bonds, angles, and dihedrals; for MMFF it also includes out-of-plane impropers. If a force field has no improper table, do not synthesize one just to satisfy a generic abstraction.
The Match¶
Match(nodes, links, styles=..., pairs=...):
nodes: one mapping ofkey -> annotationper atom, positional againstgraph.atoms.links: relation class (mp.Bond,mp.Angle,mp.Dihedral,mp.Improper) to one mapping per row, positional againstgraph.links.exact_bucket(cls).styles:(category, style, params)to declare.pairs:(style, name, endpoints, params)pair rows.
An annotation is a plain value (stamped, defines nothing) or a type
(style, name, endpoints, params), which stamps name and every param and
defines the type on endpoints — atom-type names, empty for an atom type.
Names are opaque: endpoints are always given, never parsed out of a name.
class ElementBondTypifier(mp.typifier.Typifier):
"""Atom types from elements; one harmonic bond type per element pair."""
def match(self, graph):
nodes = [
{"type": ("full", atom["element"], (), {"mass": atom["mass"]})}
for atom in graph.atoms
]
bonds = []
for bond in graph.links.exact_bucket(mp.Bond):
ends = sorted(atom["element"] for atom in bond.endpoints)
bonds.append({"type": ("harmonic", "-".join(ends), ends, {"k": 300.0, "r0": 1.5})})
return mp.typifier.Match(
nodes, {mp.Bond: bonds}, styles=[("atom", "full", {}), ("bond", "harmonic", {})]
)
typifier = ElementBondTypifier()
typed = typifier.typify(mp.io.read_smiles("CCO"))
assert sorted({bond["type"] for bond in typed.bonds}) == ["C-C", "C-O"]
assert {t.name for t in typifier.forcefield().get_style("bond", "harmonic").types} == {"C-C", "C-O"}
Matcher Boundary¶
The matcher is an implementation detail of a typifier, not the typifier itself. Use molrs SMARTS matching directly:
mol = mp.io.read_smiles("CCO")
pattern = mp.SmartsPattern("[C:1][O:2]")
matches = pattern.find_matches(mol)
Matches are bindings: atom ids plus optional mapping labels. They are not graphs and not frames. Do not use a Python igraph matcher or MolPy-side layered matcher classes; OPLS-AA and MMFF matching live in molrs.
Where a Typifier Lives¶
Force-field typifiers that decide types by SMARTS rules (OPLS-AA, MMFF94) are
native: they live in molrs and mp.typifier re-exports them one by one. A new
rule-based force field belongs there too.
A MolPy-side typifier is one that drives an external tool, as
AntechamberTypifier and TLeapTypifier in molpy/typifier/ambertools.py
do: match writes the graph for the tool, runs it through a molpy.wrapper
wrapper, reads the result back and returns it as a Match. Keep the rules
explicit:
- Keep atom order: the tool's row i is graph atom i, and bonded terms are matched by their endpoint rows — raise when the two disagree.
- Raise when the tool is missing, fails, or leaves its output unwritten, with the tool's stderr in the message.
- Declare units and scaling in
library(), not by patching the output.
Tests¶
New typifiers need focused tests at three levels:
- Atom coverage: expected
typeand charge on representative real molecules. - Topology coverage: expected bond, angle, dihedral, and improper types plus
parameter columns after
typify(). - Force-field coverage:
forcefield()holds exactly the typestypifyassigned, with their parameters.
A typifier that shells out never runs the tool in unit tests: patch
subprocess.run so each call copies a committed output fixture into place, as
tests/test_typifier/test_ambertools.py does.