Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
dcd3b92
rm ambertools
lilyminium Feb 20, 2026
28e1ad7
update
lilyminium Feb 20, 2026
18dc316
update again
lilyminium Feb 13, 2026
7ee6cac
fix spacing
lilyminium Feb 13, 2026
d1b7479
give up on multi-stage
lilyminium Feb 20, 2026
713f062
move parmed import
lilyminium Feb 16, 2026
8622da2
add openeye
lilyminium Feb 20, 2026
8c5b788
modify tests to use rdkit preferentially
lilyminium Feb 17, 2026
f53aec0
update
lilyminium Feb 17, 2026
27290bd
fine use importlib
lilyminium Feb 17, 2026
2614728
fix imports
lilyminium Feb 17, 2026
82b9509
updates
lilyminium Feb 17, 2026
2d18f21
update formaldehyde
lilyminium Feb 17, 2026
5c864c6
add openeye ambertools envs
lilyminium Feb 20, 2026
939f2c8
rm accidental vscode
lilyminium Feb 20, 2026
4b87512
fix precommit
lilyminium Feb 20, 2026
263ad72
update all
lilyminium Feb 20, 2026
15f3df0
don't lock env
lilyminium Feb 20, 2026
42bfbd0
update lock files for new envs
lilyminium Feb 20, 2026
febc21e
add openff-models
lilyminium Feb 23, 2026
ece87fb
ruff
lilyminium Feb 23, 2026
ba82df1
Update .github/workflows/ci.yaml
lilyminium Mar 3, 2026
94ec27c
try solving mac again
lilyminium Mar 3, 2026
9aea28c
rm ambertools from dev
lilyminium Mar 3, 2026
d7ce999
switch to 2.3
lilyminium Mar 3, 2026
92b610a
add nagl
lilyminium Mar 3, 2026
6ec8525
update toolkit pin
lilyminium Mar 3, 2026
832bd63
Revert "update toolkit pin"
lilyminium Mar 3, 2026
4d53309
Revert "add nagl"
lilyminium Mar 3, 2026
44c153e
Revert "switch to 2.3"
lilyminium Mar 3, 2026
93e0c2d
give up on nagl charges
lilyminium Mar 3, 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
38 changes: 22 additions & 16 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@ jobs:
uses: pre-commit/action@v3.0.1

test-matrix:
env:
OE_LICENSE: ${{ github.workspace }}/oe_license.txt

needs: pre-commit
strategy:
matrix:
include:
- os: ubuntu-latest
pixi-platform: linux-64
- os: macos-latest
pixi-platform: osx-arm64
os: [ubuntu-latest, macos-latest]
pixi-environment: [default, pydantic-1-cpu, ambertools, openeye]
fail-fast: false

runs-on: ${{ matrix.os }}
Expand All @@ -36,28 +36,34 @@ jobs:
- name: Checkout
uses: actions/checkout@v4

- name: Make oe_license.txt file from GH org secret "OE_LICENSE"
env:
OE_LICENSE_TEXT: ${{ secrets.OE_LICENSE }}
run: echo "${OE_LICENSE_TEXT}" > ${OE_LICENSE}

- name: Setup pixi
uses: prefix-dev/setup-pixi@v0.9.3
with:
pixi-version: v0.62.2
cache: true
environments: default pydantic-1-cpu

- name: Run tests with pydantic >=2
run: |
pixi run test
# we use locked installs on macos-latest
# to avoid cross-platform resolution issues with cuda
locked: false
environments: ${{ matrix.pixi-environment }}

- name: Run tests with pydantic <2
- name: Run tests
run: |
pixi run --environment pydantic-1-cpu test
pixi run --environment ${{ matrix.pixi-environment }} test

- name: Test examples with pydantic >=2
- name: Test examples (default environment only)
if: matrix.pixi-environment == 'default'
run: |
pixi run test-examples
pixi run --environment ${{ matrix.pixi-environment }} test-examples

- name: Build docs
- name: Build docs (default environment only)
if: matrix.pixi-environment == 'default'
run: |
pixi run docs
pixi run --environment ${{ matrix.pixi-environment }} docs

