diff --git a/package/MDAnalysis/core/groups.py b/package/MDAnalysis/core/groups.py index e8bf30ba11..a74d955f5d 100644 --- a/package/MDAnalysis/core/groups.py +++ b/package/MDAnalysis/core/groups.py @@ -3254,6 +3254,16 @@ def select_atoms(self, sel, *othersel, periodic=True, rtol=1e-05, For example, ``prop z >= 5.0`` selects all atoms with z coordinate greater than 5.0; ``prop abs z <= 5.0`` selects all atoms within -5.0 <= z <= 5.0. + relprop [abs] *property* *operator* *value* *selection* + selects atoms based on position relative to the center of + geometry (COG) of a given selection, using *property* + **x**, **y**, or **z** coordinate. Supports the **abs** + keyword (for absolute value) and the following + *operators*: **<, >, <=, >=, ==, !=**. + For example, ``relprop z >= 5.0 protein`` selects all atoms + with z coordinate greater than 5.0 relative to the COG + of protein; ``relprop abs z <= 5.0 protein`` selects all + atoms within -5.0 <= z <= 5.0 relative to the COG of protein. sphzone *radius* *selection* Selects all atoms that are within *radius* of the center of geometry of *selection* diff --git a/package/MDAnalysis/core/selection.py b/package/MDAnalysis/core/selection.py index 6c2ea3c817..82a03832e4 100644 --- a/package/MDAnalysis/core/selection.py +++ b/package/MDAnalysis/core/selection.py @@ -1384,6 +1384,55 @@ def _apply(self, group): return group[mask] +class RelPropertySelection(PropertySelection): + """Some of the possible properties: + x, y, z, + + .. versionadded:: 2.9.0 + """ + + token = "relprop" + precedence = 1 + + def __init__(self, parser, tokens): + super().__init__(parser, tokens) + self.sel = parser.parse_expression(self.precedence) + self.periodic = parser.periodic + + def _apply(self, group): + try: + values = getattr(group, self.props[self.prop]) + except KeyError: + errmsg = f"Expected one of {list(self.props.keys())}" + raise SelectionError(errmsg) from None + except NoDataError: + attr = self.props[self.prop] + errmsg = f"This Universe does not contain {attr} information" + raise SelectionError(errmsg) from None + + try: + col = {"x": 0, "y": 1, "z": 2}[self.prop] + except KeyError: + errmsg = f"Expected one of x y z for property, got {self.prop}" + raise SelectionError(errmsg) from None + else: + sel = self.sel.apply(group) + ref_value = sel.center_of_geometry().reshape(3).astype(np.float32) + box = group.dimensions if self.periodic else None + if box is not None: + values = distances.minimize_vectors( + values - ref_value[None, :], box=box + )[:, col] + else: + values = values[:, col] - ref_value[col] + + if self.absolute: + values = np.abs(values) + mask = self.operator(values, self.value) + + return group[mask] + + class SameSelection(Selection): """ Selects all atoms that have the same subkeyword value as any atom in selection diff --git a/testsuite/MDAnalysisTests/core/test_atomselections.py b/testsuite/MDAnalysisTests/core/test_atomselections.py index 51b395f94c..06331d0be3 100644 --- a/testsuite/MDAnalysisTests/core/test_atomselections.py +++ b/testsuite/MDAnalysisTests/core/test_atomselections.py @@ -255,6 +255,17 @@ def test_prop(self, universe): assert_equal(len(sel), 3194) assert_equal(len(sel2), 2001) + def test_relprop(self, universe): + sel1 = universe.select_atoms("relprop z <= 1 index 0") + sel2 = universe.select_atoms("relprop abs z <= 1 index 0") + + positions = universe.trajectory[0].positions + ref = positions[0, 2] + mask_1 = (positions[:, 2] - ref) <= 1 + assert_equal(len(sel1), np.count_nonzero(mask_1)) + mask_2 = np.abs(positions[:, 2] - ref) <= 1 + assert_equal(len(sel2), np.count_nonzero(mask_2)) + def test_bynum(self, universe): "Tests the bynum selection, also from AtomGroup instances (Issue 275)" sel = universe.select_atoms('bynum 5') @@ -1110,6 +1121,18 @@ def test_invalid_prop_selection(self, universe): with pytest.raises(SelectionError, match="Expected one of"): universe.select_atoms("prop parsnip < 2") + def test_invalid_relprop_selection(self, universe): + with pytest.raises(SelectionError, match="Expected one of"): + universe.select_atoms("relprop parsnip < 2 index 0") + with pytest.raises(SelectionError, match="Unknown selection token"): + universe.select_atoms("relprop z < 2") + with pytest.raises(SelectionError, match="Expected one of"): + universe.select_atoms("relprop resid < 2 index 0") + with pytest.raises( + SelectionError, match="This Universe does not contain" + ): + universe.select_atoms("relprop z <= 1 index 0") + def test_segid_and_resid(): u = make_Universe(('segids', 'resids'))