Skip to content

ForceField

forcefield

Clean, format-agnostic force field representation for Q2MM.

Decouples Q2MM's optimization from specific file formats (MM3 .fld, Tinker .prm, AMBER .frcmod). Parameters are identified by element pairs/triples, not format-specific atom type strings or line numbers.

FunctionalForm

Bases: str, Enum

Physical functional form used by a force field.

Determines which energy expressions an engine should build:

  • HARMONIC: Standard harmonic bonds/angles, periodic torsions, Lennard-Jones 12-6 vdW. Used by AMBER, OPLS, GAFF, etc.
  • MM3: Allinger's MM3 cubic bond stretch, sextic angle bend, buffered 14-7 vdW.

The enum is orthogonal to source_format (file format) — an MM3 force field can be loaded from .fld or .prm files, while a HARMONIC force field comes from .frcmod or programmatic construction.

BondParam dataclass

BondParam(elements: tuple[str, str], equilibrium: float, force_constant: float, label: str = '', env_id: str = '', ff_row: int | None = None)

A bond force field parameter.

Units (canonical): force_constant in kcal/(mol·Å²), equilibrium in Å. Energy convention: E = k·(r − r₀)².

key property

key: tuple[str, str]

Sorted element pair for canonical matching (e.g., ('C', 'F')).

AngleParam dataclass

AngleParam(elements: tuple[str, str, str], equilibrium: float, force_constant: float, label: str = '', env_id: str = '', ff_row: int | None = None)

An angle force field parameter.

Units (canonical): force_constant in kcal/(mol·rad²), equilibrium in degrees. Energy convention: E = k·(θ − θ₀)².

key property

key: tuple[str, str, str]

Canonical key: center fixed, outers sorted.

TorsionParam dataclass

TorsionParam(elements: tuple[str, str, str, str], periodicity: int = 1, force_constant: float = 0.0, phase: float = 0.0, label: str = '', env_id: str = '', ff_row: int | None = None)

A torsion/dihedral force field parameter.

Each object represents a single Fourier component (V_n). An MM3 torsion line with V1, V2, V3 produces three TorsionParam objects with periodicity 1, 2, 3 respectively.

VdwParam dataclass

VdwParam(atom_type: str, radius: float, epsilon: float, element: str = '', reduction: float = 0.0, label: str = '', ff_row: int | None = None)

An atom-type van der Waals parameter.

__post_init__

__post_init__()

Normalize atom_type and auto-extract element if not provided.

Source code in q2mm/models/forcefield.py
def __post_init__(self):
    """Normalize atom_type and auto-extract element if not provided."""
    self.atom_type = str(self.atom_type).strip()
    if not self.element:
        self.element = _extract_element(self.atom_type)

ForceField dataclass

ForceField(name: str = 'Q2MM Force Field', bonds: list[BondParam] = list(), angles: list[AngleParam] = list(), torsions: list[TorsionParam] = list(), vdws: list[VdwParam] = list(), source_path: Path | None = None, source_format: Literal['mm3_fld', 'tinker_prm', 'openmm_xml', 'amber_frcmod'] | None = None, functional_form: FunctionalForm | None = None)

Format-agnostic force field representation.

Parameters are identified by element tuples, not format-specific atom types or line numbers. This eliminates matching bugs between different parsers.

Usage

ff = ForceField.from_mm3_fld("mm3.fld") ff = ForceField(bonds=[BondParam(('C', 'F'), 1.38, 5.0)])

Export to MM3 .fld is planned but not yet implemented.

n_params property

n_params: int

Number of adjustable scalar parameters in get_param_vector().

Layout: 2 per bond (k, r0) + 2 per angle (k, theta0) + 1 per torsion (k) + 2 per vdw (radius, epsilon).

get_bond

get_bond(elem1: str, elem2: str, env_id: str = '') -> BondParam | None

Find bond parameter by element pair and optional environment ID.

Source code in q2mm/models/forcefield.py
def get_bond(self, elem1: str, elem2: str, env_id: str = "") -> BondParam | None:
    """Find bond parameter by element pair and optional environment ID."""
    key = tuple(sorted([elem1, elem2]))
    for b in self.bonds:
        if b.key == key:
            if env_id and b.env_id and b.env_id != env_id:
                continue
            return b
    return None

get_bonds

get_bonds(elem1: str, elem2: str) -> list[BondParam]

Find ALL bond parameters matching an element pair.

Source code in q2mm/models/forcefield.py
def get_bonds(self, elem1: str, elem2: str) -> list[BondParam]:
    """Find ALL bond parameters matching an element pair."""
    key = tuple(sorted([elem1, elem2]))
    return [b for b in self.bonds if b.key == key]

