Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
d25a1a3
initial implementation of NAGLChargesHandler
j-wags Apr 23, 2025
10db658
have testing env use naglcharges interchange branch
j-wags Apr 23, 2025
5f02826
implement doi and hash fields
j-wags Jul 10, 2025
de0f3df
add tests
j-wags Jul 10, 2025
58e3335
fix tests
j-wags Jul 10, 2025
37b6417
add docstring
j-wags Jul 10, 2025
47496ae
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 10, 2025
831bc8b
force use of nagl for test
j-wags Jul 10, 2025
858d893
fix whitespace
j-wags Jul 10, 2025
4dfe8e1
add doi and file_hash to nagltoolkitwrapper.assign_partial_charges, s…
j-wags Jul 15, 2025
8153199
raise specific error for blank/none model_file values
j-wags Jul 15, 2025
d1878fb
improve docstring
j-wags Jul 15, 2025
787c089
test with new openff-nagl-models
j-wags Jul 15, 2025
5981249
update nagl test for bad suffix error
j-wags Jul 17, 2025
9b095f9
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 17, 2025
a16a426
update toolkit registry check to use types rather than repr
j-wags Jul 25, 2025
1788c64
add compatibility checks for doi and hash fields, add tests
j-wags Jul 29, 2025
706c84c
lint
j-wags Jul 29, 2025
3b5687f
move imports to top of file
j-wags Jul 29, 2025
0df39aa
thread naglcharges handler into inits for correct import paths and docs
j-wags Jul 29, 2025
800c2f1
remove commented code
j-wags Aug 7, 2025
ec518e7
have tests run without interchange branch
j-wags Aug 7, 2025
ae283a8
point to nagl-models main branch for pip installs
j-wags Aug 7, 2025
ad9b5a0
update expected error message in test
j-wags Aug 7, 2025
c4a370c
revert tests to using conda packages
j-wags Aug 14, 2025
62c0edb
revert other test envs
j-wags Aug 14, 2025
78cfcdd
Merge branch 'main' into naglcharges-handler
j-wags Aug 14, 2025
c37ab5a
Merge remote-tracking branch 'origin' into naglcharges-handler
j-wags Aug 14, 2025
fc431e2
update releasenotes
j-wags Aug 14, 2025
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: 3 additions & 1 deletion docs/releasehistory.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Releases follow the `major.minor.micro` scheme recommended by [PEP440](https://w
* `minor` increments add features but do not break API compatibility
* `micro` increments represent bugfix releases or improvements in documentation

## Current development
## 0.17.0

### API-breaking changes

Expand All @@ -15,6 +15,8 @@ Releases follow the `major.minor.micro` scheme recommended by [PEP440](https://w
### Bugfixes

### New features
- [PR #2048](https://github.com/openforcefield/openff-toolkit/pull/2048): Adds NAGLChargesHandler. See [SMIRNOFF EP 11](https://github.com/openforcefield/standards/pull/71) for the new SMIRNOFF specification section and discussion.


### Improved documentation and warnings

Expand Down
1 change: 1 addition & 0 deletions docs/typing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ During ``System`` creation, each ``ParameterHandler`` registered to a ``ForceFie
ElectrostaticsHandler
LibraryChargeHandler
ToolkitAM1BCCHandler
NAGLChargesHandler
GBSAHandler
ChargeIncrementModelHandler
VirtualSiteHandler
Expand Down
2 changes: 2 additions & 0 deletions openff/toolkit/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
GLOBAL_TOOLKIT_REGISTRY,
AmberToolsToolkitWrapper,
BuiltInToolkitWrapper,
NAGLToolkitWrapper,
OpenEyeToolkitWrapper,
RDKitToolkitWrapper,
ToolkitRegistry,
Expand Down Expand Up @@ -53,6 +54,7 @@
"GLOBAL_TOOLKIT_REGISTRY": "openff.toolkit.utils.toolkits",
"AmberToolsToolkitWrapper": "openff.toolkit.utils.toolkits",
"BuiltInToolkitWrapper": "openff.toolkit.utils.toolkits",
"NAGLToolkitWrapper": "openff.toolkit.utils.toolkits",
"OpenEyeToolkitWrapper": "openff.toolkit.utils.toolkits",
"RDKitToolkitWrapper": "openff.toolkit.utils.toolkits",
"ToolkitRegistry": "openff.toolkit.utils.toolkits",
Expand Down
10 changes: 7 additions & 3 deletions openff/toolkit/_tests/test_nagl.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import pytest
from openff.utilities import has_package, skip_if_missing

from openff.nagl_models._dynamic_fetch import BadFileSuffixError
from openff.toolkit import Molecule, unit
from openff.toolkit._tests.create_molecules import (
create_acetaldehyde,
Expand All @@ -14,8 +15,8 @@
create_reversed_ethanol,
)
from openff.toolkit._tests.utils import requires_openeye
from openff.toolkit.utils import GLOBAL_TOOLKIT_REGISTRY
from openff.toolkit.utils.exceptions import (
ChargeMethodUnavailableError,
ToolkitUnavailableException,
)
from openff.toolkit.utils.nagl_wrapper import NAGLToolkitWrapper
Expand All @@ -38,6 +39,9 @@ def test_version(self):

assert parsed_version == NAGLToolkitWrapper()._toolkit_version

def test_nagl_in_global_toolkit_registry(self):
assert NAGLToolkitWrapper in {type(tk) for tk in GLOBAL_TOOLKIT_REGISTRY.registered_toolkits}

@requires_openeye
@pytest.mark.parametrize(
"molecule_function",
Expand Down Expand Up @@ -123,8 +127,8 @@ def test_conformer_argument(self):

def test_unsupported_charge_method(self):
with pytest.raises(
ChargeMethodUnavailableError,
match="Charge model hartree_fock not supported",
BadFileSuffixError,
match="Found an unrecognized file path extension on filename='hartree_fock'",
):
create_ethanol().assign_partial_charges(
partial_charge_method="hartree_fock",
Expand Down
196 changes: 196 additions & 0 deletions openff/toolkit/_tests/test_parameters.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from openff.toolkit import ForceField, Molecule, Quantity, Topology, unit
from openff.toolkit._tests.mocking import VirtualSiteMocking
from openff.toolkit._tests.utils import does_not_raise
from openff.toolkit.typing.engines.smirnoff import NAGLChargesHandler
from openff.toolkit.typing.engines.smirnoff.parameters import (
AngleHandler,
BondHandler,
Expand Down Expand Up @@ -2722,6 +2723,201 @@ def test_charge_increment_one_ci_missing(self):
],
)

class TestNAGLChargesHandler:
def test_nagl_charges_handler_serialization(self):
handler = NAGLChargesHandler(model_file="openff-gnn-am1bcc-0.1.0-rc.3.pt", skip_version_check=True)
assert handler.model_file == "openff-gnn-am1bcc-0.1.0-rc.3.pt"
handler_dict = handler.to_dict()
assert handler_dict["model_file"] == "openff-gnn-am1bcc-0.1.0-rc.3.pt"

def test_nagl_charges_handler_with_optional_fields(self):
# Test with model_file_hash
handler = NAGLChargesHandler(
model_file="openff-gnn-am1bcc-0.1.0-rc.3.pt",
model_file_hash="144ed56e46c5b3ad80157b342c8c0f8f7340e4d382a678e30dd300c811646bd0",
skip_version_check=True
)
assert handler.model_file == "openff-gnn-am1bcc-0.1.0-rc.3.pt"
assert handler.model_file_hash == "144ed56e46c5b3ad80157b342c8c0f8f7340e4d382a678e30dd300c811646bd0"
assert handler.digital_object_identifier is None

# Test with digital_object_identifier
handler = NAGLChargesHandler(
model_file="openff-gnn-am1bcc-0.1.0-rc.3.pt",
digital_object_identifier="10.5072/zenodo.203601",
skip_version_check=True
)
assert handler.model_file == "openff-gnn-am1bcc-0.1.0-rc.3.pt"
assert handler.model_file_hash is None
assert handler.digital_object_identifier == "10.5072/zenodo.203601"

# Test with both optional fields
handler = NAGLChargesHandler(
model_file="openff-gnn-am1bcc-0.1.0-rc.3.pt",
model_file_hash="144ed56e46c5b3ad80157b342c8c0f8f7340e4d382a678e30dd300c811646bd0",
digital_object_identifier="10.5072/zenodo.203601",
skip_version_check=True
)
assert handler.model_file == "openff-gnn-am1bcc-0.1.0-rc.3.pt"
assert handler.model_file_hash == "144ed56e46c5b3ad80157b342c8c0f8f7340e4d382a678e30dd300c811646bd0"
assert handler.digital_object_identifier == "10.5072/zenodo.203601"

def test_nagl_charges_handler_serialization_with_optional_fields(self):
# Test serialization with all fields
handler = NAGLChargesHandler(
model_file="openff-gnn-am1bcc-0.1.0-rc.3.pt",
model_file_hash="144ed56e46c5b3ad80157b342c8c0f8f7340e4d382a678e30dd300c811646bd0",
digital_object_identifier="10.5072/zenodo.203601",
skip_version_check=True
)
handler_dict = handler.to_dict()
assert handler_dict["model_file"] == "openff-gnn-am1bcc-0.1.0-rc.3.pt"
assert handler_dict["model_file_hash"] == "144ed56e46c5b3ad80157b342c8c0f8f7340e4d382a678e30dd300c811646bd0"
assert handler_dict["digital_object_identifier"] == "10.5072/zenodo.203601"

# Test deserialization via constructor
handler_from_dict = NAGLChargesHandler(**handler_dict)
assert handler_from_dict.model_file == "openff-gnn-am1bcc-0.1.0-rc.3.pt"
assert handler_from_dict.model_file_hash == "144ed56e46c5b3ad80157b342c8c0f8f7340e4d382a678e30dd300c811646bd0"
assert handler_from_dict.digital_object_identifier == "10.5072/zenodo.203601"

def test_nagl_charges_handler_compatibility(self):
# Test compatible handlers (same model_file)
handler1 = NAGLChargesHandler(
model_file="openff-gnn-am1bcc-0.1.0-rc.3.pt",
skip_version_check=True
)
handler2 = NAGLChargesHandler(
model_file="openff-gnn-am1bcc-0.1.0-rc.3.pt",
model_file_hash="144ed56e46c5b3ad80157b342c8c0f8f7340e4d382a678e30dd300c811646bd0",
skip_version_check=True
)
# Should not raise exception
handler1.check_handler_compatibility(handler2)
Comment thread
mattwthompson marked this conversation as resolved.

# Test incompatible handlers (different model_file)
handler3 = NAGLChargesHandler(
model_file="different-model-file.pt",
skip_version_check=True
)
with pytest.raises(IncompatibleParameterError, match="different model_files"):
handler1.check_handler_compatibility(handler3)

def test_nagl_charges_handler_defaults(self):
# Test that optional fields default to None
handler = NAGLChargesHandler(
model_file="openff-gnn-am1bcc-0.1.0-rc.3.pt",
skip_version_check=True
)
assert handler.model_file_hash is None
assert handler.digital_object_identifier is None

def test_nagl_charges_handler_hash_compatibility(self):
"""Test compatibility checks for model_file_hash"""
# Test compatible handlers with same hash
handler1 = NAGLChargesHandler(
model_file="openff-gnn-am1bcc-0.1.0-rc.3.pt",
model_file_hash="144ed56e46c5b3ad80157b342c8c0f8f7340e4d382a678e30dd300c811646bd0",
skip_version_check=True
)
handler2 = NAGLChargesHandler(
model_file="openff-gnn-am1bcc-0.1.0-rc.3.pt",
model_file_hash="144ed56e46c5b3ad80157b342c8c0f8f7340e4d382a678e30dd300c811646bd0",
skip_version_check=True
)
# Should not raise exception
handler1.check_handler_compatibility(handler2)

# Test incompatible handlers with different hashes
handler3 = NAGLChargesHandler(
model_file="openff-gnn-am1bcc-0.1.0-rc.3.pt",
model_file_hash="different_hash_value",
skip_version_check=True
)
with pytest.raises(IncompatibleParameterError, match="different model_file_hash values"):
handler1.check_handler_compatibility(handler3)

# Test compatibility when only one handler has hash (should be compatible)
handler4 = NAGLChargesHandler(
model_file="openff-gnn-am1bcc-0.1.0-rc.3.pt",
skip_version_check=True
)
# Should not raise exception
handler1.check_handler_compatibility(handler4)
handler4.check_handler_compatibility(handler1)

def test_nagl_charges_handler_doi_compatibility(self):
"""Test compatibility checks for digital_object_identifier"""
# Test compatible handlers with same DOI
handler1 = NAGLChargesHandler(
model_file="openff-gnn-am1bcc-0.1.0-rc.3.pt",
digital_object_identifier="10.5072/zenodo.203601",
skip_version_check=True
)
handler2 = NAGLChargesHandler(
model_file="openff-gnn-am1bcc-0.1.0-rc.3.pt",
digital_object_identifier="10.5072/zenodo.203601",
skip_version_check=True
)
# Should not raise exception
handler1.check_handler_compatibility(handler2)

# Test incompatible handlers with different DOIs
handler3 = NAGLChargesHandler(
model_file="openff-gnn-am1bcc-0.1.0-rc.3.pt",
digital_object_identifier="10.5072/zenodo.999999",
skip_version_check=True
)
with pytest.raises(IncompatibleParameterError, match="different digital_object_identifier values"):
handler1.check_handler_compatibility(handler3)
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.

(not blocking) Might be worth symmetrizing each of these, though really unlikely to be an issue


# Test compatibility when only one handler has DOI (should be compatible)
handler4 = NAGLChargesHandler(
model_file="openff-gnn-am1bcc-0.1.0-rc.3.pt",
skip_version_check=True
)
# Should not raise exception
handler1.check_handler_compatibility(handler4)
handler4.check_handler_compatibility(handler1)

def test_nagl_charges_handler_combined_compatibility(self):
"""Test compatibility checks with both hash and DOI"""
# Test compatible handlers with same hash and DOI
handler1 = NAGLChargesHandler(
model_file="openff-gnn-am1bcc-0.1.0-rc.3.pt",
model_file_hash="144ed56e46c5b3ad80157b342c8c0f8f7340e4d382a678e30dd300c811646bd0",
digital_object_identifier="10.5072/zenodo.203601",
skip_version_check=True
)
handler2 = NAGLChargesHandler(
model_file="openff-gnn-am1bcc-0.1.0-rc.3.pt",
model_file_hash="144ed56e46c5b3ad80157b342c8c0f8f7340e4d382a678e30dd300c811646bd0",
digital_object_identifier="10.5072/zenodo.203601",
skip_version_check=True
)
# Should not raise exception
handler1.check_handler_compatibility(handler2)

# Test incompatible with same hash but different DOI
handler3 = NAGLChargesHandler(
model_file="openff-gnn-am1bcc-0.1.0-rc.3.pt",
model_file_hash="144ed56e46c5b3ad80157b342c8c0f8f7340e4d382a678e30dd300c811646bd0",
digital_object_identifier="10.5072/zenodo.999999",
skip_version_check=True
)
with pytest.raises(IncompatibleParameterError, match="different digital_object_identifier values"):
handler1.check_handler_compatibility(handler3)

# Test incompatible with different hash but same DOI
handler4 = NAGLChargesHandler(
model_file="openff-gnn-am1bcc-0.1.0-rc.3.pt",
model_file_hash="different_hash_value",
digital_object_identifier="10.5072/zenodo.203601",
skip_version_check=True
)
with pytest.raises(IncompatibleParameterError, match="different model_file_hash values"):
handler1.check_handler_compatibility(handler4)
Comment on lines +2918 to +2919
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.

(not blocking) if the DOI and hash are BOTH non-matching one will be part of the error message and the other won't be. If it's trivial, it might be useful to include both in the error message. Either way, might be worth having a test that encodes which of them are included in the error message


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.

(certainly not blocking) the earlier tests looked at combinations of optional arguments missing but in ways that could be combined. A marginal add would be those cases in this test, but not particularly important


class TestGBSAHandler:
def test_create_default_gbsahandler(self):
Expand Down
1 change: 1 addition & 0 deletions openff/toolkit/typing/engines/smirnoff/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
IndexedParameterAttribute,
LibraryChargeHandler,
MappedParameterAttribute,
NAGLChargesHandler,
ParameterAttribute,
ParameterHandler,
ParameterList,
Expand Down
Loading
Loading