From 5f513831301492ffcf054c7b57ee6092bf153c41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Bouysset?= Date: Sun, 22 Mar 2020 18:44:27 +0100 Subject: [PATCH] tqdm ProgressBar replaces deprecated ProgressMeter (PR #2617) * Fixes #928 * Fixes #2504 * Added the ProgressBar class which inherits the tqdm.auto.tqdm object. - For now, the disable keyword takes precedence over verbose, i.e. if both are set to True the progress bar won't show. Setting disable=True will disable the progress bar as expected. Setting verbose=False will disable the progress bar. - Automatically detect which version of tqdm to use (console or notebook): If run from a jupyter notebook or jupyter lab, will use the tqdm.notebook.tqdm class, else (ipython, shell, or anything else) will use the default tqdm.tqdm class * add tqdm to dependencies * Deprecate ProgressMeter through warning and text * Replace ProgressMeter with ProgressBar everywhere in MDAnalysis * add tests (for #928 and #2504) * update CHANGELOG and AUTHORS --- .appveyor.yml | 2 +- .travis.yml | 4 +- package/AUTHORS | 1 + package/CHANGELOG | 4 +- package/MDAnalysis/analysis/base.py | 15 +- package/MDAnalysis/analysis/density.py | 13 +- .../analysis/hbonds/hbond_analysis.py | 12 +- .../analysis/hbonds/hbond_autocorrel.py | 11 +- package/MDAnalysis/analysis/helanal.py | 11 +- package/MDAnalysis/analysis/pca.py | 18 +-- package/MDAnalysis/analysis/rms.py | 5 - package/MDAnalysis/analysis/waterdynamics.py | 20 +-- package/MDAnalysis/core/universe.py | 144 +++++++++--------- package/MDAnalysis/lib/log.py | 29 +++- package/setup.py | 3 +- .../MDAnalysisTests/analysis/test_base.py | 16 ++ .../MDAnalysisTests/analysis/test_rms.py | 8 - testsuite/MDAnalysisTests/lib/test_log.py | 78 ++++++++++ 18 files changed, 233 insertions(+), 161 deletions(-) create mode 100644 testsuite/MDAnalysisTests/lib/test_log.py diff --git a/.appveyor.yml b/.appveyor.yml index 6f07a88fc36..b3a24c9b0f7 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -10,7 +10,7 @@ cache: environment: global: CONDA_CHANNELS: conda-forge - CONDA_DEPENDENCIES: pip setuptools wheel cython mock six biopython networkx joblib matplotlib scipy vs2015_runtime pytest mmtf-python GridDataFormats hypothesis pytest-cov codecov chemfiles + CONDA_DEPENDENCIES: pip setuptools wheel cython mock six biopython networkx joblib matplotlib scipy vs2015_runtime pytest mmtf-python GridDataFormats hypothesis pytest-cov codecov chemfiles tqdm PIP_DEPENDENCIES: gsd==1.9.3 duecredit parmed DEBUG: "False" MINGW_64: C:\mingw-w64\x86_64-6.3.0-posix-seh-rt_v5-rev1\mingw64\bin diff --git a/.travis.yml b/.travis.yml index 1249fc9a70d..d526eb12d93 100644 --- a/.travis.yml +++ b/.travis.yml @@ -28,7 +28,7 @@ env: - SETUP_CMD="${PYTEST_FLAGS}" - BUILD_CMD="pip install -e package/ && (cd testsuite/ && python setup.py build)" - CONDA_MIN_DEPENDENCIES="mmtf-python mock six biopython networkx cython matplotlib scipy griddataformats hypothesis gsd codecov" - - CONDA_DEPENDENCIES="${CONDA_MIN_DEPENDENCIES} seaborn>=0.7.0 clustalw=2.1 netcdf4 scikit-learn joblib>=0.12 chemfiles" + - CONDA_DEPENDENCIES="${CONDA_MIN_DEPENDENCIES} seaborn>=0.7.0 clustalw=2.1 netcdf4 scikit-learn joblib>=0.12 chemfiles tqdm>=4.43.0" - CONDA_CHANNELS='biobuilds conda-forge' - CONDA_CHANNEL_PRIORITY=True - PIP_DEPENDENCIES="duecredit parmed" @@ -73,7 +73,7 @@ matrix: PIP_DEPENDENCIES="${PIP_DEPENDENCIES} setuptools<45.0.0" - env: NAME="asv check" - PYTHON_VERSION=2.7 + PYTHON_VERSION=2.7 CODECOV="false" MAIN_CMD="" SETUP_CMD="" diff --git a/package/AUTHORS b/package/AUTHORS index 010463455c0..33c3a97683d 100644 --- a/package/AUTHORS +++ b/package/AUTHORS @@ -136,6 +136,7 @@ Chronological list of authors - Anshul Angaria - Shubham Sharma - Yuxuan Zhuang + - Cédric Bouysset - Abhishek Shandilya - Morgan L. Nance - Faraaz Shah diff --git a/package/CHANGELOG b/package/CHANGELOG index 3cd34f4b38f..d46c0cc62af 100644 --- a/package/CHANGELOG +++ b/package/CHANGELOG @@ -17,7 +17,7 @@ mm/dd/yy richardjgowers, kain88-de, lilyminium, p-j-smith, bdice, joaomcteixeira PicoCentauri, davidercruz, jbarnoud, RMeli, IAlibay, mtiberti, CCook96, Yuan-Yu, xiki-tempula, HTian1997, Iv-Hristov, hmacdope, AnshulAngaria, ss62171, Luthaf, yuxuanzhuang, abhishandy, mlnance, shfrz, orbeckst, - wvandertoorn + wvandertoorn, cbouy * 0.21.0 @@ -114,6 +114,8 @@ Enhancements * PersistenceLength.plot() now create new axes if current axes not provided (Issue #2590) Changes + * Deprecated :class:`ProgressMeter` and replaced it with :class:`ProgressBar` using + the tqdm package (Issue #928, PR #2617). Also fixes issue #2504. * Removed `details` from `ClusteringMethod`s (Issue #2575, PR #2620) * Removed deprecated :meth:`PersistenceLength.perform_fit` (Issue #2596) * Changed :meth:`PSAnalysis.generate_paths` keywords `store` and `filename` diff --git a/package/MDAnalysis/analysis/base.py b/package/MDAnalysis/analysis/base.py index b564ba64a6b..c2775645590 100644 --- a/package/MDAnalysis/analysis/base.py +++ b/package/MDAnalysis/analysis/base.py @@ -38,7 +38,7 @@ import numpy as np from MDAnalysis import coordinates from MDAnalysis.core.groups import AtomGroup -from MDAnalysis.lib.log import ProgressMeter +from MDAnalysis.lib.log import ProgressBar logger = logging.getLogger(__name__) @@ -131,13 +131,6 @@ def _setup_frames(self, trajectory, start=None, stop=None, step=None): self.stop = stop self.step = step self.n_frames = len(range(start, stop, step)) - interval = int(self.n_frames // 100) - if interval == 0: - interval = 1 - - verbose = getattr(self, '_verbose', False) - self._pm = ProgressMeter(self.n_frames if self.n_frames else 1, - interval=interval, verbose=verbose) def _single_frame(self): """Calculate data from a single frame of trajectory @@ -178,13 +171,13 @@ def run(self, start=None, stop=None, step=None, verbose=None): self._setup_frames(self._trajectory, start, stop, step) logger.info("Starting preparation") self._prepare() - for i, ts in enumerate( - self._trajectory[self.start:self.stop:self.step]): + for i, ts in enumerate(ProgressBar( + self._trajectory[self.start:self.stop:self.step], + verbose=verbose)): self._frame_index = i self._ts = ts # logger.info("--> Doing frame {} of {}".format(i+1, self.n_frames)) self._single_frame() - self._pm.echo(self._frame_index) logger.info("Finishing up") self._conclude() return self diff --git a/package/MDAnalysis/analysis/density.py b/package/MDAnalysis/analysis/density.py index cd15e989478..6bc1dd6d5dd 100644 --- a/package/MDAnalysis/analysis/density.py +++ b/package/MDAnalysis/analysis/density.py @@ -191,7 +191,8 @@ from MDAnalysis import NoDataError, MissingDataWarning from .. import units from ..lib import distances -from MDAnalysis.lib.log import ProgressMeter +from MDAnalysis.lib.log import ProgressBar +from MDAnalysis.lib.log import ProgressMeter # remove in 2.0 from MDAnalysis.lib.util import deprecate from .base import AnalysisBase @@ -1078,21 +1079,15 @@ def current_coordinates(): start, stop, step = u.trajectory.check_slice_indices(start, stop, step) n_frames = len(range(start, stop, step)) - pm = ProgressMeter(n_frames, interval=interval, - verbose=verbose, - format="Histogramming %(n_atoms)6d atoms in frame " - "%(step)5d/%(numsteps)d [%(percentage)5.1f%%]") - - for index, ts in enumerate(u.trajectory[start:stop:step]): + for ts in ProgressBar(u.trajectory[start:stop:step], + verbose=verbose, desc="Histogramming"): coord = current_coordinates() - pm.echo(index, n_atoms=len(coord)) if len(coord) == 0: continue h[:], edges[:] = np.histogramdd(coord, bins=bins, range=arange, normed=False) grid += h # accumulate average histogram - grid /= float(n_frames) metadata = metadata if metadata is not None else {} diff --git a/package/MDAnalysis/analysis/hbonds/hbond_analysis.py b/package/MDAnalysis/analysis/hbonds/hbond_analysis.py index 9810151de89..d158267de0f 100644 --- a/package/MDAnalysis/analysis/hbonds/hbond_analysis.py +++ b/package/MDAnalysis/analysis/hbonds/hbond_analysis.py @@ -331,7 +331,7 @@ class HydrogenBondAnalysis_OtherFF(HydrogenBondAnalysis): from MDAnalysis import MissingDataWarning, NoDataError, SelectionError, SelectionWarning from .. import base -from MDAnalysis.lib.log import ProgressMeter +from MDAnalysis.lib.log import ProgressBar from MDAnalysis.lib.NeighborSearch import AtomNeighborSearch from MDAnalysis.lib import distances @@ -952,10 +952,6 @@ def run(self, start=None, stop=None, step=None, verbose=None, **kwargs): self._timeseries = [] self.timesteps = [] - pm = ProgressMeter(self.n_frames, - format="HBonds frame {current_step:5d}: {step:5d}/{numsteps} [{percentage:5.1f}%]\r", - verbose=kwargs.get('verbose', False)) - try: self.u.trajectory.time def _get_timestep(): @@ -970,7 +966,9 @@ def _get_timestep(): logger.info("Starting analysis (frame index start=%d stop=%d, step=%d)", self.start, self.stop, self.step) - for progress, ts in enumerate(self.u.trajectory[self.start:self.stop:self.step]): + for ts in ProgressBar(self.u.trajectory[self.start:self.stop:self.step], + desc="HBond analysis", + verbose=kwargs.get('verbose', False)): # all bonds for this timestep frame_results = [] # dict of tuples (atom.index, atom.index) for quick check if @@ -981,7 +979,6 @@ def _get_timestep(): timestep = _get_timestep() self.timesteps.append(timestep) - pm.echo(progress, current_step=frame) self.logger_debug("Analyzing frame %(frame)d, timestep %(timestep)f ps", vars()) if self.update_selection1: self._update_selection_1() @@ -1041,7 +1038,6 @@ def _get_timestep(): dist, angle]) self._timeseries.append(frame_results) - logger.info("HBond analysis: complete; timeseries %s.timeseries", self.__class__.__name__) diff --git a/package/MDAnalysis/analysis/hbonds/hbond_autocorrel.py b/package/MDAnalysis/analysis/hbonds/hbond_autocorrel.py index f2c00a51aca..c40f1d16537 100644 --- a/package/MDAnalysis/analysis/hbonds/hbond_autocorrel.py +++ b/package/MDAnalysis/analysis/hbonds/hbond_autocorrel.py @@ -213,7 +213,7 @@ import warnings -from MDAnalysis.lib.log import ProgressMeter +from MDAnalysis.lib.log import ProgressBar from MDAnalysis.lib.distances import capped_distance, calc_angles, calc_bonds from MDAnalysis.core.groups import requires @@ -392,12 +392,9 @@ def run(self, force=False): # for normalising later counter = np.zeros_like(master_results, dtype=np.float32) - pm = ProgressMeter(self.nruns, interval=1, - format="Performing run %(step)5d/%(numsteps)d" - "[%(percentage)5.1f%%]") - - for i, (start, stop) in enumerate(zip(self._starts, self._stops)): - pm.echo(i) + for i, (start, stop) in ProgressBar(enumerate(zip(self._starts, + self._stops)), total=self.nruns, + desc="Performing run"): # needed else trj seek thinks a np.int64 isn't an int? results = self._single_run(int(start), int(stop)) diff --git a/package/MDAnalysis/analysis/helanal.py b/package/MDAnalysis/analysis/helanal.py index ebb3a3247af..c636ab69e3c 100644 --- a/package/MDAnalysis/analysis/helanal.py +++ b/package/MDAnalysis/analysis/helanal.py @@ -124,7 +124,7 @@ import numpy as np import MDAnalysis -from MDAnalysis.lib.log import ProgressMeter +from MDAnalysis.lib.log import ProgressBar from MDAnalysis.lib import mdamath import warnings @@ -346,14 +346,10 @@ def helanal_trajectory(universe, select="name CA", global_fitted_tilts = [] global_screw = [] - pm = ProgressMeter(n_frames, verbose=verbose, - format="Frame {step:5d}/{numsteps} " - " [{percentage:5.1f}%]") + for ts in ProgressBar(trajectory[start_frame:end_frame:frame_step], + verbose=verbose, desc="Helix analysis"): - for index, ts in enumerate(trajectory[start_frame:end_frame:frame_step]): - pm.echo(index) frame = ts.frame - ca_positions = ca.positions twist, bending_angles, height, rnou, origins, local_helix_axes, local_screw_angles = \ main_loop(ca_positions, ref_axis=ref_axis) @@ -414,7 +410,6 @@ def helanal_trajectory(universe, select="name CA", store.append(tmp) #for store,tmp in zip(global_tilt,local_helix_axes): store.append(mdamath.angle(tmp,ref_axis)) - twist_mean, twist_sd, twist_abdev = stats(global_twist) height_mean, height_sd, height_abdev = stats(global_height) rnou_mean, rnou_sd, rnou_abdev = stats(global_rnou) diff --git a/package/MDAnalysis/analysis/pca.py b/package/MDAnalysis/analysis/pca.py index 27cd90f338f..eaeec9e2695 100644 --- a/package/MDAnalysis/analysis/pca.py +++ b/package/MDAnalysis/analysis/pca.py @@ -111,7 +111,7 @@ from MDAnalysis import Universe from MDAnalysis.analysis.align import _fit_to -from MDAnalysis.lib.log import ProgressMeter +from MDAnalysis.lib.log import ProgressBar from .base import AnalysisBase @@ -161,11 +161,11 @@ class PCA(AnalysisBase): Notes ----- Computation can be speed up by supplying a precalculated mean structure - + .. versionchanged:: 1.0.0 align=True now correctly aligns the trajectory and computes the correct means and covariance matrix - + .. versionchanged:: 0.19.0 The start frame is used when performing selections and calculating mean positions. Previously the 0th frame was always used. @@ -230,15 +230,8 @@ def _prepare(self): self._ref_atom_positions -= self._ref_cog if self._calc_mean: - interval = int(self.n_frames // 100) - interval = interval if interval > 0 else 1 - format = ("Mean Calculation Step" - "%(step)5d/%(numsteps)d [%(percentage)5.1f%%]") - mean_pm = ProgressMeter(self.n_frames if self.n_frames else 1, - interval=interval, verbose=self._verbose, - format=format) - for i, ts in enumerate(self._u.trajectory[self.start:self.stop: - self.step]): + for ts in ProgressBar(self._u.trajectory[self.start:self.stop:self.step], + verbose=self._verbose, desc="Mean Calculation"): if self.align: mobile_cog = self._atoms.center_of_geometry() mobile_atoms, old_rmsd = _fit_to(self._atoms.positions - mobile_cog, @@ -248,7 +241,6 @@ def _prepare(self): ref_com=self._ref_cog) self.mean += self._atoms.positions.ravel() - mean_pm.echo(i) self.mean /= self.n_frames self.mean_atoms = self._atoms diff --git a/package/MDAnalysis/analysis/rms.py b/package/MDAnalysis/analysis/rms.py index d3ebed2a313..df108bc5e29 100644 --- a/package/MDAnalysis/analysis/rms.py +++ b/package/MDAnalysis/analysis/rms.py @@ -145,7 +145,6 @@ import MDAnalysis.lib.qcprot as qcp from MDAnalysis.analysis.base import AnalysisBase from MDAnalysis.exceptions import SelectionError, NoDataError -from MDAnalysis.lib.log import ProgressMeter from MDAnalysis.lib.util import asiterable, iterable, get_weights @@ -625,8 +624,6 @@ def _prepare(self): self.rmsd = np.zeros((self.n_frames, 3 + len(self._groupselections_atoms))) - self._pm.format = ("RMSD {rmsd:5.2f} A at frame " - "{step:5d}/{numsteps} [{percentage:5.1f}%]") self._mobile_coordinates64 = self.mobile_atoms.positions.copy().astype(np.float64) def _single_frame(self): @@ -673,8 +670,6 @@ def _single_frame(self): self._ref_coordinates64, self._mobile_coordinates64, self._n_atoms, None, self.weights_select) - self._pm.rmsd = self.rmsd[self._frame_index, 2] - class RMSF(AnalysisBase): r"""Calculate RMSF of given atoms across a trajectory. diff --git a/package/MDAnalysis/analysis/waterdynamics.py b/package/MDAnalysis/analysis/waterdynamics.py index b1723382f19..0fa1e9b5033 100644 --- a/package/MDAnalysis/analysis/waterdynamics.py +++ b/package/MDAnalysis/analysis/waterdynamics.py @@ -455,7 +455,7 @@ import numpy as np import MDAnalysis.analysis.hbonds -from MDAnalysis.lib.log import ProgressMeter +from MDAnalysis.lib.log import ProgressBar class HydrogenBondLifetimes(object): @@ -808,11 +808,9 @@ def _sameMolecTandDT(self, selection, t0d, tf): def _selection_serial(self, universe, selection_str): selection = [] - pm = ProgressMeter(universe.trajectory.n_frames, - interval=10, verbose=True) - for ts in universe.trajectory: + for ts in ProgressBar(universe.trajectory, verbose=True, + total=universe.trajectory.n_frames): selection.append(universe.select_atoms(selection_str)) - pm.echo(ts.frame) return selection @staticmethod @@ -983,11 +981,9 @@ def run(self, **kwargs): def _selection_serial(self, universe, selection_str): selection = [] - pm = ProgressMeter(universe.trajectory.n_frames, - interval=10, verbose=True) - for ts in universe.trajectory: + for ts in ProgressBar(universe.trajectory, verbose=True, + total=universe.trajectory.n_frames): selection.append(universe.select_atoms(selection_str)) - pm.echo(ts.frame) return selection @@ -1125,11 +1121,9 @@ def _sameMolecTandDT(self, selection, t0d, tf): def _selection_serial(self, universe, selection_str): selection = [] - pm = ProgressMeter(universe.trajectory.n_frames, - interval=10, verbose=True) - for ts in universe.trajectory: + for ts in ProgressBar(universe.trajectory, verbose=True, + total=universe.trajectory.n_frames): selection.append(universe.select_atoms(selection_str)) - pm.echo(ts.frame) return selection def run(self, **kwargs): diff --git a/package/MDAnalysis/core/universe.py b/package/MDAnalysis/core/universe.py index 523397412f9..160bd6a328b 100644 --- a/package/MDAnalysis/core/universe.py +++ b/package/MDAnalysis/core/universe.py @@ -90,7 +90,7 @@ from .. import _ANCHOR_UNIVERSES, _TOPOLOGY_ATTRS, _PARSERS from ..exceptions import NoDataError from ..lib import util -from ..lib.log import ProgressMeter +from ..lib.log import ProgressBar from ..lib.util import cached, NamedStream, isstream from ..lib.mdamath import find_fragments from . import groups @@ -172,7 +172,7 @@ def _resolve_coordinates(*args, **kwargs): else: coordinates = (filename,) + coordinates return coordinates - + def _generate_from_topology(universe): # generate Universe version of each class # AG, RG, SG, A, R, S @@ -240,16 +240,16 @@ class Universe(object): A CHARMM/XPLOR PSF topology file, PDB file or Gromacs GRO file; used to define the list of atoms. If the file includes bond information, partial charges, atom masses, ... then these data will be available to - MDAnalysis. Alternatively, an existing - :class:`MDAnalysis.core.topology.Topology` instance may be given, + MDAnalysis. Alternatively, an existing + :class:`MDAnalysis.core.topology.Topology` instance may be given, numpy coordinates, or None for an empty universe. coordinates: str, stream, list of str, list of stream (optional) - Coordinates can be provided as files of - a single frame (eg a PDB, CRD, or GRO file); a list of single - frames; or a trajectory file (in CHARMM/NAMD/LAMMPS DCD, Gromacs - XTC/TRR, or generic XYZ format). The coordinates must be - ordered in the same way as the list of atoms in the topology. - See :ref:`Supported coordinate formats` for what can be read + Coordinates can be provided as files of + a single frame (eg a PDB, CRD, or GRO file); a list of single + frames; or a trajectory file (in CHARMM/NAMD/LAMMPS DCD, Gromacs + XTC/TRR, or generic XYZ format). The coordinates must be + ordered in the same way as the list of atoms in the topology. + See :ref:`Supported coordinate formats` for what can be read as coordinates. Alternatively, streams can be given. topology_format: str, ``None``, default ``None`` Provide the file format of the topology file; ``None`` guesses it from @@ -261,7 +261,7 @@ class Universe(object): guesses it from the file extension. Note that this keyword has no effect if a list of file names is supplied because the "chained" reader has to guess the file format for each individual list member. - Can also pass a subclass of :class:`MDAnalysis.coordinates.base.ProtoReader` + Can also pass a subclass of :class:`MDAnalysis.coordinates.base.ProtoReader` to define a custom reader to be used on the trajectory file. all_coordinates: bool, default ``False`` If set to ``True`` specifies that if more than one filename is passed @@ -269,7 +269,7 @@ class Universe(object): :class:`MDAnalysis.coordinates.chain.ChainReader`). The default behavior is to take the first file as a topology and the remaining as coordinates. The first argument will always always be used - to infer a topology regardless of *all_coordinates*. + to infer a topology regardless of *all_coordinates*. guess_bonds: bool, default ``False`` Once Universe has been loaded, attempt to guess the connectivity between atoms. This will populate the .bonds, .angles, and .dihedrals @@ -289,8 +289,8 @@ class Universe(object): *anchor_name* is found. Even if *anchor_name* is set *is_anchor* will still be honored when unpickling. transformations: function or list, ``None``, default ``None`` - Provide a list of transformations that you wish to apply to the - trajectory upon reading. Transformations can be found in + Provide a list of transformations that you wish to apply to the + trajectory upon reading. Transformations can be found in :mod:`MDAnalysis.transformations`, or can be user-created. in_memory: bool, default ``False`` After reading in the trajectory, transfer it to an in-memory @@ -367,7 +367,7 @@ def __init__(self, *args, **kwargs): if not isinstance(topology, Topology) and not topology is None: self.filename = _check_file_like(topology) - topology = _topology_from_file_like(self.filename, + topology = _topology_from_file_like(self.filename, topology_format=topology_format, **kwargs) @@ -380,11 +380,11 @@ def __init__(self, *args, **kwargs): 'your own Universe.') _generate_from_topology(self) # make real atoms, res, segments - + coordinates = _resolve_coordinates(self.filename, *coordinates, format=format, all_coordinates=all_coordinates) - + if coordinates: self.load_new(coordinates, format=format, in_memory=in_memory, in_memory_step=in_memory_step, **kwargs) @@ -393,7 +393,7 @@ def __init__(self, *args, **kwargs): if callable(transformations): transformations = [transformations] self._trajectory.add_transformations(*transformations) - + if guess_bonds: self.atoms.guess_bonds(vdwradii=vdwradii) @@ -462,7 +462,7 @@ def empty(cls, n_atoms, n_residues=1, n_segments=1, if not n_atoms: n_residues = 0 n_segments = 0 - + if atom_resindex is None: warnings.warn( 'Residues specified but no atom_resindex given. ' @@ -504,7 +504,7 @@ def universe(self): # It is also cleaner than a weakref. return self - def load_new(self, filename, format=None, in_memory=False, + def load_new(self, filename, format=None, in_memory=False, in_memory_step=1, **kwargs): """Load coordinates from `filename`. @@ -625,9 +625,6 @@ def transfer_to_memory(self, start=None, stop=None, step=None, *self.trajectory.check_slice_indices(start, stop, step) )) n_atoms = len(self.atoms) - pm_format = '{step}/{numsteps} frames copied to memory (frame {frame})' - pm = ProgressMeter(n_frames, interval=1, - verbose=verbose, format=pm_format) coordinates = np.zeros((n_frames, n_atoms, 3), dtype=np.float32) ts = self.trajectory.ts has_vels = ts.has_velocities @@ -639,7 +636,9 @@ def transfer_to_memory(self, start=None, stop=None, step=None, dimensions = (np.zeros((n_frames, 6), dtype=np.float32) if has_dims else None) - for i, ts in enumerate(self.trajectory[start:stop:step]): + for i, ts in enumerate(ProgressBar(self.trajectory[start:stop:step], + verbose=verbose, + desc="Loading frames")): np.copyto(coordinates[i], ts.positions) if has_vels: np.copyto(velocities[i], ts.velocities) @@ -647,7 +646,6 @@ def transfer_to_memory(self, start=None, stop=None, step=None, np.copyto(forces[i], ts.forces) if has_dims: np.copyto(dimensions[i], ts.dimensions) - pm.echo(i, frame=ts.frame) # Overwrite trajectory in universe with an MemoryReader # object, to provide fast access and allow coordinates @@ -960,7 +958,7 @@ def add_Segment(self, **attrs): self.segments = SegmentGroup(np.arange(self._topology.n_segments), self) # return the new segment return self.segments[segidx] - + def _add_topology_objects(self, object_type, values, types=None, guessed=False, order=None): """Add new TopologyObjects to this Universe @@ -970,10 +968,10 @@ def _add_topology_objects(self, object_type, values, types=None, guessed=False, object_type : {'bonds', 'angles', 'dihedrals', 'impropers'} The type of TopologyObject to add. values : TopologyGroup or iterable of tuples, AtomGroups, or TopologyObjects - An iterable of: tuples of atom indices, or AtomGroups, - or TopologyObjects. If every value is a TopologyObject, all + An iterable of: tuples of atom indices, or AtomGroups, + or TopologyObjects. If every value is a TopologyObject, all keywords are ignored. - If AtomGroups or TopologyObjects are passed, they *must* be from the same + If AtomGroups or TopologyObjects are passed, they *must* be from the same Universe. types : iterable (optional, default None) None, or an iterable of hashable values with the same length as ``values`` @@ -992,7 +990,7 @@ def _add_topology_objects(self, object_type, values, types=None, guessed=False, types = None guessed = [t.is_guessed for t in values] order = [t.order for t in values] - + indices = [] for x in values: if isinstance(x, (AtomGroup, TopologyObject)): @@ -1009,13 +1007,13 @@ def _add_topology_objects(self, object_type, values, types=None, guessed=False, istr = ', '.join(map(str, nonexistent)) err_msg = 'Cannot add {} for nonexistent atom indices: {}' raise ValueError(err_msg.format(object_type, istr)) - + try: attr = getattr(self._topology, object_type) except AttributeError: self.add_TopologyAttr(object_type, []) attr = getattr(self._topology, object_type) - + attr._add_bonds(indices, types=types, guessed=guessed, order=order) @@ -1025,10 +1023,10 @@ def add_bonds(self, values, types=None, guessed=False, order=None): Parameters ---------- values : iterable of tuples, AtomGroups, or Bonds; or TopologyGroup - An iterable of: tuples of 2 atom indices, or AtomGroups with 2 atoms, - or Bonds. If every value is a Bond, all - keywords are ignored. - If AtomGroups, Bonds, or a TopologyGroup are passed, + An iterable of: tuples of 2 atom indices, or AtomGroups with 2 atoms, + or Bonds. If every value is a Bond, all + keywords are ignored. + If AtomGroups, Bonds, or a TopologyGroup are passed, they *must* be from the same Universe. types : iterable (optional, default None) None, or an iterable of hashable values with the same length as ``values`` @@ -1052,13 +1050,13 @@ def add_bonds(self, values, types=None, guessed=False, order=None): ow_mw = sol.select_atoms('name OW or name MW').split('residue') u.add_bonds(ow_hw1 + ow_hw2 + ow_mw) - You can only add bonds from the same Universe. If you would like to add - AtomGroups, Bonds, or a TopologyGroup from a different Universe, convert + You can only add bonds from the same Universe. If you would like to add + AtomGroups, Bonds, or a TopologyGroup from a different Universe, convert them to indices first. :: from MDAnalysis.tests.datafiles import PSF u2 = mda.Universe(PSF) - + # assuming you have already added bonds to u u2.add_bonds(u.bonds.to_indices()) @@ -1068,17 +1066,17 @@ def add_bonds(self, values, types=None, guessed=False, order=None): self._add_topology_objects('bonds', values, types=types, guessed=guessed, order=order) self._cache.pop('fragments', None) - + def add_angles(self, values, types=None, guessed=False): """Add new Angles to this Universe. Parameters ---------- values : iterable of tuples, AtomGroups, or Angles; or TopologyGroup - An iterable of: tuples of 3 atom indices, or AtomGroups with 3 atoms, - or Angles. If every value is a Angle, all + An iterable of: tuples of 3 atom indices, or AtomGroups with 3 atoms, + or Angles. If every value is a Angle, all keywords are ignored. - If AtomGroups, Angles, or a TopologyGroup are passed, + If AtomGroups, Angles, or a TopologyGroup are passed, they *must* be from the same Universe. types : iterable (optional, default None) None, or an iterable of hashable values with the same length as ``values`` @@ -1089,17 +1087,17 @@ def add_angles(self, values, types=None, guessed=False): """ self._add_topology_objects('angles', values, types=types, guessed=guessed) - + def add_dihedrals(self, values, types=None, guessed=False): """Add new Dihedrals to this Universe. Parameters ---------- values : iterable of tuples, AtomGroups, or Dihedrals; or TopologyGroup - An iterable of: tuples of 4 atom indices, or AtomGroups with 4 atoms, - or Dihedrals. If every value is a Dihedral, all + An iterable of: tuples of 4 atom indices, or AtomGroups with 4 atoms, + or Dihedrals. If every value is a Dihedral, all keywords are ignored. - If AtomGroups, Dihedrals, or a TopologyGroup are passed, + If AtomGroups, Dihedrals, or a TopologyGroup are passed, they *must* be from the same Universe. types : iterable (optional, default None) None, or an iterable of hashable values with the same length as ``values`` @@ -1111,17 +1109,17 @@ def add_dihedrals(self, values, types=None, guessed=False): """ self._add_topology_objects('dihedrals', values, types=types, guessed=guessed) - + def add_impropers(self, values, types=None, guessed=False): """Add new Impropers to this Universe. Parameters ---------- values : iterable of tuples, AtomGroups, or Impropers; or TopologyGroup - An iterable of: tuples of 4 atom indices, or AtomGroups with 4 atoms, - or Impropers. If every value is an Improper, all + An iterable of: tuples of 4 atom indices, or AtomGroups with 4 atoms, + or Impropers. If every value is an Improper, all keywords are ignored. - If AtomGroups, Impropers, or a TopologyGroup are passed, + If AtomGroups, Impropers, or a TopologyGroup are passed, they *must* be from the same Universe. types : iterable (optional, default None) None, or an iterable of hashable values with the same length as ``values`` @@ -1142,9 +1140,9 @@ def _delete_topology_objects(self, object_type, values): object_type : {'bonds', 'angles', 'dihedrals', 'impropers'} The type of TopologyObject to add. values : iterable of tuples, AtomGroups, or TopologyObjects; or TopologyGroup - An iterable of: tuples of atom indices, or AtomGroups, + An iterable of: tuples of atom indices, or AtomGroups, or TopologyObjects. - If AtomGroups, TopologyObjects, or a TopologyGroup are passed, + If AtomGroups, TopologyObjects, or a TopologyGroup are passed, they *must* be from the same Universe. .. versionadded:: 0.21.0 @@ -1163,7 +1161,7 @@ def _delete_topology_objects(self, object_type, values): attr = getattr(self._topology, object_type) except AttributeError: raise ValueError('There are no {} to delete'.format(object_type)) - + attr._delete_bonds(indices) def delete_bonds(self, values): @@ -1172,9 +1170,9 @@ def delete_bonds(self, values): Parameters ---------- values : iterable of tuples, AtomGroups, or Bonds; or TopologyGroup - An iterable of: tuples of 2 atom indices, or AtomGroups with 2 atoms, + An iterable of: tuples of 2 atom indices, or AtomGroups with 2 atoms, or Bonds. - If AtomGroups, Bonds, or a TopologyGroup are passed, + If AtomGroups, Bonds, or a TopologyGroup are passed, they *must* be from the same Universe. @@ -1186,13 +1184,13 @@ def delete_bonds(self, values): import MDAnalysis as mda from MDAnalysis.tests.datafiles import PSF u = mda.Universe(PSF) - + # delete first 5 bonds u.delete_bonds(u.bonds[:5]) - - If you are deleting bonds in the form of AtomGroups, Bonds, or a - TopologyGroup, they must come from the same Universe. If you want to + + If you are deleting bonds in the form of AtomGroups, Bonds, or a + TopologyGroup, they must come from the same Universe. If you want to delete bonds from another Universe, convert them to indices first. :: from MDAnalysis.tests.datafiles import PDB @@ -1205,51 +1203,51 @@ def delete_bonds(self, values): """ self._delete_topology_objects('bonds', values) self._cache.pop('fragments', None) - + def delete_angles(self, values): """Delete Angles from this Universe. Parameters ---------- values : iterable of tuples, AtomGroups, or Angles; or TopologyGroup - An iterable of: tuples of 3 atom indices, or AtomGroups with 3 atoms, + An iterable of: tuples of 3 atom indices, or AtomGroups with 3 atoms, or Angles. - If AtomGroups, Angles, or a TopologyGroup are passed, + If AtomGroups, Angles, or a TopologyGroup are passed, they *must* be from the same Universe. - + .. versionadded:: 0.21.0 """ self._delete_topology_objects('angles', values) - + def delete_dihedrals(self, values): """Delete Dihedrals from this Universe. Parameters ---------- values : iterable of tuples, AtomGroups, or Dihedrals; or TopologyGroup - An iterable of: tuples of 4 atom indices, or AtomGroups with 4 atoms, + An iterable of: tuples of 4 atom indices, or AtomGroups with 4 atoms, or Dihedrals. - If AtomGroups, Dihedrals, or a TopologyGroup are passed, + If AtomGroups, Dihedrals, or a TopologyGroup are passed, they *must* be from the same Universe. - + .. versionadded:: 0.21.0 """ self._delete_topology_objects('dihedrals', values) - + def delete_impropers(self, values): """Delete Impropers from this Universe. Parameters ---------- values : iterable of tuples, AtomGroups, or Impropers; or TopologyGroup - An iterable of: tuples of 4 atom indices, or AtomGroups with 4 atoms, + An iterable of: tuples of 4 atom indices, or AtomGroups with 4 atoms, or Impropers. - If AtomGroups, Angles, or a TopologyGroup are passed, + If AtomGroups, Angles, or a TopologyGroup are passed, they *must* be from the same Universe. - + .. versionadded:: 0.21.0 """ self._delete_topology_objects('impropers', values) @@ -1506,5 +1504,5 @@ def Merge(*args): except AttributeError: #Create universe without coordinates if they dont exists in args u = Universe(top) - + return u diff --git a/package/MDAnalysis/lib/log.py b/package/MDAnalysis/lib/log.py index fa8ad4476cf..73e60437c32 100644 --- a/package/MDAnalysis/lib/log.py +++ b/package/MDAnalysis/lib/log.py @@ -87,6 +87,8 @@ import re import warnings +from tqdm.auto import tqdm + from .. import version @@ -223,7 +225,11 @@ def _guess_string_format(template): class ProgressMeter(object): - r"""Simple progress meter + r"""Simple progress meter (Deprecated) + + ..Warning: + This class is deprecated and will be removed in version 2.0. + Please use :class:`MDAnalysis.lib.log.ProgressBar` instead. The :class:`ProgressMeter` class can be used to show running progress such as frames processed or percentage done to give the user feedback on the @@ -323,11 +329,20 @@ class ProgressMeter(object): .. deprecated:: 0.16 Keyword argument *quiet* is deprecated in favor of *verbose*. + .. deprecated:: 1.0 + This class is deprecated in favor of *ProgressBar*. + """ def __init__(self, numsteps, format=None, interval=10, offset=1, verbose=True, dynamic=True, format_handling='auto'): + warnings.warn( + "This class is deprecated as of MDAnalysis version 1.0. " + "It will be removed in MDAnalysis version 2.0. " + "Please use MDAnalysis.lib.log.ProgressBar instead.", + category=DeprecationWarning + ) self.numsteps = numsteps self.interval = int(interval) self.offset = int(offset) @@ -394,3 +409,15 @@ def echo(self, step, **kwargs): return echo(self.format_handler(format, vars(self)), replace=self.dynamic, newline=newline) + + +class ProgressBar(tqdm): + def __init__(self, *args, **kwargs): + verbose = kwargs.pop('verbose', True) + # disable: Whether to disable the entire progressbar wrapper [default: False]. + # If set to None, disable on non-TTY. + # disable should be the opposite of verbose unless it's None + disable = verbose if verbose is None else not verbose + # disable should take precedence over verbose if both are set + kwargs['disable'] = kwargs.pop('disable', disable) + super(ProgressBar, self).__init__(*args, **kwargs) diff --git a/package/setup.py b/package/setup.py index 01e8aedecc5..28f8af503ef 100755 --- a/package/setup.py +++ b/package/setup.py @@ -558,6 +558,7 @@ def dynamic_author_list(): 'scipy>=1.0.0', 'matplotlib>=1.5.1', 'mock', + 'tqdm>=4.43.0', ] if not os.name == 'nt': install_requires.append('gsd>=1.4.0') @@ -592,7 +593,7 @@ def dynamic_author_list(): ext_modules=exts, requires=['numpy (>=1.13.3)', 'biopython (>= 1.71)', 'mmtf (>=1.0.0)', 'networkx (>=1.0)', 'GridDataFormats (>=0.3.2)', 'joblib', - 'scipy (>=1.0.0)', 'matplotlib (>=1.5.1)'], + 'scipy (>=1.0.0)', 'matplotlib (>=1.5.1)', 'tqdm (>=4.43.0)'], # all standard requirements are available through PyPi and # typically can be installed without difficulties through setuptools setup_requires=[ diff --git a/testsuite/MDAnalysisTests/analysis/test_base.py b/testsuite/MDAnalysisTests/analysis/test_base.py index 702b18cfd7b..05046756817 100644 --- a/testsuite/MDAnalysisTests/analysis/test_base.py +++ b/testsuite/MDAnalysisTests/analysis/test_base.py @@ -95,6 +95,22 @@ def test_verbose(u): assert a._verbose +def test_verbose_progressbar(u, capsys): + an = FrameAnalysis(u.trajectory).run() + out, err = capsys.readouterr() + expected = '' + actual = err.strip().split('\r')[-1] + assert actual == expected + + +def test_verbose_progressbar_run(u, capsys): + an = FrameAnalysis(u.trajectory).run(verbose=True) + out, err = capsys.readouterr() + expected = u'100%|██████████| 98/98 [00:00<00:00, 8799.49it/s]' + actual = err.strip().split('\r')[-1] + assert actual[:24] == expected[:24] + + def test_incomplete_defined_analysis(u): with pytest.raises(NotImplementedError): IncompleteAnalysis(u.trajectory).run() diff --git a/testsuite/MDAnalysisTests/analysis/test_rms.py b/testsuite/MDAnalysisTests/analysis/test_rms.py index 004e7a31c7f..ef48679bddf 100644 --- a/testsuite/MDAnalysisTests/analysis/test_rms.py +++ b/testsuite/MDAnalysisTests/analysis/test_rms.py @@ -186,14 +186,6 @@ def correct_values_backbone_group(self): return [[0, 1, 0, 0, 0], [49, 50, 4.6997, 1.9154, 2.7139]] - def test_progress_meter(self, capsys, universe): - RMSD = MDAnalysis.analysis.rms.RMSD(universe, verbose=True) - RMSD.run() - out, err = capsys.readouterr() - expected = 'RMSD 6.93 A at frame 98/98 [100.0%]' - actual = err.strip().split('\r')[-1] - assert_equal(actual, expected) - def test_rmsd(self, universe, correct_values): RMSD = MDAnalysis.analysis.rms.RMSD(universe, select='name CA') RMSD.run(step=49) diff --git a/testsuite/MDAnalysisTests/lib/test_log.py b/testsuite/MDAnalysisTests/lib/test_log.py new file mode 100644 index 00000000000..91369a85c78 --- /dev/null +++ b/testsuite/MDAnalysisTests/lib/test_log.py @@ -0,0 +1,78 @@ +# -*- Mode: python; tab-width: 4; indent-tabs-mode:nil; coding:utf-8 -*- +# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 fileencoding=utf-8 +# +# MDAnalysis --- https://www.mdanalysis.org +# Copyright (c) 2006-2017 The MDAnalysis Development Team and contributors +# (see the file AUTHORS for the full list of names) +# +# Released under the GNU Public Licence, v2 or any higher version +# +# Please cite your use of MDAnalysis in published work: +# +# R. J. Gowers, M. Linke, J. Barnoud, T. J. E. Reddy, M. N. Melo, S. L. Seyler, +# D. L. Dotson, J. Domanski, S. Buchoux, I. M. Kenney, and O. Beckstein. +# MDAnalysis: A Python package for the rapid analysis of molecular dynamics +# simulations. In S. Benthall and S. Rostrup editors, Proceedings of the 15th +# Python in Science Conference, pages 102-109, Austin, TX, 2016. SciPy. +# doi: 10.25080/majora-629e541a-00e +# +# N. Michaud-Agrawal, E. J. Denning, T. B. Woolf, and O. Beckstein. +# MDAnalysis: A Toolkit for the Analysis of Molecular Dynamics Simulations. +# J. Comput. Chem. 32 (2011), 2319--2327, doi:10.1002/jcc.21787 +# + +from __future__ import absolute_import +import warnings +import pytest + +from MDAnalysis.lib.log import ProgressMeter, ProgressBar + + +class TestProgressMeter(object): + + def test_deprecated(self, capsys): + with warnings.catch_warnings(record=True) as w: + # Cause all warnings to always be triggered. + warnings.simplefilter("always") + # Trigger a warning. + pm = ProgressMeter(10) + # Verify the warning + assert len(w) == 1 + assert issubclass(w[-1].category, DeprecationWarning) + assert "MDAnalysis.lib.log.ProgressBar" in str(w[-1].message) + + def test_output(self, capsys): + pm = ProgressMeter(10, interval=1) + for i in range(10): + pm.echo(i) + out, err = capsys.readouterr() + expected = 'Step 10/10 [100.0%]' + actual = err.strip().split('\r')[-1] + assert actual == expected + + +class TestProgressBar(object): + + def test_output(self, capsys): + for i in ProgressBar(list(range(10))): + pass + out, err = capsys.readouterr() + expected = u'100%|██████████| 10/10 [00:00<00:00, 583.67it/s]' + actual = err.strip().split('\r')[-1] + assert actual[:24] == expected[:24] + + def test_disable(self, capsys): + for i in ProgressBar(list(range(10)), disable=True): + pass + out, err = capsys.readouterr() + expected = '' + actual = err.strip().split('\r')[-1] + assert actual == expected + + def test_verbose_disable(self, capsys): + for i in ProgressBar(list(range(10)), verbose=False): + pass + out, err = capsys.readouterr() + expected = '' + actual = err.strip().split('\r')[-1] + assert actual == expected