get_angle

get_angle(elem1: str, elem_center: str, elem2: str, env_id: str = '') -> AngleParam | None

Find angle parameter by element triple and optional environment ID.

Source code in q2mm/models/forcefield.py
def get_angle(self, elem1: str, elem_center: str, elem2: str, env_id: str = "") -> AngleParam | None:
    """Find angle parameter by element triple and optional environment ID."""
    outer = tuple(sorted([elem1, elem2]))
    key = (outer[0], elem_center, outer[1])
    for a in self.angles:
        if a.key == key:
            if env_id and a.env_id and a.env_id != env_id:
                continue
            return a
    return None

get_vdw

get_vdw(atom_type: str = '', element: str = '') -> VdwParam | None

Find vdW parameter by atom type or element.

Parameters:

Name Type Description Default
atom_type str

Exact atom type string to match.

''
element str

Element symbol to match (returns only if unique).

''

Returns:

Type Description
VdwParam | None

VdwParam | None: Matching parameter, or None if not found.

Source code in q2mm/models/forcefield.py
def get_vdw(self, atom_type: str = "", element: str = "") -> VdwParam | None:
    """Find vdW parameter by atom type or element.

    Args:
        atom_type (str): Exact atom type string to match.
        element (str): Element symbol to match (returns only if unique).

    Returns:
        VdwParam | None: Matching parameter, or None if not found.
    """
    if atom_type:
        normalized = atom_type.strip()
        for vdw in self.vdws:
            if vdw.atom_type == normalized:
                return vdw
    if element:
        normalized = _extract_element(element)
        matches = [vdw for vdw in self.vdws if vdw.element == normalized]
        if len(matches) == 1:
            return matches[0]
    return None

get_torsion

get_torsion(elem1: str, elem2: str, elem3: str, elem4: str, periodicity: int | None = None, env_id: str = '') -> TorsionParam | None

Find torsion parameter by element quad and optional periodicity/env_id.

Source code in q2mm/models/forcefield.py
def get_torsion(
    self, elem1: str, elem2: str, elem3: str, elem4: str, periodicity: int | None = None, env_id: str = ""
) -> TorsionParam | None:
    """Find torsion parameter by element quad and optional periodicity/env_id."""
    target = (elem1, elem2, elem3, elem4)
    target_rev = (elem4, elem3, elem2, elem1)
    for t in self.torsions:
        if t.elements not in (target, target_rev):
            continue
        if periodicity is not None and t.periodicity != periodicity:
            continue
        if env_id and t.env_id and t.env_id != env_id:
            continue
        return t
    return None

get_param_vector

get_param_vector() -> ndarray

Get all adjustable parameters as a flat vector.

Order: bond (k, r0), angle (k, theta0), torsion (k), vdw (radius, epsilon).

Source code in q2mm/models/forcefield.py
def get_param_vector(self) -> np.ndarray:
    """Get all adjustable parameters as a flat vector.

    Order: bond (k, r0), angle (k, theta0), torsion (k), vdw (radius, epsilon).
    """
    values = []
    for b in self.bonds:
        values.extend([b.force_constant, b.equilibrium])
    for a in self.angles:
        values.extend([a.force_constant, a.equilibrium])
    for t in self.torsions:
        values.append(t.force_constant)
    for vdw in self.vdws:
        values.extend([vdw.radius, vdw.epsilon])
    return np.array(values)

match_bond

match_bond(elements: tuple[str, str], env_id: str = '', ff_row: int | None = None) -> BondParam | None

Match a bond parameter using ff_row, then env_id, then elements.

Source code in q2mm/models/forcefield.py
def match_bond(self, elements: tuple[str, str], env_id: str = "", ff_row: int | None = None) -> BondParam | None:
    """Match a bond parameter using ff_row, then env_id, then elements."""
    if ff_row is not None:
        for bond in self.bonds:
            if bond.ff_row == ff_row:
                return bond
    if env_id:
        matched = self.get_bond(elements[0], elements[1], env_id=env_id)
        if matched is not None:
            return matched
    return self.get_bond(elements[0], elements[1])

match_angle

match_angle(elements: tuple[str, str, str], env_id: str = '', ff_row: int | None = None) -> AngleParam | None

Match an angle parameter using ff_row, then env_id, then elements.

