MDAnalysis / cookiecutter-mdakit

Cookiecutter for Python packages based on MDAnalysis
MIT License
7 stars 5 forks source link

Get started on analysis #19

Closed lilyminium closed 2 years ago

lilyminium commented 2 years ago

This PR adds an analysis template and some tests.

Relates to #11

pep8speaks commented 2 years ago

Hello @lilyminium! Thanks for updating this PR. We checked the lines you've touched for PEP 8 issues, and found:

Line 33:80: E501 line too long (100 > 79 characters)

Line 25:80: E501 line too long (82 > 79 characters)

Line 26:1: E302 expected 2 blank lines, found 1 Line 27:80: E501 line too long (89 > 79 characters)

[Line 2:80](https://github.com/MDAnalysis/cookiecutter-mdakit/blob/e3d28d3e5c1b3111fa927c47b98cd53ee42e6bae/{{cookiecutter.repo_name}}/{{cookiecutter.package_name}}/analysis/{{cookiecutter.template_analysis_class.lower()}}.py#L2): E501 line too long (137 > 79 characters) [Line 5:80](https://github.com/MDAnalysis/cookiecutter-mdakit/blob/e3d28d3e5c1b3111fa927c47b98cd53ee42e6bae/{{cookiecutter.repo_name}}/{{cookiecutter.package_name}}/analysis/{{cookiecutter.template_analysis_class.lower()}}.py#L5): E501 line too long (83 > 79 characters) [Line 16:1](https://github.com/MDAnalysis/cookiecutter-mdakit/blob/e3d28d3e5c1b3111fa927c47b98cd53ee42e6bae/{{cookiecutter.repo_name}}/{{cookiecutter.package_name}}/analysis/{{cookiecutter.template_analysis_class.lower()}}.py#L16): E302 expected 2 blank lines, found 1 [Line 16:9](https://github.com/MDAnalysis/cookiecutter-mdakit/blob/e3d28d3e5c1b3111fa927c47b98cd53ee42e6bae/{{cookiecutter.repo_name}}/{{cookiecutter.package_name}}/analysis/{{cookiecutter.template_analysis_class.lower()}}.py#L16): E201 whitespace after '{' [Line 16:46](https://github.com/MDAnalysis/cookiecutter-mdakit/blob/e3d28d3e5c1b3111fa927c47b98cd53ee42e6bae/{{cookiecutter.repo_name}}/{{cookiecutter.package_name}}/analysis/{{cookiecutter.template_analysis_class.lower()}}.py#L16): E202 whitespace before '}' [Line 23:80](https://github.com/MDAnalysis/cookiecutter-mdakit/blob/e3d28d3e5c1b3111fa927c47b98cd53ee42e6bae/{{cookiecutter.repo_name}}/{{cookiecutter.package_name}}/analysis/{{cookiecutter.template_analysis_class.lower()}}.py#L23): E501 line too long (116 > 79 characters) [Line 28:80](https://github.com/MDAnalysis/cookiecutter-mdakit/blob/e3d28d3e5c1b3111fa927c47b98cd53ee42e6bae/{{cookiecutter.repo_name}}/{{cookiecutter.package_name}}/analysis/{{cookiecutter.template_analysis_class.lower()}}.py#L28): E501 line too long (82 > 79 characters) [Line 56:5](https://github.com/MDAnalysis/cookiecutter-mdakit/blob/e3d28d3e5c1b3111fa927c47b98cd53ee42e6bae/{{cookiecutter.repo_name}}/{{cookiecutter.package_name}}/analysis/{{cookiecutter.template_analysis_class.lower()}}.py#L56): E303 too many blank lines (2) [Line 74:1](https://github.com/MDAnalysis/cookiecutter-mdakit/blob/e3d28d3e5c1b3111fa927c47b98cd53ee42e6bae/{{cookiecutter.repo_name}}/{{cookiecutter.package_name}}/analysis/{{cookiecutter.template_analysis_class.lower()}}.py#L74): W293 blank line contains whitespace [Line 76:5](https://github.com/MDAnalysis/cookiecutter-mdakit/blob/e3d28d3e5c1b3111fa927c47b98cd53ee42e6bae/{{cookiecutter.repo_name}}/{{cookiecutter.package_name}}/analysis/{{cookiecutter.template_analysis_class.lower()}}.py#L76): E303 too many blank lines (3) [Line 95:1](https://github.com/MDAnalysis/cookiecutter-mdakit/blob/e3d28d3e5c1b3111fa927c47b98cd53ee42e6bae/{{cookiecutter.repo_name}}/{{cookiecutter.package_name}}/analysis/{{cookiecutter.template_analysis_class.lower()}}.py#L95): W293 blank line contains whitespace [Line 110:80](https://github.com/MDAnalysis/cookiecutter-mdakit/blob/e3d28d3e5c1b3111fa927c47b98cd53ee42e6bae/{{cookiecutter.repo_name}}/{{cookiecutter.package_name}}/analysis/{{cookiecutter.template_analysis_class.lower()}}.py#L110): E501 line too long (89 > 79 characters) [Line 111:80](https://github.com/MDAnalysis/cookiecutter-mdakit/blob/e3d28d3e5c1b3111fa927c47b98cd53ee42e6bae/{{cookiecutter.repo_name}}/{{cookiecutter.package_name}}/analysis/{{cookiecutter.template_analysis_class.lower()}}.py#L111): E501 line too long (90 > 79 characters)

[Line 4:80](https://github.com/MDAnalysis/cookiecutter-mdakit/blob/e3d28d3e5c1b3111fa927c47b98cd53ee42e6bae/{{cookiecutter.repo_name}}/{{cookiecutter.package_name}}/tests/analysis/test_{{cookiecutter.template_analysis_class.lower()}}.py#L4): E501 line too long (132 > 79 characters) [Line 7:1](https://github.com/MDAnalysis/cookiecutter-mdakit/blob/e3d28d3e5c1b3111fa927c47b98cd53ee42e6bae/{{cookiecutter.repo_name}}/{{cookiecutter.package_name}}/tests/analysis/test_{{cookiecutter.template_analysis_class.lower()}}.py#L7): E302 expected 2 blank lines, found 1 [Line 8:1](https://github.com/MDAnalysis/cookiecutter-mdakit/blob/e3d28d3e5c1b3111fa927c47b98cd53ee42e6bae/{{cookiecutter.repo_name}}/{{cookiecutter.package_name}}/tests/analysis/test_{{cookiecutter.template_analysis_class.lower()}}.py#L8): W293 blank line contains whitespace [Line 27:5](https://github.com/MDAnalysis/cookiecutter-mdakit/blob/e3d28d3e5c1b3111fa927c47b98cd53ee42e6bae/{{cookiecutter.repo_name}}/{{cookiecutter.package_name}}/tests/analysis/test_{{cookiecutter.template_analysis_class.lower()}}.py#L27): E303 too many blank lines (2) [Line 37:80](https://github.com/MDAnalysis/cookiecutter-mdakit/blob/e3d28d3e5c1b3111fa927c47b98cd53ee42e6bae/{{cookiecutter.repo_name}}/{{cookiecutter.package_name}}/tests/analysis/test_{{cookiecutter.template_analysis_class.lower()}}.py#L37): E501 line too long (84 > 79 characters) [Line 39:1](https://github.com/MDAnalysis/cookiecutter-mdakit/blob/e3d28d3e5c1b3111fa927c47b98cd53ee42e6bae/{{cookiecutter.repo_name}}/{{cookiecutter.package_name}}/tests/analysis/test_{{cookiecutter.template_analysis_class.lower()}}.py#L39): W293 blank line contains whitespace [Line 41:5](https://github.com/MDAnalysis/cookiecutter-mdakit/blob/e3d28d3e5c1b3111fa927c47b98cd53ee42e6bae/{{cookiecutter.repo_name}}/{{cookiecutter.package_name}}/tests/analysis/test_{{cookiecutter.template_analysis_class.lower()}}.py#L41): E303 too many blank lines (2) [Line 53:80](https://github.com/MDAnalysis/cookiecutter-mdakit/blob/e3d28d3e5c1b3111fa927c47b98cd53ee42e6bae/{{cookiecutter.repo_name}}/{{cookiecutter.package_name}}/tests/analysis/test_{{cookiecutter.template_analysis_class.lower()}}.py#L53): E501 line too long (80 > 79 characters) [Line 58:24](https://github.com/MDAnalysis/cookiecutter-mdakit/blob/e3d28d3e5c1b3111fa927c47b98cd53ee42e6bae/{{cookiecutter.repo_name}}/{{cookiecutter.package_name}}/tests/analysis/test_{{cookiecutter.template_analysis_class.lower()}}.py#L58): E261 at least two spaces before inline comment [Line 59:20](https://github.com/MDAnalysis/cookiecutter-mdakit/blob/e3d28d3e5c1b3111fa927c47b98cd53ee42e6bae/{{cookiecutter.repo_name}}/{{cookiecutter.package_name}}/tests/analysis/test_{{cookiecutter.template_analysis_class.lower()}}.py#L59): E261 at least two spaces before inline comment

Comment last updated at 2022-07-26 11:28:16 UTC
lilyminium commented 2 years ago

The files have just been printed out in CI until we figure out how to have example repos:

Analysis file:

"""
MyAnalysis --- :mod:`testmdakit_deps_condaforge_rtd_y.analysis.MyAnalysis`
===========================================================

This module contains the :class:`MyAnalysis` class.

"""
from typing import Union, TYPE_CHECKING

from MDAnalysis.analysis.base import AnalysisBase
import numpy as np

if TYPE_CHECKING:
    from MDAnalysis.core.universe import Universe, AtomGroup

class MyAnalysis(AnalysisBase):
    """MyAnalysis class.

    This class is used to perform analysis on a trajectory.

    Parameters
    ----------
    universe_or_atomgroup: :class:`~MDAnalysis.core.universe.Universe` or :class:`~MDAnalysis.core.groups.AtomGroup`
        Universe or group of atoms to apply this analysis to.
        If a trajectory is associated with the atoms,
        then the computation iterates over the trajectory.

    Attributes
    ----------
    universe: :class:`~MDAnalysis.core.universe.Universe`
        The universe to which this analysis is applied
    atomgroup: :class:`~MDAnalysis.core.groups.AtomGroup`
        The atoms to which this analysis is applied
    results: :class:`~MDAnalysis.analysis.base.Results`
        results of calculation are stored here, after calling
        :meth:`MyAnalysis.run`
    start: Optional[int]
        The first frame of the trajectory used to compute the analysis
    stop: Optional[int]
        The frame to stop at for the analysis
    step: Optional[int]
        Number of frames to skip between each analyzed frame
    n_frames: int
        Number of frames analysed in the trajectory
    times: numpy.ndarray
        array of Timestep times. Only exists after calling
        :meth:`MyAnalysis.run`
    frames: numpy.ndarray
        array of Timestep frame indices. Only exists after calling
        :meth:`MyAnalysis.run`
    """

    def __init__(
        self,
        universe_or_atomgroup: Union[Universe, AtomGroup],
        select: str = "all",
        # TODO: add your own parameters here
        **kwargs
    ):
        # the below line must be kept to initialize the AnalysisBase class!
        super().__init__(universe_or_atomgroup.trajectory)
        # after this you will be able to access `self.results`

        self.universe = universe_or_atomgroup.universe
        self.atomgroup = universe_or_atomgroup.select_atoms(select)

    def _prepare(self):
        """Set things up before the analysis loop begins"""
        # This is an optional method that runs before
        # _single_frame loops over the trajectory.
        # It is useful for setting up results arrays
        # For example, below we create an array to store
        # the number of atoms with negative coordinates
        # in each frame.
        self.results.is_negative = np.zeros(
            (self.n_frames, self.atomgroup.n_atoms),
            dtype=bool,
        )

    def _single_frame(self):
        """Calculate data from a single frame of trajectory"""
        # This runs once for each frame of the trajectory
        # It can contain the main analysis method, or just collect data
        # so that analysis can be done over the aggregate data
        # in _conclude.

        # The trajectory positions update automatically
        negative = self.atomgroup.positions < 0
        # You can access the frame number using self._frame_index
        self.results.is_negative[self._frame_index] = negative.any(axis=1)

    def _conclude(self):
        """Calculate the final results of the analysis"""
        # This is an optional method that runs after
        # _single_frame loops over the trajectory.
        # It is useful for calculating the final results
        # of the analysis.
        # For example, below we determine the
        # which atoms always have negative coordinates.
        self.results.always_negative = self.results.is_negative.all(axis=0)
        self.results.always_negative_atoms = self.atomgroup[self.results.always_negative]
        self.results.always_negative_atom_names = self.results.always_negative_atoms.names

        # results don't have to be arrays -- they can be any value, e.g. floats
        self.results.n_negative_atoms = self.results.is_negative.sum(axis=1)
        self.results.mean_negative_atoms = self.results.n_negative_atoms.mean()

Test file:
```python
import pytest
from numpy.testing import assert_allclose

from testmdakit_deps_condaforge_rtd_y.analysis.MyAnalysis import MyAnalysis
from testmdakit_deps_condaforge_rtd_y.tests.utils import make_Universe

class TestMyAnalysis:

    # fixtures are helpful functions that set up a test
    # See more at https://docs.pytest.org/en/stable/how-to/fixtures.html
    @pytest.fixture
    def universe(self):
        u = make_Universe(
            extras=("names", "resnames",),
            n_frames=3,
        )
        # create toy data to test assumptions
        for ts in u.trajectory.ts:
            ts.positions[:ts.frame] *= -1
        return u

    @pytest.fixture
    def analysis(self, universe):
        return MyAnalysis(universe)

    @pytest.mark.parametrize(
        "select, n_atoms",  # argument names
        [  # argument values in a tuple, in order
            ("all", 125),
            ("index < 10", 10),
            ("resindex > 2", 50),
        ]
    )
    def test_atom_selection(self, universe, select, n_atoms):
        # `universe` here is the fixture defined above
        analysis = MyAnalysis(universe, select=select)
        assert analysis.atomgroup.n_atoms == n_atoms

    @pytest.mark.parametrize(
        "stop, expected_mean",
        [
            (0, 0),
            (1, 1),
            (2, 1.5),
        ]
    )
    def test_mean_negative_atoms(self, analysis, stop, expected_mean):
        # assert we haven't run yet and the result doesn't exist yet
        assert "mean_negative_atoms" not in analysis.results
        analysis.run(stop=stop)
        # when comparing floating point values, it's best to use assert_allclose
        # to allow for floating point precision differences
        assert_allclose(
            analysis.results.mean_negative_atoms,  # computed data
            expected_mean,  # reference / desired data
            rtol=1e-07, # relative tolerance
            atol=0, # absolute tolerance
            err_msg="mean_negative_atoms is not correct",
        )