Skip to content

Commit 37bb2ce

Browse files
committed
Merge remote-tracking branch 'upstream/main' into release-0.4.3
2 parents 09012c6 + 1b67e48 commit 37bb2ce

File tree

7 files changed

+152
-5
lines changed

7 files changed

+152
-5
lines changed

docs/releasehistory.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@ Please note that all releases prior to a version 1.0.0 are considered pre-releas
1717

1818
* #1174 Support Python 3.13.
1919
* #1216 Type labels can (optionally) be included in LAMMPS files.
20+
* #1219 Adds `SMIRNOFFElectrostaticsCollection.get_charge_array`, which currently only works with systems lacking virtual sites.
21+
* #1220 Adds `Interchange.set_positions_from_gro`.
2022
* #1250 Allow `Interchange.combine` to proceed when cutoffs differ by up to 1e-6 nanometers.
21-
* #1219 Adds `SMIRNOFFElectrostaticsCollection.get_charge_array`.
2223

2324
### Behavior changes
2425

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
integrator = steep
2+
nsteps = 10000
3+
nstxout = 0
4+
nstlog = 0
5+
nstenergy = 0
6+
7+
cutoff-scheme = Verlet
8+
9+
coulombtype = PME
10+
rcoulomb = 0.9
11+
verlet-buffer-tolerance = 0.005
12+
rlist = 0.9
13+
14+
vdwtype = Cut-off
15+
vdw-modifier = force-switch
16+
rvdw = 0.9
17+
rvdw-switch = 0.8
18+
DispCorr = EnerPres
19+
20+
constraints = h-bonds

openff/interchange/_tests/unit_tests/components/test_interchange.py

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
ElectrostaticsHandler,
88
ParameterHandler,
99
)
10+
from openff.utilities import temporary_cd
1011
from openff.utilities.testing import skip_if_missing
1112
from pydantic import ValidationError
1213

@@ -17,9 +18,10 @@
1718
needs_gmx,
1819
needs_lmp,
1920
)
20-
from openff.interchange.drivers import get_openmm_energies
21+
from openff.interchange.drivers import get_gromacs_energies, get_openmm_energies
2122
from openff.interchange.exceptions import (
2223
ExperimentalFeatureException,
24+
InvalidPositionsError,
2325
InvalidTopologyError,
2426
MissingParameterHandlerError,
2527
MissingParametersError,
@@ -124,9 +126,7 @@ def test_atom_ordering(self):
124126

125127
from openff.interchange import Interchange
126128
from openff.interchange.drivers import (
127-
get_gromacs_energies,
128129
get_lammps_energies,
129-
get_openmm_energies,
130130
)
131131

132132
oplsaa = foyer.forcefields.load_OPLSAA()
@@ -442,6 +442,75 @@ def test_from_gromacs_called(self, simple_interchange, monkeypatch):
442442
gro_file="tmp_.gro",
443443
)
444444

445+
@needs_gmx
446+
def test_set_positions_from_gro_same_coordinates(self, simple_interchange):
447+
"""Write and read back the same coordinates, make sure energies match."""
448+
with temporary_cd():
449+
simple_interchange.positions = simple_interchange.positions.round(3)
450+
simple_interchange.to_gromacs(prefix="test12")
451+
452+
original_energies = get_gromacs_energies(simple_interchange)
453+
454+
simple_interchange.set_positions_from_gro(gro_file="test12.gro")
455+
456+
new_energies = get_gromacs_energies(simple_interchange)
457+
458+
# if energies match, positions must also match
459+
assert original_energies.energies.keys() == new_energies.energies.keys()
460+
461+
for key in original_energies.energies:
462+
assert numpy.allclose(
463+
original_energies[key].m,
464+
new_energies[key].m,
465+
)
466+
467+
@needs_gmx
468+
def test_set_positions_from_gro_minimize(self, simple_interchange):
469+
"""Write, minimize, and read new coordinates, make sure energy descreased."""
470+
with temporary_cd():
471+
simple_interchange.positions = simple_interchange.positions.round(3)
472+
473+
# with 0.001 nm precision, not guaranteed a difference between conformer-generation
474+
# coordinates and MM-minimized coordinates, so just perturb a little bit
475+
simple_interchange.positions[-1] += Quantity([0.03, 0.03, 0.03], "nanometer")
476+
simple_interchange.to_gromacs(prefix="test13")
477+
478+
original_energies = get_gromacs_energies(simple_interchange)
479+
480+
mdp = get_test_file_path("mdp/em.mdp")
481+
482+
for cmd in [
483+
f"gmx grompp -f {mdp} -c test13.gro -p test13.top -o test13.tpr",
484+
"gmx mdrun -s test13.tpr -c final_coords.gro",
485+
"echo 0 | gmx trjconv -f final_coords.gro -s test13.tpr -o final_coords_wrapped.gro -pbc whole",
486+
]:
487+
subprocess.check_output(cmd, shell=True)
488+
489+
simple_interchange.set_positions_from_gro(gro_file="final_coords_wrapped.gro")
490+
491+
new_energies = get_gromacs_energies(simple_interchange)
492+
493+
assert new_energies.total_energy < original_energies.total_energy
494+
495+
@needs_gmx
496+
def test_set_positions_from_gro_wrong_coordinates(self, sage):
497+
with temporary_cd():
498+
eth = sage.create_interchange(MoleculeWithConformer.from_smiles("CCO").to_topology())
499+
eth.box = Quantity([4, 4, 4], unit.nanometer)
500+
eth.to_gromacs(prefix="ethanol")
501+
benzene = sage.create_interchange(MoleculeWithConformer.from_smiles("c1ccccc1").to_topology())
502+
benzene.box = Quantity([4, 4, 4], unit.nanometer)
503+
benzene.to_gromacs(prefix="benzene")
504+
505+
with pytest.raises(InvalidPositionsError, match="12.*9"):
506+
eth.set_positions_from_gro(gro_file="benzene.gro")
507+
508+
with pytest.raises(
509+
InvalidPositionsError,
510+
match="9.*12",
511+
):
512+
benzene.set_positions_from_gro(gro_file="ethanol.gro")
513+
445514
@skip_if_missing("openmm")
446515
def test_minimize(self, simple_interchange):
447516
original_energy = get_openmm_energies(simple_interchange).total_energy

