Skip to content

Commit 14250b7

Browse files
mattwthompsonpre-commit-ci[bot]Yoshanuikabundi
authored
Update Packmol defaults (#1332)
* API: Update default packing behavior * FIX: Fix box scale-up test * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * FIX: Fix tests * Lint * DOC: Update packed box example * API: Avoid changing private function default * ENH: Use PBCs in Packmol wrappers * DOC: Document behavior changes * FIX: Try to be backwards-compatible with older Packmol * FIX: Remove temporary code * DOC: Update release history * MAINT: Try a build without AmberTools? * Revert "MAINT: Try a build without AmberTools?" This reverts commit f16de82. * MAINT: Debug * Fix? * FIX: Fix parsing output if no error * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * LINT * REF: Only use Packmol PBCs with rectangular boxes * Apply suggestion from @Yoshanuikabundi Co-authored-by: Josh A. Mitchell <[email protected]> * Apply suggestion from @Yoshanuikabundi Co-authored-by: Josh A. Mitchell <[email protected]> * Apply suggestion from @Yoshanuikabundi Co-authored-by: Josh A. Mitchell <[email protected]> * FIX: Fix double-uninstall * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * DOC: Update docstrings * FIX: Simpler version parsing * Apply suggestions from code review Co-authored-by: Josh A. Mitchell <[email protected]> --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Josh A. Mitchell <[email protected]>
1 parent f3a2198 commit 14250b7

File tree

4 files changed

+134
-37
lines changed

4 files changed

+134
-37
lines changed

docs/releasehistory.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,16 @@ Please note that all releases prior to a version 1.0.0 are considered pre-releas
3333

3434
## 0.4.8 - 2025-10-09
3535

36+
### Behavior changes
37+
38+
* #1332 Introduces the following behavior changes to the private Packmol wrappers:
39+
* Packmol version 20.15.0 or newer is recommended.
40+
* Periodic boundary conditions are accounted for when placing molecules if the box is orthorhombic and Packmol version 20.15.0 or newer is installed; this is the minimum version supporting this feature.
41+
* This is functionally the same as the previous behavior, so no workaround is needed to recover it.
42+
* The `tolerance` parameter is subtracted from computed box lengths when placing molecules if a Packmol version older than 20.15.0 is installed; this is the previous behavior.
43+
* The `target_density` is used in box size calculations with modification; previously, box volumes were scaled up by a factor of 1.1.
44+
* The previous behavior can be restored by passing scaling the `target_density` argument down by a factor of 1.1.
45+
3646
### Bug fixes
3747

3848
* #1340 Fixes JSON deserialization issues when charges come from `NAGLCharges` or `ChargeIncrementModel`.

examples/packed_box/packed_box.ipynb

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -107,14 +107,6 @@
107107
"cell_type": "markdown",
108108
"id": "9",
109109
"metadata": {},
110-
"source": [
111-
"We can get the positions as an array from the PDB file object:"
112-
]
113-
},
114-
{
115-
"cell_type": "markdown",
116-
"id": "10",
117-
"metadata": {},
118110
"source": [
119111
"## Parametrize with Interchange\n",
120112
"\n",
@@ -124,7 +116,7 @@
124116
{
125117
"cell_type": "code",
126118
"execution_count": null,
127-
"id": "11",
119+
"id": "10",
128120
"metadata": {},
129121
"outputs": [],
130122
"source": [
@@ -135,7 +127,7 @@
135127
},
136128
{
137129
"cell_type": "markdown",
138-
"id": "12",
130+
"id": "11",
139131
"metadata": {},
140132
"source": [
141133
"We can visualize it (though, since we can't see the stored physics parameters, it'll look the same):"
@@ -144,7 +136,7 @@
144136
{
145137
"cell_type": "code",
146138
"execution_count": null,
147-
"id": "13",
139+
"id": "12",
148140
"metadata": {},
149141
"outputs": [],
150142
"source": [
@@ -153,16 +145,34 @@
153145
},
154146
{
155147
"cell_type": "markdown",
148+
"id": "13",
149+
"metadata": {},
150+
"source": [
151+
"Packmol places molecules at random positions in the box, satisfying geometric constraints but no thermodynamic considerations. The resulting configurations need to be subject to energy minimization and equilibration routines before being used in production simulations. A full equilibration routine is outside the scope of this example, but a brief energy minimization immediately after packing lessens the likelihood of unfavorable energetics or crashes."
152+
]
153+
},
154+
{
155+
"cell_type": "code",
156+
"execution_count": null,
156157
"id": "14",
157158
"metadata": {},
159+
"outputs": [],
160+
"source": [
161+
"interchange.minimize(max_iterations=1000)"
162+
]
163+
},
164+
{
165+
"cell_type": "markdown",
166+
"id": "15",
167+
"metadata": {},
158168
"source": [
159169
"And we can calculate and compare energies with different MD engines! (The LAMMPS exporter isn't optimized yet for large systems, so we're only looking at OpenMM, GROMACS, and Amber.)"
160170
]
161171
},
162172
{
163173
"cell_type": "code",
164174
"execution_count": null,
165-
"id": "15",
175+
"id": "16",
166176
"metadata": {},
167177
"outputs": [],
168178
"source": [
@@ -187,7 +197,7 @@
187197
"name": "python",
188198
"nbconvert_exporter": "python",
189199
"pygments_lexer": "ipython3",
190-
"version": "3.11.4"
200+
"version": "3.13.7"
191201
}
192202
},
193203
"nbformat": 4,

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

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,15 +51,24 @@ class TestPackmolWrapper:
5151
Quantity(234, "angstrom**3"),
5252
],
5353
)
54-
def test_scale_box(self, box, volume):
54+
@pytest.mark.parametrize(
55+
"factor",
56+
[
57+
0.9,
58+
1.0,
59+
1.1,
60+
2.0,
61+
],
62+
)
63+
def test_scale_box(self, box, volume, factor):
5564
"""Test that _scale_box() produces a box with the desired volume."""
56-
scaled_box = _scale_box(box, volume)
65+
scaled_box = _scale_box(box, volume, factor)
5766
a, b, c = scaled_box
5867

5968
# | (a x b) . c | is the volume of the box
6069
# _scale_box uses numpy.linalg.det instead
6170
# linear dimensions are scaled by 1.1, so volumes are scaled by 1.1 ** 3
62-
assert numpy.isclose(numpy.abs(numpy.dot(numpy.cross(a, b), c)), volume * 1.1**3)
71+
assert numpy.isclose(numpy.abs(numpy.dot(numpy.cross(a, b), c)), volume * factor**3)
6372

6473
assert str(scaled_box.u) == "angstrom"
6574

@@ -487,6 +496,7 @@ def test_solvate_topology_highly_charged_low_nacl_conc(self, n_monomers):
487496

488497
topology = solvate_topology(
489498
solute.to_topology(),
499+
target_density=Quantity(0.8, "gram / milliliter"),
490500
nacl_conc=Quantity(1e-3, "molar"),
491501
padding=Quantity(2, "nanometer"),
492502
)

openff/interchange/components/_packmol.py

Lines changed: 88 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from numpy.typing import ArrayLike, NDArray
1515
from openff.toolkit import Molecule, Quantity, RDKitToolkitWrapper, Topology
1616
from openff.utilities.utilities import requires_package, temporary_cd
17+
from packaging.version import Version
1718

1819
from openff.interchange.exceptions import PACKMOLRuntimeError, PACKMOLValueError
1920

@@ -345,10 +346,14 @@ def _box_from_density(
345346
total_mass = sum(sum([atom.mass for atom in molecule.atoms]) * n for molecule, n in zip(molecules, n_copies))
346347
volume = total_mass / target_density
347348

348-
return _scale_box(box_shape, volume)
349+
return _scale_box(
350+
box=box_shape,
351+
volume=volume,
352+
box_scaleup_factor=1.0,
353+
)
349354

350355

351-
def _scale_box(box: NDArray, volume: Quantity, box_scaleup_factor=1.1) -> Quantity:
356+
def _scale_box(box: NDArray, volume: Quantity, box_scaleup_factor: float = 1.1) -> Quantity:
352357
"""
353358
Scale the parallelepiped spanned by ``box`` to the given volume.
354359
@@ -442,12 +447,33 @@ def _create_molecule_pdbs(molecules: list[Molecule]) -> list[str]:
442447
return pdb_file_names
443448

444449

450+
def _get_packmol_version() -> Version:
451+
"""Return the version of packmol installed."""
452+
with temporary_cd(tempfile.mkdtemp()):
453+
try:
454+
result = subprocess.run(
455+
_find_packmol(),
456+
input="",
457+
stdout=subprocess.PIPE,
458+
stderr=subprocess.STDOUT,
459+
text=True,
460+
)
461+
462+
found_version = Version(result.stdout.split("Version ")[1].split(" ")[0])
463+
464+
except Exception as error:
465+
raise PACKMOLRuntimeError(f"Unexpected error ({type(error)}) while running Packmol.") from error
466+
467+
return found_version
468+
469+
445470
def _build_input_file(
446471
molecule_file_names: list[str],
447472
molecule_counts: list[int],
448473
structure_to_solvate: str | None,
449474
box_size: Quantity,
450475
tolerance: Quantity,
476+
rectangular: bool = False,
451477
) -> tuple[str, str]:
452478
"""
453479
Construct the packmol input file.
@@ -462,10 +488,11 @@ def _build_input_file(
462488
The path to the structure to solvate.
463489
box_size
464490
The lengths of each side of the box we want to fill. This is the box
465-
size of the rectangular brick representation of the simulation box; the
466-
packmol box will be shrunk by the tolerance.
491+
size of the rectangular brick representation of the simulation box
467492
tolerance
468493
The packmol convergence tolerance.
494+
rectangular
495+
Whether the box is rectangular (True) or triclinic (False).
469496
470497
Returns
471498
-------
@@ -475,7 +502,12 @@ def _build_input_file(
475502
The path to the output file.
476503
477504
"""
478-
box_size = (box_size - tolerance).m_as("angstrom")
505+
if rectangular:
506+
PACKMOL_USE_PBC = _get_packmol_version() >= Version("20.15.0")
507+
else:
508+
PACKMOL_USE_PBC = False
509+
510+
box_size = box_size.m_as("angstrom") if PACKMOL_USE_PBC else (box_size - tolerance).m_as("angstrom")
479511
tolerance = tolerance.m_as("angstrom")
480512

481513
# Add the global header options.
@@ -487,6 +519,9 @@ def _build_input_file(
487519
"",
488520
]
489521

522+
if PACKMOL_USE_PBC:
523+
input_lines.append(f"pbc 0. 0. 0. {box_size[0]} {box_size[1]} {box_size[2]}")
524+
490525
# Add the section of the molecule to solvate if provided.
491526
if structure_to_solvate is not None:
492527
input_lines.extend(
@@ -508,7 +543,7 @@ def _build_input_file(
508543
[
509544
f"structure {file_name}",
510545
f" number {count}",
511-
f" inside box 0. 0. 0. {box_size[0]} {box_size[1]} {box_size[2]}",
546+
"" if PACKMOL_USE_PBC else f" inside box 0. 0. 0. {box_size[0]} {box_size[1]} {box_size[2]}",
512547
"end structure",
513548
"",
514549
],
@@ -633,14 +668,20 @@ def pack_box(
633668
634669
Notes
635670
-----
636-
Returned topologies may have smaller or larger box vectors than what would be defined by the
637-
target density if the box vectors are determined by `target_density`. When calling Packmol, each
638-
linear dimension of the box is scaled up by 10%. However, Packmol by default adds a small
639-
buffer (defined by the `tolerance` argument which otherwise defines the minimum distance,
640-
default 2 Angstrom) at the end of the packed box, which causes small voids when tiling copies of
641-
each periodic image. This void is removed in hopes of faster equilibration times but the box
642-
density is slightly increased as a result. These changes may cancel each other out or result in
643-
larger or smaller densities than the target density, depending on argument values.
671+
Packmol places molecules at random positions in the box, satisfying
672+
geometric constraints but no thermodynamic considerations. The resulting
673+
configurations need to be subjected to energy minimization and equilibration
674+
routines before being used in production simulations.
675+
676+
Orthorhombic periodic boundary conditions are accounted for during packing
677+
if using Packmol version 20.15.0 or newer.
678+
679+
Non-orthorhombic triclinic boxes, and all boxes in Packmol versions older
680+
than 20.15.0, are accounted for by packing the
681+
`"brick" representation <https://manual.gromacs.org/current/reference-manual/algorithms/periodic-boundary-conditions.html#fig-pbc>`__
682+
after shrinking each dimension by the ``tolerance``. This introduces a small
683+
void at the edges of the box to avoid clashes. This void will quickly be
684+
filled in during equilibration.
644685
645686
"""
646687
# Make sure packmol can be found.
@@ -663,6 +704,8 @@ def pack_box(
663704
target_density,
664705
)
665706

707+
is_rectangular = numpy.all(box_shape == numpy.diag(numpy.diagonal(box_shape)))
708+
666709
# Estimate the box_vectors from mass density if one is not provided.
667710
if target_density is not None:
668711
box_vectors = _box_from_density(
@@ -683,8 +726,7 @@ def pack_box(
683726
+ "05_other_features.html#periodic-boundary-conditions",
684727
)
685728

686-
# Compute the dimensions of the equivalent brick - this is what packmol will
687-
# fill
729+
# Compute the dimensions of the equivalent brick - this is what packmol will fill
688730
brick_size = _compute_brick_from_box_vectors(box_vectors)
689731

690732
# Center the solute
@@ -721,6 +763,7 @@ def pack_box(
721763
solute_pdb_filename,
722764
brick_size,
723765
tolerance,
766+
rectangular=is_rectangular,
724767
)
725768

726769
with open(input_file_path) as file_handle:
@@ -831,7 +874,7 @@ def solvate_topology(
831874
"""
832875
Add water and ions to neutralise and solvate a topology.
833876
834-
The [SLTCAP](10.1021/acs.jctc.7b01254) method is used to determine the number of each ion to add.
877+
The [SLTCAP](10.1021/acs.jctc.7b01254) method is used to determine the number of each ion to add.
835878
836879
Parameters
837880
----------
@@ -876,8 +919,20 @@ def solvate_topology(
876919
877920
Notes
878921
-----
879-
Returned topologies may have larger box vectors than what would be defined
880-
by the target density.
922+
Packmol places solvent molecules at random positions in the box, satisfying
923+
geometric constraints but no thermodynamic considerations. The resulting
924+
configurations need to be subjected to energy minimization and equilibration
925+
routines before being used in production simulations.
926+
927+
Orthorhombic periodic boundary conditions are accounted for during packing
928+
if using Packmol version 20.15.0 or newer.
929+
930+
Non-orthorhombic triclinic boxes, and all boxes in Packmol versions older
931+
than 20.15.0, are accounted for by packing the
932+
`"brick" representation <https://manual.gromacs.org/current/reference-manual/algorithms/periodic-boundary-conditions.html#fig-pbc>`__
933+
after shrinking each dimension by the ``tolerance``. This introduces a small
934+
void at the edges of the box to avoid clashes. This void will quickly be
935+
filled in during equilibration.
881936
882937
Returned topologies may have ion concentrations higher than the value of the
883938
the `nacl_conc` argument.
@@ -1040,8 +1095,20 @@ def solvate_topology_nonwater(
10401095
10411096
Notes
10421097
-----
1043-
Returned topologies may have larger box vectors than what would be defined
1044-
by the target density.
1098+
Packmol places solvent molecules at random positions in the box, satisfying
1099+
geometric constraints but no thermodynamic considerations. The resulting
1100+
configurations need to be subjected to energy minimization and equilibration
1101+
routines before being used in production simulations.
1102+
1103+
Orthorhombic periodic boundary conditions are accounted for during packing
1104+
if using Packmol version 20.15.0 or newer.
1105+
1106+
Non-orthorhombic triclinic boxes, and all boxes in Packmol versions older
1107+
than 20.15.0, are accounted for by packing the
1108+
`"brick" representation <https://manual.gromacs.org/current/reference-manual/algorithms/periodic-boundary-conditions.html#fig-pbc>`__
1109+
after shrinking each dimension by the ``tolerance``. This introduces a small
1110+
void at the edges of the box to avoid clashes. This void will quickly be
1111+
filled in during equilibration.
10451112
"""
10461113
_check_box_shape_shape(box_shape)
10471114

0 commit comments

Comments
 (0)