Skip to content

Commit

Permalink
Allow user to pad bounding box for small cmpds (#1007)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
justinGilmer and daico007 authored Feb 28, 2022
1 parent 146a77d commit d6bf1b1
Show file tree
Hide file tree
Showing 2 changed files with 76 additions and 2 deletions.
41 changes: 39 additions & 2 deletions mbuild/compound.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
37 changes: 37 additions & 0 deletions mbuild/tests/test_compound.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down

0 comments on commit d6bf1b1

Please sign in to comment.