diff --git a/CHANGES b/CHANGES index 982449bd..1b9f55e7 100644 --- a/CHANGES +++ b/CHANGES @@ -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 diff --git a/doc/sphinx/source/analysis/ensemble_analysis.txt b/doc/sphinx/source/analysis/ensemble_analysis.txt index 67c6bc21..693b2c32 100644 --- a/doc/sphinx/source/analysis/ensemble_analysis.txt +++ b/doc/sphinx/source/analysis/ensemble_analysis.txt @@ -15,10 +15,18 @@ Universes and be analyzed as a group. :class:`~mdpow.analysis.ensemble.EnsembleAnalysis` is a class inspired by the :class:`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: diff --git a/mdpow/analysis/dihedral.py b/mdpow/analysis/dihedral.py index 241c1e37..69b3c0a9 100644 --- a/mdpow/analysis/dihedral.py +++ b/mdpow/analysis/dihedral.py @@ -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) diff --git a/mdpow/analysis/ensemble.py b/mdpow/analysis/ensemble.py index bc75f012..d4ba40a6 100644 --- a/mdpow/analysis/ensemble.py +++ b/mdpow/analysis/ensemble.py @@ -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 + ` object. + + Run on each :class:`MDAnalysis.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 ` + 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 @@ -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() diff --git a/mdpow/tests/test_dihedral.py b/mdpow/tests/test_dihedral.py index 6f2c65bd..21025b58 100644 --- a/mdpow/tests/test_dihedral.py +++ b/mdpow/tests/test_dihedral.py @@ -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() + diff --git a/mdpow/tests/test_ensemble.py b/mdpow/tests/test_ensemble.py index 9d8b2462..e361dcb3 100644 --- a/mdpow/tests/test_ensemble.py +++ b/mdpow/tests/test_ensemble.py @@ -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()