Source code in q2mm/models/forcefield.py
def match_angle(
    self, elements: tuple[str, str, str], env_id: str = "", ff_row: int | None = None
) -> AngleParam | None:
    """Match an angle parameter using ff_row, then env_id, then elements."""
    if ff_row is not None:
        for angle in self.angles:
            if angle.ff_row == ff_row:
                return angle
    if env_id:
        matched = self.get_angle(elements[0], elements[1], elements[2], env_id=env_id)
        if matched is not None:
            return matched
    return self.get_angle(elements[0], elements[1], elements[2])

match_vdw

match_vdw(atom_type: str = '', element: str = '', ff_row: int | None = None) -> VdwParam | None

Match a vdW parameter using ff_row, then atom_type/element lookup (with fallback).

Source code in q2mm/models/forcefield.py
def match_vdw(self, atom_type: str = "", element: str = "", ff_row: int | None = None) -> VdwParam | None:
    """Match a vdW parameter using ff_row, then atom_type/element lookup (with fallback)."""
    if ff_row is not None:
        for vdw in self.vdws:
            if vdw.ff_row == ff_row:
                return vdw
    return self.get_vdw(atom_type=atom_type, element=element)

set_param_vector

set_param_vector(vec: ndarray)

Set parameters from a flat vector (inverse of get_param_vector).

Source code in q2mm/models/forcefield.py
def set_param_vector(self, vec: np.ndarray):
    """Set parameters from a flat vector (inverse of get_param_vector)."""
    if len(vec) != self.n_params:
        raise ValueError(f"Parameter vector length {len(vec)} does not match expected {self.n_params} parameters.")
    idx = 0
    for b in self.bonds:
        b.force_constant = vec[idx]
        b.equilibrium = vec[idx + 1]
        idx += 2
    for a in self.angles:
        a.force_constant = vec[idx]
        a.equilibrium = vec[idx + 1]
        idx += 2
    for t in self.torsions:
        t.force_constant = vec[idx]
        idx += 1
    for vdw in self.vdws:
        vdw.radius = vec[idx]
        vdw.epsilon = vec[idx + 1]
        idx += 2

get_param_indices_by_type

get_param_indices_by_type() -> dict[str, list[int]]

Map parameter type names to their indices in the param vector.

Returns a dict with keys bond_k, bond_eq, angle_k, angle_eq, torsion_k, vdw_radius, vdw_epsilon and values that are lists of integer indices into :meth:get_param_vector.

Source code in q2mm/models/forcefield.py
def get_param_indices_by_type(self) -> dict[str, list[int]]:
    """Map parameter type names to their indices in the param vector.

    Returns a dict with keys ``bond_k``, ``bond_eq``, ``angle_k``,
    ``angle_eq``, ``torsion_k``, ``vdw_radius``, ``vdw_epsilon`` and
    values that are lists of integer indices into
    :meth:`get_param_vector`.
    """
    idx = 0
    result: dict[str, list[int]] = {
        "bond_k": [],
        "bond_eq": [],
        "angle_k": [],
        "angle_eq": [],
        "torsion_k": [],
        "vdw_radius": [],
        "vdw_epsilon": [],
    }
    for _ in self.bonds:
        result["bond_k"].append(idx)
        result["bond_eq"].append(idx + 1)
        idx += 2
    for _ in self.angles:
        result["angle_k"].append(idx)
        result["angle_eq"].append(idx + 1)
        idx += 2
    for _ in self.torsions:
        result["torsion_k"].append(idx)
        idx += 1
    for _ in self.vdws:
        result["vdw_radius"].append(idx)
        result["vdw_epsilon"].append(idx + 1)
        idx += 2
    return result

get_param_type_labels

get_param_type_labels() -> list[str]

Return the type label for each element of the param vector.

Same length as :meth:get_param_vector, useful for mapping each scalar to its per-type step size or bounds category.

Source code in q2mm/models/forcefield.py
def get_param_type_labels(self) -> list[str]:
    """Return the type label for each element of the param vector.

    Same length as :meth:`get_param_vector`, useful for mapping each
    scalar to its per-type step size or bounds category.
    """
    labels: list[str] = []
    for _ in self.bonds:
        labels.extend(["bond_k", "bond_eq"])
    for _ in self.angles:
        labels.extend(["angle_k", "angle_eq"])
    for _ in self.torsions:
        labels.append("torsion_k")
    for _ in self.vdws:
        labels.extend(["vdw_radius", "vdw_epsilon"])
    return labels

get_step_sizes

get_step_sizes() -> ndarray

Per-element differentiation step sizes for the param vector.

Uses the legacy STEPS dictionary values from :mod:q2mm.optimizers.defaults, mapped via :attr:_PARAM_TYPE_TO_STEP_KEY.

