diff --git a/docs/releasehistory.md b/docs/releasehistory.md index 3b3e598f5..dd167471c 100644 --- a/docs/releasehistory.md +++ b/docs/releasehistory.md @@ -11,6 +11,10 @@ Dates are given in YYYY-MM-DD format. Please note that all releases prior to a version 1.0.0 are considered pre-releases and many API changes will come before a stable release. +## Current development + +* #909 Fixes a bug in which numerical values such as `scale_14` were lost when parsing JSON dumps. + ## 0.3.21 - 2023-02-20 * #906 Fixes a bug in which intramolecular interactions between virtual sites were not properly excluded with OpenMM. diff --git a/openff/interchange/_tests/energy_tests/smirnoff/__init__.py b/openff/interchange/_tests/energy_tests/smirnoff/__init__.py new file mode 100644 index 000000000..98fa78e36 --- /dev/null +++ b/openff/interchange/_tests/energy_tests/smirnoff/__init__.py @@ -0,0 +1 @@ +"""Energy tests directly related to the SMIRNOFF submodule.""" diff --git a/openff/interchange/_tests/energy_tests/smirnoff/test_base.py b/openff/interchange/_tests/energy_tests/smirnoff/test_base.py new file mode 100644 index 000000000..47450093b --- /dev/null +++ b/openff/interchange/_tests/energy_tests/smirnoff/test_base.py @@ -0,0 +1,27 @@ +from openff.toolkit import Quantity +from openff.utilities.testing import skip_if_missing + +from openff.interchange import Interchange +from openff.interchange._tests import MoleculeWithConformer, needs_gmx +from openff.interchange.drivers import get_gromacs_energies, get_openmm_energies + + +@needs_gmx +@skip_if_missing("openmm") +def test_issue_908(sage_unconstrained): + molecule = MoleculeWithConformer.from_smiles("ClCCl") + topology = molecule.to_topology() + topology.box_vectors = Quantity([4, 4, 4], "nanometer") + + state1 = sage_unconstrained.create_interchange(topology) + + with open("test.json", "w") as f: + f.write(state1.json()) + + state2 = Interchange.parse_file("test.json") + + get_gromacs_energies(state1).compare(get_gromacs_energies(state2)) + get_openmm_energies( + state1, + combine_nonbonded_forces=False, + ).compare(get_openmm_energies(state2, combine_nonbonded_forces=False)) diff --git a/openff/interchange/_tests/unit_tests/smirnoff/test_base.py b/openff/interchange/_tests/unit_tests/smirnoff/test_base.py index 431129da5..8591ed885 100644 --- a/openff/interchange/_tests/unit_tests/smirnoff/test_base.py +++ b/openff/interchange/_tests/unit_tests/smirnoff/test_base.py @@ -1,3 +1,5 @@ +import random + import pytest from openff.toolkit.topology import Topology from openff.toolkit.typing.engines.smirnoff.parameters import ( @@ -6,7 +8,11 @@ ) from openff.interchange.exceptions import InvalidParameterHandlerError -from openff.interchange.smirnoff import SMIRNOFFAngleCollection, SMIRNOFFCollection +from openff.interchange.smirnoff import ( + SMIRNOFFAngleCollection, + SMIRNOFFCollection, + SMIRNOFFElectrostaticsCollection, +) class TestSMIRNOFFCollection: @@ -49,3 +55,16 @@ def supported_parameters(cls): parameter_handler=angle_Handler, topology=Topology(), ) + + +def test_json_roundtrip_preserves_float_values(): + """Reproduce issue #908.""" + scale_factor = 0.5 + random.random() * 0.5 + + collection = SMIRNOFFElectrostaticsCollection(scale_14=scale_factor) + + assert collection.scale_14 == scale_factor + + roundtripped = SMIRNOFFElectrostaticsCollection.parse_raw(collection.json()) + + assert roundtripped.scale_14 == scale_factor diff --git a/openff/interchange/smirnoff/_base.py b/openff/interchange/smirnoff/_base.py index 49e36a71e..024432166 100644 --- a/openff/interchange/smirnoff/_base.py +++ b/openff/interchange/smirnoff/_base.py @@ -54,7 +54,11 @@ def collection_loader(data: str) -> dict: tmp: dict[str, Optional[Union[int, bool, str]]] = {} for key, val in json.loads(data).items(): - if isinstance(val, (str, bool, type(None))): + if val is None: + tmp[key] = val + elif isinstance(val, (int, float, bool)): + tmp[key] = val + elif isinstance(val, (str)): # These are stored as string but must be parsed into `Quantity` if key in ("cutoff", "switch_width"): tmp[key] = unit.Quantity(*json.loads(val).values()) # type: ignore[arg-type]