From d6bf1b1de77e46141fc6aacde20f5811fc884d51 Mon Sep 17 00:00:00 2001 From: Justin Gilmer Date: Mon, 28 Feb 2022 12:10:48 -0600 Subject: [PATCH] Allow user to pad bounding box for small cmpds (#1007) * Allow user to pad bounding box for small cmpds Raised by @daico007 in #1006, for very small molecules, the bounding boxes will be so close to zero that the determinant of the box vectors will be close to zero, signaling a co-linear box (psuedo-{1D, 2D}) and raising an exception. This PR provides an additional parmeter for the `mb.Compound.get_bounding_box` method, `pad_box` which is a boolean to avoid these co-linear systems by adding a 1nm pad to all box edges. `pad_box` is `False` by default, and unit tests have been provided to check both cases. Closes #1006 * Update pad_box and add unit tests * Update docstring to support indented list. Co-authored-by: Co Quach --- mbuild/compound.py | 41 +++++++++++++++++++++++++++++++++-- mbuild/tests/test_compound.py | 37 +++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 2 deletions(-) diff --git a/mbuild/compound.py b/mbuild/compound.py index f9e15190f..56a8c98c3 100644 --- a/mbuild/compound.py +++ b/mbuild/compound.py @@ -7,6 +7,7 @@ from collections import OrderedDict from collections.abc import Iterable from copy import deepcopy +from typing import Sequence from warnings import warn import ele @@ -1298,11 +1299,22 @@ def maxs(self): """Return the maximum x, y, z coordinate of any particle in this compound.""" return self.xyz.max(axis=0) - def get_boundingbox(self): + def get_boundingbox(self, pad_box=None): """Compute the bounding box of the compound. Compute and store the rectangular bounding box of the Compound. + Parameters + ---------- + pad_box: Sequence, optional, default=None + Pad all lengths or a list of lengths by a specified amount in nm. + Acceptable values are: + + - A single float: apply this pad value to all 3 box lengths. + - A sequence of length 1: apply this pad value to all 3 box lengths. + - A sequence of length 3: apply these pad values to the a, b, c box lengths. + + Returns ------- mb.Box @@ -1347,7 +1359,32 @@ def get_boundingbox(self): for i, dim in enumerate(has_dimension): if not dim: vecs[i][i] = 1.0 - return Box.from_vectors(vectors=np.asarray([vecs]).reshape(3, 3)) + + if pad_box is not None: + if isinstance(pad_box, (int, float, str, Sequence)): + if isinstance(pad_box, Sequence): + if len(pad_box) == 1: + padding = [float(pad_box[0])] * 3 + elif len(pad_box) == 3: + padding = [float(val) for val in pad_box] + else: + raise TypeError( + f"Expected a Sequence of length 1 or 3 for pad_box. Provided: {len(pad_box)}" + ) + else: + pad_box = float(pad_box) + padding = [pad_box] * 3 + else: + raise TypeError( + f"Expected a value of type: int, float, str, or Sequence, was provided: {type(pad_box)}" + ) + for dim, val in enumerate(padding): + vecs[dim][dim] = vecs[dim][dim] + val + + bounding_box = Box.from_vectors( + vectors=np.asarray([vecs]).reshape(3, 3) + ) + return bounding_box def min_periodic_distance(self, xyz0, xyz1): """Vectorized distance calculation considering minimum image. diff --git a/mbuild/tests/test_compound.py b/mbuild/tests/test_compound.py index 41d1e4685..8a5e47315 100644 --- a/mbuild/tests/test_compound.py +++ b/mbuild/tests/test_compound.py @@ -1575,6 +1575,43 @@ def test_box(self): with pytest.warns(UserWarning): compound.box = Box(lengths=[1.0, 1.0, 1.0], angles=angles) + def test_get_boundingbox_extrema(self): + h1 = mb.Compound() + h2 = mb.Compound() + h1.pos = [-0.07590747, 0.00182889, 0.00211742] + h2.pos = [0.07590747, -0.00182889, -0.00211742] + container = mb.Compound([h1, h2]) + distances = container.maxs - container.mins + with pytest.raises( + MBuildError, match=r"The vectors to define the box are co\-linear\," + ): + container.get_boundingbox() + distance_list = [val for val in distances] + distance_list = [val + 1.0 for val in distance_list] + np.testing.assert_almost_equal( + container.get_boundingbox(pad_box=1.0).lengths, + distance_list, + decimal=6, + ) + + distance_list = [val for val in distances] + distance_list[0] = distance_list[0] + 1.0 + distance_list[1] = distance_list[1] + 2.0 + distance_list[2] = distance_list[2] + 3.0 + np.testing.assert_almost_equal( + container.get_boundingbox(pad_box=[1.0, 2.0, 3.0]).lengths, + distance_list, + decimal=6, + ) + + @pytest.mark.parametrize( + "bad_value", [[1.0, 2.0], set([1, 2, 3]), {"x": 1.0}] + ) + def test_get_boundingbox_error(self, bad_value): + with pytest.raises(TypeError): + meth = mb.load(get_fn("methyl.pdb")) + meth.get_boundingbox(pad_box=bad_value) + @pytest.mark.skipif(not has_py3Dmol, reason="Py3Dmol is not installed") def test_visualize_py3dmol(self, ethane): py3Dmol = import_("py3Dmol")