Returns

np.ndarray Array of step sizes, same length as :meth:get_param_vector.

Source code in q2mm/models/forcefield.py
def get_step_sizes(self) -> np.ndarray:
    """Per-element differentiation step sizes for the param vector.

    Uses the legacy ``STEPS`` dictionary values from
    :mod:`q2mm.optimizers.defaults`, mapped via
    :attr:`_PARAM_TYPE_TO_STEP_KEY`.

    Returns
    -------
    np.ndarray
        Array of step sizes, same length as :meth:`get_param_vector`.
    """
    from q2mm.optimizers.defaults import STEPS

    labels = self.get_param_type_labels()
    return np.array([STEPS[self._PARAM_TYPE_TO_STEP_KEY[lbl]] for lbl in labels])

get_bounds

get_bounds(overrides: dict[str, tuple[float, float]] | None = None) -> list[tuple[float, float]]

Get (min, max) bounds for each element of the param vector.

Matches the layout of :meth:get_param_vector: bond (k, r0), angle (k, theta0), torsion (k), vdw (radius, epsilon).

Parameters

overrides : dict, optional Override default bounds per type. Keys: bond_k, bond_eq, angle_k, angle_eq, torsion_k, vdw_radius, vdw_epsilon.

Source code in q2mm/models/forcefield.py
def get_bounds(self, overrides: dict[str, tuple[float, float]] | None = None) -> list[tuple[float, float]]:
    """Get (min, max) bounds for each element of the param vector.

    Matches the layout of :meth:`get_param_vector`:
    bond (k, r0), angle (k, theta0), torsion (k), vdw (radius, epsilon).

    Parameters
    ----------
    overrides : dict, optional
        Override default bounds per type. Keys: ``bond_k``,
        ``bond_eq``, ``angle_k``, ``angle_eq``, ``torsion_k``,
        ``vdw_radius``, ``vdw_epsilon``.
    """
    b = {**self.DEFAULT_BOUNDS, **(overrides or {})}
    bounds: list[tuple[float, float]] = []
    for _bond in self.bonds:
        bounds.append(b["bond_k"])
        bounds.append(b["bond_eq"])
    for _angle in self.angles:
        bounds.append(b["angle_k"])
        bounds.append(b["angle_eq"])
    for _torsion in self.torsions:
        bounds.append(b["torsion_k"])
    for _vdw in self.vdws:
        bounds.append(b["vdw_radius"])
        bounds.append(b["vdw_epsilon"])
    return bounds

copy

copy() -> ForceField

Deep copy.

Source code in q2mm/models/forcefield.py
def copy(self) -> ForceField:
    """Deep copy."""
    return copy.deepcopy(self)

from_mm3_fld classmethod

from_mm3_fld(path: str | Path) -> ForceField

Load from Schrödinger MM3 .fld file.

Source code in q2mm/models/forcefield.py
@classmethod
def from_mm3_fld(cls, path: str | Path) -> ForceField:
    """Load from Schrödinger MM3 .fld file."""
    from q2mm.models.ff_io import load_mm3_fld

    return load_mm3_fld(path)

from_tinker_prm classmethod

from_tinker_prm(path: str | Path) -> ForceField

Load bond and angle parameters from a Tinker .prm file.

Source code in q2mm/models/forcefield.py
@classmethod
def from_tinker_prm(cls, path: str | Path) -> ForceField:
    """Load bond and angle parameters from a Tinker .prm file."""
    from q2mm.models.ff_io import load_tinker_prm

    return load_tinker_prm(path)

to_mm3_fld

to_mm3_fld(path: str | Path, template_path: str | Path | None = None, *, substructure_name: str = 'OPT Generated', smiles: str = 'AUTO') -> Path

Export to MM3 .fld format.

Source code in q2mm/models/forcefield.py
def to_mm3_fld(
    self,
    path: str | Path,
    template_path: str | Path | None = None,
    *,
    substructure_name: str = "OPT Generated",
    smiles: str = "AUTO",
) -> Path:
    """Export to MM3 .fld format."""
    from q2mm.models.ff_io import save_mm3_fld

    return save_mm3_fld(self, path, template_path, substructure_name=substructure_name, smiles=smiles)

to_tinker_prm

to_tinker_prm(path: str | Path, template_path: str | Path | None = None, *, section_name: str = 'OPT Generated') -> Path

Export to Tinker .prm format.

