Skip to content

Commit

Permalink
fix: set default dmin and dmax for box selection
Browse files Browse the repository at this point in the history
  • Loading branch information
Cloudac7 committed Nov 8, 2023
1 parent a63f9c5 commit 1cbb282
Show file tree
Hide file tree
Showing 4 changed files with 76 additions and 42 deletions.
20 changes: 20 additions & 0 deletions package/MDAnalysis/core/groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -3111,6 +3111,26 @@ def select_atoms(self, sel, *othersel, periodic=True, rtol=1e-05,
radius 5, external radius 10 centered on the COG. In z, the
cylinder extends from 10 above the COG to 8 below. Positive
values for *zMin*, or negative ones for *zMax*, are allowed.
box *dimensions* *d1_min* *d1_max* (*d2_min* *d2_max*) (*d3_min* *d3_max*) *selection*
Select all atoms within a box region centered
on the center of geometry (COG) of a given selection.
*dimensions* Specifies which dimension(s) to apply
the box selection on. Can be ``x``, ``y``, ``z``,
or any combination like ``xy``, ``yz``, ``zx``, ``xyz``
(up to 3 characters). *d\*_min*, *d\*_max* are the minimum and
maximum bounds along the first specified dimension.
Positive values are above/right/front of the COG,
negatives are below/left/behind. Should be specified
for each dimension. *selection* specifies the selection
to center the box on. e.g. ``box x -5 10 protein``
selects a 15 Angstrom box along x centered
on the COG of protein, extending 5 Angstroms
below to 10 Angstroms above. ``box yz -8 10 -10 6 protein``
selects a box with y extending 8 below to 10 above the COG,
and z extending 10 below to 6 above.
``box xyz -5 10 -8 6 -7 9 protein`` selects
a 3D box with x -5 to 10, y -8 to 6, and z -7 to 9 relative
to the protein COG.
**Connectivity**
Expand Down
46 changes: 28 additions & 18 deletions package/MDAnalysis/core/selection.py
Original file line number Diff line number Diff line change
Expand Up @@ -540,7 +540,6 @@ def _apply(self, group):
class BoxSelection(Selection):
token = "box"
precedence = 1
index_map = {"x": 0, "y": 1, "z": 2}
combination = [
"x",
"y",
Expand All @@ -564,15 +563,25 @@ def __init__(self, parser, tokens):
super().__init__(parser, tokens)
self.periodic = parser.periodic
self.direction = tokens.popleft()
self.xmin, self.xmax = None, None
self.ymin, self.ymax = None, None
self.zmin, self.zmax = None, None
if self.direction not in self.combination:
raise ValueError(
"The direction '{}' is not valid. Must be combination of {}"
"".format(self.direction, list(self.index_map.keys()))
"".format(self.direction, ["x", "y", "z"])
)
else:
for d in self.direction:
setattr(self, "{}max".format(d), float(tokens.popleft()))
setattr(self, "{}min".format(d), float(tokens.popleft()))
if d == "x":
self.xmin = float(tokens.popleft())
self.xmax = float(tokens.popleft())
elif d == "y":
self.ymin = float(tokens.popleft())
self.ymax = float(tokens.popleft())
elif d == "z":
self.zmin = float(tokens.popleft())
self.zmax = float(tokens.popleft())
self.sel = parser.parse_expression(self.precedence)

@return_empty_on_apply
Expand All @@ -582,21 +591,20 @@ def _apply(self, group):
return group[[]]
# Calculate vectors between point of interest and our group
vecs = group.positions - sel.center_of_geometry()
range_map = {}

for d in self.direction:
axis_index = self.index_map.get(d)
axis_max = self.__getattribute__("{}max".format(d))
axis_min = self.__getattribute__("{}min".format(d))
range_map[axis_index] = (axis_min, axis_max)
range_map = {
0: (self.xmin, self.xmax),
1: (self.ymin, self.ymax),
2: (self.zmin, self.zmax),
}

if self.periodic and group.dimensions is not None:
box = group.dimensions[:3]

for k, v in range_map.items():
axis_index = k
axis_min, axis_max = v[0], v[1]

for idx, limits in range_map.items():
axis_index = idx
axis_min, axis_max = limits[0], limits[1]
if axis_min is None or axis_max is None:
continue
axis_height = axis_max - axis_min
if axis_height > box[axis_index]:
raise NotImplementedError(
Expand All @@ -615,11 +623,13 @@ def _apply(self, group):

# Deal with each dimension criteria
mask = None
for k, v in range_map.items():
for idx, limits in range_map.items():
if limits[0] is None or limits[1] is None:
continue
if mask is None:
mask = (vecs[:, k] > v[0]) & (vecs[:, k] < v[1])
mask = (vecs[:, idx] > limits[0]) & (vecs[:, idx] < limits[1])
else:
mask &= (vecs[:, k] > v[0]) & (vecs[:, k] < v[1])
mask &= (vecs[:, idx] > limits[0]) & (vecs[:, idx] < limits[1])

return group[mask]

Expand Down
36 changes: 20 additions & 16 deletions package/doc/sphinx/source/documentation_pages/selections.rst
Original file line number Diff line number Diff line change
Expand Up @@ -277,22 +277,26 @@ cyzone *externalRadius* *zMax* *zMin* *selection*
relative to the COG of *selection*, instead of absolute z-values
in the box.

box *dimension(s)* *d1_Max* *d1_Min* (*d2_Max* *d2_Min*) (*d3_Max* *d3_Min*) *selection*
select all atoms around a box centered in the center of geometry (COG)
of a given selection, e.g. ``box x 10 -5 protein`` selects the center
of geometry of protein, and creates a zone of 15 Angstroms in x axis,
extending from 10 above the COG to 5 below. ``box yz 10 -8 6 -10 protein``
selects COG of protein, and creates an orthogonal zone extending from
10 above the COG to 8 below in y, and from 6 above the COG to 10 below in z.
``box xyz 10 -5 6 -8 9 -7 protein`` selects COG of protein, and creates
an orthogonal box of extending from 10 above the COG to 5 below in x,
from 6 above the COG to 8 below in y, and from 9 above the COG to
7 below in z. *dimension(s)* can be any or any combination of **x**,
**y**, and **z**, but should not be longer than 3 characters,
e.g. ``x``, ``yz``, ``zx``, ``xyz``. Positive values for *d\*_Min*,
or negative ones for *d\*_Max* are allowed. Number of groups of
*d\*_Max* and *d\*_Min* should be equal to the number of characters in
*dimension(s)*.
box *dimensions* *d1_min* *d1_max* (*d2_min* *d2_max*) (*d3_min* *d3_max*) *selection*
Select all atoms within a box region centered
on the center of geometry (COG) of a given selection.
*dimensions* Specifies which dimension(s) to apply
the box selection on. Can be ``x``, ``y``, ``z``,
or any combination like ``xy``, ``yz``, ``zx``, ``xyz``
(up to 3 characters). *d\*_min*, *d\*_max* are the minimum and
maximum bounds along the first specified dimension.
Positive values are above/right/front of the COG,
negatives are below/left/behind. Should be specified
for each dimension. *selection* specifies the selection
to center the box on. e.g. ``box x -5 10 protein``
selects a 15 Angstrom box along x centered
on the COG of protein, extending 5 Angstroms
below to 10 Angstroms above. ``box yz -8 10 -10 6 protein``
selects a box with y extending 8 below to 10 above the COG,
and z extending 10 below to 6 above.
``box xyz -5 10 -8 6 -7 9 protein`` selects
a 3D box with x -5 to 10, y -8 to 6, and z -7 to 9 relative
to the protein COG.

point *x* *y* *z* *distance*
selects all atoms within a cutoff of a point in space, make sure
Expand Down
16 changes: 8 additions & 8 deletions testsuite/MDAnalysisTests/core/test_atomselections.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,9 +249,9 @@ def test_point(self, universe):
@pytest.mark.parametrize(
"selstr, expected_value",
[
("box x 2.0 -2.0 index 1281", 418),
("box yz 2.0 -2.0 2.0 -2.0 index 1280", 58),
("box xyz 2.0 -2.0 2.0 -2.0 2.0 -2.0 index 1279", 10),
("box x -2.0 2.0 index 1281", 418),
("box yz -2.0 2.0 -2.0 2.0 index 1280", 58),
("box xyz -2.0 2.0 -2.0 2.0 -2.0 2.0 index 1279", 10),
],
)
def test_box(self, universe, selstr, expected_value):
Expand Down Expand Up @@ -816,20 +816,20 @@ def test_sphzone(self, u, periodic, expected):

@pytest.mark.parametrize("periodic,expected", ([True, 29], [False, 17]))
def test_box(self, u, periodic, expected):
sel = u.select_atoms("box xyz 5 2 10 -5 6 -2 resid 1", periodic=periodic)
sel = u.select_atoms("box xyz 2 5 -5 10 -2 6 resid 1", periodic=periodic)

assert len(sel) == expected

@pytest.mark.parametrize(
"selection,error,expected",
(
[
"box xyz 10 -5 90 -90 6 -2 resid 1",
"box xyz -5 10 -90 90 -2 6 resid 1",
NotImplementedError,
"The total length of the box selection in y",
],
[
"box yyy 10 -5 7 -7 6 -2 resid 1",
"box yyy -5 10 -7 7 -2 6 resid 1",
SelectionError,
"Must be combination of",
],
Expand Down Expand Up @@ -906,11 +906,11 @@ def test_empty_sphzone(self, u):
assert len(empty) == 0

def test_box(self, u):
ag = u.select_atoms("box z 2.5 -2.5 resid 1")
ag = u.select_atoms("box z -2.5 2.5 resid 1")
assert len(ag) == 4237

def test_empty_box(self, u):
ag = u.select_atoms("box z 2.5 -2.5 name NOT_A_NAME")
ag = u.select_atoms("box z -2.5 2.5 name NOT_A_NAME")
assert len(ag) == 0

def test_point_1(self, u):
Expand Down

0 comments on commit 1cbb282

Please sign in to comment.