openff/interchange/_tests/unit_tests/smirnoff/test_nonbonded.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,20 @@ def test_get_charge_array(self, sage):
287287
)
288288

289289

290+
def test_get_charge_array_fails_if_virtual_sites_present(water, tip4p):
291+
with pytest.raises(
292+
NotImplementedError,
293+
match="Not yet implemented with virtual sites",
294+
):
295+
tip4p.create_interchange(water.to_topology())["Electrostatics"].get_charge_array(include_virtual_sites=True)
296+
297+
with pytest.raises(
298+
NotImplementedError,
299+
match="Not yet implemented when virtual sites are present",
300+
):
301+
tip4p.create_interchange(water.to_topology())["Electrostatics"].get_charge_array(include_virtual_sites=False)
302+
303+
290304
def test_nonintegral_molecule_charge_error(sage, water):
291305
funky_charges = Quantity([0, 0, -5.5], "elementary_charge")
292306

openff/interchange/components/interchange.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from openff.interchange.components.mdconfig import MDConfig
2828
from openff.interchange.components.potentials import Collection, _AnnotatedCollections
2929
from openff.interchange.exceptions import (
30+
InvalidPositionsError,
3031
MissingParameterHandlerError,
3132
MissingPositionsError,
3233
UnsupportedExportError,
@@ -903,6 +904,33 @@ def from_gromacs(
903904
),
904905
)
905906

907+
def set_positions_from_gro(
908+
self,
909+
gro_file: Path | str,
910+
):
911+
"""
912+
Set the positions of this `Interchange` from a GROMACS coordinate `.gro` file.
913+
914+
Only the coordinates from the `.gro` file are used. No effort is made to ensure the topologies are compatible
915+
with each other. This includes, for example, a lack of guarantee that the atom ordering in the `.gro` file
916+
matches the atom ordering in the `Interchange` object.
917+
918+
`InvalidPositionsError` is raised if the number of rows in the coordinate array does not match the number of
919+
atoms in the topology of this `Interchange`.
920+
"""
921+
from openff.interchange.interop.gromacs._import._import import _read_coordinates
922+
923+
# should already be in nm, might not be necessary
924+
coordinates = _read_coordinates(gro_file).to(unit.nanometer)
925+
926+
if coordinates.shape != (self.topology.n_atoms, 3):
927+
raise InvalidPositionsError(
928+
f"Coordinates in {gro_file} do not match the number of atoms in the topology. ",
929+
f"Parsed coordinates have shape {coordinates.shape} but topology has {self.topology.n_atoms} atoms.",
930+
)
931+
932+
self.positions = coordinates
933+
906934
@classmethod
907935
def from_openmm(
908936
cls,

openff/interchange/exceptions.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@ class InvalidBoxError(InterchangeException, ValueError):
3838
"""
3939

4040

41+
class InvalidPositionsError(InterchangeException, ValueError):
42+
"""
43+
Generic exception for errors with positions.
44+
"""
45+
46+
4147
class InvalidTopologyError(InterchangeException, ValueError):
4248
"""
4349
Generic exception for errors reading chemical topology data.

openff/interchange/smirnoff/_nonbonded.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -330,10 +330,19 @@ def charges(
330330
return self._charges
331331

332332
def get_charge_array(self, include_virtual_sites: bool = False) -> Quantity:
333-
"""Return a one-dimensional array-like of atomic charges, ordered topologically."""
333+
"""
334+
Return a one-dimensional array-like of atomic charges, ordered topologically.
335+
336+
If virtual sites are present in the system, `NotImplementedError` is raised.
337+
"""
334338
if include_virtual_sites:
335339
raise NotImplementedError("Not yet implemented with virtual sites")
336340

341+
if VirtualSiteKey in {type(key) for key in self.key_map}:
342+
raise NotImplementedError(
343+
"Not yet implemented when virtual sites are present, even with `include_virtual_sites=False`.",
344+
)
345+
337346
return Quantity.from_list([q for _, q in sorted(self.charges.items(), key=lambda x: x[0].atom_indices)])
338347

339348
def _get_charges(

0 commit comments

Comments
 (0)