Source code in q2mm/models/forcefield.py
def to_tinker_prm(
    self,
    path: str | Path,
    template_path: str | Path | None = None,
    *,
    section_name: str = "OPT Generated",
) -> Path:
    """Export to Tinker .prm format."""
    from q2mm.models.ff_io import save_tinker_prm

    return save_tinker_prm(self, path, template_path, section_name=section_name)

to_openmm_xml

to_openmm_xml(path: str | Path, molecule=None) -> Path

Export to OpenMM ForceField XML format.

Produces a standalone <ForceField> XML file loadable by openmm.app.ForceField(path). Uses custom force definitions with MM3 functional forms (cubic bond, sextic angle, buffered 14-7 vdW).

Parameters:

Name Type Description Default
path str | Path

Output file path.

required
molecule Q2MMMolecule | list[Q2MMMolecule] | None

Optional molecule(s) for generating <AtomTypes> and <Residues> sections.

None

Returns:

Type Description
Path

The resolved output path.

Source code in q2mm/models/forcefield.py
def to_openmm_xml(
    self,
    path: str | Path,
    molecule=None,
) -> Path:
    """Export to OpenMM ForceField XML format.

    Produces a standalone ``<ForceField>`` XML file loadable by
    ``openmm.app.ForceField(path)``.  Uses custom force definitions
    with MM3 functional forms (cubic bond, sextic angle, buffered
    14-7 vdW).

    Args:
        path (str | Path): Output file path.
        molecule (Q2MMMolecule | list[Q2MMMolecule] | None): Optional molecule(s) for generating
            ``<AtomTypes>`` and ``<Residues>`` sections.

    Returns:
        The resolved output path.
    """
    from q2mm.models.ff_io import save_openmm_xml

    return save_openmm_xml(self, path, molecule=molecule)

from_amber_frcmod classmethod

from_amber_frcmod(path: str | Path) -> ForceField

Load from an AMBER .frcmod file.

Source code in q2mm/models/forcefield.py
@classmethod
def from_amber_frcmod(cls, path: str | Path) -> ForceField:
    """Load from an AMBER .frcmod file."""
    from q2mm.models.ff_io import load_amber_frcmod

    return load_amber_frcmod(path)

to_amber_frcmod

to_amber_frcmod(path: str | Path, template_path: str | Path | None = None, *, remark: str = 'Q2MM generated frcmod') -> Path

Export to AMBER .frcmod format.

Source code in q2mm/models/forcefield.py
def to_amber_frcmod(
    self,
    path: str | Path,
    template_path: str | Path | None = None,
    *,
    remark: str = "Q2MM generated frcmod",
) -> Path:
    """Export to AMBER .frcmod format."""
    from q2mm.models.ff_io import save_amber_frcmod

    return save_amber_frcmod(self, path, template_path, remark=remark)

create_for_molecule classmethod

create_for_molecule(molecule: Q2MMMolecule, default_bond_k: float = 5.0, default_angle_k: float = 0.5, name: str = '') -> ForceField

Create a force field with default parameters for a molecule.

Auto-detects unique bond and angle types from the molecule's geometry and creates parameters with sensible defaults.

Source code in q2mm/models/forcefield.py
@classmethod
def create_for_molecule(
    cls, molecule: Q2MMMolecule, default_bond_k: float = 5.0, default_angle_k: float = 0.5, name: str = ""
) -> ForceField:
    """Create a force field with default parameters for a molecule.

    Auto-detects unique bond and angle types from the molecule's
    geometry and creates parameters with sensible defaults.
    """
    # Unique bond types
    bond_types: dict[tuple[str, str], list[float]] = {}
    for bond in molecule.bonds:
        key = bond.element_pair
        if key not in bond_types:
            bond_types[key] = []
        bond_types[key].append(bond.length)

    bonds = []
    for key, lengths in bond_types.items():
        avg_len = np.mean(lengths)
        bonds.append(
            BondParam(
                elements=key,
                equilibrium=avg_len,
                force_constant=default_bond_k,
                label=f"{key[0]}-{key[1]} (auto)",
            )
        )

    # Unique angle types
    angle_types: dict[tuple[str, str, str], list[float]] = {}
    for angle in molecule.angles:
        key = angle.element_triple
        if key not in angle_types:
            angle_types[key] = []
        angle_types[key].append(angle.value)

    angles = []
    for key, values in angle_types.items():
        avg_val = np.mean(values)
        angles.append(
            AngleParam(
                elements=key,
                equilibrium=avg_val,
                force_constant=default_angle_k,
                label=f"{key[0]}-{key[1]}-{key[2]} (auto)",
            )
        )

    return cls(
        name=name or f"Auto FF for {molecule.name}",
        bonds=bonds,
        angles=angles,
    )