- name: CodeCov
uses: codecov/codecov-action@v4.1.1
Expand Down
6,464 changes: 4,134 additions & 2,330 deletions pixi.lock

Large diffs are not rendered by default.

23 changes: 18 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ pip = "*"
openff-units = "*"
openff-toolkit-base = ">=0.9.2"
openff-interchange-base = ">=0.3.17"
openff-models = "*"
nnpops = "*"
pydantic-units = "*"
networkx = "*"
Expand All @@ -86,14 +87,22 @@ msgpack-python = "*"
mdtraj = "*"
absolv = ">=1.0.1"

[tool.pixi.feature.openeye]
channels = ["openeye"]

[tool.pixi.feature.openeye.dependencies]
openeye-toolkits = "*"

[tool.pixi.feature.ambertools.dependencies]
ambertools = "*"

[tool.pixi.feature.examples.dependencies]
jupyter = "*"
nbconvert = "*"
nglview = "*"
smirnoff-plugins = "*"

[tool.pixi.feature.dev.dependencies]
ambertools = "*"
scipy = "*"
setuptools_scm = ">=8"
pre-commit = "*"
Expand Down Expand Up @@ -123,11 +132,15 @@ pydantic = ">=2.0"
pydantic = "<2.0"

[tool.pixi.environments]
default = { features = ["mm", "fe", "examples", "dev", "docs", "pydantic-2"] }
cu12 = { features = ["cu12", "mm", "fe", "examples", "dev", "docs", "pydantic-2"] }
default = { features = ["mm", "fe", "examples", "dev", "docs", "pydantic-2", "openeye"] }
ambertools = { features = ["mm", "fe", "examples", "dev", "docs", "pydantic-2", "ambertools"] }
# we get rid of examples here until smirnoff-plugins can be installed without ambertools
# we also get rid of fe as absolv also installs ambertools
openeye = { features = ["mm", "dev", "docs", "pydantic-2", "openeye"] }
cu12 = { features = ["cu12", "mm", "fe", "examples", "dev", "docs", "pydantic-2", "ambertools"] }
# The fe feature is excluded when pydantic < 2 as femto (used by absolv) needs pydantic >= 2
pydantic-1-cpu = { features = ["mm", "examples", "dev", "docs", "pydantic-1"] }
pydantic-1-cu12 = { features = ["cu12", "mm", "examples", "dev", "docs", "pydantic-1"] }
pydantic-1-cpu = { features = ["mm", "examples", "dev", "docs", "pydantic-1", "ambertools"] }
pydantic-1-cu12 = { features = ["cu12", "mm", "examples", "dev", "docs", "pydantic-1", "ambertools"] }

[tool.pixi.tasks]
lint = "ruff check smee examples"
Expand Down
2 changes: 1 addition & 1 deletion smee/mm/_fe.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import numpy
import openff.toolkit
import openmm.unit
import parmed.openmm
import torch
import yaml

