From 630beea35997e1ad63cdcab6a4644a0b5a034251 Mon Sep 17 00:00:00 2001 From: Cloudac7 <812556867@qq.com> Date: Fri, 13 Oct 2023 23:15:32 +0800 Subject: [PATCH 01/13] feat: add box selection doc: add doc about box selection fix: reformat with darker and flake8 --- package/AUTHORS | 1 + package/CHANGELOG | 1 + package/MDAnalysis/core/selection.py | 83 +++++++++++++++++++ .../source/documentation_pages/selections.rst | 17 ++++ .../core/test_atomselections.py | 59 ++++++++++--- 5 files changed, 151 insertions(+), 10 deletions(-) diff --git a/package/AUTHORS b/package/AUTHORS index c413c90462d..a5929bc129a 100644 --- a/package/AUTHORS +++ b/package/AUTHORS @@ -222,6 +222,7 @@ Chronological list of authors - Shubham Kumar - Zaheer Timol - Geongi Moon + - Yunpei Liu External code ------------- diff --git a/package/CHANGELOG b/package/CHANGELOG index 1cc96ddea05..a51e172a5c4 100644 --- a/package/CHANGELOG +++ b/package/CHANGELOG @@ -42,6 +42,7 @@ Enhancements (Issue #3994, PR #4281) * Add support for reading chainID info from Autodock PDBQT files (Issue #4207, PR #4284) + * Add a new geometric selection: box Changes * Biopython is now an optional dependency (Issue #3820, PR #4332) diff --git a/package/MDAnalysis/core/selection.py b/package/MDAnalysis/core/selection.py index 2edbf79e01b..f85a45a43b8 100644 --- a/package/MDAnalysis/core/selection.py +++ b/package/MDAnalysis/core/selection.py @@ -537,6 +537,89 @@ def _apply(self, group): return group[np.asarray(indices, dtype=np.int64)] +class BoxSelection(Selection): + token = "box" + precedence = 1 + index_map = {"x": 0, "y": 1, "z": 2} + + def __init__(self, parser, tokens): + super().__init__(parser, tokens) + self.periodic = parser.periodic + self.direction = tokens.popleft() + if len(self.direction) > 3: + raise ValueError( + "The direction '{}' is not valid. Must be combination of {}" + "".format(self.direction, list(self.index_map.keys())) + ) + else: + for d in self.direction: + if d not in self.index_map: + raise ValueError( + "The direction '{}' is not valid." + "Must be combination " + "of {}".format(self.direction, list(self.index_map.keys())) + ) + setattr(self, "{}max".format(d), float(tokens.popleft())) + setattr(self, "{}min".format(d), float(tokens.popleft())) + self.sel = parser.parse_expression(self.precedence) + + @return_empty_on_apply + def _apply(self, group): + sel = self.sel.apply(group) + if len(sel) == 0: + 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) + + 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] + + axis_height = axis_max - axis_min + if axis_height > box[axis_index]: + raise NotImplementedError( + "The total length of the box selection in {} ({:.3f}) " + "is larger than the unit cell's {} dimension ({:.3f}). Can " + "only do selections where it is smaller or equal." + "".format( + self.direction[axis_index], + axis_height, + self.direction[axis_index], + box[axis_index], + ) + ) + + if np.all(group.dimensions[3:] == 90.0): + # Orthogonal version + vecs -= box[:3] * np.rint(vecs / box[:3]) + else: + # Triclinic version + tribox = group.universe.trajectory.ts.triclinic_dimensions + vecs -= tribox[2] * np.rint(vecs[:, 2] / tribox[2][2])[:, None] + vecs -= tribox[1] * np.rint(vecs[:, 1] / tribox[1][1])[:, None] + vecs -= tribox[0] * np.rint(vecs[:, 0] / tribox[0][0])[:, None] + + # Deal with each dimension criteria + mask = None + for k, v in range_map.items(): + if mask is None: + mask = (vecs[:, k] > v[0]) & (vecs[:, k] < v[1]) + else: + mask &= (vecs[:, k] > v[0]) & (vecs[:, k] < v[1]) + + return group[mask] + + class AtomSelection(Selection): token = 'atom' diff --git a/package/doc/sphinx/source/documentation_pages/selections.rst b/package/doc/sphinx/source/documentation_pages/selections.rst index 6ee7f26bc2d..605d21bcd6a 100644 --- a/package/doc/sphinx/source/documentation_pages/selections.rst +++ b/package/doc/sphinx/source/documentation_pages/selections.rst @@ -277,6 +277,23 @@ 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)*. + point *x* *y* *z* *distance* selects all atoms within a cutoff of a point in space, make sure coordinate is separated by spaces, e.g. ``point 5.0 5.0 5.0 3.5`` diff --git a/testsuite/MDAnalysisTests/core/test_atomselections.py b/testsuite/MDAnalysisTests/core/test_atomselections.py index 6915e6a95d1..72c67d84c8b 100644 --- a/testsuite/MDAnalysisTests/core/test_atomselections.py +++ b/testsuite/MDAnalysisTests/core/test_atomselections.py @@ -246,6 +246,22 @@ def test_point(self, universe): assert_equal(set(ag.indices), set(idx)) + @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), + ], + ) + def test_box(self, universe, selstr, expected_value): + sel = universe.select_atoms(selstr) + assert_equal(len(sel), expected_value) + + def test_empty_box(self, universe): + empty = universe.select_atoms("box y 10 -10 name NOT_A_NAME") + assert_equal(len(empty), 0) + def test_prop(self, universe): sel = universe.select_atoms('prop y <= 16') sel2 = universe.select_atoms('prop abs z < 8') @@ -1270,16 +1286,39 @@ def test_similarity_selection_icodes(u_pdb_icodes, selection, n_atoms): assert len(sel.atoms) == n_atoms -@pytest.mark.parametrize('selection', [ - 'all', 'protein', 'backbone', 'nucleic', 'nucleicbackbone', - 'name O', 'name N*', 'resname stuff', 'resname ALA', 'type O', - 'index 0', 'index 1', 'bynum 1-10', - 'segid SYSTEM', 'resid 163', 'resid 1-10', 'resnum 2', - 'around 10 resid 1', 'point 0 0 0 10', 'sphzone 10 resid 1', - 'sphlayer 0 10 index 1', 'cyzone 15 4 -8 index 0', - 'cylayer 5 10 10 -8 index 1', 'prop abs z <= 100', - 'byres index 0', 'same resid as index 0', -]) + +@pytest.mark.parametrize( + "selection", + [ + "all", + "protein", + "backbone", + "nucleic", + "nucleicbackbone", + "name O", + "name N*", + "resname stuff", + "resname ALA", + "type O", + "index 0", + "index 1", + "bynum 1-10", + "segid SYSTEM", + "resid 163", + "resid 1-10", + "resnum 2", + "around 10 resid 1", + "point 0 0 0 10", + "sphzone 10 resid 1", + "sphlayer 0 10 index 1", + "cyzone 15 4 -8 index 0", + "cylayer 5 10 10 -8 index 1", + "prop abs z <= 100", + "byres index 0", + "same resid as index 0", + "box xz 3 2 4 -5 index 0", + ], +) def test_selections_on_empty_group(u_pdb_icodes, selection): ag = u_pdb_icodes.atoms[[]].select_atoms(selection) assert len(ag) == 0 From 2f94545a8f5cfc325aa2c63571b039e47b0dc5a2 Mon Sep 17 00:00:00 2001 From: Cloudac7 <812556867@qq.com> Date: Wed, 25 Oct 2023 22:43:36 +0800 Subject: [PATCH 02/13] test: add test for orthogonal distance style: reformat as pep8 --- package/CHANGELOG | 2 +- package/MDAnalysis/core/selection.py | 14 ++++++++------ package/doc/sphinx/source/conf.py | 2 ++ .../MDAnalysisTests/core/test_atomselections.py | 6 ++++++ 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/package/CHANGELOG b/package/CHANGELOG index a51e172a5c4..ae67b27b38a 100644 --- a/package/CHANGELOG +++ b/package/CHANGELOG @@ -42,7 +42,7 @@ Enhancements (Issue #3994, PR #4281) * Add support for reading chainID info from Autodock PDBQT files (Issue #4207, PR #4284) - * Add a new geometric selection: box + * Add a new geometric selection: box (Issue #4323, PR #4324) Changes * Biopython is now an optional dependency (Issue #3820, PR #4332) diff --git a/package/MDAnalysis/core/selection.py b/package/MDAnalysis/core/selection.py index f85a45a43b8..a8ddf23c01e 100644 --- a/package/MDAnalysis/core/selection.py +++ b/package/MDAnalysis/core/selection.py @@ -541,6 +541,7 @@ class BoxSelection(Selection): token = "box" precedence = 1 index_map = {"x": 0, "y": 1, "z": 2} + axis_map = ["x", "y", "z"] def __init__(self, parser, tokens): super().__init__(parser, tokens) @@ -556,8 +557,9 @@ def __init__(self, parser, tokens): if d not in self.index_map: raise ValueError( "The direction '{}' is not valid." - "Must be combination " - "of {}".format(self.direction, list(self.index_map.keys())) + "Must be combination of {}".format( + self.direction, list(self.index_map.keys()) + ) ) setattr(self, "{}max".format(d), float(tokens.popleft())) setattr(self, "{}min".format(d), float(tokens.popleft())) @@ -589,12 +591,12 @@ def _apply(self, group): if axis_height > box[axis_index]: raise NotImplementedError( "The total length of the box selection in {} ({:.3f}) " - "is larger than the unit cell's {} dimension ({:.3f}). Can " - "only do selections where it is smaller or equal." + "is larger than the unit cell's {} dimension ({:.3f}). " + "Can only do selections where it is smaller or equal." "".format( - self.direction[axis_index], + self.axis_map[axis_index], axis_height, - self.direction[axis_index], + self.axis_map[axis_index], box[axis_index], ) ) diff --git a/package/doc/sphinx/source/conf.py b/package/doc/sphinx/source/conf.py index a1a461ed14b..1ad76dfb9aa 100644 --- a/package/doc/sphinx/source/conf.py +++ b/package/doc/sphinx/source/conf.py @@ -14,6 +14,8 @@ import sys import os import datetime +sys.path.insert(0, os.path.abspath('../../..')) + import MDAnalysis as mda # Custom MDA Formating from pybtex.style.formatting.unsrt import Style as UnsrtStyle diff --git a/testsuite/MDAnalysisTests/core/test_atomselections.py b/testsuite/MDAnalysisTests/core/test_atomselections.py index 72c67d84c8b..c340d823133 100644 --- a/testsuite/MDAnalysisTests/core/test_atomselections.py +++ b/testsuite/MDAnalysisTests/core/test_atomselections.py @@ -814,6 +814,12 @@ def test_sphzone(self, u, periodic, expected): assert len(sel) == 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) + + assert len(sel) == expected + class TestTriclinicDistanceSelections(BaseDistanceSelection): @pytest.fixture() From 01758fce9badf27d179b16719106483c3b9fa6a0 Mon Sep 17 00:00:00 2001 From: Cloudac7 <812556867@qq.com> Date: Thu, 26 Oct 2023 13:18:21 +0800 Subject: [PATCH 03/13] test: add test in triclinic cell for box selection test: add error catching for box selection fix: prevent abnormal input in direction --- package/MDAnalysis/core/selection.py | 26 +++++++++++----- .../core/test_atomselections.py | 30 +++++++++++++++++++ 2 files changed, 48 insertions(+), 8 deletions(-) diff --git a/package/MDAnalysis/core/selection.py b/package/MDAnalysis/core/selection.py index a8ddf23c01e..5fd710d6555 100644 --- a/package/MDAnalysis/core/selection.py +++ b/package/MDAnalysis/core/selection.py @@ -541,26 +541,36 @@ class BoxSelection(Selection): token = "box" precedence = 1 index_map = {"x": 0, "y": 1, "z": 2} + combination = [ + "x", + "y", + "z", + "xy", + "xz", + "yz", + "yx", + "zx", + "zy", + "xyz", + "xzy", + "yxz", + "yzx", + "zxy", + "zyx", + ] axis_map = ["x", "y", "z"] def __init__(self, parser, tokens): super().__init__(parser, tokens) self.periodic = parser.periodic self.direction = tokens.popleft() - if len(self.direction) > 3: + 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())) ) else: for d in self.direction: - if d not in self.index_map: - raise ValueError( - "The direction '{}' is not valid." - "Must be combination of {}".format( - self.direction, list(self.index_map.keys()) - ) - ) setattr(self, "{}max".format(d), float(tokens.popleft())) setattr(self, "{}min".format(d), float(tokens.popleft())) self.sel = parser.parse_expression(self.precedence) diff --git a/testsuite/MDAnalysisTests/core/test_atomselections.py b/testsuite/MDAnalysisTests/core/test_atomselections.py index c340d823133..499170ba4e1 100644 --- a/testsuite/MDAnalysisTests/core/test_atomselections.py +++ b/testsuite/MDAnalysisTests/core/test_atomselections.py @@ -820,6 +820,28 @@ def test_box(self, u, periodic, expected): assert len(sel) == expected + @pytest.mark.parametrize( + "selection,error,expected", + ( + [ + "box xyz 10 -5 90 -90 6 -2 resid 1", + NotImplementedError, + "The total length of the box selection in y", + ], + [ + "box yyy 10 -5 7 -7 6 -2 resid 1", + SelectionError, + "Must be combination of", + ], + ["box a 10 -5 resid 1", SelectionError, "Must be combination of"], + ), + ) + def test_box_error(self, u, selection, error, expected): + with pytest.raises(error) as excinfo: + u.select_atoms(selection) + exec_msg = str(excinfo.value) + assert expected in exec_msg + class TestTriclinicDistanceSelections(BaseDistanceSelection): @pytest.fixture() @@ -883,6 +905,14 @@ def test_empty_sphzone(self, u): empty = u.select_atoms('sphzone 5.0 name NOT_A_NAME') assert len(empty) == 0 + def test_box(self, u): + ag = u.select_atoms("box z 2.5 -2.5 resid 1") + assert len(ag) == 4241 + + def test_empty_box(self, u): + ag = u.select_atoms("box z 2.5 -2.5 name NOT_A_NAME") + assert len(ag) == 0 + def test_point_1(self, u): # The example selection ag = u.select_atoms('point 5.0 5.0 5.0 3.5') From 953b33a3d512fdc347ad8951585ab0b827685e6f Mon Sep 17 00:00:00 2001 From: Cloudac7 <812556867@qq.com> Date: Mon, 6 Nov 2023 10:31:35 +0800 Subject: [PATCH 04/13] fix: remove extra line in sphinx conf.py style: reformat CHANGELOG --- package/CHANGELOG | 4 ++-- package/doc/sphinx/source/conf.py | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/package/CHANGELOG b/package/CHANGELOG index ae67b27b38a..052fb6f8b00 100644 --- a/package/CHANGELOG +++ b/package/CHANGELOG @@ -15,7 +15,7 @@ The rules for this file: ------------------------------------------------------------------------------- ??/??/?? IAlibay, ianmkenney, PicoCentauri, pgbarletta, p-j-smith, - richardjgowers, lilyminium, ALescoulie, hmacdope + richardjgowers, lilyminium, ALescoulie, hmacdope, Cloudac7 * 2.7.0 @@ -30,6 +30,7 @@ Fixes * Fix atom charge reading in PDBQT parser (Issue #4282, PR #4283) Enhancements + * Add a new geometric selection: box (Issue #4323, PR #4324) * Add faster nucleic acid Major and Minor pair distance calculators using AnalysisBase for updated nucleicacids module (Issue #3720, PR #3735) * Adds external sidebar links (Issue #4296) @@ -42,7 +43,6 @@ Enhancements (Issue #3994, PR #4281) * Add support for reading chainID info from Autodock PDBQT files (Issue #4207, PR #4284) - * Add a new geometric selection: box (Issue #4323, PR #4324) Changes * Biopython is now an optional dependency (Issue #3820, PR #4332) diff --git a/package/doc/sphinx/source/conf.py b/package/doc/sphinx/source/conf.py index 1ad76dfb9aa..083e0a39244 100644 --- a/package/doc/sphinx/source/conf.py +++ b/package/doc/sphinx/source/conf.py @@ -14,7 +14,6 @@ import sys import os import datetime -sys.path.insert(0, os.path.abspath('../../..')) import MDAnalysis as mda # Custom MDA Formating From 79e307fb15afdc7640afeb0106a02e326edf9427 Mon Sep 17 00:00:00 2001 From: Cloudac7 <812556867@qq.com> Date: Wed, 8 Nov 2023 10:15:28 +0800 Subject: [PATCH 05/13] fix: use `minimize_vectors` to calculate min vec --- package/MDAnalysis/core/selection.py | 10 +--------- testsuite/MDAnalysisTests/core/test_atomselections.py | 2 +- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/package/MDAnalysis/core/selection.py b/package/MDAnalysis/core/selection.py index 5fd710d6555..f523590c22b 100644 --- a/package/MDAnalysis/core/selection.py +++ b/package/MDAnalysis/core/selection.py @@ -611,15 +611,7 @@ def _apply(self, group): ) ) - if np.all(group.dimensions[3:] == 90.0): - # Orthogonal version - vecs -= box[:3] * np.rint(vecs / box[:3]) - else: - # Triclinic version - tribox = group.universe.trajectory.ts.triclinic_dimensions - vecs -= tribox[2] * np.rint(vecs[:, 2] / tribox[2][2])[:, None] - vecs -= tribox[1] * np.rint(vecs[:, 1] / tribox[1][1])[:, None] - vecs -= tribox[0] * np.rint(vecs[:, 0] / tribox[0][0])[:, None] + vecs = distances.minimize_vectors(vecs, group.dimensions) # Deal with each dimension criteria mask = None diff --git a/testsuite/MDAnalysisTests/core/test_atomselections.py b/testsuite/MDAnalysisTests/core/test_atomselections.py index 499170ba4e1..18c1329b4bb 100644 --- a/testsuite/MDAnalysisTests/core/test_atomselections.py +++ b/testsuite/MDAnalysisTests/core/test_atomselections.py @@ -907,7 +907,7 @@ def test_empty_sphzone(self, u): def test_box(self, u): ag = u.select_atoms("box z 2.5 -2.5 resid 1") - assert len(ag) == 4241 + assert len(ag) == 4237 def test_empty_box(self, u): ag = u.select_atoms("box z 2.5 -2.5 name NOT_A_NAME") From 7b3fb0dd388c9b508a40834f4ea59f2b98ec3b6c Mon Sep 17 00:00:00 2001 From: Cloudac7 <812556867@qq.com> Date: Wed, 8 Nov 2023 11:08:19 +0800 Subject: [PATCH 06/13] fix: set default dmin and dmax for box selection --- package/MDAnalysis/core/groups.py | 20 ++++++++ package/MDAnalysis/core/selection.py | 46 +++++++++++-------- .../source/documentation_pages/selections.rst | 36 ++++++++------- .../core/test_atomselections.py | 16 +++---- 4 files changed, 76 insertions(+), 42 deletions(-) diff --git a/package/MDAnalysis/core/groups.py b/package/MDAnalysis/core/groups.py index a73aef445dd..6d588f3d33d 100644 --- a/package/MDAnalysis/core/groups.py +++ b/package/MDAnalysis/core/groups.py @@ -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** diff --git a/package/MDAnalysis/core/selection.py b/package/MDAnalysis/core/selection.py index f523590c22b..6813a2216fd 100644 --- a/package/MDAnalysis/core/selection.py +++ b/package/MDAnalysis/core/selection.py @@ -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", @@ -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 @@ -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( @@ -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] diff --git a/package/doc/sphinx/source/documentation_pages/selections.rst b/package/doc/sphinx/source/documentation_pages/selections.rst index 605d21bcd6a..400d612640a 100644 --- a/package/doc/sphinx/source/documentation_pages/selections.rst +++ b/package/doc/sphinx/source/documentation_pages/selections.rst @@ -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 diff --git a/testsuite/MDAnalysisTests/core/test_atomselections.py b/testsuite/MDAnalysisTests/core/test_atomselections.py index 18c1329b4bb..bfd720ef042 100644 --- a/testsuite/MDAnalysisTests/core/test_atomselections.py +++ b/testsuite/MDAnalysisTests/core/test_atomselections.py @@ -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): @@ -816,7 +816,7 @@ 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 @@ -824,12 +824,12 @@ def test_box(self, u, periodic, expected): "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", ], @@ -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): From 827397ccb5c8b781e6d73a09d40765e0eb4ac266 Mon Sep 17 00:00:00 2001 From: Cloudac7 <812556867@qq.com> Date: Wed, 8 Nov 2023 11:13:20 +0800 Subject: [PATCH 07/13] doc: format docstring as well as doc for selections --- package/MDAnalysis/core/groups.py | 34 +++++++++--------- .../source/documentation_pages/selections.rst | 35 ++++++++----------- 2 files changed, 32 insertions(+), 37 deletions(-) diff --git a/package/MDAnalysis/core/groups.py b/package/MDAnalysis/core/groups.py index 6d588f3d33d..70d826d7675 100644 --- a/package/MDAnalysis/core/groups.py +++ b/package/MDAnalysis/core/groups.py @@ -3111,24 +3111,24 @@ 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 + box *dimensions* *dN_min* *dN_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 + *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). *dN_min*, *dN_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, and 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. diff --git a/package/doc/sphinx/source/documentation_pages/selections.rst b/package/doc/sphinx/source/documentation_pages/selections.rst index 400d612640a..196446bc9df 100644 --- a/package/doc/sphinx/source/documentation_pages/selections.rst +++ b/package/doc/sphinx/source/documentation_pages/selections.rst @@ -277,26 +277,21 @@ cyzone *externalRadius* *zMax* *zMin* *selection* relative to the COG of *selection*, instead of absolute z-values in the box. -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. +box *dimensions* *dN_min* *dN_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). *dN_min*, *dN_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 From 773ef5a729be34c94cc19f6b5736acfd991613d8 Mon Sep 17 00:00:00 2001 From: Cloudac7 <812556867@qq.com> Date: Fri, 10 Nov 2023 11:04:40 +0800 Subject: [PATCH 08/13] Squashed commit of the following: commit 8d7f2ce009499430e97dacd6a4e765c2dc6cbd56 Author: Futaki Haduki <812556867@qq.com> Date: Thu Nov 9 17:23:57 2023 +0800 Update package/MDAnalysis/core/groups.py Co-authored-by: Rocco Meli commit cfdec5b44f911216ed30fae32fe472debe790afe Author: Futaki Haduki <812556867@qq.com> Date: Thu Nov 9 17:23:44 2023 +0800 Update package/MDAnalysis/core/groups.py Co-authored-by: Rocco Meli --- package/MDAnalysis/core/groups.py | 9 ++--- package/MDAnalysis/core/selection.py | 39 ++++++++----------- .../source/documentation_pages/selections.rst | 6 +-- 3 files changed, 23 insertions(+), 31 deletions(-) diff --git a/package/MDAnalysis/core/groups.py b/package/MDAnalysis/core/groups.py index 70d826d7675..f50aeca1e18 100644 --- a/package/MDAnalysis/core/groups.py +++ b/package/MDAnalysis/core/groups.py @@ -3111,14 +3111,13 @@ 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* *dN_min* *dN_max* *selection* + box *dimensions* *dN_min* *dN_max* [*dN_min* *dN_max*] [*dN_min* *dN_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). *dN_min*, *dN_max* are the minimum and - maximum bounds along the first specified dimension. + the box selection on. Can be ``x``, ``y``, ``z``, ``xy``, + ``yz``, ``xz``, or ``xyz`. *dN_min*, *dN_max* are the minimum + and maximum bounds along each specified dimension. Positive values are above/right/front of the COG, negatives are below/left/behind, and should be specified for each dimension. *selection* specifies the selection diff --git a/package/MDAnalysis/core/selection.py b/package/MDAnalysis/core/selection.py index 6813a2216fd..d66a304b246 100644 --- a/package/MDAnalysis/core/selection.py +++ b/package/MDAnalysis/core/selection.py @@ -540,24 +540,8 @@ def _apply(self, group): class BoxSelection(Selection): token = "box" precedence = 1 - combination = [ - "x", - "y", - "z", - "xy", - "xz", - "yz", - "yx", - "zx", - "zy", - "xyz", - "xzy", - "yxz", - "yzx", - "zxy", - "zyx", - ] axis_map = ["x", "y", "z"] + axis_set = {"x", "y", "z", "xy", "xz", "yz", "xyz"} def __init__(self, parser, tokens): super().__init__(parser, tokens) @@ -566,22 +550,31 @@ def __init__(self, parser, tokens): 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, ["x", "y", "z"]) - ) - else: + + if self.direction in self.axis_set: for d in self.direction: if d == "x": self.xmin = float(tokens.popleft()) self.xmax = float(tokens.popleft()) + if self.xmin > self.xmax: + raise ValueError("xmin must be less than or equal to xmax") elif d == "y": self.ymin = float(tokens.popleft()) self.ymax = float(tokens.popleft()) + if self.ymin > self.ymax: + raise ValueError("ymin must be less than or equal to ymax") elif d == "z": self.zmin = float(tokens.popleft()) self.zmax = float(tokens.popleft()) + if self.zmin > self.zmax: + raise ValueError("zmin must be less than or equal to zmax") + else: + raise ValueError( + "The direction '{}' is not valid. " + "Must be one of {}" + "".format(self.direction, ", ".join(self.axis_set)) + ) + self.sel = parser.parse_expression(self.precedence) @return_empty_on_apply diff --git a/package/doc/sphinx/source/documentation_pages/selections.rst b/package/doc/sphinx/source/documentation_pages/selections.rst index 196446bc9df..9d500e08a27 100644 --- a/package/doc/sphinx/source/documentation_pages/selections.rst +++ b/package/doc/sphinx/source/documentation_pages/selections.rst @@ -280,9 +280,9 @@ cyzone *externalRadius* *zMax* *zMin* *selection* box *dimensions* *dN_min* *dN_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). *dN_min*, *dN_max* are the minimum and maximum + which dimension(s) to apply the box selection on. + Can be ``x``, ``y``, ``z``, ``xy``, ``yz``, ``xz``, or ``xyz`. + *dN_min*, *dN_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 From 04307264c719f8070322a32054008c73f91762fd Mon Sep 17 00:00:00 2001 From: Cloudac7 <812556867@qq.com> Date: Fri, 10 Nov 2023 11:09:57 +0800 Subject: [PATCH 09/13] fix: rollback unexpected change in unittest --- .../core/test_atomselections.py | 43 +++++-------------- 1 file changed, 11 insertions(+), 32 deletions(-) diff --git a/testsuite/MDAnalysisTests/core/test_atomselections.py b/testsuite/MDAnalysisTests/core/test_atomselections.py index bfd720ef042..52e839ace3f 100644 --- a/testsuite/MDAnalysisTests/core/test_atomselections.py +++ b/testsuite/MDAnalysisTests/core/test_atomselections.py @@ -1323,38 +1323,17 @@ def test_similarity_selection_icodes(u_pdb_icodes, selection, n_atoms): assert len(sel.atoms) == n_atoms -@pytest.mark.parametrize( - "selection", - [ - "all", - "protein", - "backbone", - "nucleic", - "nucleicbackbone", - "name O", - "name N*", - "resname stuff", - "resname ALA", - "type O", - "index 0", - "index 1", - "bynum 1-10", - "segid SYSTEM", - "resid 163", - "resid 1-10", - "resnum 2", - "around 10 resid 1", - "point 0 0 0 10", - "sphzone 10 resid 1", - "sphlayer 0 10 index 1", - "cyzone 15 4 -8 index 0", - "cylayer 5 10 10 -8 index 1", - "prop abs z <= 100", - "byres index 0", - "same resid as index 0", - "box xz 3 2 4 -5 index 0", - ], -) +@pytest.mark.parametrize('selection', [ + 'all', 'protein', 'backbone', 'nucleic', 'nucleicbackbone', + 'name O', 'name N*', 'resname stuff', 'resname ALA', 'type O', + 'index 0', 'index 1', 'bynum 1-10', + 'segid SYSTEM', 'resid 163', 'resid 1-10', 'resnum 2', + 'around 10 resid 1', 'point 0 0 0 10', 'sphzone 10 resid 1', + 'sphlayer 0 10 index 1', 'cyzone 15 4 -8 index 0', + 'cylayer 5 10 10 -8 index 1', 'prop abs z <= 100', + 'byres index 0', 'same resid as index 0', + 'box xz 3 2 4 -5 index 0', +]) def test_selections_on_empty_group(u_pdb_icodes, selection): ag = u_pdb_icodes.atoms[[]].select_atoms(selection) assert len(ag) == 0 From 75928d48a5577dec726046fef0b0caf15e5043d7 Mon Sep 17 00:00:00 2001 From: Cloudac7 <812556867@qq.com> Date: Tue, 23 Jul 2024 16:06:34 +0800 Subject: [PATCH 10/13] test: fix wrong error message --- testsuite/MDAnalysisTests/core/test_atomselections.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/testsuite/MDAnalysisTests/core/test_atomselections.py b/testsuite/MDAnalysisTests/core/test_atomselections.py index 233e7f02794..f68cd0e2553 100644 --- a/testsuite/MDAnalysisTests/core/test_atomselections.py +++ b/testsuite/MDAnalysisTests/core/test_atomselections.py @@ -835,9 +835,9 @@ def test_box(self, u, periodic, expected): [ "box yyy -5 10 -7 7 -2 6 resid 1", SelectionError, - "Must be combination of", + "Must be one of", ], - ["box a 10 -5 resid 1", SelectionError, "Must be combination of"], + ["box a 10 -5 resid 1", SelectionError, "Must be one of"], ), ) def test_box_error(self, u, selection, error, expected): From 37b3bf4e90ed4ee354449d6b89c258f8a93fc4c8 Mon Sep 17 00:00:00 2001 From: Cloudac7 <812556867@qq.com> Date: Thu, 25 Jul 2024 11:08:21 +0800 Subject: [PATCH 11/13] refactor: rename the selection as `zone`; feat: remove check on unit cell for `zone` --- package/MDAnalysis/core/groups.py | 18 +++++----- package/MDAnalysis/core/selection.py | 28 +++------------- .../source/documentation_pages/selections.rst | 16 ++++----- .../core/test_atomselections.py | 33 +++++++++---------- 4 files changed, 38 insertions(+), 57 deletions(-) diff --git a/package/MDAnalysis/core/groups.py b/package/MDAnalysis/core/groups.py index ec9b66b373f..ebcab33fc93 100644 --- a/package/MDAnalysis/core/groups.py +++ b/package/MDAnalysis/core/groups.py @@ -3271,24 +3271,24 @@ 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* *dN_min* *dN_max* [*dN_min* *dN_max*] [*dN_min* *dN_max*] *selection* - Select all atoms within a box region centered + zone *dimensions* *dN_min* *dN_max* [*dN_min* *dN_max*] [*dN_min* *dN_max*] *selection* + Select all atoms within a zone in shape of an orthognol box 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``, ``xy``, + the zone selection on. Can be ``x``, ``y``, ``z``, ``xy``, ``yz``, ``xz``, or ``xyz`. *dN_min*, *dN_max* are the minimum and maximum bounds along each specified dimension. Positive values are above/right/front of the COG, negatives are below/left/behind, and 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 + to center the zone on. e.g. ``zone x -5 10 protein`` + selects a 15 Angstrom zone 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, + below to 10 Angstroms above. ``zone yz -8 10 -10 6 protein`` + selects a zone 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 + ``zone xyz -5 10 -8 6 -7 9 protein`` selects + a 3D zone with x -5 to 10, y -8 to 6, and z -7 to 9 relative to the protein COG. **Connectivity** diff --git a/package/MDAnalysis/core/selection.py b/package/MDAnalysis/core/selection.py index d66a304b246..5dc110cd463 100644 --- a/package/MDAnalysis/core/selection.py +++ b/package/MDAnalysis/core/selection.py @@ -537,8 +537,10 @@ def _apply(self, group): return group[np.asarray(indices, dtype=np.int64)] -class BoxSelection(Selection): - token = "box" +class ZoneSelection(Selection): + from MDAnalysis.lib.mdamath import triclinic_vectors + + token = 'zone' precedence = 1 axis_map = ["x", "y", "z"] axis_set = {"x", "y", "z", "xy", "xz", "yz", "xyz"} @@ -591,27 +593,7 @@ def _apply(self, group): } if self.periodic and group.dimensions is not None: - box = group.dimensions[:3] - - 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( - "The total length of the box selection in {} ({:.3f}) " - "is larger than the unit cell's {} dimension ({:.3f}). " - "Can only do selections where it is smaller or equal." - "".format( - self.axis_map[axis_index], - axis_height, - self.axis_map[axis_index], - box[axis_index], - ) - ) - + # TODO: a more general judgement vecs = distances.minimize_vectors(vecs, group.dimensions) # Deal with each dimension criteria diff --git a/package/doc/sphinx/source/documentation_pages/selections.rst b/package/doc/sphinx/source/documentation_pages/selections.rst index 9d500e08a27..cfaa71b3596 100644 --- a/package/doc/sphinx/source/documentation_pages/selections.rst +++ b/package/doc/sphinx/source/documentation_pages/selections.rst @@ -277,20 +277,20 @@ cyzone *externalRadius* *zMax* *zMin* *selection* relative to the COG of *selection*, instead of absolute z-values in the box. -box *dimensions* *dN_min* *dN_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. +zone *dimensions* *dN_min* *dN_max* *selection* + Select all atoms within a zone in shape of an orthognol box + centered on the center of geometry (COG) of a given selection. + *dimensions* Specifies which dimension(s) to apply the zone selection on. Can be ``x``, ``y``, ``z``, ``xy``, ``yz``, ``xz``, or ``xyz`. *dN_min*, *dN_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 + to center the zone on. e.g. ``zone x -5 10 protein`` selects a 15 Angstrom + zone along x centered on the COG of protein, extending 5 Angstroms below to + 10 Angstroms above. ``zone yz -8 10 -10 6 protein`` selects a zone 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, + ``zone xyz -5 10 -8 6 -7 9 protein`` selects a 3D zone with x -5 to 10, y -8 to 6, and z -7 to 9 relative to the protein COG. point *x* *y* *z* *distance* diff --git a/testsuite/MDAnalysisTests/core/test_atomselections.py b/testsuite/MDAnalysisTests/core/test_atomselections.py index f68cd0e2553..2018f901aa4 100644 --- a/testsuite/MDAnalysisTests/core/test_atomselections.py +++ b/testsuite/MDAnalysisTests/core/test_atomselections.py @@ -250,17 +250,17 @@ 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), + ("zone x -2.0 2.0 index 1281", 418), + ("zone yz -2.0 2.0 -2.0 2.0 index 1280", 58), + ("zone xyz -2.0 2.0 -2.0 2.0 -2.0 2.0 index 1279", 10), ], ) - def test_box(self, universe, selstr, expected_value): + def test_zone(self, universe, selstr, expected_value): sel = universe.select_atoms(selstr) assert_equal(len(sel), expected_value) - def test_empty_box(self, universe): - empty = universe.select_atoms("box y 10 -10 name NOT_A_NAME") + def test_empty_zone(self, universe): + empty = universe.select_atoms("zone y -10 10 name NOT_A_NAME") assert_equal(len(empty), 0) def test_prop(self, universe): @@ -819,8 +819,8 @@ def test_sphzone(self, u, periodic, expected): assert len(sel) == expected @pytest.mark.parametrize("periodic,expected", ([True, 29], [False, 17])) - def test_box(self, u, periodic, expected): - sel = u.select_atoms("box xyz 2 5 -5 10 -2 6 resid 1", periodic=periodic) + def test_zone(self, u, periodic, expected): + sel = u.select_atoms("zone xyz 2 5 -5 10 -2 6 resid 1", periodic=periodic) assert len(sel) == expected @@ -828,19 +828,18 @@ def test_box(self, u, periodic, expected): "selection,error,expected", ( [ - "box xyz -5 10 -90 90 -2 6 resid 1", - NotImplementedError, - "The total length of the box selection in y", - ], - [ - "box yyy -5 10 -7 7 -2 6 resid 1", + "zone yyy -5 10 -7 7 -2 6 resid 1", SelectionError, "Must be one of", ], - ["box a 10 -5 resid 1", SelectionError, "Must be one of"], + [ + "zone a -10 5 resid 1", + SelectionError, + "Must be one of" + ], ), ) - def test_box_error(self, u, selection, error, expected): + def test_zone_error(self, u, selection, error, expected): with pytest.raises(error) as excinfo: u.select_atoms(selection) exec_msg = str(excinfo.value) @@ -1336,7 +1335,7 @@ def test_similarity_selection_icodes(u_pdb_icodes, selection, n_atoms): 'sphlayer 0 10 index 1', 'cyzone 15 4 -8 index 0', 'cylayer 5 10 10 -8 index 1', 'prop abs z <= 100', 'byres index 0', 'same resid as index 0', - 'box xz 3 2 4 -5 index 0', + 'zone xz 2 3 -5 4 index 0', ]) def test_selections_on_empty_group(u_pdb_icodes, selection): ag = u_pdb_icodes.atoms[[]].select_atoms(selection) From 3fa7de3f33c4667b2b49cd10958c68e83a3f292c Mon Sep 17 00:00:00 2001 From: Cloudac7 <812556867@qq.com> Date: Thu, 25 Jul 2024 11:36:10 +0800 Subject: [PATCH 12/13] test: fix typo --- testsuite/MDAnalysisTests/core/test_atomselections.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/testsuite/MDAnalysisTests/core/test_atomselections.py b/testsuite/MDAnalysisTests/core/test_atomselections.py index 2018f901aa4..b8ceda18719 100644 --- a/testsuite/MDAnalysisTests/core/test_atomselections.py +++ b/testsuite/MDAnalysisTests/core/test_atomselections.py @@ -908,12 +908,12 @@ def test_empty_sphzone(self, u): empty = u.select_atoms('sphzone 5.0 name NOT_A_NAME') assert len(empty) == 0 - def test_box(self, u): - ag = u.select_atoms("box z -2.5 2.5 resid 1") + def test_zone(self, u): + ag = u.select_atoms("zone 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") + def test_empty_zone(self, u): + ag = u.select_atoms("zone z -2.5 2.5 name NOT_A_NAME") assert len(ag) == 0 def test_point_1(self, u): From 3f4f6199ce96c2ba7c0dc06974bbf43a139b0501 Mon Sep 17 00:00:00 2001 From: Cloudac7 <812556867@qq.com> Date: Thu, 25 Jul 2024 12:03:25 +0800 Subject: [PATCH 13/13] test: add tests of limits for `zone` --- .../MDAnalysisTests/core/test_atomselections.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/testsuite/MDAnalysisTests/core/test_atomselections.py b/testsuite/MDAnalysisTests/core/test_atomselections.py index b8ceda18719..80906b43ab0 100644 --- a/testsuite/MDAnalysisTests/core/test_atomselections.py +++ b/testsuite/MDAnalysisTests/core/test_atomselections.py @@ -837,6 +837,21 @@ def test_zone(self, u, periodic, expected): SelectionError, "Must be one of" ], + [ + "zone x 10 -5 resid 1", + SelectionError, + "xmin must be less than or equal to xmax" + ], + [ + "zone y 7 -7 resid 1", + SelectionError, + "ymin must be less than or equal to ymax" + ], + [ + "zone z 6 -2 resid 1", + SelectionError, + "zmin must be less than or equal to zmax" + ], ), ) def test_zone_error(self, u, selection, error, expected):