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

Serialization of AtomGroup #2893

Merged
merged 13 commits into from
Aug 21, 2020
22 changes: 11 additions & 11 deletions package/MDAnalysis/core/groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,16 +110,7 @@
from ._get_readers import get_writer_for, get_converter_for


def _unpickle(uhash, ix):
try:
u = _ANCHOR_UNIVERSES[uhash]
except KeyError:
# doesn't provide as nice an error message as before as only hash of universe is stored
# maybe if we pickled the filename too we could do better...
errmsg = (f"Couldn't find a suitable Universe to unpickle AtomGroup "
f"onto with Universe hash '{uhash}'. Availble hashes: "
f"{', '.join([str(k) for k in _ANCHOR_UNIVERSES.keys()])}")
raise RuntimeError(errmsg) from None
def _unpickle(u, ix):
return u.atoms[ix]


Expand Down Expand Up @@ -2252,21 +2243,30 @@ class AtomGroup(GroupBase):
:class:`AtomGroup` instances are always bound to a
:class:`MDAnalysis.core.universe.Universe`. They cannot exist in isolation.

During serialization, :class:`AtomGroup` will be pickled with its bound
:class:`MDAnalysis.core.universe.Universe`. If multiple :class:`AtomGroup`
are bound to the same :class:`MDAnalysis.core.universe.Universe`, they
will bound to the same one after serialization.


See Also
--------
:class:`MDAnalysis.core.universe.Universe`


.. deprecated:: 0.16.2
*Instant selectors* of :class:`AtomGroup` will be removed in the 1.0
release.
.. versionchanged:: 1.0.0
Removed instant selectors, use select_atoms('name ...') to select
atoms by name.
.. versionchanged:: 2.0.0
:class:`AtomGroup` can always be pickled with or without its universe,
instead of failing when not finding its anchored universe.
"""

def __reduce__(self):
return (_unpickle, (self.universe.anchor_name, self.ix))
return (_unpickle, (self.universe, self.ix))

def __getattr__(self, attr):
# special-case timestep info
Expand Down
102 changes: 43 additions & 59 deletions testsuite/MDAnalysisTests/utils/test_persistence.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ def universe_n():
def ag(universe):
return universe.atoms[:20]

@staticmethod
@pytest.fixture()
def ag_2(universe):
return universe.atoms[10:20]

@staticmethod
@pytest.fixture()
def ag_n(universe_n):
Expand All @@ -69,76 +74,37 @@ def pickle_str(ag):
def pickle_str_n(ag_n):
return pickle.dumps(ag_n, protocol=pickle.HIGHEST_PROTOCOL)

@staticmethod
@pytest.fixture()
def pickle_str(ag):
return pickle.dumps(ag, protocol=pickle.HIGHEST_PROTOCOL)

@staticmethod
@pytest.fixture()
def pickle_str_two_ag(ag, ag_2):
return pickle.dumps((ag, ag_2), protocol=pickle.HIGHEST_PROTOCOL)

@staticmethod
@pytest.fixture()
def pickle_str_ag_with_universe_first(ag, universe):
return pickle.dumps((universe, ag), protocol=pickle.HIGHEST_PROTOCOL)

@staticmethod
@pytest.fixture()
def pickle_str_ag_with_universe(ag, universe):
return pickle.dumps((ag, universe), protocol=pickle.HIGHEST_PROTOCOL)

def test_unpickle(self, pickle_str, ag, universe):
"""Test that an AtomGroup can be unpickled (Issue 293)"""
newag = pickle.loads(pickle_str)
# Can unpickle
assert_equal(ag.indices, newag.indices)
assert newag.universe is universe, "Unpickled AtomGroup on wrong Universe."

def test_unpickle_named(self, pickle_str_n, ag_n, universe_n):
"""Test that an AtomGroup can be unpickled (Issue 293)"""
newag = pickle.loads(pickle_str_n)
# Can unpickle
assert_equal(ag_n.indices, newag.indices)
assert newag.universe is universe_n, "Unpickled AtomGroup on wrong Universe."

def test_unpickle_missing(self):
universe = mda.Universe(PDB_small, PDB_small, PDB_small)
universe_n = mda.Universe(PDB_small, PDB_small, PDB_small,
anchor_name="test1")
ag = universe.atoms[:20] # prototypical AtomGroup
ag_n = universe_n.atoms[:10]
pickle_str_n = pickle.dumps(ag_n, protocol=pickle.HIGHEST_PROTOCOL)
# Kill AtomGroup and Universe
del ag_n
del universe_n
# and make sure they're very dead
gc.collect()
# we shouldn't be able to unpickle
# assert_raises(RuntimeError, pickle.loads, pickle_str_n)
with pytest.raises(RuntimeError):
pickle.loads(pickle_str_n)

def test_unpickle_noanchor(self, universe, pickle_str):
# Shouldn't unpickle if the universe is removed from the anchors
universe.remove_anchor()
# In the complex (parallel) testing environment there's the risk of
# other compatible Universes being available for anchoring even after
# this one is expressly removed.
# assert_raises(RuntimeError, pickle.loads, pickle_str)
with pytest.raises(RuntimeError):
pickle.loads(pickle_str)
# If this fails to raise an exception either:
# 1-the anchoring Universe failed to remove_anchor or 2-another
# Universe with the same characteristics was created for testing and is
# being used as anchor."

def test_unpickle_reanchor(self, universe, pickle_str, ag):
# universe is removed from the anchors
universe.remove_anchor()
# now it goes back into the anchor list again
universe.make_anchor()
newag = pickle.loads(pickle_str)
assert_equal(ag.indices, newag.indices)
assert newag.universe is universe, "Unpickled AtomGroup on wrong Universe."

def test_unpickle_wrongname(self, universe_n, pickle_str_n):
# we change the universe's anchor_name
universe_n.anchor_name = "test2"
# shouldn't unpickle if no name matches, even if there's a compatible
# universe in the unnamed anchor list.
with pytest.raises(RuntimeError):
pickle.loads(pickle_str_n)

def test_unpickle_rename(self, universe_n, universe, pickle_str_n, ag_n):
# we change universe_n's anchor_name
universe_n.anchor_name = "test2"
# and make universe a named anchor
universe.anchor_name = "test1"
newag = pickle.loads(pickle_str_n)
assert_equal(ag_n.indices, newag.indices)
assert newag.universe is universe, "Unpickled AtomGroup on wrong Universe."

def test_pickle_unpickle_empty(self, universe):
"""Test that an empty AtomGroup can be pickled/unpickled (Issue 293)"""
Expand All @@ -147,6 +113,24 @@ def test_pickle_unpickle_empty(self, universe):
newag = pickle.loads(pickle_str)
assert len(newag) == 0

def test_unpickle_two_ag(self, pickle_str_two_ag):
newag, newag2 = pickle.loads(pickle_str_two_ag)
assert (newag.universe is newag2.universe,
"Two AtomGroups are unpickled to two different Universe")

def test_unpickle_ag_with_universe_first(self,
pickle_str_ag_with_universe_first):
newu, newag = pickle.loads(pickle_str_ag_with_universe_first)
assert (newag.universe is newu,
"AtomGroup is not unpickled to the bound Universe"
"when Universe is pickled first")

def test_unpickle_ag_with_universe(self,
pickle_str_ag_with_universe):
newag, newu = pickle.loads(pickle_str_ag_with_universe)
assert (newag.universe is newu,
"AtomGroup is not unpickled to the bound Universe"
"when AtomGroup is pickled first")

class TestPicklingUpdatingAtomGroups(object):

Expand Down