Expand Down Expand Up @@ -112,6 +111,7 @@ def generate_dg_solv_data(
import absolv.runner
import femto.md.config
import femto.md.system
import parmed.openmm

output_dir = pathlib.Path.cwd() if output_dir is None else output_dir

Expand Down
4 changes: 3 additions & 1 deletion smee/mm/_reporters.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import os
import typing

import msgpack
import numpy
import openmm.app
import openmm.unit
Expand Down Expand Up @@ -72,6 +71,8 @@ def describeNextReport(self, simulation: openmm.app.Simulation):
return steps, True, False, False, True

def report(self, simulation: openmm.app.Simulation, state: openmm.State):
import msgpack

potential_energy = state.getPotentialEnergy()
kinetic_energy = state.getKineticEnergy()

Expand Down Expand Up @@ -107,6 +108,7 @@ def unpack_frames(
file: typing.BinaryIO,
) -> typing.Generator[tuple[torch.Tensor, torch.Tensor, float], None, None]:
"""Unpack frames saved by a ``TensorReporter``."""
import msgpack

unpacker = msgpack.Unpacker(file, object_hook=_decoder)

Expand Down
30 changes: 23 additions & 7 deletions smee/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,21 @@
_E = openff.units.unit.elementary_charge


@pytest.fixture(scope="module")
def toolkit_registry_rdkit_first():
"""Returns a toolkit registry with RDKit as the first toolkit."""
return openff.toolkit.utils.ToolkitRegistry(
toolkit_precedence=[
openff.toolkit.utils.nagl_wrapper.NAGLToolkitWrapper,
openff.toolkit.utils.rdkit_wrapper.RDKitToolkitWrapper,
openff.toolkit.utils.openeye_wrapper.OpenEyeToolkitWrapper,
openff.toolkit.utils.ambertools_wrapper.AmberToolsToolkitWrapper,
openff.toolkit.utils.builtin_wrapper.BuiltInToolkitWrapper,
],
exception_if_unavailable=False,
)


@pytest.fixture
def tmp_cwd(tmp_path, monkeypatch) -> pathlib.Path:
monkeypatch.chdir(tmp_path)
Expand Down Expand Up @@ -59,11 +74,12 @@ def ethanol_conformer(ethanol) -> torch.Tensor:


@pytest.fixture(scope="module")
def ethanol_interchange(ethanol, default_force_field) -> openff.interchange.Interchange:
def ethanol_interchange(
ethanol, default_force_field, toolkit_registry_rdkit_first
) -> openff.interchange.Interchange:
"""Returns a parameterized system of ethanol."""

return openff.interchange.Interchange.from_smirnoff(
default_force_field, ethanol.to_topology()
return default_force_field.create_interchange(
ethanol.to_topology(), toolkit_registry=toolkit_registry_rdkit_first
)


Expand All @@ -87,12 +103,12 @@ def formaldehyde_conformer(formaldehyde) -> torch.Tensor:

@pytest.fixture(scope="module")
def formaldehyde_interchange(
formaldehyde, default_force_field
formaldehyde, default_force_field, toolkit_registry_rdkit_first
) -> openff.interchange.Interchange:
"""Returns a parameterized system of formaldehyde."""

return openff.interchange.Interchange.from_smirnoff(
default_force_field, formaldehyde.to_topology()
return default_force_field.create_interchange(
formaldehyde.to_topology(), toolkit_registry=toolkit_registry_rdkit_first
)


Expand Down
40 changes: 28 additions & 12 deletions smee/tests/convertors/openff/test_nonbonded.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import importlib.util

import pytest

import openff.interchange
import openff.interchange.models
import openff.toolkit
Expand All @@ -12,6 +16,8 @@
convert_vdw,
)

SMIRNOFF_PLUGINS_AVAILABLE = importlib.util.find_spec("smirnoff_plugins") is not None


def test_convert_electrostatics_am1bcc(ethanol, ethanol_interchange):
charge_collection = ethanol_interchange.collections["Electrostatics"]
Expand Down Expand Up @@ -57,7 +63,7 @@ def test_convert_electrostatics_am1bcc(ethanol, ethanol_interchange):
assert parameter_map.exclusion_scale_idxs.shape == (n_expected_exclusions, 1)


def test_convert_electrostatics_v_site():
def test_convert_electrostatics_v_site(toolkit_registry_rdkit_first):
force_field = openff.toolkit.ForceField()
force_field.get_parameter_handler("Electrostatics")
force_field.get_parameter_handler("vdW")
Expand Down Expand Up @@ -85,8 +91,10 @@ def test_convert_electrostatics_v_site():

molecule = openff.toolkit.Molecule.from_mapped_smiles("[Cl:2]-[H:1]")

interchange = openff.interchange.Interchange.from_smirnoff(
force_field, molecule.to_topology(), allow_nonintegral_charges=True
interchange = force_field.create_interchange(
molecule.to_topology(),
toolkit_registry=toolkit_registry_rdkit_first,
allow_nonintegral_charges=True,
)
charge_collection = interchange.collections["Electrostatics"]

Expand Down Expand Up @@ -160,16 +168,18 @@ def test_convert_electrostatics_v_site():
assert torch.allclose(parameter_map.exclusion_scale_idxs, expected_scales)


def test_convert_electrostatics_tip4p():
def test_convert_electrostatics_tip4p(toolkit_registry_rdkit_first):
"""Explicitly test the case of TIP4P (FB) water to make sure v-site charges are
correct.
"""

force_field = openff.toolkit.ForceField("tip4p_fb.offxml")
molecule = openff.toolkit.Molecule.from_mapped_smiles("[H:2][O:1][H:3]")

interchange = openff.interchange.Interchange.from_smirnoff(
force_field, molecule.to_topology(), allow_nonintegral_charges=True
interchange = force_field.create_interchange(
molecule.to_topology(),
toolkit_registry=toolkit_registry_rdkit_first,
allow_nonintegral_charges=True,
)

tensor_top: smee.TensorTopology
Expand All @@ -186,7 +196,7 @@ def test_convert_electrostatics_tip4p():
assert torch.allclose(charges, expected_charges)


def test_convert_bci_and_vsite():
def test_convert_bci_and_vsite(toolkit_registry_rdkit_first):
ff_off = openff.toolkit.ForceField()
ff_off.get_parameter_handler("Electrostatics")
ff_off.get_parameter_handler("vdW")
Expand All @@ -213,8 +223,9 @@ def test_convert_bci_and_vsite():
mol = openff.toolkit.Molecule.from_mapped_smiles("[O:1]([H:2])[H:3]")
mol.assign_partial_charges(charge_handler.partial_charge_method)

interchange = openff.interchange.Interchange.from_smirnoff(
ff_off, mol.to_topology()
interchange = ff_off.create_interchange(
mol.to_topology(),
toolkit_registry=toolkit_registry_rdkit_first,
)

expected_charges = [
Expand Down Expand Up @@ -293,13 +304,18 @@ def test_convert_vdw(ethanol, ethanol_interchange):
assert potential.fn == smee.EnergyFn.VDW_LJ


def test_convert_dexp(ethanol, test_data_dir):
@pytest.mark.skipif(
not SMIRNOFF_PLUGINS_AVAILABLE,
reason="Requires SMIRNOFF plugins to be installed.",
)
def test_convert_dexp(ethanol, test_data_dir, toolkit_registry_rdkit_first):
ff = openff.toolkit.ForceField(
str(test_data_dir / "de-ff.offxml"), load_plugins=True
)

interchange = openff.interchange.Interchange.from_smirnoff(
ff, ethanol.to_topology()
interchange = ff.create_interchange(
ethanol.to_topology(),
toolkit_registry=toolkit_registry_rdkit_first,
)
vdw_collection = interchange.collections["DoubleExponential"]

Expand Down
12 changes: 7 additions & 5 deletions smee/tests/convertors/openff/test_openff.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import openff.interchange.models
import openff.toolkit
from openff.toolkit.utils.toolkit_registry import toolkit_registry_manager
import openff.units
import pytest
import torch
Expand Down Expand Up @@ -190,12 +191,13 @@ def test_convert_interchange_multiple(
ethanol_interchange,
formaldehyde_conformer,
formaldehyde_interchange,
toolkit_registry_rdkit_first,
):
force_field, topologies = convert_interchange(
[ethanol_interchange, formaldehyde_interchange]
)
assert len(topologies) == 2

with toolkit_registry_manager(toolkit_registry_rdkit_first):
force_field, topologies = convert_interchange(
[ethanol_interchange, formaldehyde_interchange]
)
assert len(topologies) == 2
expected_potentials = {
"Angles",
"Bonds",
Expand Down
8 changes: 8 additions & 0 deletions smee/tests/convertors/openmm/test_openmm.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import importlib.util

import numpy.random
import openff.interchange
import openff.toolkit
Expand All @@ -17,6 +19,8 @@
create_openmm_system,
)

SMIRNOFF_PLUGINS_AVAILABLE = importlib.util.find_spec("smirnoff_plugins") is not None


def _compute_energy(
system: openmm.System,
Expand Down Expand Up @@ -252,6 +256,10 @@ def test_convert_lj_potential_with_exceptions(with_exception):
)


@pytest.mark.skipif(
not SMIRNOFF_PLUGINS_AVAILABLE,
reason="Requires SMIRNOFF plugins to be installed.",
)
def test_convert_to_openmm_system_dexp_periodic(test_data_dir):
ff = openff.toolkit.ForceField(
str(test_data_dir / "de-ff.offxml"), load_plugins=True
Expand Down