Skip to content

Commit

Permalink
change EnsembleAnalysis developer API (#216)
Browse files Browse the repository at this point in the history
* EnsembleAnalysis base class raises NotImplementedError for BOTH _single_frame() and
   _single_universe(). Derived classes should override ONE of the two.  
   The NotImplementedError signals to run() to skip the method.
* update docs with example
* add tests
* update CHANGELOG
  • Loading branch information
cadeduckworth authored Dec 12, 2022
1 parent cc104f0 commit e395ac7
Show file tree
Hide file tree
Showing 6 changed files with 100 additions and 29 deletions.
12 changes: 9 additions & 3 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,19 @@ CHANGES for MDPOW
Add summary of changes for each release. Use ISO 8061 dates. Reference
GitHub issues numbers and PR numbers.



2022-??-?? 0.8.1
2022-??-?? 0.9.0
cadeduckworth, orbeckst, VOD555

Changes

* for ensemble.EnsembleAnalysis._single_frame()
changed 'pass' to 'raise NotImplementedError' (#216)
* for ensemble.EnsembleAnalysis._single_universe()
changed 'pass' to 'raise NotImplementedError' (#216)
* for ensemble.EnsembleAnalysis.run() changed to detect
and use _single_frame OR _single_universe (#216)
* _prepare_universe and _conclude_universe removed from
EnsembleAnalysis.run() method, no longer needed (per comments, #199)
* added support for Python 3.10
* dropped testing on Python 3.6

Expand Down
12 changes: 10 additions & 2 deletions doc/sphinx/source/analysis/ensemble_analysis.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,18 @@ Universes and be analyzed as a group.

:class:`~mdpow.analysis.ensemble.EnsembleAnalysis` is a class inspired by the
:class:`AnalysisBase <MDAnalysis.analysis.base.AnalysisBase>` from MDAnalysis which
iterates over the systems in the ensemble and the frames in the systems. It sets up both iterations between
universes and universe frames allowing for analysis to be run on both whole systems and the frames of those
iterates over the systems in the ensemble or the frames in the systems. It sets up iterations between
universes or universe frames allowing for analysis to be run on either whole systems or the frames of those
systems. This allows for users to easily run analyses on MDPOW simulations.

:exc:`NotImplementedError` will detect whether :meth:`~EnsembleAnalysis._single_universe`
or :meth:`~EnsembleAnalysis._single_frame` should be implemented, based on which
is defined in the :class:`~mdpow.analysis.ensemble.EnsembleAnalysis`. Only one of the
two methods should be defined for an :class:`~mdpow.analysis.ensemble.EnsembleAnalysis`.
For verbose functionality, the analysis may show two iteration bars,
where only one of which will actually be iterated, while the other will
load to completion instantaneously, showing the system that is being worked on.

.. autoclass:: mdpow.analysis.ensemble.EnsembleAnalysis
:members:

Expand Down
4 changes: 2 additions & 2 deletions mdpow/analysis/dihedral.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,8 @@ def check_dihedral_inputs(selections):
for group in selections:
for k in group.keys():
if len(group[k]) != 4:
msg = ''''Dihedral calculations require AtomGroups with
only 4 atoms, %s selected''' % len(group)
msg = ("Dihedral calculations require AtomGroups with "
f"only 4 atoms, {len(group)} selected")
logger.error(msg)
raise SelectionError(msg)

Expand Down
64 changes: 42 additions & 22 deletions mdpow/analysis/ensemble.py
Original file line number Diff line number Diff line change
Expand Up @@ -464,19 +464,35 @@ def _setup_frames(self, trajectory):
self.times = np.zeros(self.n_frames)

def _single_universe(self):
"""Calculations on a single Universe object.
Run on each universe in the ensemble during when
self.run in called.
"""Calculations on a single :class:`MDAnalysis.Universe
<MDAnalysis.core.groups.universe.Universe>` object.
Run on each :class:`MDAnalysis.Universe
<MDAnalysis.core.groups.universe.Universe>`
in the :class:`~mdpow.analysis.ensemble.Ensemble`
during when :meth:`run` in called.
:exc:`NotImplementedError` will detect whether
:meth:`~EnsembleAnalysis._single_universe`
or :meth:`~EnsembleAnalysis._single_frame`
should be implemented, based on which is defined
in the :class:`~mdpow.analysis.ensemble.EnsembleAnalysis`.
"""
pass # pragma: no cover
raise NotImplementedError

def _single_frame(self):
"""Calculate data from a single frame of trajectory
Called on each frame for universes in the Ensemble.
"""Calculate data from a single frame of trajectory.
Called on each frame for each
:class:`MDAnalysis.Universe <MDAnalysis.core.groups.universe.Universe>`
in the :class:`~mdpow.analysis.ensemble.Ensemble`.
:exc:`NotImplementedError` will detect whether
:meth:`~EnsembleAnalysis._single_universe`
or :meth:`~EnsembleAnalysis._single_frame`
should be implemented, based on which is defined
in the :class:`~mdpow.analysis.ensemble.EnsembleAnalysis`.
"""
pass # pragma: no cover
raise NotImplementedError

def _prepare_ensemble(self):
"""For establishing data structures used in running
Expand Down Expand Up @@ -505,27 +521,31 @@ def _conclude_ensemble(self):
pass # pragma: no cover

def run(self, start=None, stop=None, step=None):
"""Runs _single_universe on each system and _single_frame
"""Runs :meth:`~EnsembleAnalysis._single_universe`
on each system or :meth:`~EnsembleAnalysis._single_frame`
on each frame in the system.
First iterates through keys of ensemble, then runs _setup_system
which defines the system and trajectory. Then iterates over
trajectory frames.
First iterates through keys of ensemble, then runs
:meth:`~EnsembleAnalysis._setup_system`which defines
the system and trajectory. Then iterates over each
system universe or trajectory frames of each universe
as defined by :meth:`~EnsembleAnalysis._single_universe`
or :meth:`~EnsembleAnalysis._single_frame`.
"""
logger.info("Setting up systems")
self._prepare_ensemble()
for self._key in ProgressBar(self._ensemble.keys(), verbose=True):
self._setup_system(self._key, start=start, stop=stop, step=step)
self._prepare_universe()
self._single_universe()
for i, ts in enumerate(ProgressBar(self._trajectory[self.start:self.stop:self.step], verbose=True,
try:
self._single_universe()
except NotImplementedError:
for i, ts in enumerate(ProgressBar(self._trajectory[self.start:self.stop:self.step], verbose=True,
postfix=f'running system {self._key}')):
self._frame_index = i
self._ts = ts
self.frames[i] = ts.frame
self.times[i] = ts.time
self._single_frame()
self._conclude_universe()
self._frame_index = i
self._ts = ts
self.frames[i] = ts.frame
self.times[i] = ts.time
self._single_frame()
logger.info("Moving to next universe")
logger.info("Finishing up")
self._conclude_ensemble()
Expand Down
5 changes: 5 additions & 0 deletions mdpow/tests/test_dihedral.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,8 @@ def test_ValueError_different_ensemble(self):
match='Dihedral selections from different Ensembles, '):
DihedralAnalysis([dh1, dh2])

def test_single_universe(self):
dh = self.Ens.select_atoms('name C4', 'name C17', 'name S2', 'name N3')
with pytest.raises(NotImplementedError):
DihedralAnalysis([dh])._single_universe()

32 changes: 32 additions & 0 deletions mdpow/tests/test_ensemble.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,38 @@ def _conclude_universe(self):
TestRun = TestAnalysis(Sim).run(start=0, step=1, stop=10)
assert Sim.keys() == TestRun.key_list

def test_ensemble_analysis_run_frame(self):
class TestAnalysis(EnsembleAnalysis):
def __init__(self, test_ensemble):
super(TestAnalysis, self).__init__(test_ensemble)

self._ens = test_ensemble

def _single_universe(self):
pass

Sim = Ensemble(dirname=self.tmpdir.name, solvents=['water'])
TestRun = TestAnalysis(Sim)

with pytest.raises(NotImplementedError):
TestRun._single_frame()

def test_ensemble_analysis_run_universe(self):
class TestAnalysis(EnsembleAnalysis):
def __init__(self, test_ensemble):
super(TestAnalysis, self).__init__(test_ensemble)

self._ens = test_ensemble

def _single_frame(self):
pass

Sim = Ensemble(dirname=self.tmpdir.name, solvents=['water'])
TestRun = TestAnalysis(Sim)

with pytest.raises(NotImplementedError):
TestRun._single_universe()

def test_value_error(self):
ens = Ensemble(dirname=self.tmpdir.name, solvents=['water'])
copy_ens = Ensemble()
Expand Down

0 comments on commit e395ac7

Please sign in to comment.