From 8d86a6bbc50c28c99ce1948b0cc7b772fb0e51f1 Mon Sep 17 00:00:00 2001 From: Lily Wang Date: Sun, 14 Mar 2021 11:54:25 -0700 Subject: [PATCH 01/20] added get_connections --- package/MDAnalysis/core/groups.py | 35 +++++ package/MDAnalysis/core/topologyattrs.py | 33 +++- testsuite/MDAnalysisTests/core/test_groups.py | 145 +++++++++++++++++- 3 files changed, 209 insertions(+), 4 deletions(-) diff --git a/package/MDAnalysis/core/groups.py b/package/MDAnalysis/core/groups.py index 0671bcef3ad..017ca63809c 100644 --- a/package/MDAnalysis/core/groups.py +++ b/package/MDAnalysis/core/groups.py @@ -365,6 +365,41 @@ def __getattr__(self, attr): err += 'Did you mean {match}?'.format(match=match) raise AttributeError(err) + def get_connections(self, typename, outside=False): + """ + 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"} + 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 + """ + # AtomGroup has handy error messages for missing attributes + ugroup = getattr(self.universe.atoms, typename) + if not len(ugroup): + return ugroup + func = np.any if outside else np.all + 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__`. diff --git a/package/MDAnalysis/core/topologyattrs.py b/package/MDAnalysis/core/topologyattrs.py index 71ee04a850f..ae99065d7d4 100644 --- a/package/MDAnalysis/core/topologyattrs.py +++ b/package/MDAnalysis/core/topologyattrs.py @@ -2295,15 +2295,42 @@ def _bondDict(self): def set_atoms(self, ag): return NotImplementedError("Cannot set bond information") - def get_atoms(self, ag): + def get_atoms(self, ag, outside=True): + """ + Get subset for atoms. + + Parameters + ---------- + ag : AtomGroup + outside : bool (optional) + Whether to include connections to atoms outside the given + AtomGroup. + + .. versionchanged:: 1.1.0 + Added the ``outside`` keyword. Set to ``True`` by default + to give the same behavior as previously + """ 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) + if not outside: + indices = np.array([list(bd[0]) for bd in unique_bonds]) + mask = np.all(np.isin(indices, ag.indices), axis=1) + unique_bonds = unique_bonds[mask] + else: + warnings.warn("This group contains all connections " + "where at least one atom in the " + "AtomGroup is involved. In MDAnalysis " + "2.0 this behavior will change so that " + "the group only contains connections " + "where all atoms are in the AtomGroup.", + DeprecationWarning) + + 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() diff --git a/testsuite/MDAnalysisTests/core/test_groups.py b/testsuite/MDAnalysisTests/core/test_groups.py index 83ff8724f05..c7e65167036 100644 --- a/testsuite/MDAnalysisTests/core/test_groups.py +++ b/testsuite/MDAnalysisTests/core/test_groups.py @@ -32,7 +32,7 @@ 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 @@ -1461,3 +1461,146 @@ 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(): + 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_atoms", [ + ("bonds", 13), + ("angles", 27), + ("dihedrals", 38), +]) +def test_get_topologygroup_property_deprecated(tpr, typename, n_atoms): + err = ("This group contains all connections " + "where at least one atom in the " + "AtomGroup is involved. In MDAnalysis " + "2.0 this behavior will change so that " + "the group only contains connections " + "where all atoms are in the AtomGroup.") + with pytest.warns(DeprecationWarning, match=err): + ag = tpr.atoms[:10] + cxns = getattr(ag, typename) + assert len(cxns) == n_atoms + indices = np.ravel(cxns.to_indices()) + assert not np.all(np.in1d(indices, ag.indices)) From c797056afe9b576baf7e20806cebd56a8810ac25 Mon Sep 17 00:00:00 2001 From: Lily Wang Date: Thu, 1 Apr 2021 19:58:32 -0700 Subject: [PATCH 02/20] modified tests for atoms.bonds/angles/dihedrals etc --- testsuite/MDAnalysisTests/core/test_groups.py | 35 ++++++++++--------- .../core/test_topologyobjects.py | 15 ++++---- .../MDAnalysisTests/topology/test_itp.py | 18 +++++----- .../MDAnalysisTests/topology/test_parmed.py | 20 +++++------ .../MDAnalysisTests/topology/test_psf.py | 10 +++--- .../MDAnalysisTests/topology/test_top.py | 16 ++++----- 6 files changed, 59 insertions(+), 55 deletions(-) diff --git a/testsuite/MDAnalysisTests/core/test_groups.py b/testsuite/MDAnalysisTests/core/test_groups.py index c7e65167036..39090ad33e2 100644 --- a/testsuite/MDAnalysisTests/core/test_groups.py +++ b/testsuite/MDAnalysisTests/core/test_groups.py @@ -1587,20 +1587,23 @@ def test_get_empty_group(self, tpr, outside): assert len(cxns) == 0 @pytest.mark.parametrize("typename, n_atoms", [ - ("bonds", 13), - ("angles", 27), - ("dihedrals", 38), + ("bonds", 9), + ("angles", 15), + ("dihedrals", 12), ]) -def test_get_topologygroup_property_deprecated(tpr, typename, n_atoms): - err = ("This group contains all connections " - "where at least one atom in the " - "AtomGroup is involved. In MDAnalysis " - "2.0 this behavior will change so that " - "the group only contains connections " - "where all atoms are in the AtomGroup.") - with pytest.warns(DeprecationWarning, match=err): - ag = tpr.atoms[:10] - cxns = getattr(ag, typename) - assert len(cxns) == n_atoms - indices = np.ravel(cxns.to_indices()) - assert not np.all(np.in1d(indices, ag.indices)) +def test_get_topologygroup_property_gets_connections_inside(tpr, typename, n_atoms): + ag = tpr.atoms[:10] + cxns = getattr(ag, typename) + 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", 4), + ("angles", 9), + ("dihedrals", 13), +]) +def test_get_atom_property_gets_connections_outside(tpr, typename, n_atoms): + atom = tpr.atoms[0] + cxns = getattr(atom, typename) + assert len(cxns) == n_atoms diff --git a/testsuite/MDAnalysisTests/core/test_topologyobjects.py b/testsuite/MDAnalysisTests/core/test_topologyobjects.py index ec31d743e34..38fe759ff93 100644 --- a/testsuite/MDAnalysisTests/core/test_topologyobjects.py +++ b/testsuite/MDAnalysisTests/core/test_topologyobjects.py @@ -278,7 +278,8 @@ def test_td_universe(self, b_td, PSFDCD): def test_bonds_types(self, PSFDCD, res1): """Tests TopologyDict for bonds""" assert len(PSFDCD.atoms.bonds.types()) == 57 - assert len(res1.atoms.bonds.types()) == 12 + assert len(res1.atoms.get_connections("bonds", outside=True).types()) == 12 + assert len(res1.atoms.bonds.types()) == 11 def test_bonds_contains(self, b_td): assert ('57', '2') in b_td @@ -425,8 +426,8 @@ def test_TG_loose_intersection(self, PSFDCD, attr): """Pull bonds from a TG which are at least partially in an AG""" ag = PSFDCD.atoms[10:60] - TG = getattr(PSFDCD.atoms, attr) - ref = getattr(ag, attr) + TG = PSFDCD.atoms.get_connections(attr, outside=True) + ref = ag.get_connections(attr, outside=True) newTG = TG.atomgroup_intersection(ag) assert newTG == ref @@ -612,22 +613,22 @@ class TestTopologyGroup_Cython(object): @staticmethod @pytest.fixture def bgroup(PSFDCD): - return PSFDCD.atoms[:5].bonds + return PSFDCD.atoms[:5].get_connections("bonds", outside=True) @staticmethod @pytest.fixture def agroup(PSFDCD): - return PSFDCD.atoms[:5].angles + return PSFDCD.atoms[:5].get_connections("angles", outside=True) @staticmethod @pytest.fixture def dgroup(PSFDCD): - return PSFDCD.atoms[:5].dihedrals + return PSFDCD.atoms[:5].get_connections("dihedrals", outside=True) @staticmethod @pytest.fixture def igroup(PSFDCD): - return PSFDCD.atoms[:5].impropers + return PSFDCD.atoms[:5].get_connections("impropers", outside=True) # bonds def test_wrong_type_bonds(self, agroup, dgroup, igroup): diff --git a/testsuite/MDAnalysisTests/topology/test_itp.py b/testsuite/MDAnalysisTests/topology/test_itp.py index b16fd38bb12..5948db6b8d1 100644 --- a/testsuite/MDAnalysisTests/topology/test_itp.py +++ b/testsuite/MDAnalysisTests/topology/test_itp.py @@ -87,8 +87,8 @@ class TestITP(BaseITP): def test_bonds_atom_counts(self, universe): - assert len(universe.atoms[[0]].bonds) == 3 - assert len(universe.atoms[[42]].bonds) == 1 + assert len(universe.atoms[0].bonds) == 3 + assert len(universe.atoms[42].bonds) == 1 def test_bonds_values(self, top): vals = top.bonds.values @@ -99,8 +99,8 @@ def test_bonds_type(self, universe): assert universe.bonds[0].type == 2 def test_angles_atom_counts(self, universe): - assert len(universe.atoms[[0]].angles) == 5 - assert len(universe.atoms[[42]].angles) == 2 + assert len(universe.atoms[0].angles) == 5 + assert len(universe.atoms[42].angles) == 2 def test_angles_values(self, top): vals = top.angles.values @@ -111,7 +111,7 @@ def test_angles_type(self, universe): assert universe.angles[0].type == 2 def test_dihedrals_atom_counts(self, universe): - assert len(universe.atoms[[0]].dihedrals) == 2 + assert len(universe.atoms[0].dihedrals) == 2 def test_dihedrals_multiple_types(self, universe): ag = universe.atoms[[0, 3, 5, 7]] @@ -128,7 +128,7 @@ def test_dihedrals_type(self, universe): def test_impropers_atom_counts(self, universe): - assert len(universe.atoms[[0]].impropers) == 1 + assert len(universe.atoms[0].impropers) == 1 def test_impropers_values(self, top): vals = top.impropers.values @@ -174,11 +174,11 @@ def test_no_extra_angles(self, top): assert a not in top.angles.values def test_bonds_atom_counts(self, universe): - assert len(universe.atoms[[0]].bonds) == 5 - assert len(universe.atoms[[42]].bonds) == 5 + assert len(universe.atoms[0].bonds) == 5 + assert len(universe.atoms[42].bonds) == 5 def test_dihedrals_atom_counts(self, universe): - assert len(universe.atoms[[0]].dihedrals) == 1 + assert len(universe.atoms[0].dihedrals) == 1 def test_dihedrals_identity(self, universe): assert universe.dihedrals[0].type == (1, 1) diff --git a/testsuite/MDAnalysisTests/topology/test_parmed.py b/testsuite/MDAnalysisTests/topology/test_parmed.py index 2d7278577d6..ece0caa30aa 100644 --- a/testsuite/MDAnalysisTests/topology/test_parmed.py +++ b/testsuite/MDAnalysisTests/topology/test_parmed.py @@ -124,8 +124,8 @@ class TestParmedParserPSF(BaseTestParmedParser): expected_elems = (np.array(['' for i in range(3341)], dtype=object),) def test_bonds_atom_counts(self, universe): - assert len(universe.atoms[[0]].bonds) == 4 - assert len(universe.atoms[[42]].bonds) == 1 + assert len(universe.atoms[0].bonds) == 4 + assert len(universe.atoms[42].bonds) == 1 @pytest.mark.parametrize('value', ( (0, 1), @@ -145,8 +145,8 @@ def test_bond_types(self, universe): assert b1.type.type is None def test_angles_atom_counts(self, universe): - assert len(universe.atoms[[0]].angles), 9 - assert len(universe.atoms[[42]].angles), 2 + assert len(universe.atoms[0].angles), 9 + assert len(universe.atoms[42].angles), 2 @pytest.mark.parametrize('value', ( (1, 0, 2), @@ -158,7 +158,7 @@ def test_angles_identity(self, top, value): assert value in vals or value[::-1] in vals def test_dihedrals_atom_counts(self, universe): - assert len(universe.atoms[[0]].dihedrals) == 14 + assert len(universe.atoms[0].dihedrals) == 14 @pytest.mark.parametrize('value', ( (0, 4, 6, 7), @@ -195,8 +195,8 @@ class TestParmedParserPRM(BaseTestParmedParser): dtype=object)) def test_bonds_atom_counts(self, universe): - assert len(universe.atoms[[0]].bonds) == 4 - assert len(universe.atoms[[42]].bonds) == 1 + assert len(universe.atoms[0].bonds) == 4 + assert len(universe.atoms[42].bonds) == 1 @pytest.mark.parametrize('value', ( (10, 11), @@ -217,8 +217,8 @@ def test_bond_types(self, universe): assert b1.type.type.req == 1.010 def test_angles_atom_counts(self, universe): - assert len(universe.atoms[[0]].angles), 9 - assert len(universe.atoms[[42]].angles), 2 + assert len(universe.atoms[0].angles), 9 + assert len(universe.atoms[42].angles), 2 @pytest.mark.parametrize('value', ( (11, 10, 12), @@ -231,7 +231,7 @@ def test_angles_identity(self, top, value): assert value in vals or value[::-1] in vals def test_dihedrals_atom_counts(self, universe): - assert len(universe.atoms[[0]].dihedrals) == 14 + assert len(universe.atoms[0].dihedrals) == 14 @pytest.mark.parametrize('value', ( (11, 10, 12, 14), diff --git a/testsuite/MDAnalysisTests/topology/test_psf.py b/testsuite/MDAnalysisTests/topology/test_psf.py index 950d756758f..bfdb3a895e3 100644 --- a/testsuite/MDAnalysisTests/topology/test_psf.py +++ b/testsuite/MDAnalysisTests/topology/test_psf.py @@ -72,8 +72,8 @@ def test_bonds_total_counts(self, top): def test_bonds_atom_counts(self, filename): u = mda.Universe(filename) - assert len(u.atoms[[0]].bonds) == 4 - assert len(u.atoms[[42]].bonds) == 1 + assert len(u.atoms[0].bonds) == 4 + assert len(u.atoms[42].bonds) == 1 def test_bonds_identity(self, top): vals = top.bonds.values @@ -85,8 +85,8 @@ def test_angles_total_counts(self, top): def test_angles_atom_counts(self, filename): u = mda.Universe(filename) - assert len(u.atoms[[0]].angles), 9 - assert len(u.atoms[[42]].angles), 2 + assert len(u.atoms[0].angles), 9 + assert len(u.atoms[42].angles), 2 def test_angles_identity(self, top): vals = top.angles.values @@ -98,7 +98,7 @@ def test_dihedrals_total_counts(self, top): def test_dihedrals_atom_counts(self, filename): u = mda.Universe(filename) - assert len(u.atoms[[0]].dihedrals) == 14 + assert len(u.atoms[0].dihedrals) == 14 def test_dihedrals_identity(self, top): vals = top.dihedrals.values diff --git a/testsuite/MDAnalysisTests/topology/test_top.py b/testsuite/MDAnalysisTests/topology/test_top.py index 8a2ad2ae30c..2898e5e0dc1 100644 --- a/testsuite/MDAnalysisTests/topology/test_top.py +++ b/testsuite/MDAnalysisTests/topology/test_top.py @@ -62,24 +62,24 @@ def test_attr_size(self, top): def test_bonds_atom_counts(self, filename): u = mda.Universe(filename) - assert len(u.atoms[[0]].bonds) == self.expected_n_zero_bonds - assert len(u.atoms[[self.atom_i]].bonds) == self.expected_n_i_bonds + assert len(u.atoms[0].bonds) == self.expected_n_zero_bonds + assert len(u.atoms[self.atom_i].bonds) == self.expected_n_i_bonds def test_angles_atom_counts(self, filename): u = mda.Universe(filename) - assert len(u.atoms[[0]].angles) == self.expected_n_zero_angles - assert len(u.atoms[[self.atom_i]].angles) == self.expected_n_i_angles + assert len(u.atoms[0].angles) == self.expected_n_zero_angles + assert len(u.atoms[self.atom_i].angles) == self.expected_n_i_angles def test_dihedrals_atom_counts(self, filename): u = mda.Universe(filename) - assert len(u.atoms[[0]].dihedrals) == self.expected_n_zero_dihedrals - assert len(u.atoms[[self.atom_i]].dihedrals) == \ + assert len(u.atoms[0].dihedrals) == self.expected_n_zero_dihedrals + assert len(u.atoms[self.atom_i].dihedrals) == \ self.expected_n_i_dihedrals def test_impropers_atom_counts(self, filename): u = mda.Universe(filename) - assert len(u.atoms[[0]].impropers) == self.expected_n_zero_impropers - assert len(u.atoms[[self.atom_i]].impropers) == \ + assert len(u.atoms[0].impropers) == self.expected_n_zero_impropers + assert len(u.atoms[self.atom_i].impropers) == \ self.expected_n_i_impropers def test_bonds_identity(self, top): From d07d54b933d3f8e4c5b4eafce86ff60e5b3e6f94 Mon Sep 17 00:00:00 2001 From: Lily Wang Date: Thu, 1 Apr 2021 19:58:59 -0700 Subject: [PATCH 03/20] modified parsers and things to use get_connections or bonds --- package/MDAnalysis/coordinates/MOL2.py | 2 +- package/MDAnalysis/coordinates/ParmEd.py | 3 +-- package/MDAnalysis/core/groups.py | 2 +- package/MDAnalysis/core/selection.py | 5 +++-- package/MDAnalysis/core/topologyattrs.py | 19 +++++++++---------- 5 files changed, 15 insertions(+), 16 deletions(-) diff --git a/package/MDAnalysis/coordinates/MOL2.py b/package/MDAnalysis/coordinates/MOL2.py index 3764e77758c..dfee152ed74 100644 --- a/package/MDAnalysis/coordinates/MOL2.py +++ b/package/MDAnalysis/coordinates/MOL2.py @@ -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.bonds bonds = sorted((b[0], b[1], b.order) for b in bondgroup) bond_lines = ["@BOND"] bls = ["{0:>5} {1:>5} {2:>5} {3:>2}".format(bid, diff --git a/package/MDAnalysis/coordinates/ParmEd.py b/package/MDAnalysis/coordinates/ParmEd.py index 8b28bb47ca3..4b2170f02de 100644 --- a/package/MDAnalysis/coordinates/ParmEd.py +++ b/package/MDAnalysis/coordinates/ParmEd.py @@ -272,8 +272,7 @@ def convert(self, obj): # bonds try: - params = ag_or_ts.bonds.atomgroup_intersection(ag_or_ts, - strict=True) + params = ag_or_ts.bonds except AttributeError: pass else: diff --git a/package/MDAnalysis/core/groups.py b/package/MDAnalysis/core/groups.py index 017ca63809c..baed823410c 100644 --- a/package/MDAnalysis/core/groups.py +++ b/package/MDAnalysis/core/groups.py @@ -365,7 +365,7 @@ def __getattr__(self, attr): err += 'Did you mean {match}?'.format(match=match) raise AttributeError(err) - def get_connections(self, typename, outside=False): + def get_connections(self, typename, outside=True): """ Get bonded connections between atoms as a :class:`~MDAnalysis.core.topologyobjects.TopologyGroup`. diff --git a/package/MDAnalysis/core/selection.py b/package/MDAnalysis/core/selection.py index adff7ab3eeb..701658b1ec3 100644 --- a/package/MDAnalysis/core/selection.py +++ b/package/MDAnalysis/core/selection.py @@ -525,14 +525,15 @@ def __init__(self, parser, tokens): def apply(self, group): grp = self.sel.apply(group) # Check if we have bonds - if not group.bonds: + bonds = group.get_connections("bonds", outside=True) + if not bonds: warnings.warn("Bonded selection has 0 bonds") return group[[]] grpidx = grp.indices # (n, 2) array of bond indices - bix = np.array(group.bonds.to_indices()) + bix = np.array(bonds.to_indices()) idx = [] # left side diff --git a/package/MDAnalysis/core/topologyattrs.py b/package/MDAnalysis/core/topologyattrs.py index ae99065d7d4..012237efcaa 100644 --- a/package/MDAnalysis/core/topologyattrs.py +++ b/package/MDAnalysis/core/topologyattrs.py @@ -2295,7 +2295,7 @@ def _bondDict(self): def set_atoms(self, ag): return NotImplementedError("Cannot set bond information") - def get_atoms(self, ag, outside=True): + def get_atoms(self, ag, outside=False): """ Get subset for atoms. @@ -2309,6 +2309,9 @@ def get_atoms(self, ag, outside=True): .. versionchanged:: 1.1.0 Added the ``outside`` keyword. Set to ``True`` by default to give the same behavior as previously + + .. versionchanged:: 2.0.0 + Changed to default ``outside=False`` """ try: unique_bonds = set(itertools.chain( @@ -2316,19 +2319,15 @@ def get_atoms(self, ag, outside=True): except TypeError: # maybe we got passed an Atom unique_bonds = self._bondDict[ag.ix] + outside = True unique_bonds = np.array(sorted(unique_bonds), dtype=object) if not outside: indices = np.array([list(bd[0]) for bd in unique_bonds]) - mask = np.all(np.isin(indices, ag.indices), axis=1) + try: + mask = np.all(np.isin(indices, ag.ix), axis=1) + except np.AxisError: + mask = [] unique_bonds = unique_bonds[mask] - else: - warnings.warn("This group contains all connections " - "where at least one atom in the " - "AtomGroup is involved. In MDAnalysis " - "2.0 this behavior will change so that " - "the group only contains connections " - "where all atoms are in the AtomGroup.", - DeprecationWarning) bond_idx, types, guessed, order = np.hsplit(unique_bonds, 4) bond_idx = np.array(bond_idx.ravel().tolist(), dtype=np.int32) From 1f25fff75434716749a0733152ea90985ab2761a Mon Sep 17 00:00:00 2001 From: Lily Wang Date: Thu, 1 Apr 2021 20:03:17 -0700 Subject: [PATCH 04/20] updated CHANGELOG --- package/CHANGELOG | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/package/CHANGELOG b/package/CHANGELOG index bf10ef51bc4..cde57918b4c 100644 --- a/package/CHANGELOG +++ b/package/CHANGELOG @@ -141,7 +141,10 @@ Enhancements Changes - * TPRParser now loads TPR files with `tpr_resid_from_one=True` by default, + * AtomGroup.bonds, .angles, .dihedrals, .impropers etc. now return only + the connections involving atoms within the AtomGroup, instead of + including atoms outside the AtomGroup (Issue #1264, #2821) + * TPRParser now loads TPR files with `tpr_resid_from_one=True` by default, which starts TPR resid indexing from 1 (instead of 0 as in 1.x) (Issue #2364, PR #3152) * Introduces encore specific C compiler arguments to allow for lowering of optimisations on non-x86 platforms (Issue #1389, PR #3149) From 0e3c66cacf78b6ad13cac6eeeb44b0b9002fc5e4 Mon Sep 17 00:00:00 2001 From: Lily Wang Date: Thu, 1 Apr 2021 20:18:35 -0700 Subject: [PATCH 05/20] pep8 --- package/CHANGELOG | 2 +- package/MDAnalysis/core/topologyattrs.py | 2 +- testsuite/MDAnalysisTests/core/test_groups.py | 8 +++++--- testsuite/MDAnalysisTests/core/test_topologyobjects.py | 2 +- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/package/CHANGELOG b/package/CHANGELOG index cde57918b4c..6f7054b2ccb 100644 --- a/package/CHANGELOG +++ b/package/CHANGELOG @@ -143,7 +143,7 @@ Enhancements Changes * AtomGroup.bonds, .angles, .dihedrals, .impropers etc. now return only the connections involving atoms within the AtomGroup, instead of - including atoms outside the AtomGroup (Issue #1264, #2821) + including atoms outside the AtomGroup (Issue #1264, #2821, PR #3200) * TPRParser now loads TPR files with `tpr_resid_from_one=True` by default, which starts TPR resid indexing from 1 (instead of 0 as in 1.x) (Issue #2364, PR #3152) * Introduces encore specific C compiler arguments to allow for lowering of diff --git a/package/MDAnalysis/core/topologyattrs.py b/package/MDAnalysis/core/topologyattrs.py index 012237efcaa..d85a36ae38a 100644 --- a/package/MDAnalysis/core/topologyattrs.py +++ b/package/MDAnalysis/core/topologyattrs.py @@ -2309,7 +2309,7 @@ def get_atoms(self, ag, outside=False): .. versionchanged:: 1.1.0 Added the ``outside`` keyword. Set to ``True`` by default to give the same behavior as previously - + .. versionchanged:: 2.0.0 Changed to default ``outside=False`` """ diff --git a/testsuite/MDAnalysisTests/core/test_groups.py b/testsuite/MDAnalysisTests/core/test_groups.py index 39090ad33e2..c89042cf161 100644 --- a/testsuite/MDAnalysisTests/core/test_groups.py +++ b/testsuite/MDAnalysisTests/core/test_groups.py @@ -1472,7 +1472,7 @@ class TestGetConnectionsAtoms(object): """Test Atom and AtomGroup.get_connections""" @pytest.mark.parametrize("typename", - ["bonds", "angles", "dihedrals", "impropers"]) + ["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 @@ -1586,24 +1586,26 @@ def test_get_empty_group(self, tpr, outside): assert len(imp) == 0 assert len(cxns) == 0 + @pytest.mark.parametrize("typename, n_atoms", [ ("bonds", 9), ("angles", 15), ("dihedrals", 12), ]) -def test_get_topologygroup_property_gets_connections_inside(tpr, typename, n_atoms): +def test_topologygroup_gets_connections_inside(tpr, typename, n_atoms): ag = tpr.atoms[:10] cxns = getattr(ag, typename) 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", 4), ("angles", 9), ("dihedrals", 13), ]) -def test_get_atom_property_gets_connections_outside(tpr, typename, n_atoms): +def test_get_atom_gets_connections_outside(tpr, typename, n_atoms): atom = tpr.atoms[0] cxns = getattr(atom, typename) assert len(cxns) == n_atoms diff --git a/testsuite/MDAnalysisTests/core/test_topologyobjects.py b/testsuite/MDAnalysisTests/core/test_topologyobjects.py index 38fe759ff93..4033e204c09 100644 --- a/testsuite/MDAnalysisTests/core/test_topologyobjects.py +++ b/testsuite/MDAnalysisTests/core/test_topologyobjects.py @@ -278,7 +278,7 @@ def test_td_universe(self, b_td, PSFDCD): def test_bonds_types(self, PSFDCD, res1): """Tests TopologyDict for bonds""" assert len(PSFDCD.atoms.bonds.types()) == 57 - assert len(res1.atoms.get_connections("bonds", outside=True).types()) == 12 + assert len(res1.atoms.get_connections("bonds").types()) == 12 assert len(res1.atoms.bonds.types()) == 11 def test_bonds_contains(self, b_td): From 20dbe888a7cf5d18af6846e2eb9101913c191351 Mon Sep 17 00:00:00 2001 From: Lily Wang Date: Sun, 4 Apr 2021 14:15:44 -0700 Subject: [PATCH 06/20] undo half of PR 3160 --- package/CHANGELOG | 50 +++++++++++++++++++ package/MDAnalysis/core/topologyattrs.py | 19 +------ testsuite/MDAnalysisTests/core/test_groups.py | 24 --------- 3 files changed, 51 insertions(+), 42 deletions(-) diff --git a/package/CHANGELOG b/package/CHANGELOG index 6f7054b2ccb..039895a3c33 100644 --- a/package/CHANGELOG +++ b/package/CHANGELOG @@ -22,6 +22,7 @@ The rules for this file: * 2.0.0 Fixes +<<<<<<< HEAD * A Universe created from an ROMol with no atoms returns now a Universe with 0 atoms (Issue #3142) * ValueError raised when empty atomgroup is given to DensityAnalysis @@ -38,6 +39,55 @@ Fixes * PDB writer now uses elements attribute instead of guessing elements from atom name (Issue #2423) * Cleanup and parametrization of test_atomgroup.py (Issue #2995) +======= + * Removes use of absolute paths in setup.py to avoid Windows installation + failures (Issue #3129) + * Adds test for crashes caused by small box NSGrid searches (Issue #2670) + * Replaces decreated NumPy type aliases and fixes NEP 34 explicit ragged + array warnings (PR #3139, backports PRs #2845 and #2834) + * Fixed several issues with NSGrid and triclinic boxes not finding some pairs. + (Issues #2229 #2345 #2919, PR #2937). + +Changes + * Maximum pinned versions in setup.py removed for python 3.6+ (PR #3139) + +Deprecations + * ParmEdConverter no longer accepts Timestep objects at all + (Issue #3031, PR #3172) + * NCDFWriter `scale_factor` writing will change in version 2.0 to + better match AMBER outputs (Issue #2327) + * Deprecated using the last letter of the segid as the + chainID when writing PDB files (Issue #3144) + * Deprecated tempfactors and bfactors being separate + TopologyAttrs, with a warning (PR #3161) + * hbonds.WaterBridgeAnalysis will be removed in 2.0.0 and + replaced with hydrogenbonds.WaterBridgeAnalysis (#3111) + * TPRParser indexing resids from 0 by default is deprecated. + From 2.0 TPRParser will index resids from 1 by default. + + +Enhancements + * Added `get_connections` method to get bonds, angles, dihedrals, etc. + with or without atoms outside the group (Issues #1264, #2821, PR #3160) + * Added `tpr_resid_from_one` keyword to select whether TPRParser + indexes resids from 0 or 1 (Issue #2364, PR #3153) + + +01/17/21 richardjgowers, IAlibay, orbeckst, tylerjereddy, jbarnoud, + yuxuanzhuang, lilyminium, VOD555, p-j-smith, bieniekmateusz, + calcraven, ianmkenney, rcrehuet, manuel.nuno.melo, hanatok + + * 1.0.1 + +Fixes + * Due to issues with the reliability/accuracy of `nsgrid`, this method is + currently not recommended for use. It has also been removed as an option + from lib.capped_distance and lib.self_capped_distance. Please use PKDTree + instead (Issue #2930) + * Development status changed from beta to mature (Issue #2773) + * pip installation only requests Python 2.7-compatible packages (#2736) + * Testsuite does not use any more matplotlib.use('agg') (#2191) +>>>>>>> c58d2d370... undo half of PR 3160 * The methods provided by topology attributes now appear in the documentation (Issue #1845) * AtomGroup.center now works correctly for compounds + unwrapping diff --git a/package/MDAnalysis/core/topologyattrs.py b/package/MDAnalysis/core/topologyattrs.py index d85a36ae38a..537c94a2755 100644 --- a/package/MDAnalysis/core/topologyattrs.py +++ b/package/MDAnalysis/core/topologyattrs.py @@ -2295,23 +2295,14 @@ def _bondDict(self): def set_atoms(self, ag): return NotImplementedError("Cannot set bond information") - def get_atoms(self, ag, outside=False): + def get_atoms(self, ag): """ Get subset for atoms. Parameters ---------- ag : AtomGroup - outside : bool (optional) - Whether to include connections to atoms outside the given - AtomGroup. - - .. versionchanged:: 1.1.0 - Added the ``outside`` keyword. Set to ``True`` by default - to give the same behavior as previously - .. versionchanged:: 2.0.0 - Changed to default ``outside=False`` """ try: unique_bonds = set(itertools.chain( @@ -2321,14 +2312,6 @@ def get_atoms(self, ag, outside=False): unique_bonds = self._bondDict[ag.ix] outside = True unique_bonds = np.array(sorted(unique_bonds), dtype=object) - if not outside: - indices = np.array([list(bd[0]) for bd in unique_bonds]) - try: - mask = np.all(np.isin(indices, ag.ix), axis=1) - except np.AxisError: - mask = [] - unique_bonds = unique_bonds[mask] - bond_idx, types, guessed, order = np.hsplit(unique_bonds, 4) bond_idx = np.array(bond_idx.ravel().tolist(), dtype=np.int32) types = types.ravel() diff --git a/testsuite/MDAnalysisTests/core/test_groups.py b/testsuite/MDAnalysisTests/core/test_groups.py index c89042cf161..01fda49ea05 100644 --- a/testsuite/MDAnalysisTests/core/test_groups.py +++ b/testsuite/MDAnalysisTests/core/test_groups.py @@ -1585,27 +1585,3 @@ def test_get_empty_group(self, tpr, outside): cxns = ag.get_connections("impropers", outside=outside) assert len(imp) == 0 assert len(cxns) == 0 - - -@pytest.mark.parametrize("typename, n_atoms", [ - ("bonds", 9), - ("angles", 15), - ("dihedrals", 12), -]) -def test_topologygroup_gets_connections_inside(tpr, typename, n_atoms): - ag = tpr.atoms[:10] - cxns = getattr(ag, typename) - 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", 4), - ("angles", 9), - ("dihedrals", 13), -]) -def test_get_atom_gets_connections_outside(tpr, typename, n_atoms): - atom = tpr.atoms[0] - cxns = getattr(atom, typename) - assert len(cxns) == n_atoms From 981c22afc0ac08000280b0f17818960e0e3a8fd1 Mon Sep 17 00:00:00 2001 From: Lily Wang Date: Tue, 6 Apr 2021 23:06:06 -0700 Subject: [PATCH 07/20] add intra stuff --- package/CHANGELOG | 58 ++----------------- package/MDAnalysis/coordinates/MOL2.py | 2 +- package/MDAnalysis/coordinates/ParmEd.py | 2 +- package/MDAnalysis/core/selection.py | 5 +- package/MDAnalysis/core/topologyattrs.py | 22 +++++-- testsuite/MDAnalysisTests/core/test_groups.py | 24 ++++++++ .../core/test_topologyobjects.py | 15 +++-- .../MDAnalysisTests/topology/test_itp.py | 18 +++--- .../MDAnalysisTests/topology/test_parmed.py | 20 +++---- .../MDAnalysisTests/topology/test_psf.py | 10 ++-- .../MDAnalysisTests/topology/test_top.py | 16 ++--- 11 files changed, 88 insertions(+), 104 deletions(-) diff --git a/package/CHANGELOG b/package/CHANGELOG index 039895a3c33..6add6701c4b 100644 --- a/package/CHANGELOG +++ b/package/CHANGELOG @@ -22,7 +22,6 @@ The rules for this file: * 2.0.0 Fixes -<<<<<<< HEAD * A Universe created from an ROMol with no atoms returns now a Universe with 0 atoms (Issue #3142) * ValueError raised when empty atomgroup is given to DensityAnalysis @@ -39,55 +38,6 @@ Fixes * PDB writer now uses elements attribute instead of guessing elements from atom name (Issue #2423) * Cleanup and parametrization of test_atomgroup.py (Issue #2995) -======= - * Removes use of absolute paths in setup.py to avoid Windows installation - failures (Issue #3129) - * Adds test for crashes caused by small box NSGrid searches (Issue #2670) - * Replaces decreated NumPy type aliases and fixes NEP 34 explicit ragged - array warnings (PR #3139, backports PRs #2845 and #2834) - * Fixed several issues with NSGrid and triclinic boxes not finding some pairs. - (Issues #2229 #2345 #2919, PR #2937). - -Changes - * Maximum pinned versions in setup.py removed for python 3.6+ (PR #3139) - -Deprecations - * ParmEdConverter no longer accepts Timestep objects at all - (Issue #3031, PR #3172) - * NCDFWriter `scale_factor` writing will change in version 2.0 to - better match AMBER outputs (Issue #2327) - * Deprecated using the last letter of the segid as the - chainID when writing PDB files (Issue #3144) - * Deprecated tempfactors and bfactors being separate - TopologyAttrs, with a warning (PR #3161) - * hbonds.WaterBridgeAnalysis will be removed in 2.0.0 and - replaced with hydrogenbonds.WaterBridgeAnalysis (#3111) - * TPRParser indexing resids from 0 by default is deprecated. - From 2.0 TPRParser will index resids from 1 by default. - - -Enhancements - * Added `get_connections` method to get bonds, angles, dihedrals, etc. - with or without atoms outside the group (Issues #1264, #2821, PR #3160) - * Added `tpr_resid_from_one` keyword to select whether TPRParser - indexes resids from 0 or 1 (Issue #2364, PR #3153) - - -01/17/21 richardjgowers, IAlibay, orbeckst, tylerjereddy, jbarnoud, - yuxuanzhuang, lilyminium, VOD555, p-j-smith, bieniekmateusz, - calcraven, ianmkenney, rcrehuet, manuel.nuno.melo, hanatok - - * 1.0.1 - -Fixes - * Due to issues with the reliability/accuracy of `nsgrid`, this method is - currently not recommended for use. It has also been removed as an option - from lib.capped_distance and lib.self_capped_distance. Please use PKDTree - instead (Issue #2930) - * Development status changed from beta to mature (Issue #2773) - * pip installation only requests Python 2.7-compatible packages (#2736) - * Testsuite does not use any more matplotlib.use('agg') (#2191) ->>>>>>> c58d2d370... undo half of PR 3160 * The methods provided by topology attributes now appear in the documentation (Issue #1845) * AtomGroup.center now works correctly for compounds + unwrapping @@ -137,6 +87,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 a ValueError raised when not given a gridcenter while providing grid dimensions to DensityAnalysis, also added check for NaN in the input (Issue #3148, PR #3154) @@ -191,10 +144,7 @@ Enhancements Changes - * AtomGroup.bonds, .angles, .dihedrals, .impropers etc. now return only - the connections involving atoms within the AtomGroup, instead of - including atoms outside the AtomGroup (Issue #1264, #2821, PR #3200) - * TPRParser now loads TPR files with `tpr_resid_from_one=True` by default, + * TPRParser now loads TPR files with `tpr_resid_from_one=True` by default, which starts TPR resid indexing from 1 (instead of 0 as in 1.x) (Issue #2364, PR #3152) * Introduces encore specific C compiler arguments to allow for lowering of optimisations on non-x86 platforms (Issue #1389, PR #3149) diff --git a/package/MDAnalysis/coordinates/MOL2.py b/package/MDAnalysis/coordinates/MOL2.py index dfee152ed74..7b16a011e9d 100644 --- a/package/MDAnalysis/coordinates/MOL2.py +++ b/package/MDAnalysis/coordinates/MOL2.py @@ -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 + bondgroup = obj.intra_bonds bonds = sorted((b[0], b[1], b.order) for b in bondgroup) bond_lines = ["@BOND"] bls = ["{0:>5} {1:>5} {2:>5} {3:>2}".format(bid, diff --git a/package/MDAnalysis/coordinates/ParmEd.py b/package/MDAnalysis/coordinates/ParmEd.py index 4b2170f02de..997921e7e56 100644 --- a/package/MDAnalysis/coordinates/ParmEd.py +++ b/package/MDAnalysis/coordinates/ParmEd.py @@ -272,7 +272,7 @@ def convert(self, obj): # bonds try: - params = ag_or_ts.bonds + params = ag_or_ts.intra_bonds except AttributeError: pass else: diff --git a/package/MDAnalysis/core/selection.py b/package/MDAnalysis/core/selection.py index 701658b1ec3..adff7ab3eeb 100644 --- a/package/MDAnalysis/core/selection.py +++ b/package/MDAnalysis/core/selection.py @@ -525,15 +525,14 @@ def __init__(self, parser, tokens): def apply(self, group): grp = self.sel.apply(group) # Check if we have bonds - bonds = group.get_connections("bonds", outside=True) - if not bonds: + if not group.bonds: warnings.warn("Bonded selection has 0 bonds") return group[[]] grpidx = grp.indices # (n, 2) array of bond indices - bix = np.array(bonds.to_indices()) + bix = np.array(group.bonds.to_indices()) idx = [] # left side diff --git a/package/MDAnalysis/core/topologyattrs.py b/package/MDAnalysis/core/topologyattrs.py index 537c94a2755..edc257fd354 100644 --- a/package/MDAnalysis/core/topologyattrs.py +++ b/package/MDAnalysis/core/topologyattrs.py @@ -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) @@ -263,6 +265,11 @@ def _attach_transplant_stubs(attribute_name, topology_attribute_class): setattr(dest_class, method_name, stub) +def intra_connection(self, ag): + """Get connections only within this AtomGroup + """ + return ag.get_connections(self.attrname, outside=False) + class _TopologyAttrMeta(type): r"""Register TopologyAttrs on class creation @@ -291,6 +298,12 @@ def __init__(cls, name, bases, classdict): if attrname is None: attrname = singular + # add intra connections + if any("Connection" in x.__name__ for x in cls.__bases__): + method = MethodType(intra_connection, cls) + prop = property(method, None, None, method.__doc__) + cls.transplants[AtomGroup].append((f"intra_{attrname}", prop)) + if singular: _TOPOLOGY_ATTRS[singular] = _TOPOLOGY_ATTRS[attrname] = cls _singular = singular.lower().replace('_', '') @@ -2310,7 +2323,6 @@ def get_atoms(self, ag): except TypeError: # maybe we got passed an Atom unique_bonds = self._bondDict[ag.ix] - outside = True 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) diff --git a/testsuite/MDAnalysisTests/core/test_groups.py b/testsuite/MDAnalysisTests/core/test_groups.py index 01fda49ea05..6142043a110 100644 --- a/testsuite/MDAnalysisTests/core/test_groups.py +++ b/testsuite/MDAnalysisTests/core/test_groups.py @@ -1585,3 +1585,27 @@ def test_get_empty_group(self, tpr, outside): 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 diff --git a/testsuite/MDAnalysisTests/core/test_topologyobjects.py b/testsuite/MDAnalysisTests/core/test_topologyobjects.py index 4033e204c09..ec31d743e34 100644 --- a/testsuite/MDAnalysisTests/core/test_topologyobjects.py +++ b/testsuite/MDAnalysisTests/core/test_topologyobjects.py @@ -278,8 +278,7 @@ def test_td_universe(self, b_td, PSFDCD): def test_bonds_types(self, PSFDCD, res1): """Tests TopologyDict for bonds""" assert len(PSFDCD.atoms.bonds.types()) == 57 - assert len(res1.atoms.get_connections("bonds").types()) == 12 - assert len(res1.atoms.bonds.types()) == 11 + assert len(res1.atoms.bonds.types()) == 12 def test_bonds_contains(self, b_td): assert ('57', '2') in b_td @@ -426,8 +425,8 @@ def test_TG_loose_intersection(self, PSFDCD, attr): """Pull bonds from a TG which are at least partially in an AG""" ag = PSFDCD.atoms[10:60] - TG = PSFDCD.atoms.get_connections(attr, outside=True) - ref = ag.get_connections(attr, outside=True) + TG = getattr(PSFDCD.atoms, attr) + ref = getattr(ag, attr) newTG = TG.atomgroup_intersection(ag) assert newTG == ref @@ -613,22 +612,22 @@ class TestTopologyGroup_Cython(object): @staticmethod @pytest.fixture def bgroup(PSFDCD): - return PSFDCD.atoms[:5].get_connections("bonds", outside=True) + return PSFDCD.atoms[:5].bonds @staticmethod @pytest.fixture def agroup(PSFDCD): - return PSFDCD.atoms[:5].get_connections("angles", outside=True) + return PSFDCD.atoms[:5].angles @staticmethod @pytest.fixture def dgroup(PSFDCD): - return PSFDCD.atoms[:5].get_connections("dihedrals", outside=True) + return PSFDCD.atoms[:5].dihedrals @staticmethod @pytest.fixture def igroup(PSFDCD): - return PSFDCD.atoms[:5].get_connections("impropers", outside=True) + return PSFDCD.atoms[:5].impropers # bonds def test_wrong_type_bonds(self, agroup, dgroup, igroup): diff --git a/testsuite/MDAnalysisTests/topology/test_itp.py b/testsuite/MDAnalysisTests/topology/test_itp.py index 5948db6b8d1..b16fd38bb12 100644 --- a/testsuite/MDAnalysisTests/topology/test_itp.py +++ b/testsuite/MDAnalysisTests/topology/test_itp.py @@ -87,8 +87,8 @@ class TestITP(BaseITP): def test_bonds_atom_counts(self, universe): - assert len(universe.atoms[0].bonds) == 3 - assert len(universe.atoms[42].bonds) == 1 + assert len(universe.atoms[[0]].bonds) == 3 + assert len(universe.atoms[[42]].bonds) == 1 def test_bonds_values(self, top): vals = top.bonds.values @@ -99,8 +99,8 @@ def test_bonds_type(self, universe): assert universe.bonds[0].type == 2 def test_angles_atom_counts(self, universe): - assert len(universe.atoms[0].angles) == 5 - assert len(universe.atoms[42].angles) == 2 + assert len(universe.atoms[[0]].angles) == 5 + assert len(universe.atoms[[42]].angles) == 2 def test_angles_values(self, top): vals = top.angles.values @@ -111,7 +111,7 @@ def test_angles_type(self, universe): assert universe.angles[0].type == 2 def test_dihedrals_atom_counts(self, universe): - assert len(universe.atoms[0].dihedrals) == 2 + assert len(universe.atoms[[0]].dihedrals) == 2 def test_dihedrals_multiple_types(self, universe): ag = universe.atoms[[0, 3, 5, 7]] @@ -128,7 +128,7 @@ def test_dihedrals_type(self, universe): def test_impropers_atom_counts(self, universe): - assert len(universe.atoms[0].impropers) == 1 + assert len(universe.atoms[[0]].impropers) == 1 def test_impropers_values(self, top): vals = top.impropers.values @@ -174,11 +174,11 @@ def test_no_extra_angles(self, top): assert a not in top.angles.values def test_bonds_atom_counts(self, universe): - assert len(universe.atoms[0].bonds) == 5 - assert len(universe.atoms[42].bonds) == 5 + assert len(universe.atoms[[0]].bonds) == 5 + assert len(universe.atoms[[42]].bonds) == 5 def test_dihedrals_atom_counts(self, universe): - assert len(universe.atoms[0].dihedrals) == 1 + assert len(universe.atoms[[0]].dihedrals) == 1 def test_dihedrals_identity(self, universe): assert universe.dihedrals[0].type == (1, 1) diff --git a/testsuite/MDAnalysisTests/topology/test_parmed.py b/testsuite/MDAnalysisTests/topology/test_parmed.py index ece0caa30aa..2d7278577d6 100644 --- a/testsuite/MDAnalysisTests/topology/test_parmed.py +++ b/testsuite/MDAnalysisTests/topology/test_parmed.py @@ -124,8 +124,8 @@ class TestParmedParserPSF(BaseTestParmedParser): expected_elems = (np.array(['' for i in range(3341)], dtype=object),) def test_bonds_atom_counts(self, universe): - assert len(universe.atoms[0].bonds) == 4 - assert len(universe.atoms[42].bonds) == 1 + assert len(universe.atoms[[0]].bonds) == 4 + assert len(universe.atoms[[42]].bonds) == 1 @pytest.mark.parametrize('value', ( (0, 1), @@ -145,8 +145,8 @@ def test_bond_types(self, universe): assert b1.type.type is None def test_angles_atom_counts(self, universe): - assert len(universe.atoms[0].angles), 9 - assert len(universe.atoms[42].angles), 2 + assert len(universe.atoms[[0]].angles), 9 + assert len(universe.atoms[[42]].angles), 2 @pytest.mark.parametrize('value', ( (1, 0, 2), @@ -158,7 +158,7 @@ def test_angles_identity(self, top, value): assert value in vals or value[::-1] in vals def test_dihedrals_atom_counts(self, universe): - assert len(universe.atoms[0].dihedrals) == 14 + assert len(universe.atoms[[0]].dihedrals) == 14 @pytest.mark.parametrize('value', ( (0, 4, 6, 7), @@ -195,8 +195,8 @@ class TestParmedParserPRM(BaseTestParmedParser): dtype=object)) def test_bonds_atom_counts(self, universe): - assert len(universe.atoms[0].bonds) == 4 - assert len(universe.atoms[42].bonds) == 1 + assert len(universe.atoms[[0]].bonds) == 4 + assert len(universe.atoms[[42]].bonds) == 1 @pytest.mark.parametrize('value', ( (10, 11), @@ -217,8 +217,8 @@ def test_bond_types(self, universe): assert b1.type.type.req == 1.010 def test_angles_atom_counts(self, universe): - assert len(universe.atoms[0].angles), 9 - assert len(universe.atoms[42].angles), 2 + assert len(universe.atoms[[0]].angles), 9 + assert len(universe.atoms[[42]].angles), 2 @pytest.mark.parametrize('value', ( (11, 10, 12), @@ -231,7 +231,7 @@ def test_angles_identity(self, top, value): assert value in vals or value[::-1] in vals def test_dihedrals_atom_counts(self, universe): - assert len(universe.atoms[0].dihedrals) == 14 + assert len(universe.atoms[[0]].dihedrals) == 14 @pytest.mark.parametrize('value', ( (11, 10, 12, 14), diff --git a/testsuite/MDAnalysisTests/topology/test_psf.py b/testsuite/MDAnalysisTests/topology/test_psf.py index bfdb3a895e3..950d756758f 100644 --- a/testsuite/MDAnalysisTests/topology/test_psf.py +++ b/testsuite/MDAnalysisTests/topology/test_psf.py @@ -72,8 +72,8 @@ def test_bonds_total_counts(self, top): def test_bonds_atom_counts(self, filename): u = mda.Universe(filename) - assert len(u.atoms[0].bonds) == 4 - assert len(u.atoms[42].bonds) == 1 + assert len(u.atoms[[0]].bonds) == 4 + assert len(u.atoms[[42]].bonds) == 1 def test_bonds_identity(self, top): vals = top.bonds.values @@ -85,8 +85,8 @@ def test_angles_total_counts(self, top): def test_angles_atom_counts(self, filename): u = mda.Universe(filename) - assert len(u.atoms[0].angles), 9 - assert len(u.atoms[42].angles), 2 + assert len(u.atoms[[0]].angles), 9 + assert len(u.atoms[[42]].angles), 2 def test_angles_identity(self, top): vals = top.angles.values @@ -98,7 +98,7 @@ def test_dihedrals_total_counts(self, top): def test_dihedrals_atom_counts(self, filename): u = mda.Universe(filename) - assert len(u.atoms[0].dihedrals) == 14 + assert len(u.atoms[[0]].dihedrals) == 14 def test_dihedrals_identity(self, top): vals = top.dihedrals.values diff --git a/testsuite/MDAnalysisTests/topology/test_top.py b/testsuite/MDAnalysisTests/topology/test_top.py index 2898e5e0dc1..8a2ad2ae30c 100644 --- a/testsuite/MDAnalysisTests/topology/test_top.py +++ b/testsuite/MDAnalysisTests/topology/test_top.py @@ -62,24 +62,24 @@ def test_attr_size(self, top): def test_bonds_atom_counts(self, filename): u = mda.Universe(filename) - assert len(u.atoms[0].bonds) == self.expected_n_zero_bonds - assert len(u.atoms[self.atom_i].bonds) == self.expected_n_i_bonds + assert len(u.atoms[[0]].bonds) == self.expected_n_zero_bonds + assert len(u.atoms[[self.atom_i]].bonds) == self.expected_n_i_bonds def test_angles_atom_counts(self, filename): u = mda.Universe(filename) - assert len(u.atoms[0].angles) == self.expected_n_zero_angles - assert len(u.atoms[self.atom_i].angles) == self.expected_n_i_angles + assert len(u.atoms[[0]].angles) == self.expected_n_zero_angles + assert len(u.atoms[[self.atom_i]].angles) == self.expected_n_i_angles def test_dihedrals_atom_counts(self, filename): u = mda.Universe(filename) - assert len(u.atoms[0].dihedrals) == self.expected_n_zero_dihedrals - assert len(u.atoms[self.atom_i].dihedrals) == \ + assert len(u.atoms[[0]].dihedrals) == self.expected_n_zero_dihedrals + assert len(u.atoms[[self.atom_i]].dihedrals) == \ self.expected_n_i_dihedrals def test_impropers_atom_counts(self, filename): u = mda.Universe(filename) - assert len(u.atoms[0].impropers) == self.expected_n_zero_impropers - assert len(u.atoms[self.atom_i].impropers) == \ + assert len(u.atoms[[0]].impropers) == self.expected_n_zero_impropers + assert len(u.atoms[[self.atom_i]].impropers) == \ self.expected_n_i_impropers def test_bonds_identity(self, top): From 829bb9f287cdb0699804ee039819d45b58134354 Mon Sep 17 00:00:00 2001 From: Lily Wang <31115101+lilyminium@users.noreply.github.com> Date: Wed, 28 Apr 2021 09:54:05 -0700 Subject: [PATCH 08/20] Update package/MDAnalysis/core/groups.py Co-authored-by: Jonathan Barnoud --- package/MDAnalysis/core/groups.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/MDAnalysis/core/groups.py b/package/MDAnalysis/core/groups.py index c8dfc037a3b..e04c98811b8 100644 --- a/package/MDAnalysis/core/groups.py +++ b/package/MDAnalysis/core/groups.py @@ -389,7 +389,7 @@ def get_connections(self, typename, outside=True): """ # AtomGroup has handy error messages for missing attributes ugroup = getattr(self.universe.atoms, typename) - if not len(ugroup): + if not ugroup: return ugroup func = np.any if outside else np.all try: From 8b64b46757fd37fe41ff308dff53455f7f155f26 Mon Sep 17 00:00:00 2001 From: Lily Wang Date: Wed, 28 Apr 2021 10:32:02 -0700 Subject: [PATCH 09/20] tighten up base class checking --- package/MDAnalysis/core/topologyattrs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/MDAnalysis/core/topologyattrs.py b/package/MDAnalysis/core/topologyattrs.py index edc257fd354..9dd324711af 100644 --- a/package/MDAnalysis/core/topologyattrs.py +++ b/package/MDAnalysis/core/topologyattrs.py @@ -299,7 +299,7 @@ def __init__(cls, name, bases, classdict): attrname = singular # add intra connections - if any("Connection" in x.__name__ for x in cls.__bases__): + if any(x.__name__ == "_Connection" for x in cls.__bases__): method = MethodType(intra_connection, cls) prop = property(method, None, None, method.__doc__) cls.transplants[AtomGroup].append((f"intra_{attrname}", prop)) From 88f821247cba8ded2ab21a67f76304c19c9b67b9 Mon Sep 17 00:00:00 2001 From: Lily Wang Date: Wed, 28 Apr 2021 10:34:03 -0700 Subject: [PATCH 10/20] update docstring --- package/MDAnalysis/core/topologyattrs.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package/MDAnalysis/core/topologyattrs.py b/package/MDAnalysis/core/topologyattrs.py index 9dd324711af..c7faea69fc9 100644 --- a/package/MDAnalysis/core/topologyattrs.py +++ b/package/MDAnalysis/core/topologyattrs.py @@ -2310,7 +2310,8 @@ def set_atoms(self, ag): def get_atoms(self, ag): """ - Get subset for atoms. + Get connection values where the atom indices are in + the given atomgroup. Parameters ---------- From 17797e927a8f59f3f4dadc80b0031f8850f3a301 Mon Sep 17 00:00:00 2001 From: Lily Wang Date: Wed, 28 Apr 2021 10:35:22 -0700 Subject: [PATCH 11/20] suppres warnings --- testsuite/MDAnalysisTests/core/test_groups.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/testsuite/MDAnalysisTests/core/test_groups.py b/testsuite/MDAnalysisTests/core/test_groups.py index 6142043a110..aeee158074e 100644 --- a/testsuite/MDAnalysisTests/core/test_groups.py +++ b/testsuite/MDAnalysisTests/core/test_groups.py @@ -28,6 +28,7 @@ ) import pytest import operator +import warnings import MDAnalysis as mda from MDAnalysis.exceptions import NoDataError @@ -1465,8 +1466,10 @@ def test_decorator(self, compound, pbc, unwrap): @pytest.fixture() def tpr(): - return mda.Universe(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""" From 3a34048c5349cf8a0443ce19492336bd8a59f293 Mon Sep 17 00:00:00 2001 From: Aditya Kamath <48089312+aditya-kamath@users.noreply.github.com> Date: Fri, 23 Apr 2021 07:29:28 -0400 Subject: [PATCH 12/20] Use absolute file paths in ITPParser (#3108) Fixes #3037 Co-authored-by: Lily Wang <31115101+lilyminium@users.noreply.github.com> --- package/CHANGELOG | 1 + package/MDAnalysis/topology/ITPParser.py | 3 +- .../gromacs/gromos54a7_edited.ff/test.itp | 2 + .../MDAnalysisTests/topology/test_itp.py | 55 ++++++++++++++++++- 4 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 testsuite/MDAnalysisTests/data/gromacs/gromos54a7_edited.ff/test.itp diff --git a/package/CHANGELOG b/package/CHANGELOG index ba9b6376238..56ea9225d1b 100644 --- a/package/CHANGELOG +++ b/package/CHANGELOG @@ -23,6 +23,7 @@ The rules for this file: * 2.0.0 Fixes + * ITPParser now accepts relative paths (Issue #3037, PR #3108) * Fixed issue with unassigned 'GAP' variable in fasta2algin function when resids are provided in input (Issue #3124) * Improve diffusionmap coverage (Issue #3208) diff --git a/package/MDAnalysis/topology/ITPParser.py b/package/MDAnalysis/topology/ITPParser.py index 952ee866d69..228fccebe73 100644 --- a/package/MDAnalysis/topology/ITPParser.py +++ b/package/MDAnalysis/topology/ITPParser.py @@ -270,10 +270,9 @@ def find_path(self, path): current_file = self.current_file try: - path = path.name + path = os.path.abspath(path.name) except AttributeError: pass - current_dir = os.path.dirname(current_file) dir_path = os.path.join(current_dir, path) if os.path.exists(dir_path): diff --git a/testsuite/MDAnalysisTests/data/gromacs/gromos54a7_edited.ff/test.itp b/testsuite/MDAnalysisTests/data/gromacs/gromos54a7_edited.ff/test.itp new file mode 100644 index 00000000000..def77229992 --- /dev/null +++ b/testsuite/MDAnalysisTests/data/gromacs/gromos54a7_edited.ff/test.itp @@ -0,0 +1,2 @@ +[ atoms ] +1 H 1 SOL HW1 1 0.41 1.00800 \ No newline at end of file diff --git a/testsuite/MDAnalysisTests/topology/test_itp.py b/testsuite/MDAnalysisTests/topology/test_itp.py index 3f0607d8def..bd79384a0ae 100644 --- a/testsuite/MDAnalysisTests/topology/test_itp.py +++ b/testsuite/MDAnalysisTests/topology/test_itp.py @@ -21,7 +21,7 @@ # J. Comput. Chem. 32 (2011), 2319--2327, doi:10.1002/jcc.21787 # import pytest - +from pathlib import Path import MDAnalysis as mda import numpy as np from numpy.testing import assert_almost_equal, assert_equal @@ -366,3 +366,56 @@ def test_missing_endif(self): with pytest.raises(IOError): with self.parser(ITP_no_endif) as p: top = p.parse(include_dir=GMX_DIR) + + +class TestRelativePath: + def test_relstring(self, tmpdir): + content = """ #include "../sub3/test2.itp" + [ atoms ] + 1 H 1 SOL HW1 1 0.41 1.00800 + """ + content2 = """[ atoms ] + 1 H 1 SOL HW1 1 0.41 1.00800 + """ + p = tmpdir.mkdir("sub1").join("test.itp") + p.write(content) + p3 = tmpdir.mkdir("sub3").join("test2.itp") + p3.write(content2) + p2 = tmpdir.mkdir("sub2") + p2.chdir() + with p2.as_cwd() as pchange: + u = mda.Universe(str("../sub1/test.itp"), format='ITP') + + def test_relpath(self, tmpdir): + content = """ + [ atoms ] + 1 H 1 SOL HW1 1 0.41 1.00800 + """ + p = tmpdir.mkdir("sub1").join("test.itp") + p.write(content) + p2 = tmpdir.mkdir("sub2") + p2.chdir() + with p2.as_cwd() as pchange: + relpath = Path("../sub1/test.itp") + u = mda.Universe(relpath, format='ITP') + + def test_relative_path(self, tmpdir): + test_itp_content = '#include "../atoms.itp"' + atoms_itp_content = """ + [ moleculetype ] + UNK 3 + + [ atoms ] + 1 H 1 SOL HW1 1 0.41 1.00800 + """ + with tmpdir.as_cwd(): + with open("atoms.itp", "w") as f: + f.write(atoms_itp_content) + subdir = tmpdir.mkdir("subdir") + with subdir.as_cwd(): + with open("test.itp", "w") as f: + f.write(test_itp_content) + subsubdir = subdir.mkdir("subsubdir") + with subsubdir.as_cwd(): + u = mda.Universe("../test.itp") + assert len(u.atoms) == 1 From 8fcf3541569dc6d98d2bdfcc633db86e95286119 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Bouysset?= Date: Fri, 23 Apr 2021 23:40:44 +0200 Subject: [PATCH 13/20] Adds aromaticity and Gasteiger charges guessers (#2926) Towards #2468 ## Work done in this PR * Add aromaticity and Gasteiger charges guessers which work via the RDKIT converter. --- package/MDAnalysis/topology/guessers.py | 47 ++++++++++++++++++ .../MDAnalysisTests/topology/test_guessers.py | 49 +++++++++++++++++++ 2 files changed, 96 insertions(+) diff --git a/package/MDAnalysis/topology/guessers.py b/package/MDAnalysis/topology/guessers.py index 4098f58879a..b85842bc439 100644 --- a/package/MDAnalysis/topology/guessers.py +++ b/package/MDAnalysis/topology/guessers.py @@ -475,3 +475,50 @@ def guess_atom_charge(atomname): """ # TODO: do something slightly smarter, at least use name/element return 0.0 + + +def guess_aromaticities(atomgroup): + """Guess aromaticity of atoms using RDKit + + Parameters + ---------- + atomgroup : mda.core.groups.AtomGroup + Atoms for which the aromaticity will be guessed + + Returns + ------- + aromaticities : numpy.ndarray + Array of boolean values for the aromaticity of each atom + + + .. versionadded:: 2.0.0 + """ + mol = atomgroup.convert_to("RDKIT") + atoms = sorted(mol.GetAtoms(), + key=lambda a: a.GetIntProp("_MDAnalysis_index")) + return np.array([atom.GetIsAromatic() for atom in atoms]) + + +def guess_gasteiger_charges(atomgroup): + """Guess Gasteiger partial charges using RDKit + + Parameters + ---------- + atomgroup : mda.core.groups.AtomGroup + Atoms for which the charges will be guessed + + Returns + ------- + charges : numpy.ndarray + Array of float values representing the charge of each atom + + + .. versionadded:: 2.0.0 + """ + mol = atomgroup.convert_to("RDKIT") + from rdkit.Chem.rdPartialCharges import ComputeGasteigerCharges + ComputeGasteigerCharges(mol, throwOnParamFailure=True) + atoms = sorted(mol.GetAtoms(), + key=lambda a: a.GetIntProp("_MDAnalysis_index")) + return np.array([atom.GetDoubleProp("_GasteigerCharge") for atom in atoms], + dtype=np.float32) diff --git a/testsuite/MDAnalysisTests/topology/test_guessers.py b/testsuite/MDAnalysisTests/topology/test_guessers.py index 8d5758103fc..8135fe8a276 100644 --- a/testsuite/MDAnalysisTests/topology/test_guessers.py +++ b/testsuite/MDAnalysisTests/topology/test_guessers.py @@ -32,6 +32,19 @@ from MDAnalysisTests.core.test_fragments import make_starshape import MDAnalysis.tests.datafiles as datafiles +from MDAnalysisTests.util import import_not_available + + +try: + from rdkit import Chem + from rdkit.Chem.rdPartialCharges import ComputeGasteigerCharges +except ImportError: + pass + +requires_rdkit = pytest.mark.skipif(import_not_available("rdkit"), + reason="requires RDKit") + + class TestGuessMasses(object): def test_guess_masses(self): out = guessers.guess_masses(['C', 'C', 'H']) @@ -139,3 +152,39 @@ def test_guess_bonds_peptide(): bonds = guessers.guess_bonds(u.atoms, u.atoms.positions) assert_equal(np.sort(u.bonds.indices, axis=0), np.sort(bonds, axis=0)) + + +@pytest.mark.parametrize("smi", [ + "c1ccccc1", + "C1=CC=CC=C1", + "CCO", + "c1ccccc1Cc1ccccc1", + "CN1C=NC2=C1C(=O)N(C(=O)N2C)C", +]) +@requires_rdkit +def test_guess_aromaticities(smi): + mol = Chem.MolFromSmiles(smi) + mol = Chem.AddHs(mol) + expected = np.array([atom.GetIsAromatic() for atom in mol.GetAtoms()]) + u = mda.Universe(mol) + values = guessers.guess_aromaticities(u.atoms) + assert_equal(values, expected) + + +@pytest.mark.parametrize("smi", [ + "c1ccccc1", + "C1=CC=CC=C1", + "CCO", + "c1ccccc1Cc1ccccc1", + "CN1C=NC2=C1C(=O)N(C(=O)N2C)C", +]) +@requires_rdkit +def test_guess_gasteiger_charges(smi): + mol = Chem.MolFromSmiles(smi) + mol = Chem.AddHs(mol) + ComputeGasteigerCharges(mol, throwOnParamFailure=True) + expected = np.array([atom.GetDoubleProp("_GasteigerCharge") + for atom in mol.GetAtoms()], dtype=np.float32) + u = mda.Universe(mol) + values = guessers.guess_gasteiger_charges(u.atoms) + assert_equal(values, expected) From e089787974fdfcdbeb475a4c49ffe2627e9754c0 Mon Sep 17 00:00:00 2001 From: Tyler Reddy Date: Sat, 24 Apr 2021 09:18:20 -0600 Subject: [PATCH 14/20] BLD: handle gcc on MacOS (#3234) Fixes #3109 ## Work done in this PR * gracefully handle the case where `gcc` toolchain in use on MacOS has been built from source using `clang` by `spack` (so it really is `gcc` in use, not `clang`) ## Notes * we could try to add regression testing, but a few problems: - `using_clang()` is inside `setup.py`, which probably can't be safely imported because it has unguarded statements/ code blocks that run right away - testing build issues is typically tricky with mocking, etc. (though in this case, probably just need to move `using_clang()` somewhere else and then test it against a variety of compiler metadata strings --- package/setup.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/package/setup.py b/package/setup.py index bdbe9ade8a7..224ef9bd8e7 100755 --- a/package/setup.py +++ b/package/setup.py @@ -253,7 +253,19 @@ def using_clang(): compiler = new_compiler() customize_compiler(compiler) compiler_ver = getoutput("{0} -v".format(compiler.compiler[0])) - return 'clang' in compiler_ver + if 'Spack GCC' in compiler_ver: + # when gcc toolchain is built from source with spack + # using clang, the 'clang' string may be present in + # the compiler metadata, but it is not clang + is_clang = False + elif 'clang' in compiler_ver: + # by default, Apple will typically alias gcc to + # clang, with some mention of 'clang' in the + # metadata + is_clang = True + else: + is_clang = False + return is_clang def extensions(config): From e1c07a6995b36e8e80853ef96e1e0e66f359d91c Mon Sep 17 00:00:00 2001 From: Lily Wang <31115101+lilyminium@users.noreply.github.com> Date: Mon, 26 Apr 2021 16:44:10 -0700 Subject: [PATCH 15/20] Remove ParmEd Timestep writing "support" (#3240) Fixes #3031 --- package/MDAnalysis/coordinates/ParmEd.py | 5 +---- testsuite/MDAnalysisTests/coordinates/test_parmed.py | 6 ++++++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/package/MDAnalysis/coordinates/ParmEd.py b/package/MDAnalysis/coordinates/ParmEd.py index 997921e7e56..de8205ee386 100644 --- a/package/MDAnalysis/coordinates/ParmEd.py +++ b/package/MDAnalysis/coordinates/ParmEd.py @@ -172,10 +172,7 @@ def convert(self, obj): # make sure to use atoms (Issue 46) ag_or_ts = obj.atoms except AttributeError: - if isinstance(obj, base.Timestep): - ag_or_ts = obj.copy() - else: - raise_from(TypeError("No Timestep found in obj argument"), None) + raise TypeError("No atoms found in obj argument") from None # Check for topology information missing_topology = [] diff --git a/testsuite/MDAnalysisTests/coordinates/test_parmed.py b/testsuite/MDAnalysisTests/coordinates/test_parmed.py index 907c35135ed..99c93ea4bb7 100644 --- a/testsuite/MDAnalysisTests/coordinates/test_parmed.py +++ b/testsuite/MDAnalysisTests/coordinates/test_parmed.py @@ -29,6 +29,7 @@ from MDAnalysisTests.coordinates.base import _SingleFrameReader from MDAnalysisTests.coordinates.reference import RefAdKSmall +from MDAnalysis.coordinates.ParmEd import ParmEdConverter from MDAnalysisTests.datafiles import ( GRO, @@ -286,3 +287,8 @@ class TestParmEdConverterPDB(BaseTestParmEdConverter): def test_equivalent_coordinates(self, ref, output): assert_almost_equal(ref.coordinates, output.coordinates, decimal=3) +def test_incorrect_object_passed_typeerror(): + err = "No atoms found in obj argument" + with pytest.raises(TypeError, match=err): + c = ParmEdConverter() + c.convert("we still don't support emojis :(") From cc1e43a5e429517a95708a84696edbea3e36cafa Mon Sep 17 00:00:00 2001 From: Irfan Alibay Date: Tue, 27 Apr 2021 19:29:05 +0100 Subject: [PATCH 16/20] Adding py3.9 to gh actions CI matrix (#3245) * Fixes #2974 * Python 3.9 officially supported * Add Python 3.9 to testing matrix * Adds macOS CI entry, formalises 3.9 support --- .github/workflows/gh-ci.yaml | 12 +++++++++--- package/CHANGELOG | 4 +--- package/setup.py | 1 + testsuite/setup.py | 1 + 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/.github/workflows/gh-ci.yaml b/.github/workflows/gh-ci.yaml index df9a120f9fe..70419bf5ebf 100644 --- a/.github/workflows/gh-ci.yaml +++ b/.github/workflows/gh-ci.yaml @@ -28,17 +28,23 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, ] - python-version: [3.6, 3.7, 3.8] + python-version: [3.6, 3.7, 3.8, 3.9] run_type: [FULL, ] install_hole: [true, ] codecov: [true, ] include: - - name: macOS + - name: macOS_py39 os: macOS-latest - python-version: 3.7 + python-version: 3.9 run_type: FULL install_hole: true codecov: true + - name: macOS_py36_min + os: macOS-latest + python-version: 3.6 + run_type: MIN + install_hole: false + codecov: false - name: minimal-ubuntu os: ubuntu-latest python-version: 3.6 diff --git a/package/CHANGELOG b/package/CHANGELOG index 56ea9225d1b..3b1161ebbdc 100644 --- a/package/CHANGELOG +++ b/package/CHANGELOG @@ -103,9 +103,7 @@ 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) + * Adds python 3.9 support (Issue #2974, PR #3027, #3245) * Added an MDAnalysis shields.io badge to the README (Issue #3227, PR #3229) * Added sort method to the atomgroup (Issue #2976, PR #3188) * ITPParser now reads [ atomtypes ] sections in ITP files, used for charges diff --git a/package/setup.py b/package/setup.py index 224ef9bd8e7..09ba962520c 100755 --- a/package/setup.py +++ b/package/setup.py @@ -577,6 +577,7 @@ def long_description(readme): 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', 'Programming Language :: C', 'Topic :: Scientific/Engineering', 'Topic :: Scientific/Engineering :: Bio-Informatics', diff --git a/testsuite/setup.py b/testsuite/setup.py index 0bb0d73ca73..b799b52cbe0 100755 --- a/testsuite/setup.py +++ b/testsuite/setup.py @@ -104,6 +104,7 @@ def run(self): 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', 'Programming Language :: C', 'Topic :: Scientific/Engineering', 'Topic :: Scientific/Engineering :: Bio-Informatics', From eb0e5c06454899048f2066db687339a424b48acf Mon Sep 17 00:00:00 2001 From: Lily Wang Date: Wed, 28 Apr 2021 10:44:34 -0700 Subject: [PATCH 17/20] fix changelog --- package/CHANGELOG | 3 +++ 1 file changed, 3 insertions(+) diff --git a/package/CHANGELOG b/package/CHANGELOG index 3b1161ebbdc..45257c74c73 100644 --- a/package/CHANGELOG +++ b/package/CHANGELOG @@ -103,6 +103,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) * Adds python 3.9 support (Issue #2974, PR #3027, #3245) * Added an MDAnalysis shields.io badge to the README (Issue #3227, PR #3229) * Added sort method to the atomgroup (Issue #2976, PR #3188) From e041366a7749224eaf54ec6f3a73b6a390939ffd Mon Sep 17 00:00:00 2001 From: Lily Wang Date: Thu, 29 Apr 2021 15:07:57 -0700 Subject: [PATCH 18/20] special metaclass --- package/MDAnalysis/core/topologyattrs.py | 29 ++++++++++++++++++------ 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/package/MDAnalysis/core/topologyattrs.py b/package/MDAnalysis/core/topologyattrs.py index c7faea69fc9..89106a18dc1 100644 --- a/package/MDAnalysis/core/topologyattrs.py +++ b/package/MDAnalysis/core/topologyattrs.py @@ -298,12 +298,6 @@ def __init__(cls, name, bases, classdict): if attrname is None: attrname = singular - # add intra connections - if any(x.__name__ == "_Connection" for x in cls.__bases__): - method = MethodType(intra_connection, cls) - prop = property(method, None, None, method.__doc__) - cls.transplants[AtomGroup].append((f"intra_{attrname}", prop)) - if singular: _TOPOLOGY_ATTRS[singular] = _TOPOLOGY_ATTRS[attrname] = cls _singular = singular.lower().replace('_', '') @@ -2260,7 +2254,28 @@ 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: + # add intra connections + if any(x.__name__ == "_Connection" for x in cls.__bases__): + 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 From 2a06cb76d42eaf4c36219573e4c13f63b0fc97c9 Mon Sep 17 00:00:00 2001 From: Lily Wang Date: Thu, 29 Apr 2021 15:11:28 -0700 Subject: [PATCH 19/20] move function down --- package/MDAnalysis/core/topologyattrs.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/package/MDAnalysis/core/topologyattrs.py b/package/MDAnalysis/core/topologyattrs.py index 89106a18dc1..f75dfc9d6bf 100644 --- a/package/MDAnalysis/core/topologyattrs.py +++ b/package/MDAnalysis/core/topologyattrs.py @@ -265,11 +265,6 @@ def _attach_transplant_stubs(attribute_name, topology_attribute_class): setattr(dest_class, method_name, stub) -def intra_connection(self, ag): - """Get connections only within this AtomGroup - """ - return ag.get_connections(self.attrname, outside=False) - class _TopologyAttrMeta(type): r"""Register TopologyAttrs on class creation @@ -2267,6 +2262,12 @@ def __init__(cls, name, bases, classdict): if attrname is not None: # add intra connections + + def intra_connection(self, ag): + """Get connections only within this AtomGroup + """ + return ag.get_connections(attrname, outside=False) + if any(x.__name__ == "_Connection" for x in cls.__bases__): method = MethodType(intra_connection, cls) prop = property(method, None, None, method.__doc__) From 0eacf67cd25565de90129b48b33084ab8469337b Mon Sep 17 00:00:00 2001 From: Lily Wang Date: Thu, 29 Apr 2021 15:15:24 -0700 Subject: [PATCH 20/20] tidy code --- package/MDAnalysis/core/topologyattrs.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/package/MDAnalysis/core/topologyattrs.py b/package/MDAnalysis/core/topologyattrs.py index 0e4a29c1d97..a3d0940544c 100644 --- a/package/MDAnalysis/core/topologyattrs.py +++ b/package/MDAnalysis/core/topologyattrs.py @@ -2272,17 +2272,14 @@ def __init__(cls, name, bases, classdict): attrname = classdict.get('attrname') if attrname is not None: - # add intra connections - def intra_connection(self, ag): """Get connections only within this AtomGroup """ return ag.get_connections(attrname, outside=False) - if any(x.__name__ == "_Connection" for x in cls.__bases__): - method = MethodType(intra_connection, cls) - prop = property(method, None, None, method.__doc__) - cls.transplants[AtomGroup].append((f"intra_{attrname}", prop)) + 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)