From e1cad7e880734cc4339da24e178b156a2df89d48 Mon Sep 17 00:00:00 2001 From: Philip Loche Date: Thu, 29 Apr 2021 15:13:32 +0200 Subject: [PATCH 01/37] Add results class for storing analysis results --- package/MDAnalysis/analysis/__init__.py | 14 +++ package/MDAnalysis/analysis/base.py | 111 +++++++++++++----- package/MDAnalysis/analysis/gnm.py | 44 +++++-- package/MDAnalysis/analysis/lineardensity.py | 105 +++++++++++------ package/MDAnalysis/analysis/polymer.py | 14 ++- .../MDAnalysisTests/analysis/test_gnm.py | 28 ++--- .../analysis/test_lineardensity.py | 2 +- .../analysis/test_persistencelength.py | 4 +- 8 files changed, 219 insertions(+), 103 deletions(-) diff --git a/package/MDAnalysis/analysis/__init__.py b/package/MDAnalysis/analysis/__init__.py index 1ac4648978a..2a4ab1ea1cd 100644 --- a/package/MDAnalysis/analysis/__init__.py +++ b/package/MDAnalysis/analysis/__init__.py @@ -27,6 +27,20 @@ The :mod:`MDAnalysis.analysis` sub-package contains various recipes and algorithms that can be used to analyze MD trajectories. +If not stated differently, an analysis conducted by the available modules +always follows the same structure + +1. Initiliaze the object prevously imported. +2. Run the analysis for specific trajectory slices +3. Acces the analysis from the `results` attribute (if available) + + .. code-block:: python + + from MDAnalysis.anlysis import AnalysisModule + + analysis_obj = AnalysisModule(, ...) + analysis_obj.run(, , ) + print(analysis_obj.results) If you use them please check if the documentation mentions any specific caveats and also if there are any published papers associated with these algorithms. diff --git a/package/MDAnalysis/analysis/base.py b/package/MDAnalysis/analysis/base.py index 7237fcae26e..4fbeb005681 100644 --- a/package/MDAnalysis/analysis/base.py +++ b/package/MDAnalysis/analysis/base.py @@ -40,12 +40,56 @@ logger = logging.getLogger(__name__) +class _Results(object): + r"""Class storing results obtained from an analysis. + + The class is stores all results obatined from + an analysis after the ``run`` call. + """ + + def __init__(self, analysis_cls): + """ + Parameters + ---------- + analysis_cls : AnalysisClass + parent analysis class + """ + self.analysis_cls = analysis_cls + + def __len__(self): + # Subract the analysis class attribute + return len(self.__dict__) - 1 + + def get_attribute_list(self): + """A list of all result attribute names.""" + return [key for key in self.__dict__.keys() if key != "analysis_cls"] + + def __repr__(self): + analysis_name = type(self.analysis_cls).__name__ + return f"<{analysis_name} results with {len(self)} attribute{'s'[len(self) == 1:]}>" + + def __str__(self): + analysis_name = type(self.analysis_cls).__name__ + attribute_list = self.get_attribute_list() + str_repr = f"<{analysis_name} results with " + str_repr += f"attribute{'s'[len(self) == 1:]}: " + + if len(self) <= 10: + str_repr += ", ".join(attribute_list) + else: + str_repr += ", ".join(attribute_list[:3]) + str_repr += " ... " + ", ".join(attribute_list[-3:]) + + return str_repr + ">" + + class AnalysisBase(object): - """Base class for defining multi frame analysis + r"""Base class for defining multi frame analysis The class it is designed as a template for creating multiframe analyses. This class will automatically take care of setting up the trajectory - reader for iterating, and it offers to show a progress meter. + reader for iterating, and it offers to show a progress meter. + Computed results are stored inside the `results` attribute. To define a new Analysis, `AnalysisBase` needs to be subclassed `_single_frame` must be defined. It is also possible to define @@ -70,21 +114,22 @@ def _prepare(self): def _single_frame(self): # REQUIRED # Called after the trajectory is moved onto each new frame. - # store result of `some_function` for a single frame - self.result.append(some_function(self._ag, self._parameter)) + # store a example_result of `some_function` for a single frame + self.example_result.append(some_function(self._ag, self._parameter)) def _conclude(self): # OPTIONAL # Called once iteration on the trajectory is finished. # Apply normalisation and averaging to results here. - self.result = np.asarray(self.result) / np.sum(self.result) + self.results.example_result = np.asarray(self.example_result) + self.results.example_result /= np.sum(self.result) Afterwards the new analysis can be run like this. .. code-block:: python na = NewAnalysis(u.select_atoms('name CA'), 35).run(start=10, stop=20) - print(na.result) + print(na.results.example_result) Attributes ---------- @@ -92,7 +137,8 @@ def _conclude(self): array of Timestep times. Only exists after calling run() frames: np.ndarray array of Timestep frame indices. Only exists after calling run() - + results: _Results + results of calculation are stored after call to ``run`` """ def __init__(self, trajectory, verbose=False, **kwargs): @@ -112,6 +158,7 @@ def __init__(self, trajectory, verbose=False, **kwargs): """ self._trajectory = trajectory self._verbose = verbose + self.results = _Results(self) def _setup_frames(self, trajectory, start=None, stop=None, step=None): """ @@ -198,21 +245,21 @@ def run(self, start=None, stop=None, step=None, verbose=None): class AnalysisFromFunction(AnalysisBase): - """ - Create an analysis from a function working on AtomGroups + r"""Create an analysis from a function working on AtomGroups - Attributes - ---------- - results : ndarray - results of calculation are stored after call to ``run`` + .. code-block:: python - Example - ------- - >>> def rotation_matrix(mobile, ref): - >>> return mda.analysis.align.rotation_matrix(mobile, ref)[0] + def rotation_matrix(mobile, ref): + return mda.analysis.align.rotation_matrix(mobile, ref)[0] - >>> rot = AnalysisFromFunction(rotation_matrix, trajectory, mobile, ref).run() - >>> print(rot.results) + rot = AnalysisFromFunction(rotation_matrix, trajectory, mobile, ref).run() + print(rot.results) + + Attributes + ---------- + results : asarray + calculation results for each frame opf the underlaying function + stored after call to ``run`` Raises ------ @@ -271,23 +318,25 @@ def _conclude(self): def analysis_class(function): - """ - Transform a function operating on a single frame to an analysis class + r"""Transform a function operating on a single frame to an analysis class - For an usage in a library we recommend the following style: + For an usage in a library we recommend the following style - >>> def rotation_matrix(mobile, ref): - >>> return mda.analysis.align.rotation_matrix(mobile, ref)[0] - >>> RotationMatrix = analysis_class(rotation_matrix) + .. code-block:: python - It can also be used as a decorator: + def rotation_matrix(mobile, ref): + return mda.analysis.align.rotation_matrix(mobile, ref)[0] + RotationMatrix = analysis_class(rotation_matrix) - >>> @analysis_class - >>> def RotationMatrix(mobile, ref): - >>> return mda.analysis.align.rotation_matrix(mobile, ref)[0] + It can also be used as a decorator + + .. code-block:: python + @analysis_class + def RotationMatrix(mobile, ref): + return mda.analysis.align.rotation_matrix(mobile, ref)[0] - >>> rot = RotationMatrix(u.trajectory, mobile, ref).run(step=2) - >>> print(rot.results) + rot = RotationMatrix(u.trajectory, mobile, ref).run(step=2) + print(rot.results) """ class WrapperClass(AnalysisFromFunction): diff --git a/package/MDAnalysis/analysis/gnm.py b/package/MDAnalysis/analysis/gnm.py index d4fd2700f34..e6ee66868ff 100644 --- a/package/MDAnalysis/analysis/gnm.py +++ b/package/MDAnalysis/analysis/gnm.py @@ -41,7 +41,9 @@ .. _Cookbook: https://github.com/MDAnalysis/MDAnalysisCookbook The basic approach is to pass a trajectory to :class:`GNMAnalysis` and then run -the analysis:: +the analysis: + +.. code-block:: python u = MDAnalysis.Universe(PSF, DCD) C = MDAnalysis.analysis.gnm.GNMAnalysis(u, ReportVector="output.txt") @@ -94,6 +96,8 @@ from .base import AnalysisBase +from MDAnalysis.analysis.base import _Results + logger = logging.getLogger('MDAnalysis.analysis.GNM') @@ -221,10 +225,12 @@ class GNMAnalysis(AnalysisBase): Attributes ---------- - results : list - GNM results per frame: - results = [(time,eigenvalues[1],eigenvectors[1]), - (time,eigenvalues[1],eigenvectors[1]), ...] + results.times : list + simulatiom times taken for evaluation + results.eiegenvalues : list + calculated eigenvalues + results.eiegenvectors : list + calculated eigenvectors See Also -------- @@ -238,7 +244,9 @@ class GNMAnalysis(AnalysisBase): Changed `selection` keyword to `select` .. versionchanged:: 2.0.0 - Use :class:`~MDAnalysis.analysis.AnalysisBase` as parent class. + Use :class:`~MDAnalysis.analysis.AnalysisBase` as parent class and + store results as attributes `times`, `eigenvalues` and + `eigenvectors` of the `results` attribute. """ def __init__(self, @@ -251,7 +259,10 @@ def __init__(self, self.u = universe self.select = select self.cutoff = cutoff - self.results = [] # final result + self.results = _Results(self) + self.results.times = [] + self.results.eigenvalues = [] + self.results.eigenvectors = [] self._timesteps = None # time for each frame self.ReportVector = ReportVector self.Bonus_groups = [self.u.select_atoms(item) for item in Bonus_groups] \ @@ -260,7 +271,7 @@ def __init__(self, def _generate_output(self, w, v, outputobject, time, matrix, nmodes=2, ReportVector=None, counter=0): - """Appends eigenvalues and eigenvectors to results. + """Appends time, eigenvalues and eigenvectors to results. This generates the output by adding eigenvalue and eigenvector data to an appendable object and optionally @@ -280,9 +291,10 @@ def _generate_output(self, w, v, outputobject, time, matrix, w[list_map[1]], item[1], file=oup) - outputobject.append((time, w[list_map[1]], v[list_map[1]])) - # outputobject.append((time, [ w[list_map[i]] for i in range(nmodes) ], - # [ v[list_map[i]] for i in range( nmodes) ] )) + + outputobject.times.append(time) + outputobject.eigenvalues.append(w[list_map[1]]) + outputobject.eigenvectors.append(v[list_map[1]]) def generate_kirchoff(self): """Generate the Kirchhoff matrix of contacts. @@ -370,6 +382,16 @@ class closeContactGNMAnalysis(GNMAnalysis): number of atoms in the residues :math:`i` and :math:`j` that contain the atoms that form a contact. + + Attributes + ---------- + results.times : list + simulatiom times taken for evaluation + results.eiegenvalues : list + calculated eigenvalues + results.eiegenvectors : list + calculated eigenvectors + Notes ----- The `MassWeight` option has now been removed. diff --git a/package/MDAnalysis/analysis/lineardensity.py b/package/MDAnalysis/analysis/lineardensity.py index 0ee87a5d720..bfae3956a36 100644 --- a/package/MDAnalysis/analysis/lineardensity.py +++ b/package/MDAnalysis/analysis/lineardensity.py @@ -55,19 +55,49 @@ class LinearDensity(AnalysisBase): Attributes ---------- - results : dict - Keys 'x', 'y', and 'z' for the three directions. Under these - keys, find 'pos', 'pos_std' (mass-weighted density and - standard deviation), 'char', 'char_std' (charge density and - its standard deviation), 'slice_volume' (volume of bin). + results.x_pos : numpy.ndarray + mass density in x direction + results.x_pos_std : numpy.ndarray + standard deviation of the mass density in x direction + results.x_char : numpy.ndarray + charge density in x direction + results.x_char_std : numpy.ndarray + standard deviation of the charge density in x direction + results.x_slice_volume : float + volume of bin in x direction + results.y_pos : numpy.ndarray + mass density in y direction + results.y_pos_std : numpy.ndarray + standard deviation of the mass density in y direction + results.y_char : numpy.ndarray + charge density in y direction + results.y_char_std : numpy.ndarray + standard deviation of the charge density in y direction + results.y_slice_volume : float + volume of bin in y direction + results.z_pos : numpy.ndarray + mass density in z direction + results.z_pos_std : numpy.ndarray + standard deviation of the mass density in z direction + results.z_char : numpy.ndarray + charge density in z direction + results.z_char_std : numpy.ndarray + standard deviation of the charge density in z direction + results.z_slice_volume : float + volume of bin in z direction + Example ------- First create a LinearDensity object by supplying a selection, - then use the :meth:`run` method:: + then use the :meth:`run` method::. Finally access the results + stored in results, i.e. the mass density in the x direction. + + .. code-block:: python - ldens = LinearDensity(selection) - ldens.run() + ldens = LinearDensity(selection) + ldens.run() + print(ldens.results.x_pos) .. versionadded:: 0.14.0 @@ -81,6 +111,9 @@ class LinearDensity(AnalysisBase): .. versionchanged:: 1.0.0 Changed `selection` keyword to `select` + + .. versionchanged:: 2.0.0 + Changed structure of the the `results` dictionary to an object. """ def __init__(self, select, grouping='atoms', binsize=0.25, **kwargs): @@ -96,8 +129,6 @@ def __init__(self, select, grouping='atoms', binsize=0.25, **kwargs): # AtomGroup.wrap()) self.grouping = grouping - # Dictionary containing results - self.results = {'x': {'dim': 0}, 'y': {'dim': 1}, 'z': {'dim': 2}} # Box sides self.dimensions = self._universe.dimensions[:3] self.volume = np.prod(self.dimensions) @@ -109,14 +140,15 @@ def __init__(self, select, grouping='atoms', binsize=0.25, **kwargs): self.nbins = bins.max() slices_vol = self.volume / bins - self.keys = ['pos', 'pos_std', 'char', 'char_std'] + # Create an alias for the results object to save writing + self._results = self.results.__dict__ # Initialize results array with zeros - for dim in self.results: - idx = self.results[dim]['dim'] - self.results[dim].update({'slice volume': slices_vol[idx]}) - for key in self.keys: - self.results[dim].update({key: np.zeros(self.nbins)}) + for idx in [0, 1, 2]: + dim = "xyz"[idx] + self._results[f'{dim}_slice volume'] = slices_vol[idx] + for attr in ['pos', 'pos_std', 'char', 'char_std']: + self._results[f"{dim}_{attr}"] = np.zeros(self.nbins) # Variables later defined in _prepare() method self.masses = None @@ -149,8 +181,8 @@ def _single_frame(self): # COM for res/frag/etc positions = np.array([elem.centroid() for elem in self.group]) - for dim in ['x', 'y', 'z']: - idx = self.results[dim]['dim'] + for idx in [0, 1, 2]: + dim = 'xyz'[idx] key = 'pos' key_std = 'pos_std' @@ -160,8 +192,8 @@ def _single_frame(self): bins=self.nbins, range=(0.0, max(self.dimensions))) - self.results[dim][key] += hist - self.results[dim][key_std] += np.square(hist) + self._results[f"{dim}_{key}"] += hist + self._results[f"{dim}_{key_std}"] += np.square(hist) key = 'char' key_std = 'char_std' @@ -171,8 +203,8 @@ def _single_frame(self): bins=self.nbins, range=(0.0, max(self.dimensions))) - self.results[dim][key] += hist - self.results[dim][key_std] += np.square(hist) + self._results[f"{dim}_{key}"] += hist + self._results[f"{dim}_{key_std}"] += np.square(hist) def _conclude(self): k = 6.022e-1 # divide by avodagro and convert from A3 to cm3 @@ -180,29 +212,30 @@ def _conclude(self): # Average results over the number of configurations for dim in ['x', 'y', 'z']: for key in ['pos', 'pos_std', 'char', 'char_std']: - self.results[dim][key] /= self.n_frames + self._results[f"{dim}_{key}"] /= self.n_frames # Compute standard deviation for the error - self.results[dim]['pos_std'] = np.sqrt(self.results[dim][ - 'pos_std'] - np.square(self.results[dim]['pos'])) - self.results[dim]['char_std'] = np.sqrt(self.results[dim][ - 'char_std'] - np.square(self.results[dim]['char'])) + self._results[f'{dim}_pos_std'] = np.sqrt( + self._results[f'{dim}_pos_std'] - + np.square(self._results[f'{dim}_pos'])) + self._results[f'{dim}_char_std'] = np.sqrt( + self._results[f'{dim}_char_std'] - + np.square(self._results[f'{dim}_char'])) for dim in ['x', 'y', 'z']: - self.results[dim]['pos'] /= self.results[dim]['slice volume'] * k - self.results[dim]['char'] /= self.results[dim]['slice volume'] * k - self.results[dim]['pos_std'] /= self.results[dim]['slice volume'] * k - self.results[dim]['char_std'] /= self.results[dim]['slice volume'] * k + self._results[f'{dim}_pos'] /= self._results[f'{dim}_slice volume'] * k + self._results[f'{dim}_char'] /= self._results[f'{dim}_slice volume'] * k + self._results[f'{dim}_pos_std'] /= self._results[f'{dim}_slice volume'] * k + self._results[f'{dim}_char_std'] /= self._results[f'{dim}_slice volume'] * k def _add_other_results(self, other): # For parallel analysis - results = self.results for dim in ['x', 'y', 'z']: key = 'pos' key_std = 'pos_std' - results[dim][key] += other[dim][key] - results[dim][key_std] += other[dim][key_std] + self._results[f"{dim}_{key}"] += other[f"{dim}_{key}"] + self._results[f"{dim}_{key_std}"] += other[f"{dim}_{key_std}"] key = 'char' key_std = 'char_std' - results[dim][key] += other[dim][key] - results[dim][key_std] += other[dim][key_std] + self._results[f"{dim}_{key}"] += other[f"{dim}_{key}"] + self._results[f"{dim}_{key_std}"] += other[f"{dim}_{key}"] diff --git a/package/MDAnalysis/analysis/polymer.py b/package/MDAnalysis/analysis/polymer.py index 78d99131e34..1de2e81d7a2 100644 --- a/package/MDAnalysis/analysis/polymer.py +++ b/package/MDAnalysis/analysis/polymer.py @@ -168,7 +168,7 @@ class PersistenceLength(AnalysisBase): Attributes ---------- - results : numpy.ndarray + results.bond_autocorrelation : numpy.ndarray the measured bond autocorrelation lb : float the average bond length @@ -187,6 +187,8 @@ class PersistenceLength(AnalysisBase): The run method now automatically performs the exponential fit .. versionchanged:: 1.0.0 Deprecated :meth:`PersistenceLength.perform_fit` has now been removed. + .. versionchanged:: 2.0.0 + Changed structure of the the `results` array to an object. """ def __init__(self, atomgroups, **kwargs): super(PersistenceLength, self).__init__( @@ -224,7 +226,7 @@ def _conclude(self): norm = np.linspace(n - 1, 1, n - 1) norm *= len(self._atomgroups) * self.n_frames - self.results = self._results / norm + self.results.bond_autocorrelation = self._results / norm self._calc_bond_length() self._perform_fit() @@ -241,12 +243,12 @@ def _calc_bond_length(self): def _perform_fit(self): """Fit the results to an exponential decay""" try: - self.results + self.results.bond_autocorrelation except AttributeError: raise NoDataError("Use the run method first") from None - self.x = np.arange(len(self.results)) * self.lb + self.x = np.arange(len(self.results.bond_autocorrelation)) * self.lb - self.lp = fit_exponential_decay(self.x, self.results) + self.lp = fit_exponential_decay(self.x, self.results.bond_autocorrelation) self.fit = np.exp(-self.x/self.lp) @@ -265,7 +267,7 @@ def plot(self, ax=None): import matplotlib.pyplot as plt if ax is None: fig, ax = plt.subplots() - ax.plot(self.x, self.results, 'ro', label='Result') + ax.plot(self.x, self.results.bond_autocorrelation, 'ro', label='Result') ax.plot(self.x, self.fit, label='Fit') ax.set_xlabel(r'x') ax.set_ylabel(r'$C(x)$') diff --git a/testsuite/MDAnalysisTests/analysis/test_gnm.py b/testsuite/MDAnalysisTests/analysis/test_gnm.py index 0bb25e1b4c7..6521c08eb86 100644 --- a/testsuite/MDAnalysisTests/analysis/test_gnm.py +++ b/testsuite/MDAnalysisTests/analysis/test_gnm.py @@ -43,10 +43,9 @@ def test_gnm(universe, tmpdir): gnm = mda.analysis.gnm.GNMAnalysis(universe, ReportVector=output) gnm.run() result = gnm.results - assert len(result) == 10 - time, eigenvalues, eigenvectors = zip(*result) - assert_almost_equal(time, np.arange(0, 1000, 100), decimal=4) - assert_almost_equal(eigenvalues, + assert len(result.times) == 10 + assert_almost_equal(gnm.results.times, np.arange(0, 1000, 100), decimal=4) + assert_almost_equal(gnm.results.eigenvalues, [2.0287113e-15, 4.1471575e-15, 1.8539533e-15, 4.3810359e-15, 3.9607304e-15, 4.1289113e-15, 2.5501084e-15, 4.0498182e-15, 4.2058769e-15, 3.9839431e-15]) @@ -56,10 +55,9 @@ def test_gnm_run_step(universe): gnm = mda.analysis.gnm.GNMAnalysis(universe) gnm.run(step=3) result = gnm.results - assert len(result) == 4 - time, eigenvalues, eigenvectors = zip(*result) - assert_almost_equal(time, np.arange(0, 1200, 300), decimal=4) - assert_almost_equal(eigenvalues, + assert len(result.times) == 4 + assert_almost_equal(gnm.results.times, np.arange(0, 1200, 300), decimal=4) + assert_almost_equal(gnm.results.eigenvalues, [2.0287113e-15, 4.3810359e-15, 2.5501084e-15, 3.9839431e-15]) @@ -94,10 +92,9 @@ def test_closeContactGNMAnalysis(universe): gnm = mda.analysis.gnm.closeContactGNMAnalysis(universe, weights="size") gnm.run(stop=2) result = gnm.results - assert len(result) == 2 - time, eigenvalues, eigenvectors = zip(*result) - assert_almost_equal(time, (0, 100), decimal=4) - assert_almost_equal(eigenvalues, [0.1502614, 0.1426407]) + assert len(result.times) == 2 + assert_almost_equal(gnm.results.times, (0, 100), decimal=4) + assert_almost_equal(gnm.results.eigenvalues, [0.1502614, 0.1426407]) gen = gnm.generate_kirchoff() assert_almost_equal(gen[0], [16.326744128018923, -2.716098853586913, -1.94736842105263, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, @@ -121,10 +118,9 @@ def test_closeContactGNMAnalysis_weights_None(universe): gnm = mda.analysis.gnm.closeContactGNMAnalysis(universe, weights=None) gnm.run(stop=2) result = gnm.results - assert len(result) == 2 - time, eigenvalues, eigenvectors = zip(*result) - assert_almost_equal(time, (0, 100), decimal=4) - assert_almost_equal(eigenvalues, [2.4328739, 2.2967251]) + assert len(result.times) == 2 + assert_almost_equal(gnm.results.times, (0, 100), decimal=4) + assert_almost_equal(gnm.results.eigenvalues, [2.4328739, 2.2967251]) gen = gnm.generate_kirchoff() assert_almost_equal(gen[0], [303.0, -58.0, -37.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, -1.0, diff --git a/testsuite/MDAnalysisTests/analysis/test_lineardensity.py b/testsuite/MDAnalysisTests/analysis/test_lineardensity.py index 2fd2533046e..e79640e7b7f 100644 --- a/testsuite/MDAnalysisTests/analysis/test_lineardensity.py +++ b/testsuite/MDAnalysisTests/analysis/test_lineardensity.py @@ -36,4 +36,4 @@ def test_serial(): xpos = np.array([0., 0., 0., 0.0072334, 0.00473299, 0., 0., 0., 0., 0.]) ld = LinearDensity(selection, binsize=5).run() - assert_almost_equal(xpos, ld.results['x']['pos']) + assert_almost_equal(xpos, ld.results.x_pos) diff --git a/testsuite/MDAnalysisTests/analysis/test_persistencelength.py b/testsuite/MDAnalysisTests/analysis/test_persistencelength.py index 864a2aed06c..d2fb8d68968 100644 --- a/testsuite/MDAnalysisTests/analysis/test_persistencelength.py +++ b/testsuite/MDAnalysisTests/analysis/test_persistencelength.py @@ -62,14 +62,14 @@ def test_ag_ValueError(self, u): polymer.PersistenceLength(ags) def test_run(self, p_run): - assert len(p_run.results) == 280 + assert len(p_run.results.bond_autocorrelation) == 280 def test_lb(self, p_run): assert_almost_equal(p_run.lb, 1.485, 3) def test_fit(self, p_run): assert_almost_equal(p_run.lp, 6.504, 3) - assert len(p_run.fit) == len(p_run.results) + assert len(p_run.fit) == len(p_run.results.bond_autocorrelation) def test_raise_NoDataError(self, p): #Ensure that a NoDataError is raised if perform_fit() From 4e9a671064988ff5b1512298887328b68e35fa6a Mon Sep 17 00:00:00 2001 From: Philip Loche Date: Sat, 17 Apr 2021 15:48:10 +0200 Subject: [PATCH 02/37] Fixed Docs and PEP8 issues --- package/MDAnalysis/analysis/__init__.py | 2 +- package/MDAnalysis/analysis/base.py | 28 +++++++++++-------- package/MDAnalysis/analysis/lineardensity.py | 20 ++++++------- .../analysis/test_persistencelength.py | 2 +- 4 files changed, 28 insertions(+), 24 deletions(-) diff --git a/package/MDAnalysis/analysis/__init__.py b/package/MDAnalysis/analysis/__init__.py index 2a4ab1ea1cd..b04eb0c0eaa 100644 --- a/package/MDAnalysis/analysis/__init__.py +++ b/package/MDAnalysis/analysis/__init__.py @@ -27,7 +27,7 @@ The :mod:`MDAnalysis.analysis` sub-package contains various recipes and algorithms that can be used to analyze MD trajectories. -If not stated differently, an analysis conducted by the available modules +If not stated differently, an analysis conducted by the available modules always follows the same structure 1. Initiliaze the object prevously imported. diff --git a/package/MDAnalysis/analysis/base.py b/package/MDAnalysis/analysis/base.py index 4fbeb005681..90537306783 100644 --- a/package/MDAnalysis/analysis/base.py +++ b/package/MDAnalysis/analysis/base.py @@ -42,11 +42,11 @@ class _Results(object): r"""Class storing results obtained from an analysis. - - The class is stores all results obatined from + + The class is stores all results obatined from an analysis after the ``run`` call. """ - + def __init__(self, analysis_cls): """ Parameters @@ -59,21 +59,22 @@ def __init__(self, analysis_cls): def __len__(self): # Subract the analysis class attribute return len(self.__dict__) - 1 - + def get_attribute_list(self): """A list of all result attribute names.""" return [key for key in self.__dict__.keys() if key != "analysis_cls"] def __repr__(self): analysis_name = type(self.analysis_cls).__name__ - return f"<{analysis_name} results with {len(self)} attribute{'s'[len(self) == 1:]}>" - - def __str__(self): + return f"<{analysis_name} results with {len(self)}" \ + f" attribute{'s'[len(self) == 1:]}>" + + def __str__(self): analysis_name = type(self.analysis_cls).__name__ attribute_list = self.get_attribute_list() str_repr = f"<{analysis_name} results with " str_repr += f"attribute{'s'[len(self) == 1:]}: " - + if len(self) <= 10: str_repr += ", ".join(attribute_list) else: @@ -88,7 +89,7 @@ class AnalysisBase(object): The class it is designed as a template for creating multiframe analyses. This class will automatically take care of setting up the trajectory - reader for iterating, and it offers to show a progress meter. + reader for iterating, and it offers to show a progress meter. Computed results are stored inside the `results` attribute. To define a new Analysis, `AnalysisBase` needs to be subclassed @@ -115,7 +116,8 @@ def _single_frame(self): # REQUIRED # Called after the trajectory is moved onto each new frame. # store a example_result of `some_function` for a single frame - self.example_result.append(some_function(self._ag, self._parameter)) + self.example_result.append(some_function(self._ag, + self._parameter)) def _conclude(self): # OPTIONAL @@ -252,13 +254,14 @@ class AnalysisFromFunction(AnalysisBase): def rotation_matrix(mobile, ref): return mda.analysis.align.rotation_matrix(mobile, ref)[0] - rot = AnalysisFromFunction(rotation_matrix, trajectory, mobile, ref).run() + rot = AnalysisFromFunction(rotation_matrix, trajectory, + mobile, ref).run() print(rot.results) Attributes ---------- results : asarray - calculation results for each frame opf the underlaying function + calculation results for each frame opf the underlaying function stored after call to ``run`` Raises @@ -331,6 +334,7 @@ def rotation_matrix(mobile, ref): It can also be used as a decorator .. code-block:: python + @analysis_class def RotationMatrix(mobile, ref): return mda.analysis.align.rotation_matrix(mobile, ref)[0] diff --git a/package/MDAnalysis/analysis/lineardensity.py b/package/MDAnalysis/analysis/lineardensity.py index bfae3956a36..e14da7a0a5b 100644 --- a/package/MDAnalysis/analysis/lineardensity.py +++ b/package/MDAnalysis/analysis/lineardensity.py @@ -90,8 +90,8 @@ class LinearDensity(AnalysisBase): Example ------- First create a LinearDensity object by supplying a selection, - then use the :meth:`run` method::. Finally access the results - stored in results, i.e. the mass density in the x direction. + then use the :meth:`run` method::. Finally access the results + stored in results, i.e. the mass density in the x direction. .. code-block:: python @@ -140,6 +140,7 @@ def __init__(self, select, grouping='atoms', binsize=0.25, **kwargs): self.nbins = bins.max() slices_vol = self.volume / bins + self._keys = ['pos', 'pos_std', 'char', 'char_std'] # Create an alias for the results object to save writing self._results = self.results.__dict__ @@ -147,7 +148,7 @@ def __init__(self, select, grouping='atoms', binsize=0.25, **kwargs): for idx in [0, 1, 2]: dim = "xyz"[idx] self._results[f'{dim}_slice volume'] = slices_vol[idx] - for attr in ['pos', 'pos_std', 'char', 'char_std']: + for attr in self._keys: self._results[f"{dim}_{attr}"] = np.zeros(self.nbins) # Variables later defined in _prepare() method @@ -211,21 +212,20 @@ def _conclude(self): # Average results over the number of configurations for dim in ['x', 'y', 'z']: - for key in ['pos', 'pos_std', 'char', 'char_std']: + for key in self._keys: self._results[f"{dim}_{key}"] /= self.n_frames # Compute standard deviation for the error self._results[f'{dim}_pos_std'] = np.sqrt( - self._results[f'{dim}_pos_std'] - + self._results[f'{dim}_pos_std'] - np.square(self._results[f'{dim}_pos'])) self._results[f'{dim}_char_std'] = np.sqrt( - self._results[f'{dim}_char_std'] - + self._results[f'{dim}_char_std'] - np.square(self._results[f'{dim}_char'])) for dim in ['x', 'y', 'z']: - self._results[f'{dim}_pos'] /= self._results[f'{dim}_slice volume'] * k - self._results[f'{dim}_char'] /= self._results[f'{dim}_slice volume'] * k - self._results[f'{dim}_pos_std'] /= self._results[f'{dim}_slice volume'] * k - self._results[f'{dim}_char_std'] /= self._results[f'{dim}_slice volume'] * k + norm = k * self._results[f'{dim}_slice volume'] + for attr in self._keys: + self._results[f'{dim}_{attr}'] /= norm def _add_other_results(self, other): # For parallel analysis diff --git a/testsuite/MDAnalysisTests/analysis/test_persistencelength.py b/testsuite/MDAnalysisTests/analysis/test_persistencelength.py index d2fb8d68968..ebc941fc729 100644 --- a/testsuite/MDAnalysisTests/analysis/test_persistencelength.py +++ b/testsuite/MDAnalysisTests/analysis/test_persistencelength.py @@ -90,7 +90,7 @@ def test_plot_with_ax(self, p_run): ax2 = p_run.plot(ax=ax) assert ax2 is ax - + def test_current_axes(self, p_run): fig, ax = plt.subplots() ax2 = p_run.plot(ax=None) From 99ddda2f00f7e26a48cf8ed66715b1dfd9b8bca6 Mon Sep 17 00:00:00 2001 From: Philip Loche Date: Sat, 17 Apr 2021 16:25:19 +0200 Subject: [PATCH 03/37] Some minor doc improvements --- package/MDAnalysis/analysis/base.py | 11 ++++++----- package/MDAnalysis/analysis/lineardensity.py | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/package/MDAnalysis/analysis/base.py b/package/MDAnalysis/analysis/base.py index 90537306783..234b48c3970 100644 --- a/package/MDAnalysis/analysis/base.py +++ b/package/MDAnalysis/analysis/base.py @@ -94,8 +94,9 @@ class AnalysisBase(object): To define a new Analysis, `AnalysisBase` needs to be subclassed `_single_frame` must be defined. It is also possible to define - `_prepare` and `_conclude` for pre and post processing. See the example - below. + `_prepare` and `_conclude` for pre and post processing. All results + should be stored as attributes of the `results` object. + See the example below. .. code-block:: python @@ -110,14 +111,14 @@ def _prepare(self): # OPTIONAL # Called before iteration on the trajectory has begun. # Data structures can be set up at this time - self.result = [] + self.results.example_result = [] def _single_frame(self): # REQUIRED # Called after the trajectory is moved onto each new frame. # store a example_result of `some_function` for a single frame - self.example_result.append(some_function(self._ag, - self._parameter)) + self.results.example_result.append(some_function(self._ag, + self._parameter)) def _conclude(self): # OPTIONAL diff --git a/package/MDAnalysis/analysis/lineardensity.py b/package/MDAnalysis/analysis/lineardensity.py index e14da7a0a5b..b92da28020d 100644 --- a/package/MDAnalysis/analysis/lineardensity.py +++ b/package/MDAnalysis/analysis/lineardensity.py @@ -90,7 +90,7 @@ class LinearDensity(AnalysisBase): Example ------- First create a LinearDensity object by supplying a selection, - then use the :meth:`run` method::. Finally access the results + then use the :meth:`run` method. Finally access the results stored in results, i.e. the mass density in the x direction. .. code-block:: python From 707b1f7547357ea8e412774934ebb637be62faca Mon Sep 17 00:00:00 2001 From: Philip Loche Date: Thu, 29 Apr 2021 15:13:59 +0200 Subject: [PATCH 04/37] Fixed typos and improved versionchanged notice --- package/MDAnalysis/analysis/__init__.py | 6 +++--- package/MDAnalysis/analysis/base.py | 17 ++++++++--------- package/MDAnalysis/analysis/lineardensity.py | 2 +- package/MDAnalysis/analysis/polymer.py | 2 +- 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/package/MDAnalysis/analysis/__init__.py b/package/MDAnalysis/analysis/__init__.py index b04eb0c0eaa..2775ed8b846 100644 --- a/package/MDAnalysis/analysis/__init__.py +++ b/package/MDAnalysis/analysis/__init__.py @@ -30,13 +30,13 @@ If not stated differently, an analysis conducted by the available modules always follows the same structure -1. Initiliaze the object prevously imported. +1. Initialize the object previously imported. 2. Run the analysis for specific trajectory slices -3. Acces the analysis from the `results` attribute (if available) +3. Access the analysis from the `results` attribute (if available) .. code-block:: python - from MDAnalysis.anlysis import AnalysisModule + from MDAnalysis.anlaysis import AnalysisModule analysis_obj = AnalysisModule(, ...) analysis_obj.run(, , ) diff --git a/package/MDAnalysis/analysis/base.py b/package/MDAnalysis/analysis/base.py index 234b48c3970..26eb2146a80 100644 --- a/package/MDAnalysis/analysis/base.py +++ b/package/MDAnalysis/analysis/base.py @@ -40,10 +40,10 @@ logger = logging.getLogger(__name__) -class _Results(object): +class _Results: r"""Class storing results obtained from an analysis. - The class is stores all results obatined from + The class stores all results obatined from an analysis after the ``run`` call. """ @@ -93,10 +93,9 @@ class AnalysisBase(object): Computed results are stored inside the `results` attribute. To define a new Analysis, `AnalysisBase` needs to be subclassed - `_single_frame` must be defined. It is also possible to define - `_prepare` and `_conclude` for pre and post processing. All results - should be stored as attributes of the `results` object. - See the example below. + `_single_frame` must be defined. It is also possible to define `_prepare` + and `_conclude` for pre and post processing. All results should be stored + as attributes of the `results` object. See the example below. .. code-block:: python @@ -117,7 +116,7 @@ def _single_frame(self): # REQUIRED # Called after the trajectory is moved onto each new frame. # store a example_result of `some_function` for a single frame - self.results.example_result.append(some_function(self._ag, + self.results.example_result.append(some_function(self._ag, self._parameter)) def _conclude(self): @@ -255,14 +254,14 @@ class AnalysisFromFunction(AnalysisBase): def rotation_matrix(mobile, ref): return mda.analysis.align.rotation_matrix(mobile, ref)[0] - rot = AnalysisFromFunction(rotation_matrix, trajectory, + rot = AnalysisFromFunction(rotation_matrix, trajectory, mobile, ref).run() print(rot.results) Attributes ---------- results : asarray - calculation results for each frame opf the underlaying function + calculation results for each frame of the underlaying function stored after call to ``run`` Raises diff --git a/package/MDAnalysis/analysis/lineardensity.py b/package/MDAnalysis/analysis/lineardensity.py index b92da28020d..7b0de5cf54d 100644 --- a/package/MDAnalysis/analysis/lineardensity.py +++ b/package/MDAnalysis/analysis/lineardensity.py @@ -113,7 +113,7 @@ class LinearDensity(AnalysisBase): Changed `selection` keyword to `select` .. versionchanged:: 2.0.0 - Changed structure of the the `results` dictionary to an object. + Results are stored in attribute names and not as keys of a dictionary. """ def __init__(self, select, grouping='atoms', binsize=0.25, **kwargs): diff --git a/package/MDAnalysis/analysis/polymer.py b/package/MDAnalysis/analysis/polymer.py index 1de2e81d7a2..09b48198bc3 100644 --- a/package/MDAnalysis/analysis/polymer.py +++ b/package/MDAnalysis/analysis/polymer.py @@ -188,7 +188,7 @@ class PersistenceLength(AnalysisBase): .. versionchanged:: 1.0.0 Deprecated :meth:`PersistenceLength.perform_fit` has now been removed. .. versionchanged:: 2.0.0 - Changed structure of the the `results` array to an object. + Results are stored in attribute names and not as a numpy.ndarray. """ def __init__(self, atomgroups, **kwargs): super(PersistenceLength, self).__init__( From b4424a4b5bc40679941bfd820e3d0baf05e9b1de Mon Sep 17 00:00:00 2001 From: Philip Loche Date: Wed, 21 Apr 2021 20:02:49 +0200 Subject: [PATCH 05/37] Add instance comparison and tests --- package/MDAnalysis/analysis/base.py | 9 +++ .../MDAnalysisTests/analysis/test_base.py | 68 +++++++++++++++++++ 2 files changed, 77 insertions(+) diff --git a/package/MDAnalysis/analysis/base.py b/package/MDAnalysis/analysis/base.py index 26eb2146a80..f6d088b3b1c 100644 --- a/package/MDAnalysis/analysis/base.py +++ b/package/MDAnalysis/analysis/base.py @@ -83,6 +83,15 @@ def __str__(self): return str_repr + ">" + def __eq__(self, other): + if self.__dict__.keys() != other.__dict__.keys() : + raise TypeError("Can't compare results from with different" + " attributes.") + return self.__dict__ == other.__dict__ + + def __ne__(self, other): + return not self == other + class AnalysisBase(object): r"""Base class for defining multi frame analysis diff --git a/testsuite/MDAnalysisTests/analysis/test_base.py b/testsuite/MDAnalysisTests/analysis/test_base.py index 95a7c8a19bf..d0cc4d4956e 100644 --- a/testsuite/MDAnalysisTests/analysis/test_base.py +++ b/testsuite/MDAnalysisTests/analysis/test_base.py @@ -33,6 +33,69 @@ from MDAnalysisTests.util import no_deprecated_call +class DummyClass: + def __init__(self): + self.results = base._Results(self) + + +class Test_Results: + foo = 42 + bar = "John Doe" + baz = np.array([1,2,3,4,5]) + + @pytest.fixture + def dummy_results(self): + obj = DummyClass() + obj.results.foo = self.foo + obj.results.bar = self.bar + obj.results.baz = self.baz + + return obj.results + + @pytest.fixture + def dummy_results_2(self): + obj = DummyClass() + obj.results.foo = self.foo + obj.results.bar = self.bar + + return obj.results + + def test_results_length(self, dummy_results): + assert dummy_results.__len__() == 3 + + def test_get_attribute_list(self, dummy_results): + assert dummy_results.get_attribute_list() == ["foo", "bar", "baz"] + + def test_repr(self, dummy_results): + assert dummy_results.__repr__() == ("") + + def test_str(self, dummy_results): + assert dummy_results.__str__() == ("") + + def tes_str_long(self, dummy_results): + for i in "abcdefg": + dummy_results.__dict__[i] = i + + assert dummy_results.__str__() == ("") + + def test_eq(self, dummy_results, dummy_results_2): + assert dummy_results == dummy_results + + def test_eq_err(self, dummy_results, dummy_results_2): + msg = ("Can't compare results from with different attributes.") + with pytest.raises(TypeError, match=msg): + dummy_results == dummy_results_2 + + def test_neq(self, dummy_results, dummy_results_2): + dummy_results_2.baz = 1 + self.baz + assert dummy_results != dummy_results_2 + + + class FrameAnalysis(base.AnalysisBase): """Just grabs frame numbers of frames it goes over""" @@ -152,6 +215,11 @@ def simple_function(mobile): return mobile.center_of_geometry() +def test_results_type(u): + an = FrameAnalysis(u.trajectory) + assert type(an.results) == base._Results + + @pytest.mark.parametrize('start, stop, step, nframes', [ (None, None, 2, 49), (None, 50, 2, 25), From f50715d89aeb6e6d1fd29fc6ef1f092417e57d21 Mon Sep 17 00:00:00 2001 From: Philip Loche Date: Fri, 23 Apr 2021 19:42:20 +0200 Subject: [PATCH 06/37] Spelling correction Co-authored-by: Manuel Nuno Melo --- package/MDAnalysis/analysis/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/MDAnalysis/analysis/base.py b/package/MDAnalysis/analysis/base.py index f6d088b3b1c..ea517c8f41e 100644 --- a/package/MDAnalysis/analysis/base.py +++ b/package/MDAnalysis/analysis/base.py @@ -85,7 +85,7 @@ def __str__(self): def __eq__(self, other): if self.__dict__.keys() != other.__dict__.keys() : - raise TypeError("Can't compare results from with different" + raise TypeError("Can't compare results with different" " attributes.") return self.__dict__ == other.__dict__ From 1b13405a8d5f613cdd8c37b382e753c99cef70b9 Mon Sep 17 00:00:00 2001 From: Philip Loche Date: Thu, 29 Apr 2021 15:16:01 +0200 Subject: [PATCH 07/37] Changed architecture of `Results` --- package/MDAnalysis/analysis/base.py | 136 ++++++++++-------- package/MDAnalysis/analysis/gnm.py | 24 ++-- package/MDAnalysis/analysis/lineardensity.py | 106 +++++++------- package/MDAnalysis/analysis/polymer.py | 2 +- .../MDAnalysisTests/analysis/test_base.py | 84 +++-------- .../analysis/test_lineardensity.py | 2 +- 6 files changed, 171 insertions(+), 183 deletions(-) diff --git a/package/MDAnalysis/analysis/base.py b/package/MDAnalysis/analysis/base.py index ea517c8f41e..3a6fc96c01d 100644 --- a/package/MDAnalysis/analysis/base.py +++ b/package/MDAnalysis/analysis/base.py @@ -40,57 +40,44 @@ logger = logging.getLogger(__name__) -class _Results: - r"""Class storing results obtained from an analysis. - - The class stores all results obatined from - an analysis after the ``run`` call. +class Results(dict): + r"""Container object for storing results. + + Results are extend dictionaries by enabling values to be accessed by key, + `results["value_key"]`, or by an attribute, `results.value_key`. + They store all results obatined from an analysis after the ``run`` call. + + The current is similar to the `Bunch` class in sklearn. + + Examples + -------- + >>> results = Results(a=1, b=2) + >>> results['b'] + 2 + >>> results.b + 2 + >>> results.a = 3 + >>> results['a'] + 3 + >>> results.c = [1, 2, 3, 4] + >>> results['c'] + [1, 2, 3, 4] """ - def __init__(self, analysis_cls): - """ - Parameters - ---------- - analysis_cls : AnalysisClass - parent analysis class - """ - self.analysis_cls = analysis_cls - - def __len__(self): - # Subract the analysis class attribute - return len(self.__dict__) - 1 - - def get_attribute_list(self): - """A list of all result attribute names.""" - return [key for key in self.__dict__.keys() if key != "analysis_cls"] + def __init__(self, **kwargs): + super().__init__(kwargs) - def __repr__(self): - analysis_name = type(self.analysis_cls).__name__ - return f"<{analysis_name} results with {len(self)}" \ - f" attribute{'s'[len(self) == 1:]}>" + def __setattr__(self, key, value): + self[key] = value - def __str__(self): - analysis_name = type(self.analysis_cls).__name__ - attribute_list = self.get_attribute_list() - str_repr = f"<{analysis_name} results with " - str_repr += f"attribute{'s'[len(self) == 1:]}: " + def __dir__(self): + return self.keys() - if len(self) <= 10: - str_repr += ", ".join(attribute_list) + def __getattr__(self, key): + if key in self.keys(): + return self[key] else: - str_repr += ", ".join(attribute_list[:3]) - str_repr += " ... " + ", ".join(attribute_list[-3:]) - - return str_repr + ">" - - def __eq__(self, other): - if self.__dict__.keys() != other.__dict__.keys() : - raise TypeError("Can't compare results with different" - " attributes.") - return self.__dict__ == other.__dict__ - - def __ne__(self, other): - return not self == other + raise AttributeError(f"'Results' object has no attribute '{key}'") class AnalysisBase(object): @@ -102,9 +89,10 @@ class AnalysisBase(object): Computed results are stored inside the `results` attribute. To define a new Analysis, `AnalysisBase` needs to be subclassed - `_single_frame` must be defined. It is also possible to define `_prepare` - and `_conclude` for pre and post processing. All results should be stored - as attributes of the `results` object. See the example below. + :meth:`_single_frame` must be defined. It is also possible to define + :meth:`_prepare` and :meth:`_conclude` for pre and post processing. + All results should be stored as attributes of the :class:`Results` + container. See the example below. .. code-block:: python @@ -141,6 +129,8 @@ def _conclude(self): na = NewAnalysis(u.select_atoms('name CA'), 35).run(start=10, stop=20) print(na.results.example_result) + # results can also accessed by key + print(na.results["example_result"]) Attributes ---------- @@ -148,8 +138,9 @@ def _conclude(self): array of Timestep times. Only exists after calling run() frames: np.ndarray array of Timestep frame indices. Only exists after calling run() - results: _Results + results: :class:`Results` results of calculation are stored after call to ``run`` + """ def __init__(self, trajectory, verbose=False, **kwargs): @@ -169,7 +160,7 @@ def __init__(self, trajectory, verbose=False, **kwargs): """ self._trajectory = trajectory self._verbose = verbose - self.results = _Results(self) + self.results = Results() def _setup_frames(self, trajectory, start=None, stop=None, step=None): """ @@ -256,7 +247,7 @@ def run(self, start=None, stop=None, step=None, verbose=None): class AnalysisFromFunction(AnalysisBase): - r"""Create an analysis from a function working on AtomGroups + r"""Create an :class:`AnalysisBase` from a function working on AtomGroups .. code-block:: python @@ -265,17 +256,22 @@ def rotation_matrix(mobile, ref): rot = AnalysisFromFunction(rotation_matrix, trajectory, mobile, ref).run() - print(rot.results) + print(rot.results.timeseries) Attributes ---------- - results : asarray - calculation results for each frame of the underlaying function - stored after call to ``run`` + results.times : numpy.ndarray + simulatiom times taken for evaluation + results.timeseries : asarray + Results for each frame of the underlaying function + stored after call to ``run``. Raises ------ ValueError : if ``function`` has the same kwargs as ``BaseAnalysis`` + + .. versionchanged:: 2.0.0 + Former `results` are now stored as `results.timeseries` """ def __init__(self, function, trajectory=None, *args, **kwargs): @@ -320,17 +316,22 @@ def __init__(self, function, trajectory=None, *args, **kwargs): super(AnalysisFromFunction, self).__init__(trajectory) def _prepare(self): - self.results = [] + self.results.times = [] + self.results.timeseries = [] def _single_frame(self): - self.results.append(self.function(*self.args, **self.kwargs)) + self.results.times.append(self._ts.time) + self.results.timeseries.append(self.function(*self.args, + **self.kwargs)) def _conclude(self): - self.results = np.asarray(self.results) + self.results.times = np.asarray(self.results.times) + self.results.timeseries = np.asarray(self.results.timeseries) def analysis_class(function): - r"""Transform a function operating on a single frame to an analysis class + r"""Transform a function operating on a single frame to an + :class:`AnalysisBase` class. For an usage in a library we recommend the following style @@ -349,7 +350,22 @@ def RotationMatrix(mobile, ref): return mda.analysis.align.rotation_matrix(mobile, ref)[0] rot = RotationMatrix(u.trajectory, mobile, ref).run(step=2) - print(rot.results) + print(rot.results.timeseries) + + Attributes + ---------- + results.times : numpy.ndarray + simulatiom times taken for evaluation + results.timeseries : asarray + Results for each frame of the underlaying function + stored after call to ``run``. + + Raises + ------ + ValueError : if ``function`` has the same kwargs as ``BaseAnalysis`` + + .. versionchanged:: 2.0.0 + Former `results` are now stored as `results.timeseries` """ class WrapperClass(AnalysisFromFunction): diff --git a/package/MDAnalysis/analysis/gnm.py b/package/MDAnalysis/analysis/gnm.py index e6ee66868ff..b56b1b5566a 100644 --- a/package/MDAnalysis/analysis/gnm.py +++ b/package/MDAnalysis/analysis/gnm.py @@ -96,7 +96,7 @@ from .base import AnalysisBase -from MDAnalysis.analysis.base import _Results +from MDAnalysis.analysis.base import Results logger = logging.getLogger('MDAnalysis.analysis.GNM') @@ -225,11 +225,11 @@ class GNMAnalysis(AnalysisBase): Attributes ---------- - results.times : list + results.times : numpy.ndarray simulatiom times taken for evaluation - results.eiegenvalues : list + results.eigenvalues : numpy.ndarray calculated eigenvalues - results.eiegenvectors : list + results.eigenvectors : numpy.ndarray calculated eigenvectors See Also @@ -259,7 +259,7 @@ def __init__(self, self.u = universe self.select = select self.cutoff = cutoff - self.results = _Results(self) + self.results = Results() self.results.times = [] self.results.eigenvalues = [] self.results.eigenvectors = [] @@ -356,6 +356,10 @@ def _single_frame(self): def _conclude(self): self._timesteps = self.times + self.results.times = np.asarray(self.results.times) + self.results.eigenvalues = np.asarray(self.results.eigenvalues) + self.results.eigenvectors = np.asarray(self.results.eigenvectors) + class closeContactGNMAnalysis(GNMAnalysis): r"""GNMAnalysis only using close contacts. @@ -385,11 +389,11 @@ class closeContactGNMAnalysis(GNMAnalysis): Attributes ---------- - results.times : list + results.times : numpy.ndarray simulatiom times taken for evaluation - results.eiegenvalues : list + results.eiegenvalues : numpy.ndarray calculated eigenvalues - results.eiegenvectors : list + results.eiegenvectors : numpy.ndarray calculated eigenvectors Notes @@ -412,7 +416,9 @@ class closeContactGNMAnalysis(GNMAnalysis): Changed `selection` keyword to `select` .. versionchanged:: 2.0.0 - Use :class:`~MDAnalysis.analysis.AnalysisBase` as parent class. + Use :class:`~MDAnalysis.analysis.AnalysisBase` as parent class and + store results as attributes `times`, `eigenvalues` and + `eigenvectors` of the `results` attribute. """ def __init__(self, diff --git a/package/MDAnalysis/analysis/lineardensity.py b/package/MDAnalysis/analysis/lineardensity.py index 7b0de5cf54d..0eb1dd057ff 100644 --- a/package/MDAnalysis/analysis/lineardensity.py +++ b/package/MDAnalysis/analysis/lineardensity.py @@ -32,7 +32,7 @@ import numpy as np -from MDAnalysis.analysis.base import AnalysisBase +from MDAnalysis.analysis.base import AnalysisBase, Results class LinearDensity(AnalysisBase): @@ -55,41 +55,46 @@ class LinearDensity(AnalysisBase): Attributes ---------- - results.x_pos : numpy.ndarray + results.x.dim : int + index of the x axes (0) + results.x.pos : numpy.ndarray mass density in x direction - results.x_pos_std : numpy.ndarray + results.x.pos_std : numpy.ndarray standard deviation of the mass density in x direction - results.x_char : numpy.ndarray + results.x.char : numpy.ndarray charge density in x direction - results.x_char_std : numpy.ndarray + results.x.char_std : numpy.ndarray standard deviation of the charge density in x direction - results.x_slice_volume : float + results.x.slice_volume : float volume of bin in x direction - results.y_pos : numpy.ndarray + results.y.dim : int + index of the y axes (1) + results.y.pos : numpy.ndarray mass density in y direction - results.y_pos_std : numpy.ndarray + results.y.pos_std : numpy.ndarray standard deviation of the mass density in y direction - results.y_char : numpy.ndarray + results.y.char : numpy.ndarray charge density in y direction - results.y_char_std : numpy.ndarray + results.y.char_std : numpy.ndarray standard deviation of the charge density in y direction - results.y_slice_volume : float + results.y.slice_volume : float volume of bin in y direction - results.z_pos : numpy.ndarray + results.z.dim : int + index of the z axes (2) + results.z.pos : numpy.ndarray mass density in z direction - results.z_pos_std : numpy.ndarray + results.z.pos_std : numpy.ndarray standard deviation of the mass density in z direction - results.z_char : numpy.ndarray + results.z.char : numpy.ndarray charge density in z direction - results.z_char_std : numpy.ndarray + results.z.char_std : numpy.ndarray standard deviation of the charge density in z direction - results.z_slice_volume : float + results.z.slice_volume : float volume of bin in z direction - Example ------- - First create a LinearDensity object by supplying a selection, + First create a `LinearDensity` object by supplying a selection, then use the :meth:`run` method. Finally access the results stored in results, i.e. the mass density in the x direction. @@ -97,7 +102,7 @@ class LinearDensity(AnalysisBase): ldens = LinearDensity(selection) ldens.run() - print(ldens.results.x_pos) + print(ldens.results.x.pos) .. versionadded:: 0.14.0 @@ -113,7 +118,9 @@ class LinearDensity(AnalysisBase): Changed `selection` keyword to `select` .. versionchanged:: 2.0.0 - Results are stored in attribute names and not as keys of a dictionary. + Results are now instances of + :class:`~MDAnalysis.core.analysis.Results` allowing access + via key and attribute. """ def __init__(self, select, grouping='atoms', binsize=0.25, **kwargs): @@ -129,6 +136,10 @@ def __init__(self, select, grouping='atoms', binsize=0.25, **kwargs): # AtomGroup.wrap()) self.grouping = grouping + # Initiate result instances + self.results.x = Results(dim=0) + self.results.y = Results(dim=1) + self.results.z = Results(dim=2) # Box sides self.dimensions = self._universe.dimensions[:3] self.volume = np.prod(self.dimensions) @@ -140,16 +151,14 @@ def __init__(self, select, grouping='atoms', binsize=0.25, **kwargs): self.nbins = bins.max() slices_vol = self.volume / bins - self._keys = ['pos', 'pos_std', 'char', 'char_std'] - # Create an alias for the results object to save writing - self._results = self.results.__dict__ + self.keys = ['pos', 'pos_std', 'char', 'char_std'] # Initialize results array with zeros - for idx in [0, 1, 2]: - dim = "xyz"[idx] - self._results[f'{dim}_slice volume'] = slices_vol[idx] - for attr in self._keys: - self._results[f"{dim}_{attr}"] = np.zeros(self.nbins) + for dim in self.results: + idx = self.results[dim]['dim'] + self.results[dim]['slice volume'] = slices_vol[idx] + for key in self.keys: + self.results[dim][key] = np.zeros(self.nbins) # Variables later defined in _prepare() method self.masses = None @@ -182,8 +191,8 @@ def _single_frame(self): # COM for res/frag/etc positions = np.array([elem.centroid() for elem in self.group]) - for idx in [0, 1, 2]: - dim = 'xyz'[idx] + for dim in ['x', 'y', 'z']: + idx = self.results[dim]['dim'] key = 'pos' key_std = 'pos_std' @@ -193,8 +202,8 @@ def _single_frame(self): bins=self.nbins, range=(0.0, max(self.dimensions))) - self._results[f"{dim}_{key}"] += hist - self._results[f"{dim}_{key_std}"] += np.square(hist) + self.results[dim][key] += hist + self.results[dim][key_std] += np.square(hist) key = 'char' key_std = 'char_std' @@ -204,38 +213,37 @@ def _single_frame(self): bins=self.nbins, range=(0.0, max(self.dimensions))) - self._results[f"{dim}_{key}"] += hist - self._results[f"{dim}_{key_std}"] += np.square(hist) + self.results[dim][key] += hist + self.results[dim][key_std] += np.square(hist) def _conclude(self): k = 6.022e-1 # divide by avodagro and convert from A3 to cm3 # Average results over the number of configurations for dim in ['x', 'y', 'z']: - for key in self._keys: - self._results[f"{dim}_{key}"] /= self.n_frames + for key in ['pos', 'pos_std', 'char', 'char_std']: + self.results[dim][key] /= self.n_frames # Compute standard deviation for the error - self._results[f'{dim}_pos_std'] = np.sqrt( - self._results[f'{dim}_pos_std'] - - np.square(self._results[f'{dim}_pos'])) - self._results[f'{dim}_char_std'] = np.sqrt( - self._results[f'{dim}_char_std'] - - np.square(self._results[f'{dim}_char'])) + self.results[dim]['pos_std'] = np.sqrt(self.results[dim][ + 'pos_std'] - np.square(self.results[dim]['pos'])) + self.results[dim]['char_std'] = np.sqrt(self.results[dim][ + 'char_std'] - np.square(self.results[dim]['char'])) for dim in ['x', 'y', 'z']: - norm = k * self._results[f'{dim}_slice volume'] - for attr in self._keys: - self._results[f'{dim}_{attr}'] /= norm + norm = k * self.results[dim]['slice volume'] + for key in self.keys: + self.results[dim][key] /= norm def _add_other_results(self, other): # For parallel analysis + results = self.results for dim in ['x', 'y', 'z']: key = 'pos' key_std = 'pos_std' - self._results[f"{dim}_{key}"] += other[f"{dim}_{key}"] - self._results[f"{dim}_{key_std}"] += other[f"{dim}_{key_std}"] + results[dim][key] += other[dim][key] + results[dim][key_std] += other[dim][key_std] key = 'char' key_std = 'char_std' - self._results[f"{dim}_{key}"] += other[f"{dim}_{key}"] - self._results[f"{dim}_{key_std}"] += other[f"{dim}_{key}"] + results[dim][key] += other[dim][key] + results[dim][key_std] += other[dim][key_std] diff --git a/package/MDAnalysis/analysis/polymer.py b/package/MDAnalysis/analysis/polymer.py index 09b48198bc3..a7fbfb8d505 100644 --- a/package/MDAnalysis/analysis/polymer.py +++ b/package/MDAnalysis/analysis/polymer.py @@ -188,7 +188,7 @@ class PersistenceLength(AnalysisBase): .. versionchanged:: 1.0.0 Deprecated :meth:`PersistenceLength.perform_fit` has now been removed. .. versionchanged:: 2.0.0 - Results are stored in attribute names and not as a numpy.ndarray. + Former `results` are now stored as `results.bond_autocorrelation` """ def __init__(self, atomgroups, **kwargs): super(PersistenceLength, self).__init__( diff --git a/testsuite/MDAnalysisTests/analysis/test_base.py b/testsuite/MDAnalysisTests/analysis/test_base.py index d0cc4d4956e..bd12084d509 100644 --- a/testsuite/MDAnalysisTests/analysis/test_base.py +++ b/testsuite/MDAnalysisTests/analysis/test_base.py @@ -33,67 +33,21 @@ from MDAnalysisTests.util import no_deprecated_call -class DummyClass: - def __init__(self): - self.results = base._Results(self) - - class Test_Results: - foo = 42 - bar = "John Doe" - baz = np.array([1,2,3,4,5]) - - @pytest.fixture - def dummy_results(self): - obj = DummyClass() - obj.results.foo = self.foo - obj.results.bar = self.bar - obj.results.baz = self.baz - - return obj.results @pytest.fixture - def dummy_results_2(self): - obj = DummyClass() - obj.results.foo = self.foo - obj.results.bar = self.bar - - return obj.results + def results(self): + return base.Results(a=1, b=2) - def test_results_length(self, dummy_results): - assert dummy_results.__len__() == 3 - - def test_get_attribute_list(self, dummy_results): - assert dummy_results.get_attribute_list() == ["foo", "bar", "baz"] - - def test_repr(self, dummy_results): - assert dummy_results.__repr__() == ("") + def test_get(self, results): + assert results.a == results["a"] - def test_str(self, dummy_results): - assert dummy_results.__str__() == ("") - - def tes_str_long(self, dummy_results): - for i in "abcdefg": - dummy_results.__dict__[i] = i - - assert dummy_results.__str__() == ("") - - def test_eq(self, dummy_results, dummy_results_2): - assert dummy_results == dummy_results - - def test_eq_err(self, dummy_results, dummy_results_2): - msg = ("Can't compare results from with different attributes.") - with pytest.raises(TypeError, match=msg): - dummy_results == dummy_results_2 - - def test_neq(self, dummy_results, dummy_results_2): - dummy_results_2.baz = 1 + self.baz - assert dummy_results != dummy_results_2 + def test_no_attr(self, results): + with pytest.raises(AttributeError): + results.c + def test_dir(self, results): + assert list(results.__dir__()) == ["a", "b"] class FrameAnalysis(base.AnalysisBase): @@ -217,7 +171,7 @@ def simple_function(mobile): def test_results_type(u): an = FrameAnalysis(u.trajectory) - assert type(an.results) == base._Results + assert type(an.results) == base.Results @pytest.mark.parametrize('start, stop, step, nframes', [ @@ -236,17 +190,21 @@ def test_AnalysisFromFunction(u, start, stop, step, nframes): ana3 = base.AnalysisFromFunction(simple_function, u.trajectory, u.atoms) ana3.run(start=start, stop=stop, step=step) - results = [] + times = [] + timeseries = [] for ts in u.trajectory[start:stop:step]: - results.append(simple_function(u.atoms)) + times.append(ts.time) + timeseries.append(simple_function(u.atoms)) - results = np.asarray(results) + times = np.asarray(times) + timeseries = np.asarray(timeseries) - assert np.size(results, 0) == nframes + assert np.size(timeseries, 0) == nframes for ana in (ana1, ana2, ana3): - assert_equal(results, ana.results) + assert_equal(times, ana.results.times) + assert_equal(timeseries, ana.results.timeseries) def mass_xyz(atomgroup1, atomgroup2, masses): @@ -259,7 +217,7 @@ def test_AnalysisFromFunction_args_content(u): another = mda.Universe(TPR, XTC).select_atoms("protein") ans = base.AnalysisFromFunction(mass_xyz, protein, another, masses) assert len(ans.args) == 3 - result = np.sum(ans.run().results) + result = np.sum(ans.run().results.timeseries) assert_almost_equal(result, -317054.67757345125, decimal=6) assert (ans.args[0] is protein) and (ans.args[1] is another) assert ans._trajectory is protein.universe.trajectory @@ -279,7 +237,7 @@ def test_analysis_class(): results.append(simple_function(u.atoms)) results = np.asarray(results) - assert_equal(results, ana.results) + assert_equal(results, ana.results.timeseries) with pytest.raises(ValueError): ana_class(2) diff --git a/testsuite/MDAnalysisTests/analysis/test_lineardensity.py b/testsuite/MDAnalysisTests/analysis/test_lineardensity.py index e79640e7b7f..2fd2533046e 100644 --- a/testsuite/MDAnalysisTests/analysis/test_lineardensity.py +++ b/testsuite/MDAnalysisTests/analysis/test_lineardensity.py @@ -36,4 +36,4 @@ def test_serial(): xpos = np.array([0., 0., 0., 0.0072334, 0.00473299, 0., 0., 0., 0., 0.]) ld = LinearDensity(selection, binsize=5).run() - assert_almost_equal(xpos, ld.results.x_pos) + assert_almost_equal(xpos, ld.results['x']['pos']) From f8116b6fcf2131338e4fa240a14637d529f505ae Mon Sep 17 00:00:00 2001 From: Philip Loche Date: Sat, 24 Apr 2021 19:00:05 +0200 Subject: [PATCH 08/37] Added frames to AnalysisFromFunction --- package/MDAnalysis/analysis/base.py | 5 ++--- testsuite/MDAnalysisTests/analysis/test_base.py | 4 ++++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/package/MDAnalysis/analysis/base.py b/package/MDAnalysis/analysis/base.py index 3a6fc96c01d..270afa5fb61 100644 --- a/package/MDAnalysis/analysis/base.py +++ b/package/MDAnalysis/analysis/base.py @@ -316,16 +316,15 @@ def __init__(self, function, trajectory=None, *args, **kwargs): super(AnalysisFromFunction, self).__init__(trajectory) def _prepare(self): - self.results.times = [] self.results.timeseries = [] def _single_frame(self): - self.results.times.append(self._ts.time) self.results.timeseries.append(self.function(*self.args, **self.kwargs)) def _conclude(self): - self.results.times = np.asarray(self.results.times) + self.results.frames = self.frames + self.results.times = self.times self.results.timeseries = np.asarray(self.results.timeseries) diff --git a/testsuite/MDAnalysisTests/analysis/test_base.py b/testsuite/MDAnalysisTests/analysis/test_base.py index bd12084d509..796896ebae7 100644 --- a/testsuite/MDAnalysisTests/analysis/test_base.py +++ b/testsuite/MDAnalysisTests/analysis/test_base.py @@ -190,19 +190,23 @@ def test_AnalysisFromFunction(u, start, stop, step, nframes): ana3 = base.AnalysisFromFunction(simple_function, u.trajectory, u.atoms) ana3.run(start=start, stop=stop, step=step) + frames = [] times = [] timeseries = [] for ts in u.trajectory[start:stop:step]: + frames.append(ts.frame) times.append(ts.time) timeseries.append(simple_function(u.atoms)) + frames = np.asarray(frames) times = np.asarray(times) timeseries = np.asarray(timeseries) assert np.size(timeseries, 0) == nframes for ana in (ana1, ana2, ana3): + assert_equal(frames, ana.results.frames) assert_equal(times, ana.results.times) assert_equal(timeseries, ana.results.timeseries) From 6561cadc7d0beb0b0046c09901fca7966db295f8 Mon Sep 17 00:00:00 2001 From: Philip Loche Date: Sat, 24 Apr 2021 20:51:44 +0200 Subject: [PATCH 09/37] Added missing doc --- package/MDAnalysis/analysis/base.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/package/MDAnalysis/analysis/base.py b/package/MDAnalysis/analysis/base.py index 270afa5fb61..2f6f2b0d0f8 100644 --- a/package/MDAnalysis/analysis/base.py +++ b/package/MDAnalysis/analysis/base.py @@ -260,6 +260,8 @@ def rotation_matrix(mobile, ref): Attributes ---------- + results.frames : numpy.ndarray + simulatiom frames taken for evaluation results.times : numpy.ndarray simulatiom times taken for evaluation results.timeseries : asarray @@ -353,6 +355,8 @@ def RotationMatrix(mobile, ref): Attributes ---------- + results.frames : numpy.ndarray + simulatiom frames taken for evaluation results.times : numpy.ndarray simulatiom times taken for evaluation results.timeseries : asarray From fcfd316e7fe563b51ffc4b162be859fb21c101d0 Mon Sep 17 00:00:00 2001 From: Philip Loche Date: Sat, 24 Apr 2021 21:00:04 +0200 Subject: [PATCH 10/37] Convert lists to arrays --- package/MDAnalysis/analysis/gnm.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/package/MDAnalysis/analysis/gnm.py b/package/MDAnalysis/analysis/gnm.py index b56b1b5566a..62fdcb4377e 100644 --- a/package/MDAnalysis/analysis/gnm.py +++ b/package/MDAnalysis/analysis/gnm.py @@ -386,7 +386,6 @@ class closeContactGNMAnalysis(GNMAnalysis): number of atoms in the residues :math:`i` and :math:`j` that contain the atoms that form a contact. - Attributes ---------- results.times : numpy.ndarray @@ -462,4 +461,8 @@ def generate_kirchoff(self): matrix[iresidue][iresidue] += contact matrix[jresidue][jresidue] += contact + self.results.times = np.asarray(self.results.times) + self.results.eigenvalues = np.asarray(self.results.eigenvalues) + self.results.eigenvectors = np.asarray(self.results.eigenvectors) + return matrix From a7ec35ab7d0cbfa9cf146c2e6ac9b0870b719322 Mon Sep 17 00:00:00 2001 From: Philip Loche Date: Sat, 24 Apr 2021 21:27:26 +0200 Subject: [PATCH 11/37] Revoked a change --- package/MDAnalysis/analysis/gnm.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/package/MDAnalysis/analysis/gnm.py b/package/MDAnalysis/analysis/gnm.py index 62fdcb4377e..6b1b8a660c2 100644 --- a/package/MDAnalysis/analysis/gnm.py +++ b/package/MDAnalysis/analysis/gnm.py @@ -461,8 +461,4 @@ def generate_kirchoff(self): matrix[iresidue][iresidue] += contact matrix[jresidue][jresidue] += contact - self.results.times = np.asarray(self.results.times) - self.results.eigenvalues = np.asarray(self.results.eigenvalues) - self.results.eigenvectors = np.asarray(self.results.eigenvectors) - return matrix From 96d0243cef197134478216d72d572a27c0432451 Mon Sep 17 00:00:00 2001 From: Philip Loche Date: Sat, 24 Apr 2021 22:11:24 +0200 Subject: [PATCH 12/37] Add missing word --- package/MDAnalysis/analysis/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/MDAnalysis/analysis/base.py b/package/MDAnalysis/analysis/base.py index 2f6f2b0d0f8..06e30fe6bff 100644 --- a/package/MDAnalysis/analysis/base.py +++ b/package/MDAnalysis/analysis/base.py @@ -47,7 +47,7 @@ class Results(dict): `results["value_key"]`, or by an attribute, `results.value_key`. They store all results obatined from an analysis after the ``run`` call. - The current is similar to the `Bunch` class in sklearn. + The current implementation is similar to the `Bunch` class in `sklearn`. Examples -------- From f9f743f90c31336fd096bfb1e2d5578aa24735f4 Mon Sep 17 00:00:00 2001 From: Philip Loche Date: Mon, 26 Apr 2021 23:12:11 +0200 Subject: [PATCH 13/37] Improved __getattr__ --- package/MDAnalysis/analysis/base.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/package/MDAnalysis/analysis/base.py b/package/MDAnalysis/analysis/base.py index 06e30fe6bff..38b097482ce 100644 --- a/package/MDAnalysis/analysis/base.py +++ b/package/MDAnalysis/analysis/base.py @@ -74,10 +74,11 @@ def __dir__(self): return self.keys() def __getattr__(self, key): - if key in self.keys(): + try: return self[key] - else: - raise AttributeError(f"'Results' object has no attribute '{key}'") + except KeyError as err: + raise AttributeError("'Results' object has no " + f"attribute '{key}'") from err class AnalysisBase(object): From a7bc8aa84284d75568539c91cba05d08284b81b1 Mon Sep 17 00:00:00 2001 From: Philip Loche Date: Thu, 29 Apr 2021 15:06:38 +0200 Subject: [PATCH 14/37] Better docs Co-authored-by: Oliver Beckstein --- package/MDAnalysis/analysis/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/MDAnalysis/analysis/base.py b/package/MDAnalysis/analysis/base.py index 38b097482ce..c23b2d76084 100644 --- a/package/MDAnalysis/analysis/base.py +++ b/package/MDAnalysis/analysis/base.py @@ -43,7 +43,7 @@ class Results(dict): r"""Container object for storing results. - Results are extend dictionaries by enabling values to be accessed by key, + Results are dictionaries that provide two ways in which can values be accessed: `results["value_key"]`, or by an attribute, `results.value_key`. They store all results obatined from an analysis after the ``run`` call. From 8d67b9586434f1d2d7df0430b21168d8554499a6 Mon Sep 17 00:00:00 2001 From: Philip Loche Date: Thu, 29 Apr 2021 15:07:07 +0200 Subject: [PATCH 15/37] Better docs Co-authored-by: Oliver Beckstein --- package/MDAnalysis/analysis/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/MDAnalysis/analysis/base.py b/package/MDAnalysis/analysis/base.py index c23b2d76084..97c2addcaa2 100644 --- a/package/MDAnalysis/analysis/base.py +++ b/package/MDAnalysis/analysis/base.py @@ -44,7 +44,7 @@ class Results(dict): r"""Container object for storing results. Results are dictionaries that provide two ways in which can values be accessed: - `results["value_key"]`, or by an attribute, `results.value_key`. + by dict key ``results["value_key"]`` or by object attribute, ``results.value_key``. They store all results obatined from an analysis after the ``run`` call. The current implementation is similar to the `Bunch` class in `sklearn`. From 8d5db6fc6ea5c12934586ce368e50273e7eb10e8 Mon Sep 17 00:00:00 2001 From: Philip Loche Date: Thu, 29 Apr 2021 15:07:57 +0200 Subject: [PATCH 16/37] Better docs Co-authored-by: Oliver Beckstein --- package/MDAnalysis/analysis/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/MDAnalysis/analysis/base.py b/package/MDAnalysis/analysis/base.py index 97c2addcaa2..5b2d210598b 100644 --- a/package/MDAnalysis/analysis/base.py +++ b/package/MDAnalysis/analysis/base.py @@ -87,7 +87,7 @@ class AnalysisBase(object): The class it is designed as a template for creating multiframe analyses. This class will automatically take care of setting up the trajectory reader for iterating, and it offers to show a progress meter. - Computed results are stored inside the `results` attribute. + Computed results are stored inside the :attr:`results` attribute. To define a new Analysis, `AnalysisBase` needs to be subclassed :meth:`_single_frame` must be defined. It is also possible to define From 33bba7db29781c4b6d05a9cbaa7ed9164ae6ea22 Mon Sep 17 00:00:00 2001 From: Philip Loche Date: Thu, 29 Apr 2021 15:24:20 +0200 Subject: [PATCH 17/37] PEP8 issues --- package/MDAnalysis/analysis/base.py | 21 ++++++++++--------- package/MDAnalysis/analysis/gnm.py | 8 +++---- package/MDAnalysis/analysis/polymer.py | 12 ++++++++--- .../MDAnalysisTests/analysis/test_base.py | 2 +- 4 files changed, 25 insertions(+), 18 deletions(-) diff --git a/package/MDAnalysis/analysis/base.py b/package/MDAnalysis/analysis/base.py index 5b2d210598b..8a871bfcb2d 100644 --- a/package/MDAnalysis/analysis/base.py +++ b/package/MDAnalysis/analysis/base.py @@ -43,9 +43,10 @@ class Results(dict): r"""Container object for storing results. - Results are dictionaries that provide two ways in which can values be accessed: - by dict key ``results["value_key"]`` or by object attribute, ``results.value_key``. - They store all results obatined from an analysis after the ``run`` call. + Results are dictionaries that provide two ways in which can values be + accessed: by dict key ``results["value_key"]`` or by object attribute, + ``results.value_key``. They store all results obatined from an analysis + after the ``run`` call. The current implementation is similar to the `Bunch` class in `sklearn`. @@ -77,8 +78,8 @@ def __getattr__(self, key): try: return self[key] except KeyError as err: - raise AttributeError("'Results' object has no " - f"attribute '{key}'") from err + raise AttributeError("'Results' object has no " + f"attribute '{key}'") from err class AnalysisBase(object): @@ -90,9 +91,9 @@ class AnalysisBase(object): Computed results are stored inside the :attr:`results` attribute. To define a new Analysis, `AnalysisBase` needs to be subclassed - :meth:`_single_frame` must be defined. It is also possible to define - :meth:`_prepare` and :meth:`_conclude` for pre and post processing. - All results should be stored as attributes of the :class:`Results` + :meth:`_single_frame` must be defined. It is also possible to define + :meth:`_prepare` and :meth:`_conclude` for pre and post processing. + All results should be stored as attributes of the :class:`Results` container. See the example below. .. code-block:: python @@ -322,7 +323,7 @@ def _prepare(self): self.results.timeseries = [] def _single_frame(self): - self.results.timeseries.append(self.function(*self.args, + self.results.timeseries.append(self.function(*self.args, **self.kwargs)) def _conclude(self): @@ -332,7 +333,7 @@ def _conclude(self): def analysis_class(function): - r"""Transform a function operating on a single frame to an + r"""Transform a function operating on a single frame to an :class:`AnalysisBase` class. For an usage in a library we recommend the following style diff --git a/package/MDAnalysis/analysis/gnm.py b/package/MDAnalysis/analysis/gnm.py index 6b1b8a660c2..31e44be35a4 100644 --- a/package/MDAnalysis/analysis/gnm.py +++ b/package/MDAnalysis/analysis/gnm.py @@ -244,8 +244,8 @@ class GNMAnalysis(AnalysisBase): Changed `selection` keyword to `select` .. versionchanged:: 2.0.0 - Use :class:`~MDAnalysis.analysis.AnalysisBase` as parent class and - store results as attributes `times`, `eigenvalues` and + Use :class:`~MDAnalysis.analysis.AnalysisBase` as parent class and + store results as attributes `times`, `eigenvalues` and `eigenvectors` of the `results` attribute. """ @@ -415,8 +415,8 @@ class closeContactGNMAnalysis(GNMAnalysis): Changed `selection` keyword to `select` .. versionchanged:: 2.0.0 - Use :class:`~MDAnalysis.analysis.AnalysisBase` as parent class and - store results as attributes `times`, `eigenvalues` and + Use :class:`~MDAnalysis.analysis.AnalysisBase` as parent class and + store results as attributes `times`, `eigenvalues` and `eigenvectors` of the `results` attribute. """ diff --git a/package/MDAnalysis/analysis/polymer.py b/package/MDAnalysis/analysis/polymer.py index a7fbfb8d505..c0e8f1db5f1 100644 --- a/package/MDAnalysis/analysis/polymer.py +++ b/package/MDAnalysis/analysis/polymer.py @@ -248,7 +248,8 @@ def _perform_fit(self): raise NoDataError("Use the run method first") from None self.x = np.arange(len(self.results.bond_autocorrelation)) * self.lb - self.lp = fit_exponential_decay(self.x, self.results.bond_autocorrelation) + self.lp = fit_exponential_decay(self.x, + self.results.bond_autocorrelation) self.fit = np.exp(-self.x/self.lp) @@ -267,8 +268,13 @@ def plot(self, ax=None): import matplotlib.pyplot as plt if ax is None: fig, ax = plt.subplots() - ax.plot(self.x, self.results.bond_autocorrelation, 'ro', label='Result') - ax.plot(self.x, self.fit, label='Fit') + ax.plot(self.x, + self.results.bond_autocorrelation, + 'ro', + label='Result') + ax.plot(self.x, + self.fit, + label='Fit') ax.set_xlabel(r'x') ax.set_ylabel(r'$C(x)$') ax.set_xlim(0.0, 40 * self.lb) diff --git a/testsuite/MDAnalysisTests/analysis/test_base.py b/testsuite/MDAnalysisTests/analysis/test_base.py index 796896ebae7..7173d1602b4 100644 --- a/testsuite/MDAnalysisTests/analysis/test_base.py +++ b/testsuite/MDAnalysisTests/analysis/test_base.py @@ -38,7 +38,7 @@ class Test_Results: @pytest.fixture def results(self): return base.Results(a=1, b=2) - + def test_get(self, results): assert results.a == results["a"] From 8f2871d52aeea3a003e6f2aa086ad3d5650e9092 Mon Sep 17 00:00:00 2001 From: Philip Loche Date: Thu, 29 Apr 2021 15:27:13 +0200 Subject: [PATCH 18/37] More and even better docs Thanks to @orbeckstein Co-authored-by: Oliver Beckstein --- package/MDAnalysis/analysis/base.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/package/MDAnalysis/analysis/base.py b/package/MDAnalysis/analysis/base.py index 8a871bfcb2d..7ae00e40674 100644 --- a/package/MDAnalysis/analysis/base.py +++ b/package/MDAnalysis/analysis/base.py @@ -48,7 +48,10 @@ class Results(dict): ``results.value_key``. They store all results obatined from an analysis after the ``run`` call. - The current implementation is similar to the `Bunch` class in `sklearn`. + The current implementation is similar to the :class:`sklearn.utils.Bunch` class in `scikit-learn`_. + + .. _`scikit-learn`: https://scikit-learn.org/ + Examples -------- @@ -275,7 +278,7 @@ def rotation_matrix(mobile, ref): ValueError : if ``function`` has the same kwargs as ``BaseAnalysis`` .. versionchanged:: 2.0.0 - Former `results` are now stored as `results.timeseries` + Former :attr:`results` are now stored as :attr:`results.timeseries` """ def __init__(self, function, trajectory=None, *args, **kwargs): From 9ac4c4ca6e8421414941e997f3f7c3eeffebefff Mon Sep 17 00:00:00 2001 From: Philip Loche Date: Thu, 29 Apr 2021 16:43:47 +0200 Subject: [PATCH 19/37] Consistent indention --- package/MDAnalysis/analysis/lineardensity.py | 234 +++++++++---------- 1 file changed, 117 insertions(+), 117 deletions(-) diff --git a/package/MDAnalysis/analysis/lineardensity.py b/package/MDAnalysis/analysis/lineardensity.py index 0eb1dd057ff..46ef76683cd 100644 --- a/package/MDAnalysis/analysis/lineardensity.py +++ b/package/MDAnalysis/analysis/lineardensity.py @@ -100,9 +100,9 @@ class LinearDensity(AnalysisBase): .. code-block:: python - ldens = LinearDensity(selection) - ldens.run() - print(ldens.results.x.pos) + ldens = LinearDensity(selection) + ldens.run() + print(ldens.results.x.pos) .. versionadded:: 0.14.0 @@ -118,132 +118,132 @@ class LinearDensity(AnalysisBase): Changed `selection` keyword to `select` .. versionchanged:: 2.0.0 - Results are now instances of + Results are now instances of :class:`~MDAnalysis.core.analysis.Results` allowing access via key and attribute. """ def __init__(self, select, grouping='atoms', binsize=0.25, **kwargs): - super(LinearDensity, self).__init__(select.universe.trajectory, - **kwargs) - # allows use of run(parallel=True) - self._ags = [select] - self._universe = select.universe - - self.binsize = binsize - - # group of atoms on which to compute the COM (same as used in - # AtomGroup.wrap()) - self.grouping = grouping - - # Initiate result instances - self.results.x = Results(dim=0) - self.results.y = Results(dim=1) - self.results.z = Results(dim=2) - # Box sides - self.dimensions = self._universe.dimensions[:3] - self.volume = np.prod(self.dimensions) - # number of bins - bins = (self.dimensions // self.binsize).astype(int) - - # Here we choose a number of bins of the largest cell side so that - # x, y and z values can use the same "coord" column in the output file - self.nbins = bins.max() - slices_vol = self.volume / bins - - self.keys = ['pos', 'pos_std', 'char', 'char_std'] - - # Initialize results array with zeros - for dim in self.results: - idx = self.results[dim]['dim'] - self.results[dim]['slice volume'] = slices_vol[idx] - for key in self.keys: - self.results[dim][key] = np.zeros(self.nbins) - - # Variables later defined in _prepare() method - self.masses = None - self.charges = None - self.totalmass = None + super(LinearDensity, self).__init__(select.universe.trajectory, + **kwargs) + # allows use of run(parallel=True) + self._ags = [select] + self._universe = select.universe + + self.binsize = binsize + + # group of atoms on which to compute the COM (same as used in + # AtomGroup.wrap()) + self.grouping = grouping + + # Initiate result instances + self.results.x = Results(dim=0) + self.results.y = Results(dim=1) + self.results.z = Results(dim=2) + # Box sides + self.dimensions = self._universe.dimensions[:3] + self.volume = np.prod(self.dimensions) + # number of bins + bins = (self.dimensions // self.binsize).astype(int) + + # Here we choose a number of bins of the largest cell side so that + # x, y and z values can use the same "coord" column in the output file + self.nbins = bins.max() + slices_vol = self.volume / bins + + self.keys = ['pos', 'pos_std', 'char', 'char_std'] + + # Initialize results array with zeros + for dim in self.results: + idx = self.results[dim]['dim'] + self.results[dim]['slice volume'] = slices_vol[idx] + for key in self.keys: + self.results[dim][key] = np.zeros(self.nbins) + + # Variables later defined in _prepare() method + self.masses = None + self.charges = None + self.totalmass = None def _prepare(self): - # group must be a local variable, otherwise there will be - # issues with parallelization - group = getattr(self._ags[0], self.grouping) + # group must be a local variable, otherwise there will be + # issues with parallelization + group = getattr(self._ags[0], self.grouping) - # Get masses and charges for the selection - try: # in case it's not an atom - self.masses = np.array([elem.total_mass() for elem in group]) - self.charges = np.array([elem.total_charge() for elem in group]) - except AttributeError: # much much faster for atoms - self.masses = self._ags[0].masses - self.charges = self._ags[0].charges + # Get masses and charges for the selection + try: # in case it's not an atom + self.masses = np.array([elem.total_mass() for elem in group]) + self.charges = np.array([elem.total_charge() for elem in group]) + except AttributeError: # much much faster for atoms + self.masses = self._ags[0].masses + self.charges = self._ags[0].charges - self.totalmass = np.sum(self.masses) + self.totalmass = np.sum(self.masses) def _single_frame(self): - self.group = getattr(self._ags[0], self.grouping) - self._ags[0].wrap(compound=self.grouping) - - # Find position of atom/group of atoms - if self.grouping == 'atoms': - positions = self._ags[0].positions # faster for atoms - else: - # COM for res/frag/etc - positions = np.array([elem.centroid() for elem in self.group]) - - for dim in ['x', 'y', 'z']: - idx = self.results[dim]['dim'] - - key = 'pos' - key_std = 'pos_std' - # histogram for positions weighted on masses - hist, _ = np.histogram(positions[:, idx], - weights=self.masses, - bins=self.nbins, - range=(0.0, max(self.dimensions))) - - self.results[dim][key] += hist - self.results[dim][key_std] += np.square(hist) - - key = 'char' - key_std = 'char_std' - # histogram for positions weighted on charges - hist, _ = np.histogram(positions[:, idx], - weights=self.charges, - bins=self.nbins, - range=(0.0, max(self.dimensions))) - - self.results[dim][key] += hist - self.results[dim][key_std] += np.square(hist) + self.group = getattr(self._ags[0], self.grouping) + self._ags[0].wrap(compound=self.grouping) + + # Find position of atom/group of atoms + if self.grouping == 'atoms': + positions = self._ags[0].positions # faster for atoms + else: + # COM for res/frag/etc + positions = np.array([elem.centroid() for elem in self.group]) + + for dim in ['x', 'y', 'z']: + idx = self.results[dim]['dim'] + + key = 'pos' + key_std = 'pos_std' + # histogram for positions weighted on masses + hist, _ = np.histogram(positions[:, idx], + weights=self.masses, + bins=self.nbins, + range=(0.0, max(self.dimensions))) + + self.results[dim][key] += hist + self.results[dim][key_std] += np.square(hist) + + key = 'char' + key_std = 'char_std' + # histogram for positions weighted on charges + hist, _ = np.histogram(positions[:, idx], + weights=self.charges, + bins=self.nbins, + range=(0.0, max(self.dimensions))) + + self.results[dim][key] += hist + self.results[dim][key_std] += np.square(hist) def _conclude(self): - k = 6.022e-1 # divide by avodagro and convert from A3 to cm3 - - # Average results over the number of configurations - for dim in ['x', 'y', 'z']: - for key in ['pos', 'pos_std', 'char', 'char_std']: - self.results[dim][key] /= self.n_frames - # Compute standard deviation for the error - self.results[dim]['pos_std'] = np.sqrt(self.results[dim][ - 'pos_std'] - np.square(self.results[dim]['pos'])) - self.results[dim]['char_std'] = np.sqrt(self.results[dim][ - 'char_std'] - np.square(self.results[dim]['char'])) - - for dim in ['x', 'y', 'z']: - norm = k * self.results[dim]['slice volume'] - for key in self.keys: - self.results[dim][key] /= norm + k = 6.022e-1 # divide by avodagro and convert from A3 to cm3 + + # Average results over the number of configurations + for dim in ['x', 'y', 'z']: + for key in ['pos', 'pos_std', 'char', 'char_std']: + self.results[dim][key] /= self.n_frames + # Compute standard deviation for the error + self.results[dim]['pos_std'] = np.sqrt(self.results[dim][ + 'pos_std'] - np.square(self.results[dim]['pos'])) + self.results[dim]['char_std'] = np.sqrt(self.results[dim][ + 'char_std'] - np.square(self.results[dim]['char'])) + + for dim in ['x', 'y', 'z']: + norm = k * self.results[dim]['slice volume'] + for key in self.keys: + self.results[dim][key] /= norm def _add_other_results(self, other): - # For parallel analysis - results = self.results - for dim in ['x', 'y', 'z']: - key = 'pos' - key_std = 'pos_std' - results[dim][key] += other[dim][key] - results[dim][key_std] += other[dim][key_std] - - key = 'char' - key_std = 'char_std' - results[dim][key] += other[dim][key] - results[dim][key_std] += other[dim][key_std] + # For parallel analysis + results = self.results + for dim in ['x', 'y', 'z']: + key = 'pos' + key_std = 'pos_std' + results[dim][key] += other[dim][key] + results[dim][key_std] += other[dim][key_std] + + key = 'char' + key_std = 'char_std' + results[dim][key] += other[dim][key] + results[dim][key_std] += other[dim][key_std] From b460c63fd38b867d9974af46a9351918912a8d60 Mon Sep 17 00:00:00 2001 From: Philip Loche Date: Thu, 29 Apr 2021 16:44:34 +0200 Subject: [PATCH 20/37] PEP8 again --- package/MDAnalysis/analysis/base.py | 3 ++- package/MDAnalysis/analysis/polymer.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/package/MDAnalysis/analysis/base.py b/package/MDAnalysis/analysis/base.py index 7ae00e40674..873f1aecf44 100644 --- a/package/MDAnalysis/analysis/base.py +++ b/package/MDAnalysis/analysis/base.py @@ -48,7 +48,8 @@ class Results(dict): ``results.value_key``. They store all results obatined from an analysis after the ``run`` call. - The current implementation is similar to the :class:`sklearn.utils.Bunch` class in `scikit-learn`_. + The current implementation is similar to the :class:`sklearn.utils.Bunch` + class in `scikit-learn`_. .. _`scikit-learn`: https://scikit-learn.org/ diff --git a/package/MDAnalysis/analysis/polymer.py b/package/MDAnalysis/analysis/polymer.py index c0e8f1db5f1..6b4e0ba2cf0 100644 --- a/package/MDAnalysis/analysis/polymer.py +++ b/package/MDAnalysis/analysis/polymer.py @@ -248,7 +248,7 @@ def _perform_fit(self): raise NoDataError("Use the run method first") from None self.x = np.arange(len(self.results.bond_autocorrelation)) * self.lb - self.lp = fit_exponential_decay(self.x, + self.lp = fit_exponential_decay(self.x, self.results.bond_autocorrelation) self.fit = np.exp(-self.x/self.lp) From 7df72d6d9fed6fd7ce00c1a90a141b23d370d9f6 Mon Sep 17 00:00:00 2001 From: Philip Loche Date: Thu, 29 Apr 2021 16:48:48 +0200 Subject: [PATCH 21/37] Revoked my stupid changes --- package/MDAnalysis/analysis/base.py | 4 +- package/MDAnalysis/analysis/lineardensity.py | 226 +++++++++---------- 2 files changed, 115 insertions(+), 115 deletions(-) diff --git a/package/MDAnalysis/analysis/base.py b/package/MDAnalysis/analysis/base.py index 873f1aecf44..5397c357a0e 100644 --- a/package/MDAnalysis/analysis/base.py +++ b/package/MDAnalysis/analysis/base.py @@ -50,9 +50,9 @@ class Results(dict): The current implementation is similar to the :class:`sklearn.utils.Bunch` class in `scikit-learn`_. - + .. _`scikit-learn`: https://scikit-learn.org/ - + Examples -------- diff --git a/package/MDAnalysis/analysis/lineardensity.py b/package/MDAnalysis/analysis/lineardensity.py index 46ef76683cd..0c765c8b139 100644 --- a/package/MDAnalysis/analysis/lineardensity.py +++ b/package/MDAnalysis/analysis/lineardensity.py @@ -124,126 +124,126 @@ class LinearDensity(AnalysisBase): """ def __init__(self, select, grouping='atoms', binsize=0.25, **kwargs): - super(LinearDensity, self).__init__(select.universe.trajectory, - **kwargs) - # allows use of run(parallel=True) - self._ags = [select] - self._universe = select.universe - - self.binsize = binsize - - # group of atoms on which to compute the COM (same as used in - # AtomGroup.wrap()) - self.grouping = grouping - - # Initiate result instances - self.results.x = Results(dim=0) - self.results.y = Results(dim=1) - self.results.z = Results(dim=2) - # Box sides - self.dimensions = self._universe.dimensions[:3] - self.volume = np.prod(self.dimensions) - # number of bins - bins = (self.dimensions // self.binsize).astype(int) - - # Here we choose a number of bins of the largest cell side so that - # x, y and z values can use the same "coord" column in the output file - self.nbins = bins.max() - slices_vol = self.volume / bins - - self.keys = ['pos', 'pos_std', 'char', 'char_std'] - - # Initialize results array with zeros - for dim in self.results: - idx = self.results[dim]['dim'] - self.results[dim]['slice volume'] = slices_vol[idx] - for key in self.keys: - self.results[dim][key] = np.zeros(self.nbins) - - # Variables later defined in _prepare() method - self.masses = None - self.charges = None - self.totalmass = None + super(LinearDensity, self).__init__(select.universe.trajectory, + **kwargs) + # allows use of run(parallel=True) + self._ags = [select] + self._universe = select.universe + + self.binsize = binsize + + # group of atoms on which to compute the COM (same as used in + # AtomGroup.wrap()) + self.grouping = grouping + + # Initiate result instances + self.results.x = Results(dim=0) + self.results.y = Results(dim=1) + self.results.z = Results(dim=2) + # Box sides + self.dimensions = self._universe.dimensions[:3] + self.volume = np.prod(self.dimensions) + # number of bins + bins = (self.dimensions // self.binsize).astype(int) + + # Here we choose a number of bins of the largest cell side so that + # x, y and z values can use the same "coord" column in the output file + self.nbins = bins.max() + slices_vol = self.volume / bins + + self.keys = ['pos', 'pos_std', 'char', 'char_std'] + + # Initialize results array with zeros + for dim in self.results: + idx = self.results[dim]['dim'] + self.results[dim]['slice volume'] = slices_vol[idx] + for key in self.keys: + self.results[dim][key] = np.zeros(self.nbins) + + # Variables later defined in _prepare() method + self.masses = None + self.charges = None + self.totalmass = None def _prepare(self): - # group must be a local variable, otherwise there will be - # issues with parallelization - group = getattr(self._ags[0], self.grouping) + # group must be a local variable, otherwise there will be + # issues with parallelization + group = getattr(self._ags[0], self.grouping) - # Get masses and charges for the selection - try: # in case it's not an atom - self.masses = np.array([elem.total_mass() for elem in group]) - self.charges = np.array([elem.total_charge() for elem in group]) - except AttributeError: # much much faster for atoms - self.masses = self._ags[0].masses - self.charges = self._ags[0].charges + # Get masses and charges for the selection + try: # in case it's not an atom + self.masses = np.array([elem.total_mass() for elem in group]) + self.charges = np.array([elem.total_charge() for elem in group]) + except AttributeError: # much much faster for atoms + self.masses = self._ags[0].masses + self.charges = self._ags[0].charges - self.totalmass = np.sum(self.masses) + self.totalmass = np.sum(self.masses) def _single_frame(self): - self.group = getattr(self._ags[0], self.grouping) - self._ags[0].wrap(compound=self.grouping) - - # Find position of atom/group of atoms - if self.grouping == 'atoms': - positions = self._ags[0].positions # faster for atoms - else: - # COM for res/frag/etc - positions = np.array([elem.centroid() for elem in self.group]) - - for dim in ['x', 'y', 'z']: - idx = self.results[dim]['dim'] - - key = 'pos' - key_std = 'pos_std' - # histogram for positions weighted on masses - hist, _ = np.histogram(positions[:, idx], - weights=self.masses, - bins=self.nbins, - range=(0.0, max(self.dimensions))) - - self.results[dim][key] += hist - self.results[dim][key_std] += np.square(hist) - - key = 'char' - key_std = 'char_std' - # histogram for positions weighted on charges - hist, _ = np.histogram(positions[:, idx], - weights=self.charges, - bins=self.nbins, - range=(0.0, max(self.dimensions))) - - self.results[dim][key] += hist - self.results[dim][key_std] += np.square(hist) + self.group = getattr(self._ags[0], self.grouping) + self._ags[0].wrap(compound=self.grouping) + + # Find position of atom/group of atoms + if self.grouping == 'atoms': + positions = self._ags[0].positions # faster for atoms + else: + # COM for res/frag/etc + positions = np.array([elem.centroid() for elem in self.group]) + + for dim in ['x', 'y', 'z']: + idx = self.results[dim]['dim'] + + key = 'pos' + key_std = 'pos_std' + # histogram for positions weighted on masses + hist, _ = np.histogram(positions[:, idx], + weights=self.masses, + bins=self.nbins, + range=(0.0, max(self.dimensions))) + + self.results[dim][key] += hist + self.results[dim][key_std] += np.square(hist) + + key = 'char' + key_std = 'char_std' + # histogram for positions weighted on charges + hist, _ = np.histogram(positions[:, idx], + weights=self.charges, + bins=self.nbins, + range=(0.0, max(self.dimensions))) + + self.results[dim][key] += hist + self.results[dim][key_std] += np.square(hist) def _conclude(self): - k = 6.022e-1 # divide by avodagro and convert from A3 to cm3 - - # Average results over the number of configurations - for dim in ['x', 'y', 'z']: - for key in ['pos', 'pos_std', 'char', 'char_std']: - self.results[dim][key] /= self.n_frames - # Compute standard deviation for the error - self.results[dim]['pos_std'] = np.sqrt(self.results[dim][ - 'pos_std'] - np.square(self.results[dim]['pos'])) - self.results[dim]['char_std'] = np.sqrt(self.results[dim][ - 'char_std'] - np.square(self.results[dim]['char'])) - - for dim in ['x', 'y', 'z']: - norm = k * self.results[dim]['slice volume'] - for key in self.keys: - self.results[dim][key] /= norm + k = 6.022e-1 # divide by avodagro and convert from A3 to cm3 + + # Average results over the number of configurations + for dim in ['x', 'y', 'z']: + for key in ['pos', 'pos_std', 'char', 'char_std']: + self.results[dim][key] /= self.n_frames + # Compute standard deviation for the error + self.results[dim]['pos_std'] = np.sqrt(self.results[dim][ + 'pos_std'] - np.square(self.results[dim]['pos'])) + self.results[dim]['char_std'] = np.sqrt(self.results[dim][ + 'char_std'] - np.square(self.results[dim]['char'])) + + for dim in ['x', 'y', 'z']: + norm = k * self.results[dim]['slice volume'] + for key in self.keys: + self.results[dim][key] /= norm def _add_other_results(self, other): - # For parallel analysis - results = self.results - for dim in ['x', 'y', 'z']: - key = 'pos' - key_std = 'pos_std' - results[dim][key] += other[dim][key] - results[dim][key_std] += other[dim][key_std] - - key = 'char' - key_std = 'char_std' - results[dim][key] += other[dim][key] - results[dim][key_std] += other[dim][key_std] + # For parallel analysis + results = self.results + for dim in ['x', 'y', 'z']: + key = 'pos' + key_std = 'pos_std' + results[dim][key] += other[dim][key] + results[dim][key_std] += other[dim][key_std] + + key = 'char' + key_std = 'char_std' + results[dim][key] += other[dim][key] + results[dim][key_std] += other[dim][key_std] From 64f701d7b80d1e8ba0a3939abd80d5e85d828cb8 Mon Sep 17 00:00:00 2001 From: Philip Loche Date: Thu, 29 Apr 2021 19:00:23 +0200 Subject: [PATCH 22/37] Add key valdiation --- package/MDAnalysis/analysis/base.py | 26 +++++++++++++++++++ .../MDAnalysisTests/analysis/test_base.py | 19 ++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/package/MDAnalysis/analysis/base.py b/package/MDAnalysis/analysis/base.py index 5397c357a0e..06e2fe17b61 100644 --- a/package/MDAnalysis/analysis/base.py +++ b/package/MDAnalysis/analysis/base.py @@ -31,6 +31,7 @@ import inspect import logging import itertools +import re import numpy as np from MDAnalysis import coordinates @@ -67,14 +68,39 @@ class in `scikit-learn`_. >>> results.c = [1, 2, 3, 4] >>> results['c'] [1, 2, 3, 4] + + Raises + ------ + ValueError + If a to assigned attribute has the same name as a default dictionary + attribute. + + ValueError + If a key is not of type ``str`` and therefore is not able to be + accessed by attribute. """ + def _validate_key(self, key): + if key in dir(dict): + raise TypeError(f"'{key}' is a protected dictionary attribute") + elif not (isinstance(key, str) and key[0].isalpha() \ + and re.match("^[a-zA-Z0-9]*$", key)): + raise TypeError("Given key is not able to be " + "accessed by attribute") + def __init__(self, **kwargs): + for key in kwargs.keys(): + self._validate_key(key) super().__init__(kwargs) def __setattr__(self, key, value): + self._validate_key(key) self[key] = value + def __setitem__(self, key, value): + self._validate_key(key) + super().__setitem__(key, value) + def __dir__(self): return self.keys() diff --git a/testsuite/MDAnalysisTests/analysis/test_base.py b/testsuite/MDAnalysisTests/analysis/test_base.py index 7173d1602b4..4d6cd624eaf 100644 --- a/testsuite/MDAnalysisTests/analysis/test_base.py +++ b/testsuite/MDAnalysisTests/analysis/test_base.py @@ -49,6 +49,25 @@ def test_no_attr(self, results): def test_dir(self, results): assert list(results.__dir__()) == ["a", "b"] + @pytest.mark.parametrize('key', dir(dict)) + def test_existing_dict_attr(self, results, key): + msg = f"'{key}' is a protected dictionary attribute" + with pytest.raises(TypeError, match=key): + results[key] = None + + @pytest.mark.parametrize('key', dir(dict)) + def test_wrong_init_type(self, key): + msg = f"'{key}' is a protected dictionary attribute" + with pytest.raises(TypeError, match=msg): + base.Results(**{key: None}) + + @pytest.mark.parametrize('key', ("0123", "0j", "1.1", "{}", "a[", "a ")) + def test_weird_key(self, results, key): + msg = "Given key is not able to be accessed by attribute" + with pytest.raises(TypeError, match=msg): + results[key] = None + + class FrameAnalysis(base.AnalysisBase): """Just grabs frame numbers of frames it goes over""" From c5be870113e4d81b87f1ff0f9e0148262cad6c87 Mon Sep 17 00:00:00 2001 From: Philip Loche Date: Thu, 29 Apr 2021 21:27:57 +0200 Subject: [PATCH 23/37] More style changes --- package/MDAnalysis/analysis/__init__.py | 4 +- package/MDAnalysis/analysis/base.py | 66 ++++++++++--------- package/MDAnalysis/analysis/gnm.py | 8 +-- package/MDAnalysis/analysis/lineardensity.py | 54 +++++++-------- package/MDAnalysis/analysis/polymer.py | 2 +- .../MDAnalysisTests/analysis/test_base.py | 2 +- 6 files changed, 71 insertions(+), 65 deletions(-) diff --git a/package/MDAnalysis/analysis/__init__.py b/package/MDAnalysis/analysis/__init__.py index 2775ed8b846..fc97de56f80 100644 --- a/package/MDAnalysis/analysis/__init__.py +++ b/package/MDAnalysis/analysis/__init__.py @@ -38,8 +38,8 @@ from MDAnalysis.anlaysis import AnalysisModule - analysis_obj = AnalysisModule(, ...) - analysis_obj.run(, , ) + analysis_obj = AnalysisModule(trajectory, ...) + analysis_obj.run(start_frame, stop_frame, step) print(analysis_obj.results) If you use them please check if the documentation mentions any specific caveats diff --git a/package/MDAnalysis/analysis/base.py b/package/MDAnalysis/analysis/base.py index 06e2fe17b61..3f0203b1c83 100644 --- a/package/MDAnalysis/analysis/base.py +++ b/package/MDAnalysis/analysis/base.py @@ -72,11 +72,11 @@ class in `scikit-learn`_. Raises ------ ValueError - If a to assigned attribute has the same name as a default dictionary + If a to assigned attribute has the same name as a default dictionary attribute. ValueError - If a key is not of type ``str`` and therefore is not able to be + If a key is not of type ``str`` and therefore is not able to be accessed by attribute. """ @@ -84,9 +84,8 @@ def _validate_key(self, key): if key in dir(dict): raise TypeError(f"'{key}' is a protected dictionary attribute") elif not (isinstance(key, str) and key[0].isalpha() \ - and re.match("^[a-zA-Z0-9]*$", key)): - raise TypeError("Given key is not able to be " - "accessed by attribute") + and re.match("^[a-zA-Z0-9_]*$", key)): + raise TypeError(f"'{key}' is not able to be accessed by attribute") def __init__(self, **kwargs): for key in kwargs.keys(): @@ -171,7 +170,8 @@ def _conclude(self): frames: np.ndarray array of Timestep frame indices. Only exists after calling run() results: :class:`Results` - results of calculation are stored after call to ``run`` + results of calculation are stored after call + to :meth:`AnalysisBase.run` """ @@ -281,24 +281,27 @@ def run(self, start=None, stop=None, step=None, verbose=None): class AnalysisFromFunction(AnalysisBase): r"""Create an :class:`AnalysisBase` from a function working on AtomGroups - .. code-block:: python - - def rotation_matrix(mobile, ref): - return mda.analysis.align.rotation_matrix(mobile, ref)[0] - - rot = AnalysisFromFunction(rotation_matrix, trajectory, - mobile, ref).run() - print(rot.results.timeseries) - Attributes ---------- results.frames : numpy.ndarray simulatiom frames taken for evaluation results.times : numpy.ndarray simulatiom times taken for evaluation - results.timeseries : asarray - Results for each frame of the underlaying function - stored after call to ``run``. + results.timeseries : numpy.ndarray + Results for each frame of the underlaying function + stored after call to :meth:`AnalysisFromFunction.run`. + + Example + ------- + .. code-block:: python + + def rotation_matrix(mobile, ref): + return mda.analysis.align.rotation_matrix(mobile, ref)[0] + + rot = AnalysisFromFunction(rotation_matrix, trajectory, + mobile, ref).run() + print(rot.results.timeseries) + Raises ------ @@ -366,7 +369,20 @@ def analysis_class(function): r"""Transform a function operating on a single frame to an :class:`AnalysisBase` class. - For an usage in a library we recommend the following style + Attributes + ---------- + results.frames : numpy.ndarray + simulatiom frames taken for evaluation + results.times : numpy.ndarray + simulatiom times taken for evaluation + results.timeseries : numpy.ndarray + Results for each frame of the underlaying function + stored after call to :meth:`AnalysisFromFunction.run`. + + Examples + -------- + + For an use in a library we recommend the following style .. code-block:: python @@ -385,22 +401,12 @@ def RotationMatrix(mobile, ref): rot = RotationMatrix(u.trajectory, mobile, ref).run(step=2) print(rot.results.timeseries) - Attributes - ---------- - results.frames : numpy.ndarray - simulatiom frames taken for evaluation - results.times : numpy.ndarray - simulatiom times taken for evaluation - results.timeseries : asarray - Results for each frame of the underlaying function - stored after call to ``run``. - Raises ------ ValueError : if ``function`` has the same kwargs as ``BaseAnalysis`` .. versionchanged:: 2.0.0 - Former `results` are now stored as `results.timeseries` + Former ``results`` are now stored as ``results.timeseries`` """ class WrapperClass(AnalysisFromFunction): diff --git a/package/MDAnalysis/analysis/gnm.py b/package/MDAnalysis/analysis/gnm.py index 31e44be35a4..e19addff6d6 100644 --- a/package/MDAnalysis/analysis/gnm.py +++ b/package/MDAnalysis/analysis/gnm.py @@ -245,8 +245,8 @@ class GNMAnalysis(AnalysisBase): .. versionchanged:: 2.0.0 Use :class:`~MDAnalysis.analysis.AnalysisBase` as parent class and - store results as attributes `times`, `eigenvalues` and - `eigenvectors` of the `results` attribute. + store results as attributes ``times``, ``eigenvalues`` and + ``eigenvectors`` of the `results` attribute. """ def __init__(self, @@ -416,8 +416,8 @@ class closeContactGNMAnalysis(GNMAnalysis): .. versionchanged:: 2.0.0 Use :class:`~MDAnalysis.analysis.AnalysisBase` as parent class and - store results as attributes `times`, `eigenvalues` and - `eigenvectors` of the `results` attribute. + store results as attributes ``times``, ``eigenvalues`` and + ``eigenvectors`` of the `results` attribute. """ def __init__(self, diff --git a/package/MDAnalysis/analysis/lineardensity.py b/package/MDAnalysis/analysis/lineardensity.py index 0c765c8b139..cf675ccfc7d 100644 --- a/package/MDAnalysis/analysis/lineardensity.py +++ b/package/MDAnalysis/analysis/lineardensity.py @@ -156,9 +156,9 @@ def __init__(self, select, grouping='atoms', binsize=0.25, **kwargs): # Initialize results array with zeros for dim in self.results: idx = self.results[dim]['dim'] - self.results[dim]['slice volume'] = slices_vol[idx] - for key in self.keys: - self.results[dim][key] = np.zeros(self.nbins) + self.results[dim]['slice_volume'] = slices_vol[idx] + for key in self.keys: + self.results[dim][key] = np.zeros(self.nbins) # Variables later defined in _prepare() method self.masses = None @@ -194,45 +194,45 @@ def _single_frame(self): for dim in ['x', 'y', 'z']: idx = self.results[dim]['dim'] - key = 'pos' - key_std = 'pos_std' - # histogram for positions weighted on masses - hist, _ = np.histogram(positions[:, idx], - weights=self.masses, - bins=self.nbins, - range=(0.0, max(self.dimensions))) + key = 'pos' + key_std = 'pos_std' + # histogram for positions weighted on masses + hist, _ = np.histogram(positions[:, idx], + weights=self.masses, + bins=self.nbins, + range=(0.0, max(self.dimensions))) - self.results[dim][key] += hist - self.results[dim][key_std] += np.square(hist) + self.results[dim][key] += hist + self.results[dim][key_std] += np.square(hist) - key = 'char' - key_std = 'char_std' - # histogram for positions weighted on charges - hist, _ = np.histogram(positions[:, idx], - weights=self.charges, - bins=self.nbins, - range=(0.0, max(self.dimensions))) + key = 'char' + key_std = 'char_std' + # histogram for positions weighted on charges + hist, _ = np.histogram(positions[:, idx], + weights=self.charges, + bins=self.nbins, + range=(0.0, max(self.dimensions))) - self.results[dim][key] += hist - self.results[dim][key_std] += np.square(hist) + self.results[dim][key] += hist + self.results[dim][key_std] += np.square(hist) def _conclude(self): k = 6.022e-1 # divide by avodagro and convert from A3 to cm3 - # Average results over the number of configurations + # Average results over the number of configurations for dim in ['x', 'y', 'z']: for key in ['pos', 'pos_std', 'char', 'char_std']: self.results[dim][key] /= self.n_frames # Compute standard deviation for the error self.results[dim]['pos_std'] = np.sqrt(self.results[dim][ - 'pos_std'] - np.square(self.results[dim]['pos'])) + 'pos_std'] - np.square(self.results[dim]['pos'])) self.results[dim]['char_std'] = np.sqrt(self.results[dim][ - 'char_std'] - np.square(self.results[dim]['char'])) + 'char_std'] - np.square(self.results[dim]['char'])) for dim in ['x', 'y', 'z']: - norm = k * self.results[dim]['slice volume'] - for key in self.keys: - self.results[dim][key] /= norm + norm = k * self.results[dim]['slice_volume'] + for key in self.keys: + self.results[dim][key] /= norm def _add_other_results(self, other): # For parallel analysis diff --git a/package/MDAnalysis/analysis/polymer.py b/package/MDAnalysis/analysis/polymer.py index 6b4e0ba2cf0..4bfc2dcf674 100644 --- a/package/MDAnalysis/analysis/polymer.py +++ b/package/MDAnalysis/analysis/polymer.py @@ -188,7 +188,7 @@ class PersistenceLength(AnalysisBase): .. versionchanged:: 1.0.0 Deprecated :meth:`PersistenceLength.perform_fit` has now been removed. .. versionchanged:: 2.0.0 - Former `results` are now stored as `results.bond_autocorrelation` + Former ``results`` are now stored as ``results.bond_autocorrelation`` """ def __init__(self, atomgroups, **kwargs): super(PersistenceLength, self).__init__( diff --git a/testsuite/MDAnalysisTests/analysis/test_base.py b/testsuite/MDAnalysisTests/analysis/test_base.py index 4d6cd624eaf..04c2a9e5cc8 100644 --- a/testsuite/MDAnalysisTests/analysis/test_base.py +++ b/testsuite/MDAnalysisTests/analysis/test_base.py @@ -63,7 +63,7 @@ def test_wrong_init_type(self, key): @pytest.mark.parametrize('key', ("0123", "0j", "1.1", "{}", "a[", "a ")) def test_weird_key(self, results, key): - msg = "Given key is not able to be accessed by attribute" + msg = f"'{key}' is not able to be accessed by attribute" with pytest.raises(TypeError, match=msg): results[key] = None From c874404e4240fea7f221d3eefa7e199e21ca531a Mon Sep 17 00:00:00 2001 From: Philip Loche Date: Thu, 29 Apr 2021 22:49:07 +0200 Subject: [PATCH 24/37] Restructered doc --- package/MDAnalysis/analysis/base.py | 118 ++++++++++--------- package/MDAnalysis/analysis/gnm.py | 10 +- package/MDAnalysis/analysis/lineardensity.py | 3 +- package/MDAnalysis/analysis/polymer.py | 3 +- 4 files changed, 71 insertions(+), 63 deletions(-) diff --git a/package/MDAnalysis/analysis/base.py b/package/MDAnalysis/analysis/base.py index 3f0203b1c83..59aaec72f7a 100644 --- a/package/MDAnalysis/analysis/base.py +++ b/package/MDAnalysis/analysis/base.py @@ -69,6 +69,7 @@ class in `scikit-learn`_. >>> results['c'] [1, 2, 3, 4] + Raises ------ ValueError @@ -123,8 +124,28 @@ class AnalysisBase(object): :meth:`_single_frame` must be defined. It is also possible to define :meth:`_prepare` and :meth:`_conclude` for pre and post processing. All results should be stored as attributes of the :class:`Results` - container. See the example below. + container. + + Parameters + ---------- + trajectory : mda.Reader + A trajectory Reader + verbose : bool, optional + Turn on more logging and debugging + + Attributes + ---------- + times: np.ndarray + array of Timestep times. Only exists after calling run() + frames: np.ndarray + array of Timestep frame indices. Only exists after calling run() + results: :class:`Results` + results of calculation are stored after call + to :meth:`AnalysisBase.run` + + Example + ------- .. code-block:: python class NewAnalysis(AnalysisBase): @@ -145,7 +166,7 @@ def _single_frame(self): # Called after the trajectory is moved onto each new frame. # store a example_result of `some_function` for a single frame self.results.example_result.append(some_function(self._ag, - self._parameter)) + self._parameter)) def _conclude(self): # OPTIONAL @@ -154,7 +175,7 @@ def _conclude(self): self.results.example_result = np.asarray(self.example_result) self.results.example_result /= np.sum(self.result) - Afterwards the new analysis can be run like this. + Afterwards the new analysis can be run like this: .. code-block:: python @@ -163,33 +184,15 @@ def _conclude(self): # results can also accessed by key print(na.results["example_result"]) - Attributes - ---------- - times: np.ndarray - array of Timestep times. Only exists after calling run() - frames: np.ndarray - array of Timestep frame indices. Only exists after calling run() - results: :class:`Results` - results of calculation are stored after call - to :meth:`AnalysisBase.run` + + .. versionchanged:: 1.0.0 + Support for setting ``start``, ``stop``, and ``step`` has been + removed. These should now be directly passed to + :meth:`AnalysisBase.run`. """ def __init__(self, trajectory, verbose=False, **kwargs): - """ - Parameters - ---------- - trajectory : mda.Reader - A trajectory Reader - verbose : bool, optional - Turn on more logging and debugging, default ``False`` - - - .. versionchanged:: 1.0.0 - Support for setting ``start``, ``stop``, and ``step`` has been - removed. These should now be directly passed to - :meth:`AnalysisBase.run`. - """ self._trajectory = trajectory self._verbose = verbose self.results = Results() @@ -213,7 +216,6 @@ def _setup_frames(self, trajectory, start=None, stop=None, step=None): .. versionchanged:: 1.0.0 Added .frames and .times arrays as attributes - """ self._trajectory = trajectory start, stop, step = trajectory.check_slice_indices(start, stop, step) @@ -281,6 +283,18 @@ def run(self, start=None, stop=None, step=None, verbose=None): class AnalysisFromFunction(AnalysisBase): r"""Create an :class:`AnalysisBase` from a function working on AtomGroups + Parameters + ---------- + function : callable + function to evaluate at each frame + trajectory : mda.coordinates.Reader (optional) + trajectory to iterate over. If ``None`` the first AtomGroup found in + args and kwargs is used as a source for the trajectory. + *args : list + arguments for ``function`` + **kwargs : dict + arguments for ``function`` and ``AnalysisBase`` + Attributes ---------- results.frames : numpy.ndarray @@ -295,41 +309,30 @@ class AnalysisFromFunction(AnalysisBase): ------- .. code-block:: python - def rotation_matrix(mobile, ref): - return mda.analysis.align.rotation_matrix(mobile, ref)[0] + def rotation_matrix(mobile, ref): + return mda.analysis.align.rotation_matrix(mobile, ref)[0] - rot = AnalysisFromFunction(rotation_matrix, trajectory, - mobile, ref).run() - print(rot.results.timeseries) + rot = AnalysisFromFunction(rotation_matrix, trajectory, + mobile, ref).run() + print(rot.results.timeseries) Raises ------ - ValueError : if ``function`` has the same kwargs as ``BaseAnalysis`` + ValueError + if ``function`` has the same ``kwargs`` as ``BaseAnalysis`` + + + .. versionchanged:: 1.0.0 + Support for directly passing the ``start``, ``stop``, and ``step`` + arguments has been removed. These should instead be passed + to :meth:`AnalysisFromFunction.run`. .. versionchanged:: 2.0.0 Former :attr:`results` are now stored as :attr:`results.timeseries` """ def __init__(self, function, trajectory=None, *args, **kwargs): - """Parameters - ---------- - function : callable - function to evaluate at each frame - trajectory : mda.coordinates.Reader (optional) - trajectory to iterate over. If ``None`` the first AtomGroup found in - args and kwargs is used as a source for the trajectory. - *args : list - arguments for ``function`` - **kwargs : dict - arguments for ``function`` and ``AnalysisBase`` - - .. versionchanged:: 1.0.0 - Support for directly passing the ``start``, ``stop``, and ``step`` - arguments has been removed. These should instead be passed - to :meth:`AnalysisFromFunction.run`. - - """ if (trajectory is not None) and (not isinstance( trajectory, coordinates.base.ProtoReader)): args = (trajectory,) + args @@ -369,6 +372,11 @@ def analysis_class(function): r"""Transform a function operating on a single frame to an :class:`AnalysisBase` class. + Parameters + ---------- + function : callable + function to evaluate at each frame + Attributes ---------- results.frames : numpy.ndarray @@ -401,9 +409,12 @@ def RotationMatrix(mobile, ref): rot = RotationMatrix(u.trajectory, mobile, ref).run(step=2) print(rot.results.timeseries) + Raises ------ - ValueError : if ``function`` has the same kwargs as ``BaseAnalysis`` + ValueError + if ``function`` has the same ``kwargs`` as ``BaseAnalysis`` + .. versionchanged:: 2.0.0 Former ``results`` are now stored as ``results.timeseries`` @@ -437,7 +448,8 @@ def _filter_baseanalysis_kwargs(function, kwargs): Raises ------ - ValueError : if ``function`` has the same kwargs as ``BaseAnalysis`` + ValueError + if ``function`` has the same ``kwargs`` as ``BaseAnalysis`` """ try: # pylint: disable=deprecated-method diff --git a/package/MDAnalysis/analysis/gnm.py b/package/MDAnalysis/analysis/gnm.py index e19addff6d6..65f2cdb13c2 100644 --- a/package/MDAnalysis/analysis/gnm.py +++ b/package/MDAnalysis/analysis/gnm.py @@ -208,20 +208,19 @@ class GNMAnalysis(AnalysisBase): universe : Universe Analyze the full trajectory in the universe. select : str (optional) - MDAnalysis selection string, default "protein and name CA" + MDAnalysis selection string cutoff : float (optional) Consider selected atoms within the cutoff as neighbors for the Gaussian network model. ReportVector : str (optional) filename to write eigenvectors to, by default no output is written - (``None``) Bonus_groups : tuple This is a tuple of selection strings that identify additional groups (such as ligands). The center of mass of each group will be added as a single point in the ENM (it is a popular way of treating small ligands such as drugs). You need to ensure that none of the atoms in `Bonus_groups` is contained in `selection` as this could lead to - double counting. No checks are applied. Default is ``None``. + double counting. No checks are applied. Attributes ---------- @@ -373,13 +372,12 @@ class closeContactGNMAnalysis(GNMAnalysis): universe : Universe Analyze the full trajectory in the universe. select : str (optional) - MDAnalysis selection string, default "protein" + MDAnalysis selection string cutoff : float (optional) Consider selected atoms within the cutoff as neighbors for the - Gaussian network model [4.5 Å]. + Gaussian network model. ReportVector : str (optional) filename to write eigenvectors to, by default no output is written - (``None``) weights : {"size", None} (optional) If set to "size" (the default) then weight the contact by :math:`1/\sqrt{N_i N_j}` where :math:`N_i` and :math:`N_j` are the diff --git a/package/MDAnalysis/analysis/lineardensity.py b/package/MDAnalysis/analysis/lineardensity.py index cf675ccfc7d..a3673f560f1 100644 --- a/package/MDAnalysis/analysis/lineardensity.py +++ b/package/MDAnalysis/analysis/lineardensity.py @@ -50,8 +50,7 @@ class LinearDensity(AnalysisBase): histograms. Defines the resolution of the resulting density profile (smaller --> higher resolution) verbose : bool (optional) - Show detailed progress of the calculation if set to ``True``; the - default is ``False``. + Show detailed progress of the calculation if set to ``True`` Attributes ---------- diff --git a/package/MDAnalysis/analysis/polymer.py b/package/MDAnalysis/analysis/polymer.py index 4bfc2dcf674..21973db3057 100644 --- a/package/MDAnalysis/analysis/polymer.py +++ b/package/MDAnalysis/analysis/polymer.py @@ -163,8 +163,7 @@ class PersistenceLength(AnalysisBase): List of AtomGroups. Each should represent a single polymer chain, ordered in the correct order. verbose : bool (optional) - Show detailed progress of the calculation if set to ``True``; the - default is ``False``. + Show detailed progress of the calculation if set to ``True``. Attributes ---------- From b5603c372a7d499fed6fde67525ae88aba0151ec Mon Sep 17 00:00:00 2001 From: Philip Loche Date: Thu, 29 Apr 2021 22:53:14 +0200 Subject: [PATCH 25/37] Again some PEP8 issues --- package/MDAnalysis/analysis/base.py | 6 +++--- testsuite/MDAnalysisTests/analysis/test_base.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package/MDAnalysis/analysis/base.py b/package/MDAnalysis/analysis/base.py index 59aaec72f7a..9d205cf2e10 100644 --- a/package/MDAnalysis/analysis/base.py +++ b/package/MDAnalysis/analysis/base.py @@ -84,8 +84,8 @@ class in `scikit-learn`_. def _validate_key(self, key): if key in dir(dict): raise TypeError(f"'{key}' is a protected dictionary attribute") - elif not (isinstance(key, str) and key[0].isalpha() \ - and re.match("^[a-zA-Z0-9_]*$", key)): + elif not (isinstance(key, str) and key[0].isalpha() + and re.match("^[a-zA-Z0-9_]*$", key)): raise TypeError(f"'{key}' is not able to be accessed by attribute") def __init__(self, **kwargs): @@ -140,7 +140,7 @@ class AnalysisBase(object): frames: np.ndarray array of Timestep frame indices. Only exists after calling run() results: :class:`Results` - results of calculation are stored after call + results of calculation are stored after call to :meth:`AnalysisBase.run` diff --git a/testsuite/MDAnalysisTests/analysis/test_base.py b/testsuite/MDAnalysisTests/analysis/test_base.py index 04c2a9e5cc8..c1874085ab1 100644 --- a/testsuite/MDAnalysisTests/analysis/test_base.py +++ b/testsuite/MDAnalysisTests/analysis/test_base.py @@ -53,7 +53,7 @@ def test_dir(self, results): def test_existing_dict_attr(self, results, key): msg = f"'{key}' is a protected dictionary attribute" with pytest.raises(TypeError, match=key): - results[key] = None + results[key] = None @pytest.mark.parametrize('key', dir(dict)) def test_wrong_init_type(self, key): @@ -65,7 +65,7 @@ def test_wrong_init_type(self, key): def test_weird_key(self, results, key): msg = f"'{key}' is not able to be accessed by attribute" with pytest.raises(TypeError, match=msg): - results[key] = None + results[key] = None From 27919126a1a62f84b26e6b54df660f38b044fb8b Mon Sep 17 00:00:00 2001 From: Philip Loche Date: Thu, 29 Apr 2021 23:33:35 +0200 Subject: [PATCH 26/37] Minor docs tweaks --- package/MDAnalysis/analysis/base.py | 31 +++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/package/MDAnalysis/analysis/base.py b/package/MDAnalysis/analysis/base.py index 9d205cf2e10..c0dead438b8 100644 --- a/package/MDAnalysis/analysis/base.py +++ b/package/MDAnalysis/analysis/base.py @@ -57,6 +57,7 @@ class in `scikit-learn`_. Examples -------- + >>> from MDAnalysis.analysis.base import Results >>> results = Results(a=1, b=2) >>> results['b'] 2 @@ -72,11 +73,11 @@ class in `scikit-learn`_. Raises ------ - ValueError - If a to assigned attribute has the same name as a default dictionary + TypeError + If an attribute would have the same name as a default dictionary attribute. - ValueError + TypeError If a key is not of type ``str`` and therefore is not able to be accessed by attribute. """ @@ -135,10 +136,12 @@ class AnalysisBase(object): Attributes ---------- - times: np.ndarray - array of Timestep times. Only exists after calling run() - frames: np.ndarray - array of Timestep frame indices. Only exists after calling run() + times: numpy.ndarray + array of Timestep times. Only exists after calling + :meth:`AnalysisBase.run` + frames: numpy.ndarray + array of Timestep frame indices. Only exists after calling + :meth:`AnalysisBase.run` results: :class:`Results` results of calculation are stored after call to :meth:`AnalysisBase.run` @@ -148,6 +151,8 @@ class AnalysisBase(object): ------- .. code-block:: python + from MDAnalysis.analysis.base import AnalysisBase + class NewAnalysis(AnalysisBase): def __init__(self, atomgroup, parameter, **kwargs): super(NewAnalysis, self).__init__(atomgroup.universe.trajectory, @@ -175,11 +180,17 @@ def _conclude(self): self.results.example_result = np.asarray(self.example_result) self.results.example_result /= np.sum(self.result) - Afterwards the new analysis can be run like this: + Afterwards the new analysis can be run like this .. code-block:: python - na = NewAnalysis(u.select_atoms('name CA'), 35).run(start=10, stop=20) + import MDAnalysis as mda + from MDAnalysisTests.datafiles import PSF, DCD + + u = mda.Universe(PSF, DCD) + + na = NewAnalysis(u.select_atoms('name CA'), 35) + na.run(start=10, stop=20) print(na.results.example_result) # results can also accessed by key print(na.results["example_result"]) @@ -287,7 +298,7 @@ class AnalysisFromFunction(AnalysisBase): ---------- function : callable function to evaluate at each frame - trajectory : mda.coordinates.Reader (optional) + trajectory : mda.coordinates.Reader, optional trajectory to iterate over. If ``None`` the first AtomGroup found in args and kwargs is used as a source for the trajectory. *args : list From 2461ac614fac772e81dc4a444c2fbc17c0278221 Mon Sep 17 00:00:00 2001 From: Philip Loche Date: Fri, 30 Apr 2021 00:23:11 +0200 Subject: [PATCH 27/37] Apply suggestions from code review Co-authored-by: Lily Wang <31115101+lilyminium@users.noreply.github.com> --- package/MDAnalysis/analysis/__init__.py | 8 ++--- package/MDAnalysis/analysis/base.py | 34 ++++++++++---------- package/MDAnalysis/analysis/gnm.py | 10 +++--- package/MDAnalysis/analysis/lineardensity.py | 2 +- 4 files changed, 27 insertions(+), 27 deletions(-) diff --git a/package/MDAnalysis/analysis/__init__.py b/package/MDAnalysis/analysis/__init__.py index fc97de56f80..eb0d2e90904 100644 --- a/package/MDAnalysis/analysis/__init__.py +++ b/package/MDAnalysis/analysis/__init__.py @@ -27,18 +27,18 @@ The :mod:`MDAnalysis.analysis` sub-package contains various recipes and algorithms that can be used to analyze MD trajectories. -If not stated differently, an analysis conducted by the available modules +Unless stated otherwise, an analysis using the available modules always follows the same structure 1. Initialize the object previously imported. -2. Run the analysis for specific trajectory slices +2. Run the analysis, optionally for specific trajectory slices 3. Access the analysis from the `results` attribute (if available) .. code-block:: python - from MDAnalysis.anlaysis import AnalysisModule + from MDAnalysis.analysis import ExampleAnalysisModule # (e.g. RMSD) - analysis_obj = AnalysisModule(trajectory, ...) + analysis_obj = ExampleAnalysisModule(universe, ...) analysis_obj.run(start_frame, stop_frame, step) print(analysis_obj.results) diff --git a/package/MDAnalysis/analysis/base.py b/package/MDAnalysis/analysis/base.py index c0dead438b8..7586f350f86 100644 --- a/package/MDAnalysis/analysis/base.py +++ b/package/MDAnalysis/analysis/base.py @@ -44,10 +44,10 @@ class Results(dict): r"""Container object for storing results. - Results are dictionaries that provide two ways in which can values be + Results are dictionaries that provide two ways by which can values be accessed: by dict key ``results["value_key"]`` or by object attribute, - ``results.value_key``. They store all results obatined from an analysis - after the ``run`` call. + ``results.value_key``. They store all results obtained from an analysis + after calling :func:`run()`. The current implementation is similar to the :class:`sklearn.utils.Bunch` class in `scikit-learn`_. @@ -90,7 +90,7 @@ def _validate_key(self, key): raise TypeError(f"'{key}' is not able to be accessed by attribute") def __init__(self, **kwargs): - for key in kwargs.keys(): + for key in kwargs: self._validate_key(key) super().__init__(kwargs) @@ -114,16 +114,16 @@ def __getattr__(self, key): class AnalysisBase(object): - r"""Base class for defining multi frame analysis + r"""Base class for defining multi-frame analysis - The class it is designed as a template for creating multiframe analyses. + The class is designed as a template for creating multi-frame analyses. This class will automatically take care of setting up the trajectory reader for iterating, and it offers to show a progress meter. Computed results are stored inside the :attr:`results` attribute. To define a new Analysis, `AnalysisBase` needs to be subclassed - :meth:`_single_frame` must be defined. It is also possible to define - :meth:`_prepare` and :meth:`_conclude` for pre and post processing. + and :meth:`_single_frame` must be defined. It is also possible to define + :meth:`_prepare` and :meth:`_conclude` for pre- and post-processing. All results should be stored as attributes of the :class:`Results` container. @@ -169,7 +169,7 @@ def _prepare(self): def _single_frame(self): # REQUIRED # Called after the trajectory is moved onto each new frame. - # store a example_result of `some_function` for a single frame + # store an example_result of `some_function` for a single frame self.results.example_result.append(some_function(self._ag, self._parameter)) @@ -192,7 +192,7 @@ def _conclude(self): na = NewAnalysis(u.select_atoms('name CA'), 35) na.run(start=10, stop=20) print(na.results.example_result) - # results can also accessed by key + # results can also be accessed by key print(na.results["example_result"]) @@ -309,11 +309,11 @@ class AnalysisFromFunction(AnalysisBase): Attributes ---------- results.frames : numpy.ndarray - simulatiom frames taken for evaluation + simulation frames used in analysis results.times : numpy.ndarray - simulatiom times taken for evaluation + simulation times used in analysis results.timeseries : numpy.ndarray - Results for each frame of the underlaying function + Results for each frame of the wrapped function, stored after call to :meth:`AnalysisFromFunction.run`. Example @@ -391,17 +391,17 @@ def analysis_class(function): Attributes ---------- results.frames : numpy.ndarray - simulatiom frames taken for evaluation + simulation frames used in analysis results.times : numpy.ndarray - simulatiom times taken for evaluation + simulation times used in analysis results.timeseries : numpy.ndarray - Results for each frame of the underlaying function + Results for each frame of the wrapped function, stored after call to :meth:`AnalysisFromFunction.run`. Examples -------- - For an use in a library we recommend the following style + For use in a library, we recommend the following style .. code-block:: python diff --git a/package/MDAnalysis/analysis/gnm.py b/package/MDAnalysis/analysis/gnm.py index 65f2cdb13c2..146c443b83b 100644 --- a/package/MDAnalysis/analysis/gnm.py +++ b/package/MDAnalysis/analysis/gnm.py @@ -225,7 +225,7 @@ class GNMAnalysis(AnalysisBase): Attributes ---------- results.times : numpy.ndarray - simulatiom times taken for evaluation + simulation times used in analysis results.eigenvalues : numpy.ndarray calculated eigenvalues results.eigenvectors : numpy.ndarray @@ -245,7 +245,7 @@ class GNMAnalysis(AnalysisBase): .. versionchanged:: 2.0.0 Use :class:`~MDAnalysis.analysis.AnalysisBase` as parent class and store results as attributes ``times``, ``eigenvalues`` and - ``eigenvectors`` of the `results` attribute. + ``eigenvectors`` of the ``results`` attribute. """ def __init__(self, @@ -387,10 +387,10 @@ class closeContactGNMAnalysis(GNMAnalysis): Attributes ---------- results.times : numpy.ndarray - simulatiom times taken for evaluation - results.eiegenvalues : numpy.ndarray + simulation times used in analysis + results.eigenvalues : numpy.ndarray calculated eigenvalues - results.eiegenvectors : numpy.ndarray + results.eigenvectors : numpy.ndarray calculated eigenvectors Notes diff --git a/package/MDAnalysis/analysis/lineardensity.py b/package/MDAnalysis/analysis/lineardensity.py index a3673f560f1..5174076be53 100644 --- a/package/MDAnalysis/analysis/lineardensity.py +++ b/package/MDAnalysis/analysis/lineardensity.py @@ -93,7 +93,7 @@ class LinearDensity(AnalysisBase): Example ------- - First create a `LinearDensity` object by supplying a selection, + First create a ``LinearDensity`` object by supplying a selection, then use the :meth:`run` method. Finally access the results stored in results, i.e. the mass density in the x direction. From 4802db780ba4d70ab3b4129730e175934356e596 Mon Sep 17 00:00:00 2001 From: Philip Loche Date: Fri, 30 Apr 2021 00:56:10 +0200 Subject: [PATCH 28/37] Changed dict -> UserDict --- package/MDAnalysis/analysis/base.py | 18 ++++----- package/MDAnalysis/analysis/gnm.py | 15 ++------ package/MDAnalysis/analysis/lineardensity.py | 40 ++++---------------- 3 files changed, 20 insertions(+), 53 deletions(-) diff --git a/package/MDAnalysis/analysis/base.py b/package/MDAnalysis/analysis/base.py index 7586f350f86..68409e408c8 100644 --- a/package/MDAnalysis/analysis/base.py +++ b/package/MDAnalysis/analysis/base.py @@ -28,6 +28,7 @@ classes. """ +from collections import UserDict import inspect import logging import itertools @@ -41,7 +42,7 @@ logger = logging.getLogger(__name__) -class Results(dict): +class Results(UserDict): r"""Container object for storing results. Results are dictionaries that provide two ways by which can values be @@ -85,8 +86,8 @@ class in `scikit-learn`_. def _validate_key(self, key): if key in dir(dict): raise TypeError(f"'{key}' is a protected dictionary attribute") - elif not (isinstance(key, str) and key[0].isalpha() - and re.match("^[a-zA-Z0-9_]*$", key)): + elif not (isinstance(key, str) + and re.match("^[a-zA-Z_][a-zA-Z0-9_]*$", key)): raise TypeError(f"'{key}' is not able to be accessed by attribute") def __init__(self, **kwargs): @@ -95,7 +96,6 @@ def __init__(self, **kwargs): super().__init__(kwargs) def __setattr__(self, key, value): - self._validate_key(key) self[key] = value def __setitem__(self, key, value): @@ -129,7 +129,7 @@ class AnalysisBase(object): Parameters ---------- - trajectory : mda.Reader + trajectory : MDAnalysis.coordinates.base.ReaderBase A trajectory Reader verbose : bool, optional Turn on more logging and debugging @@ -304,7 +304,7 @@ class AnalysisFromFunction(AnalysisBase): *args : list arguments for ``function`` **kwargs : dict - arguments for ``function`` and ``AnalysisBase`` + arguments for ``function`` and :class:`AnalysisBase` Attributes ---------- @@ -331,7 +331,7 @@ def rotation_matrix(mobile, ref): Raises ------ ValueError - if ``function`` has the same ``kwargs`` as ``BaseAnalysis`` + if ``function`` has the same ``kwargs`` as :class:`AnalysisBase` .. versionchanged:: 1.0.0 @@ -424,7 +424,7 @@ def RotationMatrix(mobile, ref): Raises ------ ValueError - if ``function`` has the same ``kwargs`` as ``BaseAnalysis`` + if ``function`` has the same ``kwargs`` as :class:`AnalysisBase` .. versionchanged:: 2.0.0 @@ -460,7 +460,7 @@ def _filter_baseanalysis_kwargs(function, kwargs): Raises ------ ValueError - if ``function`` has the same ``kwargs`` as ``BaseAnalysis`` + if ``function`` has the same ``kwargs`` as :class:`AnalysisBase` """ try: # pylint: disable=deprecated-method diff --git a/package/MDAnalysis/analysis/gnm.py b/package/MDAnalysis/analysis/gnm.py index 146c443b83b..e5ef496f8ca 100644 --- a/package/MDAnalysis/analysis/gnm.py +++ b/package/MDAnalysis/analysis/gnm.py @@ -259,7 +259,6 @@ def __init__(self, self.select = select self.cutoff = cutoff self.results = Results() - self.results.times = [] self.results.eigenvalues = [] self.results.eigenvectors = [] self._timesteps = None # time for each frame @@ -268,8 +267,8 @@ def __init__(self, if Bonus_groups else [] self.ca = self.u.select_atoms(self.select) - def _generate_output(self, w, v, outputobject, time, matrix, - nmodes=2, ReportVector=None, counter=0): + def _generate_output(self, w, v, outputobject, + ReportVector=None, counter=0): """Appends time, eigenvalues and eigenvectors to results. This generates the output by adding eigenvalue and @@ -285,13 +284,11 @@ def _generate_output(self, w, v, outputobject, time, matrix, print( "", counter, - time, item[0] + 1, w[list_map[1]], item[1], file=oup) - outputobject.times.append(time) outputobject.eigenvalues.append(w[list_map[1]]) outputobject.eigenvectors.append(v[list_map[1]]) @@ -329,9 +326,6 @@ def generate_kirchoff(self): return matrix - def _prepare(self): - self.timeseries = [] - def _single_frame(self): matrix = self.generate_kirchoff() try: @@ -347,15 +341,12 @@ def _single_frame(self): w, v, self.results, - self._ts.time, matrix, ReportVector=self.ReportVector, counter=self._ts.frame) def _conclude(self): - self._timesteps = self.times - - self.results.times = np.asarray(self.results.times) + self.results.times = self.times self.results.eigenvalues = np.asarray(self.results.eigenvalues) self.results.eigenvectors = np.asarray(self.results.eigenvectors) diff --git a/package/MDAnalysis/analysis/lineardensity.py b/package/MDAnalysis/analysis/lineardensity.py index 5174076be53..9316094fac8 100644 --- a/package/MDAnalysis/analysis/lineardensity.py +++ b/package/MDAnalysis/analysis/lineardensity.py @@ -25,7 +25,7 @@ =========================================================== A tool to compute mass and charge density profiles along the three -cartesian axes of the simulation cell. Works only for orthorombic, +cartesian axes [xyz] of the simulation cell. Works only for orthorombic, fixed volume cells (thus for simulations in canonical NVT ensemble). """ import os.path as path @@ -49,47 +49,23 @@ class LinearDensity(AnalysisBase): Bin width in Angstrom used to build linear density histograms. Defines the resolution of the resulting density profile (smaller --> higher resolution) - verbose : bool (optional) + verbose : bool, optional Show detailed progress of the calculation if set to ``True`` Attributes ---------- results.x.dim : int - index of the x axes (0) + index of the [xyz] axes results.x.pos : numpy.ndarray - mass density in x direction + mass density in [xyz] direction results.x.pos_std : numpy.ndarray - standard deviation of the mass density in x direction + standard deviation of the mass density in [xyz] direction results.x.char : numpy.ndarray - charge density in x direction + charge density in [xyz] direction results.x.char_std : numpy.ndarray - standard deviation of the charge density in x direction + standard deviation of the charge density in [xyz] direction results.x.slice_volume : float - volume of bin in x direction - results.y.dim : int - index of the y axes (1) - results.y.pos : numpy.ndarray - mass density in y direction - results.y.pos_std : numpy.ndarray - standard deviation of the mass density in y direction - results.y.char : numpy.ndarray - charge density in y direction - results.y.char_std : numpy.ndarray - standard deviation of the charge density in y direction - results.y.slice_volume : float - volume of bin in y direction - results.z.dim : int - index of the z axes (2) - results.z.pos : numpy.ndarray - mass density in z direction - results.z.pos_std : numpy.ndarray - standard deviation of the mass density in z direction - results.z.char : numpy.ndarray - charge density in z direction - results.z.char_std : numpy.ndarray - standard deviation of the charge density in z direction - results.z.slice_volume : float - volume of bin in z direction + volume of bin in [xyz] direction Example ------- From f096b6bc2a0791c36ec52d8765314e592c59b987 Mon Sep 17 00:00:00 2001 From: Philip Loche Date: Fri, 30 Apr 2021 10:09:28 +0200 Subject: [PATCH 29/37] Reworked Analysis documentation --- package/MDAnalysis/analysis/__init__.py | 13 +++++----- package/MDAnalysis/analysis/base.py | 17 ++++++------ .../documentation_pages/analysis/init.rst | 1 + .../documentation_pages/analysis_modules.rst | 26 +++++++++++++------ 4 files changed, 34 insertions(+), 23 deletions(-) create mode 100644 package/doc/sphinx/source/documentation_pages/analysis/init.rst diff --git a/package/MDAnalysis/analysis/__init__.py b/package/MDAnalysis/analysis/__init__.py index eb0d2e90904..c1cc92b3386 100644 --- a/package/MDAnalysis/analysis/__init__.py +++ b/package/MDAnalysis/analysis/__init__.py @@ -22,17 +22,18 @@ # """ -:mod:`MDAnalysis.analysis` --- Analysis code based on MDAnalysis +Analysis code based on MDAnalysis --- :mod:`MDAnalysis.analysis` ================================================================ The :mod:`MDAnalysis.analysis` sub-package contains various recipes and algorithms that can be used to analyze MD trajectories. -Unless stated otherwise, an analysis using the available modules -always follows the same structure +An analysis using the available modules +usually follows the same structure -1. Initialize the object previously imported. -2. Run the analysis, optionally for specific trajectory slices -3. Access the analysis from the `results` attribute (if available) +#. Import the desired module +#. Initialize the module previously imported. +#. Run the analysis, optionally for specific trajectory slices +#. Access the analysis from the `results` attribute .. code-block:: python diff --git a/package/MDAnalysis/analysis/base.py b/package/MDAnalysis/analysis/base.py index 68409e408c8..9f146ec95bf 100644 --- a/package/MDAnalysis/analysis/base.py +++ b/package/MDAnalysis/analysis/base.py @@ -24,8 +24,7 @@ Analysis building blocks --- :mod:`MDAnalysis.analysis.base` ============================================================ -A collection of useful building blocks for creating Analysis -classes. +The building blocks for creating Analysis classes. """ from collections import UserDict @@ -45,12 +44,12 @@ class Results(UserDict): r"""Container object for storing results. - Results are dictionaries that provide two ways by which can values be - accessed: by dict key ``results["value_key"]`` or by object attribute, - ``results.value_key``. They store all results obtained from an analysis - after calling :func:`run()`. + ``Results`` are dictionaries that provide two ways by which values can be + accessed: by dictionary key ``results["value_key"]`` or by object + attribute, ``results.value_key``. ``Results`` stores all results obtained + from an analysis after calling :func:`run()`. - The current implementation is similar to the :class:`sklearn.utils.Bunch` + The implementation is similar to the :class:`sklearn.utils.Bunch` class in `scikit-learn`_. .. _`scikit-learn`: https://scikit-learn.org/ @@ -74,7 +73,7 @@ class in `scikit-learn`_. Raises ------ - TypeError + ValueError If an attribute would have the same name as a default dictionary attribute. @@ -85,7 +84,7 @@ class in `scikit-learn`_. def _validate_key(self, key): if key in dir(dict): - raise TypeError(f"'{key}' is a protected dictionary attribute") + raise ValueError(f"'{key}' is a protected dictionary attribute") elif not (isinstance(key, str) and re.match("^[a-zA-Z_][a-zA-Z0-9_]*$", key)): raise TypeError(f"'{key}' is not able to be accessed by attribute") diff --git a/package/doc/sphinx/source/documentation_pages/analysis/init.rst b/package/doc/sphinx/source/documentation_pages/analysis/init.rst new file mode 100644 index 00000000000..6a074713ece --- /dev/null +++ b/package/doc/sphinx/source/documentation_pages/analysis/init.rst @@ -0,0 +1 @@ +.. automodule:: MDAnalysis.analysis.__init__ diff --git a/package/doc/sphinx/source/documentation_pages/analysis_modules.rst b/package/doc/sphinx/source/documentation_pages/analysis_modules.rst index 95605bace5b..cb81484cb33 100644 --- a/package/doc/sphinx/source/documentation_pages/analysis_modules.rst +++ b/package/doc/sphinx/source/documentation_pages/analysis_modules.rst @@ -10,11 +10,11 @@ I/O, selections etc). The analysis modules can be used as examples for how to use MDAnalysis but also as working code for research projects; typically all contributed code has been used by the authors in their own work. -Please see the individual module documentation for additional references and -citation information. +Please see the individual module documentation for any specific caveats +and also read and cite the reference papers associated with these algorithms. -These modules are not imported by default; in order to use them one has to -import them from :mod:`MDAnalysis.analysis`, for instance :: +The analysis modules are not imported by default; in order to use them one +has to import them from :mod:`MDAnalysis.analysis`, for instance :: import MDAnalysis.analysis.align @@ -23,10 +23,12 @@ import them from :mod:`MDAnalysis.analysis`, for instance :: Some of the modules in :mod:`MDAnalysis.analysis` require additional Python packages to enable full functionality. For example, :mod:`MDAnalysis.analysis.encore` provides more options if `scikit-learn`_ is -installed. These package are *not automatically installed* with -:program:`pip`(although one can add the ``[analysis]`` requirement to the -:program:`pip` command line to force their installation). If you install -MDAnalysis with :program:`conda` (see :ref:`installation-instructions`) then a +installed. If you installed MDAnalysis with +:program:`pip` (see :ref:`installation-instructions`) +these packages are *not automatically installed*. +Although, one can add the ``[analysis]`` tag to the +:program:`pip` command to force their installation. If you installed +MDAnalysis with :program:`conda` then a *full set of dependencies* is automatically installed. Other modules require external programs. For instance, the @@ -38,10 +40,18 @@ corresponding MDAnalysis module. .. _scikit-learn: http://scikit-learn.org/ .. _HOLE: http://www.holeprogram.org/ +.. toctree:: + :maxdepth: 1 + + analysis/init Building blocks for Analysis ============================ +The building block for the analysis modules is +:class:`MDAnalysis.analysis.base.AnalysisBase`. +To build your own analysis class start by reading their documentation. + .. toctree:: :maxdepth: 1 From dae538dab32c9931e10797ed7722e89f19d5b2bd Mon Sep 17 00:00:00 2001 From: Philip Loche Date: Fri, 30 Apr 2021 11:45:08 +0200 Subject: [PATCH 30/37] Resolved recursion problem --- package/MDAnalysis/analysis/base.py | 22 ++++++++++++------- .../MDAnalysisTests/analysis/test_base.py | 16 ++++++-------- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/package/MDAnalysis/analysis/base.py b/package/MDAnalysis/analysis/base.py index 9f146ec95bf..d49eb471be9 100644 --- a/package/MDAnalysis/analysis/base.py +++ b/package/MDAnalysis/analysis/base.py @@ -82,8 +82,13 @@ class in `scikit-learn`_. accessed by attribute. """ + # The real dictionary used to store the contents of the class. + # The dictionary should be initialized, but it is not leading to infinite + # rescursions, if not provied here... + data = {} + def _validate_key(self, key): - if key in dir(dict): + if key in dir(UserDict): raise ValueError(f"'{key}' is a protected dictionary attribute") elif not (isinstance(key, str) and re.match("^[a-zA-Z_][a-zA-Z0-9_]*$", key)): @@ -92,17 +97,18 @@ def _validate_key(self, key): def __init__(self, **kwargs): for key in kwargs: self._validate_key(key) - super().__init__(kwargs) + super().__init__(**kwargs) - def __setattr__(self, key, value): - self[key] = value + # Remove the extra defined data key to not appear twice + self.__delitem__("data") - def __setitem__(self, key, value): + def __setitem__(self, key, item): self._validate_key(key) - super().__setitem__(key, value) + super().__setitem__(key, item) - def __dir__(self): - return self.keys() + def __setattr__(self, attr, value): + self._validate_key(attr) + self[attr] = value def __getattr__(self, key): try: diff --git a/testsuite/MDAnalysisTests/analysis/test_base.py b/testsuite/MDAnalysisTests/analysis/test_base.py index c1874085ab1..b5e08f4495b 100644 --- a/testsuite/MDAnalysisTests/analysis/test_base.py +++ b/testsuite/MDAnalysisTests/analysis/test_base.py @@ -20,6 +20,8 @@ # MDAnalysis: A Toolkit for the Analysis of Molecular Dynamics Simulations. # J. Comput. Chem. 32 (2011), 2319--2327, doi:10.1002/jcc.21787 # +from collections import UserDict + import pytest import numpy as np @@ -46,29 +48,25 @@ def test_no_attr(self, results): with pytest.raises(AttributeError): results.c - def test_dir(self, results): - assert list(results.__dir__()) == ["a", "b"] - - @pytest.mark.parametrize('key', dir(dict)) + @pytest.mark.parametrize('key', dir(UserDict)) def test_existing_dict_attr(self, results, key): msg = f"'{key}' is a protected dictionary attribute" - with pytest.raises(TypeError, match=key): + with pytest.raises(ValueError, match=key): results[key] = None - @pytest.mark.parametrize('key', dir(dict)) + @pytest.mark.parametrize('key', dir(UserDict)) def test_wrong_init_type(self, key): msg = f"'{key}' is a protected dictionary attribute" - with pytest.raises(TypeError, match=msg): + with pytest.raises(ValueError, match=msg): base.Results(**{key: None}) - @pytest.mark.parametrize('key', ("0123", "0j", "1.1", "{}", "a[", "a ")) + @pytest.mark.parametrize('key', ("0123", "0j", "1.1", "{}", "a b")) def test_weird_key(self, results, key): msg = f"'{key}' is not able to be accessed by attribute" with pytest.raises(TypeError, match=msg): results[key] = None - class FrameAnalysis(base.AnalysisBase): """Just grabs frame numbers of frames it goes over""" From 903007946bc0297154fa933392def38fac16bc95 Mon Sep 17 00:00:00 2001 From: Philip Loche Date: Fri, 30 Apr 2021 21:43:45 +0200 Subject: [PATCH 31/37] Fixed UserDict rewrite Co-authored-by: cbouy --- package/MDAnalysis/analysis/base.py | 40 +++++++++---------- package/MDAnalysis/analysis/gnm.py | 1 - package/MDAnalysis/analysis/lineardensity.py | 6 +-- .../MDAnalysisTests/analysis/test_base.py | 24 +++++++---- 4 files changed, 40 insertions(+), 31 deletions(-) diff --git a/package/MDAnalysis/analysis/base.py b/package/MDAnalysis/analysis/base.py index d49eb471be9..cd757cb2f15 100644 --- a/package/MDAnalysis/analysis/base.py +++ b/package/MDAnalysis/analysis/base.py @@ -73,34 +73,30 @@ class in `scikit-learn`_. Raises ------ - ValueError + AttributeError If an attribute would have the same name as a default dictionary attribute. - TypeError + ValueError If a key is not of type ``str`` and therefore is not able to be accessed by attribute. """ - - # The real dictionary used to store the contents of the class. - # The dictionary should be initialized, but it is not leading to infinite - # rescursions, if not provied here... - data = {} - def _validate_key(self, key): - if key in dir(UserDict): - raise ValueError(f"'{key}' is a protected dictionary attribute") - elif not (isinstance(key, str) - and re.match("^[a-zA-Z_][a-zA-Z0-9_]*$", key)): - raise TypeError(f"'{key}' is not able to be accessed by attribute") + if key in dir(UserDict) or (key == "data" and self._dict_frozen): + raise AttributeError(f"'{key}' is a protected dictionary " + "attribute") + elif isinstance(key, str) and not key.isidentifier(): + raise ValueError(f"'{key}' is not a valid attribute") def __init__(self, **kwargs): + if "data" in kwargs.keys(): + raise AttributeError(f"'data' is a protected dictionary attribute") + + self._dict_frozen = False for key in kwargs: self._validate_key(key) super().__init__(**kwargs) - - # Remove the extra defined data key to not appear twice - self.__delitem__("data") + self._dict_frozen = True def __setitem__(self, key, item): self._validate_key(key) @@ -108,14 +104,18 @@ def __setitem__(self, key, item): def __setattr__(self, attr, value): self._validate_key(attr) - self[attr] = value + super().__setattr__(attr, value) + + # Make attribute available as key + if self._dict_frozen and attr != "_dict_frozen": + super().__setitem__(attr, value) - def __getattr__(self, key): + def __getattr__(self, attr): try: - return self[key] + return self.data[attr] except KeyError as err: raise AttributeError("'Results' object has no " - f"attribute '{key}'") from err + f"attribute '{attr}'") from err class AnalysisBase(object): diff --git a/package/MDAnalysis/analysis/gnm.py b/package/MDAnalysis/analysis/gnm.py index e5ef496f8ca..1f67b4d45f3 100644 --- a/package/MDAnalysis/analysis/gnm.py +++ b/package/MDAnalysis/analysis/gnm.py @@ -341,7 +341,6 @@ def _single_frame(self): w, v, self.results, - matrix, ReportVector=self.ReportVector, counter=self._ts.frame) diff --git a/package/MDAnalysis/analysis/lineardensity.py b/package/MDAnalysis/analysis/lineardensity.py index 9316094fac8..78ce2fe4433 100644 --- a/package/MDAnalysis/analysis/lineardensity.py +++ b/package/MDAnalysis/analysis/lineardensity.py @@ -112,9 +112,9 @@ def __init__(self, select, grouping='atoms', binsize=0.25, **kwargs): self.grouping = grouping # Initiate result instances - self.results.x = Results(dim=0) - self.results.y = Results(dim=1) - self.results.z = Results(dim=2) + self.results["x"] = Results(dim=0) + self.results["y"] = Results(dim=1) + self.results["z"] = Results(dim=2) # Box sides self.dimensions = self._universe.dimensions[:3] self.volume = np.prod(self.dimensions) diff --git a/testsuite/MDAnalysisTests/analysis/test_base.py b/testsuite/MDAnalysisTests/analysis/test_base.py index b5e08f4495b..71ea1aea3e3 100644 --- a/testsuite/MDAnalysisTests/analysis/test_base.py +++ b/testsuite/MDAnalysisTests/analysis/test_base.py @@ -42,28 +42,38 @@ def results(self): return base.Results(a=1, b=2) def test_get(self, results): - assert results.a == results["a"] + assert results.a == results["a"] == 1 def test_no_attr(self, results): with pytest.raises(AttributeError): results.c - @pytest.mark.parametrize('key', dir(UserDict)) + def test_set_attr(self, results): + value = [1, 2, 3, 4] + results.c = value + assert results.c == results["c"] == value + + def test_set_key(self, results): + value = [1, 2, 3, 4] + results["c"] = value + assert results.c == results["c"] == value + + @pytest.mark.parametrize('key', dir(UserDict) + ["data"]) def test_existing_dict_attr(self, results, key): msg = f"'{key}' is a protected dictionary attribute" - with pytest.raises(ValueError, match=key): + with pytest.raises(AttributeError, match=key): results[key] = None - @pytest.mark.parametrize('key', dir(UserDict)) + @pytest.mark.parametrize('key', dir(UserDict) + ["data"]) def test_wrong_init_type(self, key): msg = f"'{key}' is a protected dictionary attribute" - with pytest.raises(ValueError, match=msg): + with pytest.raises(AttributeError, match=msg): base.Results(**{key: None}) @pytest.mark.parametrize('key', ("0123", "0j", "1.1", "{}", "a b")) def test_weird_key(self, results, key): - msg = f"'{key}' is not able to be accessed by attribute" - with pytest.raises(TypeError, match=msg): + msg = f"'{key}' is not a valid attribute" + with pytest.raises(ValueError, match=msg): results[key] = None From 5ed700a01d0f919ca02efa91f7981996178b43fb Mon Sep 17 00:00:00 2001 From: Philip Loche Date: Fri, 30 Apr 2021 21:48:59 +0200 Subject: [PATCH 32/37] PEP8 --- package/MDAnalysis/analysis/base.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/package/MDAnalysis/analysis/base.py b/package/MDAnalysis/analysis/base.py index cd757cb2f15..65797b1c671 100644 --- a/package/MDAnalysis/analysis/base.py +++ b/package/MDAnalysis/analysis/base.py @@ -45,8 +45,8 @@ class Results(UserDict): r"""Container object for storing results. ``Results`` are dictionaries that provide two ways by which values can be - accessed: by dictionary key ``results["value_key"]`` or by object - attribute, ``results.value_key``. ``Results`` stores all results obtained + accessed: by dictionary key ``results["value_key"]`` or by object + attribute, ``results.value_key``. ``Results`` stores all results obtained from an analysis after calling :func:`run()`. The implementation is similar to the :class:`sklearn.utils.Bunch` @@ -74,8 +74,7 @@ class in `scikit-learn`_. Raises ------ AttributeError - If an attribute would have the same name as a default dictionary - attribute. + If an assigned attribute has the same name as a default attribute. ValueError If a key is not of type ``str`` and therefore is not able to be @@ -106,7 +105,7 @@ def __setattr__(self, attr, value): self._validate_key(attr) super().__setattr__(attr, value) - # Make attribute available as key + # attribute available as key if self._dict_frozen and attr != "_dict_frozen": super().__setitem__(attr, value) @@ -142,10 +141,10 @@ class AnalysisBase(object): Attributes ---------- times: numpy.ndarray - array of Timestep times. Only exists after calling + array of Timestep times. Only exists after calling :meth:`AnalysisBase.run` frames: numpy.ndarray - array of Timestep frame indices. Only exists after calling + array of Timestep frame indices. Only exists after calling :meth:`AnalysisBase.run` results: :class:`Results` results of calculation are stored after call @@ -256,7 +255,7 @@ def _prepare(self): def _conclude(self): """Finalise the results you've gathered. - Called at the end of the run() method to finish everything up. + Called at the end of the :meth:`run` method to finish everything up. """ pass # pylint: disable=unnecessary-pass From 57d9201908e79d3ecbef8db780dffe1452f2e7df Mon Sep 17 00:00:00 2001 From: Philip Loche Date: Fri, 30 Apr 2021 23:07:23 +0200 Subject: [PATCH 33/37] Removed unused import --- package/MDAnalysis/analysis/base.py | 1 - 1 file changed, 1 deletion(-) diff --git a/package/MDAnalysis/analysis/base.py b/package/MDAnalysis/analysis/base.py index 65797b1c671..5f3595d4832 100644 --- a/package/MDAnalysis/analysis/base.py +++ b/package/MDAnalysis/analysis/base.py @@ -31,7 +31,6 @@ import inspect import logging import itertools -import re import numpy as np from MDAnalysis import coordinates From ed7f833ec588b68c2b133a84608180947c6ba037 Mon Sep 17 00:00:00 2001 From: Philip Loche Date: Sun, 2 May 2021 09:37:53 +0200 Subject: [PATCH 34/37] Moved Raises to correct position in doc --- package/MDAnalysis/analysis/base.py | 44 ++++++++++++++--------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/package/MDAnalysis/analysis/base.py b/package/MDAnalysis/analysis/base.py index 5f3595d4832..a9536dff639 100644 --- a/package/MDAnalysis/analysis/base.py +++ b/package/MDAnalysis/analysis/base.py @@ -53,6 +53,18 @@ class in `scikit-learn`_. .. _`scikit-learn`: https://scikit-learn.org/ + Raises + ------ + AttributeError + If an assigned attribute has the same name as a default attribute. + + ValueError + If a key is not of type ``str`` and therefore is not able to be + accessed by attribute. + + Notes + ----- + Pickling of ``Results`` is currently not supported Examples -------- @@ -68,16 +80,6 @@ class in `scikit-learn`_. >>> results.c = [1, 2, 3, 4] >>> results['c'] [1, 2, 3, 4] - - - Raises - ------ - AttributeError - If an assigned attribute has the same name as a default attribute. - - ValueError - If a key is not of type ``str`` and therefore is not able to be - accessed by attribute. """ def _validate_key(self, key): if key in dir(UserDict) or (key == "data" and self._dict_frozen): @@ -319,6 +321,11 @@ class AnalysisFromFunction(AnalysisBase): Results for each frame of the wrapped function, stored after call to :meth:`AnalysisFromFunction.run`. + Raises + ------ + ValueError + if ``function`` has the same ``kwargs`` as :class:`AnalysisBase` + Example ------- .. code-block:: python @@ -331,12 +338,6 @@ def rotation_matrix(mobile, ref): print(rot.results.timeseries) - Raises - ------ - ValueError - if ``function`` has the same ``kwargs`` as :class:`AnalysisBase` - - .. versionchanged:: 1.0.0 Support for directly passing the ``start``, ``stop``, and ``step`` arguments has been removed. These should instead be passed @@ -401,6 +402,11 @@ def analysis_class(function): Results for each frame of the wrapped function, stored after call to :meth:`AnalysisFromFunction.run`. + Raises + ------ + ValueError + if ``function`` has the same ``kwargs`` as :class:`AnalysisBase` + Examples -------- @@ -424,12 +430,6 @@ def RotationMatrix(mobile, ref): print(rot.results.timeseries) - Raises - ------ - ValueError - if ``function`` has the same ``kwargs`` as :class:`AnalysisBase` - - .. versionchanged:: 2.0.0 Former ``results`` are now stored as ``results.timeseries`` """ From fb1523ae6cb4554fb6dfb750806320865d3c003c Mon Sep 17 00:00:00 2001 From: Philip Loche Date: Sun, 2 May 2021 09:46:28 +0200 Subject: [PATCH 35/37] Fix minor typo --- .../doc/sphinx/source/documentation_pages/analysis_modules.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package/doc/sphinx/source/documentation_pages/analysis_modules.rst b/package/doc/sphinx/source/documentation_pages/analysis_modules.rst index cb81484cb33..d875565360c 100644 --- a/package/doc/sphinx/source/documentation_pages/analysis_modules.rst +++ b/package/doc/sphinx/source/documentation_pages/analysis_modules.rst @@ -50,7 +50,7 @@ Building blocks for Analysis The building block for the analysis modules is :class:`MDAnalysis.analysis.base.AnalysisBase`. -To build your own analysis class start by reading their documentation. +To build your own analysis class start by reading the documentation. .. toctree:: :maxdepth: 1 From 70a1eda1ab891c472b53ccf0d14db9f529625b4e Mon Sep 17 00:00:00 2001 From: Philip Loche Date: Wed, 5 May 2021 09:21:39 +0200 Subject: [PATCH 36/37] Remove analysis init doc, update versionchange --- package/MDAnalysis/analysis/__init__.py | 111 ------------------ package/MDAnalysis/analysis/base.py | 7 +- .../documentation_pages/analysis/init.rst | 1 - .../documentation_pages/analysis_modules.rst | 30 +++-- 4 files changed, 25 insertions(+), 124 deletions(-) delete mode 100644 package/doc/sphinx/source/documentation_pages/analysis/init.rst diff --git a/package/MDAnalysis/analysis/__init__.py b/package/MDAnalysis/analysis/__init__.py index c1cc92b3386..1c9f78b1880 100644 --- a/package/MDAnalysis/analysis/__init__.py +++ b/package/MDAnalysis/analysis/__init__.py @@ -21,117 +21,6 @@ # J. Comput. Chem. 32 (2011), 2319--2327, doi:10.1002/jcc.21787 # -""" -Analysis code based on MDAnalysis --- :mod:`MDAnalysis.analysis` -================================================================ - -The :mod:`MDAnalysis.analysis` sub-package contains various recipes and -algorithms that can be used to analyze MD trajectories. -An analysis using the available modules -usually follows the same structure - -#. Import the desired module -#. Initialize the module previously imported. -#. Run the analysis, optionally for specific trajectory slices -#. Access the analysis from the `results` attribute - - .. code-block:: python - - from MDAnalysis.analysis import ExampleAnalysisModule # (e.g. RMSD) - - analysis_obj = ExampleAnalysisModule(universe, ...) - analysis_obj.run(start_frame, stop_frame, step) - print(analysis_obj.results) - -If you use them please check if the documentation mentions any specific caveats -and also if there are any published papers associated with these algorithms. - -Available analysis modules --------------------------- - -:mod:`~MDAnalysis.analysis.align` - Fitting and aligning of coordinate frames, including the option to - use a sequence alignment to define equivalent atoms to fit on. - -:mod:`~MDAnalysis.analysis.contacts` - Analyse the number of native contacts relative to a reference - state, also known as a "q1-q2" analysis. - -:mod:`~MDAnalysis.analysis.density` - Creating and manipulating densities such as the density ow water - molecules around a protein. Makes use of the external - GridDataFormats_ package. - -:mod:`~MDAnalysis.analysis.distances` - Functions to calculate distances between atoms and selections; it - contains the often-used - :func:`~MDAnalysis.analysis.distances.distance_array` function. - -:mod:`~MDAnalysis.analysis.hbonds` - Analyze hydrogen bonds, including both the per frame results as well - as the dynamic properties and lifetimes. - -:mod:`~MDAnalysis.analysis.helix_analysis` - Analysis of helices with the HELANAL_ algorithm. - -:mod:`~MDAnalysis.analysis.hole2` - Run and process output from the :program:`HOLE` program - to analyze pores, tunnels and cavities in proteins. - -:mod:`~MDAnalysis.analysis.gnm` - Gaussian normal mode analysis of MD trajectories with the - help of an elastic network. - -:mod:`~MDAnalysis.analysis.leaflet` - Find lipids in the upper and lower (or inner and outer) leaflet of - a bilayer; the algorithm can deal with any deformations as long as - the two leaflets are topologically distinct. - -:mod:`~MDAnalysis.analysis.msd` - Implements the calculation of Mean Squared Displacements (MSDs) by the - Einstein relation. - -:mod:`~MDAnalysis.analysis.nuclinfo` - Analyse the nucleic acid for the backbone dihedrals, chi, sugar - pucker, and Watson-Crick distance (minor and major groove - distances). - -:mod:`~MDAnalysis.analysis.psa` - Perform Path Similarity Analysis (PSA) on a set of trajectories to measure - their mutual similarities, including the ability to perform hierarchical - clustering and generate heat map-dendrogram plots. - -:mod:`~MDAnalysis.analysis.rdf` - Calculation of pair distribution functions - -:mod:`~MDAnalysis.analysis.rms` - Calculation of RMSD and RMSF. - -:mod:`~MDAnalysis.analysis.waterdynamics` - Analysis of water. - -:mod:`~MDAnalysis.analysis.legacy.x3dna` - Analysis of helicoidal parameters driven by X3DNA_. (Note that this - module is not fully supported any more and needs to be explicitly - imported from :mod:`MDAnalysis.analysis.legacy`.) - -.. _GridDataFormats: https://github.com/orbeckst/GridDataFormats -.. _HELANAL: http://www.ccrnp.ncifcrf.gov/users/kumarsan/HELANAL/helanal.html -.. _X3DNA: http://x3dna.org/ - -.. versionchanged:: 0.10.0 - The analysis submodules are not automatically imported any more. Manually - import any submodule that you need. - -.. versionchanged:: 0.16.0 - :mod:`~MDAnalysis.analysis.legacy.x3dna` was moved to the - :mod:`MDAnalysis.analysis.legacy` package - -.. versionchanged:: 2.0.0 - Adds MSD, and changes hole for hole2.hole. - -""" - __all__ = [ 'align', 'base', diff --git a/package/MDAnalysis/analysis/base.py b/package/MDAnalysis/analysis/base.py index a9536dff639..c7619203581 100644 --- a/package/MDAnalysis/analysis/base.py +++ b/package/MDAnalysis/analysis/base.py @@ -64,7 +64,7 @@ class in `scikit-learn`_. Notes ----- - Pickling of ``Results`` is currently not supported + Pickling of ``Results`` is currently not supported. Examples -------- @@ -80,6 +80,9 @@ class in `scikit-learn`_. >>> results.c = [1, 2, 3, 4] >>> results['c'] [1, 2, 3, 4] + + + .. versionadded:: 2.0.0 """ def _validate_key(self, key): if key in dir(UserDict) or (key == "data" and self._dict_frozen): @@ -206,6 +209,8 @@ def _conclude(self): removed. These should now be directly passed to :meth:`AnalysisBase.run`. + .. versionchanged:: 2.0.0 + Added :attr:`results` """ def __init__(self, trajectory, verbose=False, **kwargs): diff --git a/package/doc/sphinx/source/documentation_pages/analysis/init.rst b/package/doc/sphinx/source/documentation_pages/analysis/init.rst deleted file mode 100644 index 6a074713ece..00000000000 --- a/package/doc/sphinx/source/documentation_pages/analysis/init.rst +++ /dev/null @@ -1 +0,0 @@ -.. automodule:: MDAnalysis.analysis.__init__ diff --git a/package/doc/sphinx/source/documentation_pages/analysis_modules.rst b/package/doc/sphinx/source/documentation_pages/analysis_modules.rst index d875565360c..274c1f56779 100644 --- a/package/doc/sphinx/source/documentation_pages/analysis_modules.rst +++ b/package/doc/sphinx/source/documentation_pages/analysis_modules.rst @@ -5,18 +5,31 @@ Analysis modules **************** The :mod:`MDAnalysis.analysis` module contains code to carry out specific -analysis functionality. It is based on the core functionality (i.e. trajectory +analysis functionality for MD trajectories. +It is based on the core functionality (i.e. trajectory I/O, selections etc). The analysis modules can be used as examples for how to use MDAnalysis but also as working code for research projects; typically all contributed code has been used by the authors in their own work. +An analysis using the available modules +usually follows the same structure -Please see the individual module documentation for any specific caveats -and also read and cite the reference papers associated with these algorithms. +#. Import the desired module, since analysis modules are not imported + by default. +#. Initialize the module previously imported. +#. Run the analysis, optionally for specific trajectory slices +#. Access the analysis from the `results` attribute + +.. code-block:: python -The analysis modules are not imported by default; in order to use them one -has to import them from :mod:`MDAnalysis.analysis`, for instance :: + from MDAnalysis.analysis import ExampleAnalysisModule # (e.g. RMSD) - import MDAnalysis.analysis.align + analysis_obj = ExampleAnalysisModule(universe, ...) + analysis_obj.run(start_frame, stop_frame, step) + print(analysis_obj.results) + + +Please see the individual module documentation for any specific caveats +and also read and cite the reference papers associated with these algorithms. .. rubric:: Additional dependencies @@ -40,11 +53,6 @@ corresponding MDAnalysis module. .. _scikit-learn: http://scikit-learn.org/ .. _HOLE: http://www.holeprogram.org/ -.. toctree:: - :maxdepth: 1 - - analysis/init - Building blocks for Analysis ============================ From 85940f90a5e9535bb497de6cab017fa7ddbd0222 Mon Sep 17 00:00:00 2001 From: Philip Loche Date: Wed, 5 May 2021 09:28:40 +0200 Subject: [PATCH 37/37] Updated CHANGELOG --- package/CHANGELOG | 3 +++ 1 file changed, 3 insertions(+) diff --git a/package/CHANGELOG b/package/CHANGELOG index f0f791f0da5..22e6ada09e6 100644 --- a/package/CHANGELOG +++ b/package/CHANGELOG @@ -106,6 +106,7 @@ Fixes * Fix syntax warning over comparison of literals using is (Issue #3066) Enhancements + * Added `Results` class for storing analysis results (#3115, PR #3233) * 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) @@ -173,6 +174,8 @@ Enhancements checking if it can be used in parallel analysis. (Issue #2996, PR #2950) Changes + * `GNMAnalysis`, `LinearDensity`, `PersistenceLength` and + `AnalysisFromFunction` use the `results` attribute. * Fixed inaccurate docstring inside the RMSD class (Issue #2796, PR #3134) * 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)