Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add isolayer selection method #3846

Merged
merged 6 commits into from
Oct 1, 2022
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package/AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ Chronological list of authors
- Tengyu Xie
- Raymond Zhao
- Haleema Khan
- Jennifer A Clark


External code
Expand Down
4 changes: 3 additions & 1 deletion package/CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ The rules for this file:
* release numbers follow "Semantic Versioning" http://semver.org

------------------------------------------------------------------------------
??/??/?? IAlibay, Luthaf, hmacdope, rafaelpap, jbarnoud, BFedder, aya9aladdin
??/??/?? IAlibay, Luthaf, hmacdope, rafaelpap, jbarnoud, BFedder, aya9aladdin,
jaclark5

* 2.4.0

Expand All @@ -25,6 +26,7 @@ Fixes
* fixes guessed_attributes and read_attributes methods of the Topology class, as
those methods were giving unexpected results for some topology attributes
(e.g. bonds, angles) (PR #3779).
* Added isolayer selection method (Issue #3845)
hmacdope marked this conversation as resolved.
Show resolved Hide resolved

hmacdope marked this conversation as resolved.
Show resolved Hide resolved


Expand Down
3 changes: 3 additions & 0 deletions package/MDAnalysis/core/groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -3081,6 +3081,9 @@ def select_atoms(self, sel, *othersel, periodic=True, rtol=1e-05,
sphlayer *inner radius* *outer radius* *selection*
Similar to sphzone, but also excludes atoms that are within
*inner radius* of the selection COG
isolayer *inner radius* *outer radius* *selection*
Similar to sphlayer, but will find layer around all reference
layer, creating an iso-surface.
cyzone *externalRadius* *zMax* *zMin* *selection*
selects all atoms within a cylindric zone centered in the
center of geometry (COG) of a given selection,
Expand Down
40 changes: 40 additions & 0 deletions package/MDAnalysis/core/selection.py
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,46 @@ def _apply(self, group):
return group[np.asarray(indices, dtype=np.int64)]


class IsoLayerSelection(Selection):
token = 'isolayer'
precedence = 1

def __init__(self, parser, tokens):
super().__init__(parser, tokens)
self.periodic = parser.periodic
self.inRadius = float(tokens.popleft())
self.exRadius = float(tokens.popleft())
self.sel = parser.parse_expression(self.precedence)

@return_empty_on_apply
def _apply(self, group):
indices = []
sel = self.sel.apply(group)
# All atoms in group that aren't in sel
sys = group[~np.in1d(group.indices, sel.indices)]

if not sys or not sel:
return sys[[]]

box = group.dimensions if self.periodic else None
pairs_outer = distances.capped_distance(sel.positions, sys.positions,
self.exRadius, box=box,
return_distances=False)
pairs_inner = distances.capped_distance(sel.positions, sys.positions,
self.inRadius, box=box,
return_distances=False)

if pairs_outer.size > 0:
sys_ind_outer = np.sort(np.unique(pairs_outer[:,1]))
if pairs_inner.size > 0:
sys_ind_inner = np.sort(np.unique(pairs_inner[:,1]))
indices = sys_ind_outer[~np.in1d(sys_ind_outer, sys_ind_inner)]
else:
indices = sys_ind_outer

return sys[np.asarray(indices, dtype=np.int64)]


class SphericalZoneSelection(Selection):
token = 'sphzone'
precedence = 1
Expand Down
10 changes: 10 additions & 0 deletions package/doc/sphinx/source/documentation_pages/selections.rst
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,16 @@ sphzone *externalRadius* *selection*
resid 130 or resid 80 ) )`` selects the center of geometry of protein,
resid 130, resid 80 and creates a sphere of radius 6.0 around the COG.

isolayer *inner radius* *outer radius* *selection*
Similar to sphlayer, but will find layer around all referenced atoms.
For example, if the atom types for a polymer backbone were chosen, then
an isolayer parallel to the backbone will be generated. As another
example, if a set of ions were chosen as a reference to isolate the second
hydration layer, then they will all be included in the same group.
However, in the instance that a molecule is in the second hydration layer
of one ion and the first hydration layer of another, those atoms will not
be included.

cylayer *innerRadius* *externalRadius* *zMax* *zMin* *selection*
selects all atoms within a cylindric layer centered in the center of
geometry (COG) of a given selection, e.g. ``cylayer 5 10 10 -8
Expand Down
40 changes: 40 additions & 0 deletions testsuite/MDAnalysisTests/core/test_atomselections.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,14 @@ def test_sphlayer(self, universe, selstr):
sel = universe.select_atoms(selstr)
assert_equal(len(sel), 66)

@pytest.mark.parametrize('selstr', [
'isolayer 4.0 6.0 bynum 1281',
'isolayer 4.0 6.0 index 1280'
])
def test_isolayer(self, universe, selstr):
sel = universe.select_atoms(selstr)
assert_equal(len(sel), 66)

@pytest.mark.parametrize('selstr', [
'sphzone 6.0 bynum 1281',
'sphzone 6.0 index 1280'
Expand Down Expand Up @@ -689,6 +697,34 @@ def test_spherical_layer(self, u, periodic):

assert ref == set(result.indices)

@pytest.mark.parametrize('periodic', (True, False))
def test_isolayer(self, u, periodic):

rmin, rmax = 2.4, 3
if u.atoms.types[0].isdigit():
r1 = u.select_atoms("resid 1 or resid 2 and type 7")
else:
r1 = u.select_atoms("resid 1 or resid 2 and type O")

sel = Parser.parse("isolayer {} {} (index {} or index {})".format(rmin, rmax, *r1.ix), u.atoms)
if periodic:
sel.periodic = True
else:
sel.periodic = False
result = sel.apply(u.atoms)

box = u.dimensions if periodic else None
cog1 = r1[0].position.reshape(1, 3)
d1 = distance_array(u.atoms.positions, cog1, box=box)
cog2 = r1[1].position.reshape(1, 3)
d2 = distance_array(u.atoms.positions, cog2, box=box)

ref_inner = set(np.where((d1 < rmin) | (d2 < rmin))[0])
ref_outer = set(np.where((d1 < rmax) | (d2 < rmax))[0])
ref_outer -= ref_inner

assert ref_outer == set(result.indices) and len(list(ref_outer)) > 0

@pytest.mark.parametrize('periodic', (True, False))
def test_spherical_zone(self, u, periodic):
sel = Parser.parse('sphzone 5.0 resid 1', u.atoms)
Expand Down Expand Up @@ -806,6 +842,10 @@ def test_empty_sphlayer(self, u):
empty = u.select_atoms('sphlayer 2.4 6.0 name NOT_A_NAME')
assert len(empty) == 0

def test_empty_isolayer(self, u):
empty = u.select_atoms('isolayer 2.4 6.0 name NOT_A_NAME')
assert len(empty) == 0

def test_sphzone(self, u):
r1 = u.select_atoms('resid 1')
cog = r1.center_of_geometry().reshape(1, 3)
Expand Down