Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
0820d49
Add geomeTRIC as non-default minimization engine
fjclark Nov 24, 2025
cb15779
Merge branch 'main' into feature-geometric-minimisation
fjclark Dec 23, 2025
d9ed802
Suppress verbose geomeTRIC output
fjclark Dec 23, 2025
0b5cd79
Merge remote-tracking branch 'upstream/main' into feature-geometric-m…
mattwthompson Jan 22, 2026
6d263c4
Add geomeTRIC dependency
mattwthompson Jan 22, 2026
0c443c1
Merge branch 'main' into feature-geometric-minimisation
fjclark Jan 25, 2026
c8404bb
Fix type errors
fjclark Jan 26, 2026
dbc1589
Ensure single-threaded np execution to improve geomeTRIC performance
fjclark Feb 13, 2026
652e98c
Pass method to torsion minimisation script
fjclark Feb 13, 2026
4b59a49
Implement torsion scans with strong torsion restraint
fjclark Mar 3, 2026
e154cb1
Remove all geomeTRIC functionality
fjclark Mar 3, 2026
18c7c6d
Remove a couple of other things related to geomeTRIC
fjclark Mar 3, 2026
4d0e869
Merge branch 'main' into feature-openmm-torsion-restraints
fjclark Mar 3, 2026
fad9cf4
Set OpenMM torsion-restrained minimisation as default
fjclark Mar 3, 2026
77a12c1
Remove devtools
fjclark Mar 3, 2026
ac554fd
Create shared MinimizationInput/Result parent class
fjclark Mar 3, 2026
a182f87
Make default number of iterations a constant
fjclark Mar 3, 2026
9881a24
Remove redundant fixture
fjclark Mar 3, 2026
50b9d94
Remove redundant tests
fjclark Mar 3, 2026
51271c4
Make logger upper case for consistency
fjclark Mar 3, 2026
be89d20
Ensure restraint energy separated from other energies
fjclark Mar 3, 2026
39f8200
Fix mypy errors
fjclark Mar 3, 2026
5524d71
Merge branch 'main' into feature-openmm-torsion-restraints
fjclark Mar 12, 2026
c0c07c1
Add specific angular difference fn
fjclark Mar 12, 2026
e6c7b4b
Store constrained minimisation fns in registry
fjclark Mar 12, 2026
6c4af15
Tighten restrained torsion angle tolerance to 0.1 deg
fjclark Mar 12, 2026
844f070
Programatically set restraint force group to avoid clashes
fjclark Mar 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions pixi.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ module = [
"qcelemental",
"openff.qcsubmit.results",
"MDAnalysis",
"MDAnalysis.analysis.dihedrals",
"bokeh",
"panel",
]
Expand Down
90 changes: 81 additions & 9 deletions yammbs/_forcefields.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,77 @@
import functools
import logging
import re

import openmm
from openff.toolkit import Molecule
from openff.toolkit import ForceField, Molecule

logger = logging.getLogger(__name__)

def _smirnoff(molecule: Molecule, force_field_path: str) -> openmm.System:
from openff.toolkit import ForceField

smirnoff_force_field = ForceField(
force_field_path,
load_plugins=True,
def _shorthand_to_full_force_field_name(
shorthand: str,
make_unconstrained: bool = True,
) -> str:
"""Make i.e. `openff-2.1.0` into `openff_unconstrained-2.1.0.offxml`."""
if make_unconstrained:
# Split on '-' immediately followed by a number;
# cannot split on '-' because of i.e. 'de-force-1.0.0'
prefix, version, _ = re.split(r"-([\d.]+)", shorthand, maxsplit=1)

return f"{prefix}_unconstrained-{version}.offxml"
else:
return shorthand + ".offxml"


@functools.lru_cache(maxsize=1)
def _lazy_load_force_field(force_field_name: str) -> ForceField:
"""Attempt to load a force field from a shorthand string or a file path.

Caching is used to speed up loading; a single force field takes O(100 ms) to
load, but the cache takes O(10 ns) to access. The cache key is simply the
argument passed to this function; a hash collision should only occur when
two identical strings are expected to return different force fields, which
seems like an assumption that the toolkit has always made anyway.
"""
if not force_field_name.endswith(".offxml"):
force_field_name = _shorthand_to_full_force_field_name(
force_field_name,
make_unconstrained=True,
)

return ForceField(
force_field_name,
allow_cosmetic_attributes=True,
load_plugins=True,
)

if "Constraints" in smirnoff_force_field.registered_parameter_handlers:
smirnoff_force_field.deregister_parameter_handler("Constraints")

return smirnoff_force_field.create_openmm_system(molecule.to_topology())
def _smirnoff(molecule: Molecule, force_field_path: str) -> openmm.System:
from openff.toolkit import ForceField
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: probably not worth lazy-loading here


try:
force_field = _lazy_load_force_field(force_field_path)
except KeyError:
# Attempt to load from local path
try:
force_field = ForceField(
force_field_path,
allow_cosmetic_attributes=True,
load_plugins=True,
)
except Exception as error:
# The toolkit does a poor job of distinguishing between a string
# argument being a file that does not exist and a file that it should
# try to parse (polymorphic input), so just have to clobber whatever
Comment on lines +63 to +65
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

For this project, I think making an effort to parse OFFXML as raw strings is a net negative. Hopefully we continue to use only files

raise NotImplementedError(
f"Could not find or parse force field {force_field_path}",
) from error

if "Constraints" in force_field.registered_parameter_handlers:
logger.info("Deregistering Constraints handler from SMIRNOFF force field")
force_field.deregister_parameter_handler("Constraints")
Comment on lines +70 to +72
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should document this (I forgot this was the default beavior)


return force_field.create_openmm_system(molecule.to_topology())


def _gaff(molecule: Molecule, force_field_name: str) -> openmm.System:
Expand Down Expand Up @@ -62,3 +119,18 @@ def _espaloma(molecule: Molecule, force_field_name: str) -> openmm.System:
model(mol_graph.heterograph)

return espaloma.graphs.deploy.openmm_system_from_graph(mol_graph, forcefield=ff[0])


NON_SMIRNOFF_SYSTEM_BUILDERS = {
"gaff": _gaff,
"espaloma": _espaloma,
}


def build_omm_system(force_field: str, molecule: Molecule) -> openmm.System:
"""Get an OpenMM System for a given force field and molecule."""
for prefix, builder in NON_SMIRNOFF_SYSTEM_BUILDERS.items():
if force_field.startswith(prefix):
return builder(molecule, force_field)

return _smirnoff(molecule, force_field)
Loading
Loading