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 intra_connections (intra_bonds, intra_angles, etc) #3200

Merged
merged 26 commits into from
May 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
8d86a6b
added get_connections
lilyminium Mar 14, 2021
c797056
modified tests for atoms.bonds/angles/dihedrals etc
lilyminium Apr 2, 2021
d07d54b
modified parsers and things to use get_connections or bonds
lilyminium Apr 2, 2021
1f25fff
updated CHANGELOG
lilyminium Apr 2, 2021
0e3c66c
pep8
lilyminium Apr 2, 2021
20dbe88
undo half of PR 3160
lilyminium Apr 4, 2021
981c22a
add intra stuff
lilyminium Apr 7, 2021
c0f0f52
Merge remote-tracking branch 'upstream/develop' into get_connections_…
lilyminium Apr 23, 2021
829bb9f
Update package/MDAnalysis/core/groups.py
lilyminium Apr 28, 2021
8b64b46
tighten up base class checking
lilyminium Apr 28, 2021
88f8212
update docstring
lilyminium Apr 28, 2021
17797e9
suppres warnings
lilyminium Apr 28, 2021
3a34048
Use absolute file paths in ITPParser (#3108)
aditya-kamath Apr 23, 2021
8fcf354
Adds aromaticity and Gasteiger charges guessers (#2926)
cbouy Apr 23, 2021
e089787
BLD: handle gcc on MacOS (#3234)
tylerjereddy Apr 24, 2021
e1c07a6
Remove ParmEd Timestep writing "support" (#3240)
lilyminium Apr 26, 2021
cc1e43a
Adding py3.9 to gh actions CI matrix (#3245)
IAlibay Apr 27, 2021
eb0e5c0
fix changelog
lilyminium Apr 28, 2021
20ba843
Merge branch 'develop' into get_connections_develop
lilyminium Apr 28, 2021
e041366
special metaclass
lilyminium Apr 29, 2021
27637ec
Merge branch 'get_connections_develop' of github.com:lilyminium/mdana…
lilyminium Apr 29, 2021
cff78e6
Merge branch 'develop' into get_connections_develop
lilyminium Apr 29, 2021
2a06cb7
move function down
lilyminium Apr 29, 2021
cb251d4
Merge branch 'get_connections_develop' of github.com:lilyminium/mdana…
lilyminium Apr 29, 2021
0eacf67
tidy code
lilyminium Apr 29, 2021
c7997b5
Merge branch 'develop' into get_connections_develop
jbarnoud May 2, 2021
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
3 changes: 3 additions & 0 deletions package/CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@ Fixes
* Fix syntax warning over comparison of literals using is (Issue #3066)

Enhancements
* Added intra_bonds, intra_angles, intra_dihedrals etc. to return only
the connections involving atoms within the AtomGroup, instead of
including atoms outside the AtomGroup (Issue #1264, #2821, PR #3200)
* Added del_TopologyAttr function (PR #3069)
* Switch GNMAnalysis to AnalysisBase (Issue #3243)
* Adds python 3.9 support (Issue #2974, PR #3027, #3245)
Expand Down
2 changes: 1 addition & 1 deletion package/MDAnalysis/coordinates/MOL2.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,7 @@ def encode_block(self, obj):
if hasattr(obj, "bonds"):
# Grab only bonds between atoms in the obj
# ie none that extend out of it
bondgroup = obj.bonds.atomgroup_intersection(obj, strict=True)
bondgroup = obj.intra_bonds
bonds = sorted((b[0], b[1], b.order) for b in bondgroup)
bond_lines = ["@<TRIPOS>BOND"]
bls = ["{0:>5} {1:>5} {2:>5} {3:>2}".format(bid,
Expand Down
3 changes: 1 addition & 2 deletions package/MDAnalysis/coordinates/ParmEd.py
Original file line number Diff line number Diff line change
Expand Up @@ -269,8 +269,7 @@ def convert(self, obj):

# bonds
try:
params = ag_or_ts.bonds.atomgroup_intersection(ag_or_ts,
strict=True)
params = ag_or_ts.intra_bonds
except AttributeError:
pass
else:
Expand Down
35 changes: 35 additions & 0 deletions package/MDAnalysis/core/groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,41 @@ def __getattr__(self, attr):
err += 'Did you mean {match}?'.format(match=match)
raise AttributeError(err)

def get_connections(self, typename, outside=True):
"""
Get bonded connections between atoms as a
:class:`~MDAnalysis.core.topologyobjects.TopologyGroup`.

Parameters
----------
typename : str
group name. One of {"bonds", "angles", "dihedrals",
"impropers", "ureybradleys", "cmaps"}
lilyminium marked this conversation as resolved.
Show resolved Hide resolved
outside : bool (optional)
Whether to include connections involving atoms outside
this group.

Returns
-------
TopologyGroup
containing the bonded group of choice, i.e. bonds, angles,
dihedrals, impropers, ureybradleys or cmaps.

.. versionadded:: 1.1.0
lilyminium marked this conversation as resolved.
Show resolved Hide resolved
"""
# AtomGroup has handy error messages for missing attributes
ugroup = getattr(self.universe.atoms, typename)
if not ugroup:
return ugroup
func = np.any if outside else np.all
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd prefer a more explicit name for that. logic_comparator maybe?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see the conversation marked as resolved, but the variable is still named func.

try:
indices = self.atoms.ix_array
except AttributeError: # if self is an Atom
indices = self.ix_array
seen = [np.in1d(col, indices) for col in ugroup._bix.T]
mask = func(seen, axis=0)
return ugroup[mask]


class _ImmutableBase(object):
"""Class used to shortcut :meth:`__new__` to :meth:`object.__new__`.
Expand Down
49 changes: 42 additions & 7 deletions package/MDAnalysis/core/topologyattrs.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,19 @@
These are usually read by the TopologyParser.
"""

import Bio.Seq
import Bio.SeqRecord
from collections import defaultdict
import copy
import functools
import itertools
import numbers
import numpy as np
from inspect import signature as inspect_signature
import warnings
import textwrap
from inspect import signature as inspect_signature
from types import MethodType

import Bio.Seq
import Bio.SeqRecord
import numpy as np

from ..lib.util import (cached, convert_aa_code, iterable, warn_if_not_unique,
unique_int_1d)
Expand Down Expand Up @@ -2258,7 +2260,31 @@ def wrapper(self, values, *args, **kwargs):
return wrapper


class _Connection(AtomAttr):
class _ConnectionTopologyAttrMeta(_TopologyAttrMeta):
"""
Specific metaclass for atom-connectivity topology attributes.

This class adds an ``intra_{attrname}`` property to groups
to return only the connections within the atoms in the group.
"""
def __init__(cls, name, bases, classdict):
type.__init__(type, name, bases, classdict)
attrname = classdict.get('attrname')

if attrname is not None:
def intra_connection(self, ag):
"""Get connections only within this AtomGroup
"""
return ag.get_connections(attrname, outside=False)

method = MethodType(intra_connection, cls)
prop = property(method, None, None, method.__doc__)
cls.transplants[AtomGroup].append((f"intra_{attrname}", prop))

super().__init__(name, bases, classdict)


class _Connection(AtomAttr, metaclass=_ConnectionTopologyAttrMeta):
"""Base class for connectivity between atoms

.. versionchanged:: 1.0.0
Expand Down Expand Up @@ -2307,14 +2333,23 @@ def set_atoms(self, ag):
return NotImplementedError("Cannot set bond information")

def get_atoms(self, ag):
"""
Get connection values where the atom indices are in
the given atomgroup.

Parameters
----------
ag : AtomGroup

"""
try:
unique_bonds = set(itertools.chain(
*[self._bondDict[a] for a in ag.ix]))
except TypeError:
# maybe we got passed an Atom
unique_bonds = self._bondDict[ag.ix]
bond_idx, types, guessed, order = np.hsplit(
np.array(sorted(unique_bonds), dtype=object), 4)
unique_bonds = np.array(sorted(unique_bonds), dtype=object)
bond_idx, types, guessed, order = np.hsplit(unique_bonds, 4)
bond_idx = np.array(bond_idx.ravel().tolist(), dtype=np.int32)
types = types.ravel()
guessed = guessed.ravel()
Expand Down
153 changes: 152 additions & 1 deletion testsuite/MDAnalysisTests/core/test_groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,12 @@
)
import pytest
import operator
import warnings

import MDAnalysis as mda
from MDAnalysis.exceptions import NoDataError
from MDAnalysisTests import make_Universe, no_deprecated_call
from MDAnalysisTests.datafiles import PSF, DCD
from MDAnalysisTests.datafiles import PSF, DCD, TPR
from MDAnalysis.core import groups


Expand Down Expand Up @@ -1461,3 +1462,153 @@ def test_decorator(self, compound, pbc, unwrap):
self.dummy_funtion(compound=compound, pbc=pbc, unwrap=unwrap)
else:
assert_equal(self.dummy_funtion(compound=compound, pbc=pbc, unwrap=unwrap), 0)


@pytest.fixture()
def tpr():
with warnings.catch_warnings():
warnings.filterwarnings("ignore",
message="No coordinate reader found")
return mda.Universe(TPR)

class TestGetConnectionsAtoms(object):
"""Test Atom and AtomGroup.get_connections"""

@pytest.mark.parametrize("typename",
["bonds", "angles", "dihedrals", "impropers"])
def test_connection_from_atom_not_outside(self, tpr, typename):
cxns = tpr.atoms[1].get_connections(typename, outside=False)
assert len(cxns) == 0

@pytest.mark.parametrize("typename, n_atoms", [
("bonds", 1),
("angles", 3),
("dihedrals", 4),
])
def test_connection_from_atom_outside(self, tpr, typename, n_atoms):
cxns = tpr.atoms[10].get_connections(typename, outside=True)
assert len(cxns) == n_atoms

@pytest.mark.parametrize("typename, n_atoms", [
("bonds", 9),
("angles", 15),
("dihedrals", 12),
])
def test_connection_from_atoms_not_outside(self, tpr, typename,
n_atoms):
ag = tpr.atoms[:10]
cxns = ag.get_connections(typename, outside=False)
assert len(cxns) == n_atoms
indices = np.ravel(cxns.to_indices())
assert np.all(np.in1d(indices, ag.indices))

@pytest.mark.parametrize("typename, n_atoms", [
("bonds", 13),
("angles", 27),
("dihedrals", 38),
])
def test_connection_from_atoms_outside(self, tpr, typename, n_atoms):
ag = tpr.atoms[:10]
cxns = ag.get_connections(typename, outside=True)
assert len(cxns) == n_atoms
indices = np.ravel(cxns.to_indices())
assert not np.all(np.in1d(indices, ag.indices))

def test_invalid_connection_error(self, tpr):
with pytest.raises(AttributeError, match="does not contain"):
ag = tpr.atoms[:10]
ag.get_connections("ureybradleys")

@pytest.mark.parametrize("outside", [True, False])
def test_get_empty_group(self, tpr, outside):
imp = tpr.impropers
ag = tpr.atoms[:10]
cxns = ag.get_connections("impropers", outside=outside)
assert len(imp) == 0
assert len(cxns) == 0


class TestGetConnectionsResidues(object):
"""Test Residue and ResidueGroup.get_connections"""

@pytest.mark.parametrize("typename, n_atoms", [
("bonds", 9),
("angles", 14),
("dihedrals", 9),
("impropers", 0),
])
def test_connection_from_res_not_outside(self, tpr, typename, n_atoms):
cxns = tpr.residues[10].get_connections(typename, outside=False)
assert len(cxns) == n_atoms

@pytest.mark.parametrize("typename, n_atoms", [
("bonds", 11),
("angles", 22),
("dihedrals", 27),
("impropers", 0),
])
def test_connection_from_res_outside(self, tpr, typename, n_atoms):
cxns = tpr.residues[10].get_connections(typename, outside=True)
assert len(cxns) == n_atoms

@pytest.mark.parametrize("typename, n_atoms", [
("bonds", 157),
("angles", 290),
("dihedrals", 351),
])
def test_connection_from_residues_not_outside(self, tpr, typename,
n_atoms):
ag = tpr.residues[:10]
cxns = ag.get_connections(typename, outside=False)
assert len(cxns) == n_atoms
indices = np.ravel(cxns.to_indices())
assert np.all(np.in1d(indices, ag.atoms.indices))

@pytest.mark.parametrize("typename, n_atoms", [
("bonds", 158),
("angles", 294),
("dihedrals", 360),
])
def test_connection_from_residues_outside(self, tpr, typename, n_atoms):
ag = tpr.residues[:10]
cxns = ag.get_connections(typename, outside=True)
assert len(cxns) == n_atoms
indices = np.ravel(cxns.to_indices())
assert not np.all(np.in1d(indices, ag.atoms.indices))

def test_invalid_connection_error(self, tpr):
with pytest.raises(AttributeError, match="does not contain"):
ag = tpr.residues[:10]
ag.get_connections("ureybradleys")

@pytest.mark.parametrize("outside", [True, False])
def test_get_empty_group(self, tpr, outside):
imp = tpr.impropers
ag = tpr.residues[:10]
cxns = ag.get_connections("impropers", outside=outside)
assert len(imp) == 0
assert len(cxns) == 0


@pytest.mark.parametrize("typename, n_inside", [
("intra_bonds", 9),
("intra_angles", 15),
("intra_dihedrals", 12),
])
def test_topologygroup_gets_connections_inside(tpr, typename, n_inside):
ag = tpr.atoms[:10]
cxns = getattr(ag, typename)
assert len(cxns) == n_inside
indices = np.ravel(cxns.to_indices())
assert np.all(np.in1d(indices, ag.indices))


@pytest.mark.parametrize("typename, n_outside", [
("bonds", 13),
("angles", 27),
("dihedrals", 38),
])
def test_topologygroup_gets_connections_outside(tpr, typename, n_outside):
ag = tpr.atoms[:10]
cxns = getattr(ag, typename)
assert len(cxns) == n_outside