From 94e3218f30a6b8501b4483d001a7402d29eda8e1 Mon Sep 17 00:00:00 2001 From: Richard Gowers Date: Thu, 22 Jun 2017 11:06:30 +0100 Subject: [PATCH] Added pytest-raises Converted test_units to use parametrize Some hacking of test_distances to use pytest features Completed moving test_distances to pytest style rewrote test_transformations into pytest style make scipy and matplotlib full dependencies (#1159) scipy and matplotlib are imported at top in analysis - updated all modules - removed any code that guards against scipy or matplotlib import - conforms to style guide https://github.com/MDAnalysis/mdanalysis/wiki/Style-Guide#module-imports-in-mdanalysisanalysis - fixes #1159 - fixes #1361 removed conditional skipping of tests when scipy or matplotlib are missing minor clean ups --- .travis.yml | 6 +- package/CHANGELOG | 3 + package/MDAnalysis/analysis/distances.py | 23 +- .../MDAnalysis/analysis/encore/similarity.py | 36 +- .../analysis/hbonds/hbond_autocorrel.py | 26 +- package/MDAnalysis/analysis/hole.py | 16 +- package/MDAnalysis/analysis/legacy/x3dna.py | 5 +- package/MDAnalysis/analysis/pca.py | 7 +- package/MDAnalysis/analysis/polymer.py | 7 +- package/MDAnalysis/analysis/psa.py | 106 ++- package/setup.py | 9 +- .../MDAnalysisTests/analysis/test_density.py | 7 - .../analysis/test_distances.py | 67 +- .../MDAnalysisTests/analysis/test_encore.py | 31 +- .../MDAnalysisTests/analysis/test_hole.py | 9 +- .../analysis/test_hydrogenbondautocorrel.py | 3 - .../MDAnalysisTests/analysis/test_leaflet.py | 2 - .../MDAnalysisTests/analysis/test_pca.py | 2 - .../analysis/test_persistencelength.py | 14 +- .../MDAnalysisTests/analysis/test_psa.py | 15 +- .../MDAnalysisTests/utils/test_distances.py | 776 ++++++++-------- .../utils/test_transformations.py | 848 ++++++++---------- testsuite/MDAnalysisTests/utils/test_units.py | 80 +- 23 files changed, 902 insertions(+), 1196 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4b9273077e3..54adc1bbeef 100644 --- a/.travis.yml +++ b/.travis.yml @@ -31,10 +31,10 @@ env: - MAIN_CMD="pytest ${PYTEST_LIST} ${PYTEST_FLAGS}; python ./testsuite/MDAnalysisTests/mda_nosetests ${NOSE_TEST_LIST} ${NOSE_FLAGS}" - SETUP_CMD="" - BUILD_CMD="pip install -v package/ && pip install testsuite/" - - CONDA_DEPENDENCIES="mmtf-python nose=1.3.7 mock six biopython networkx cython joblib nose-timer pytest=3.1.2 pytest-cov=2.5.1" - - CONDA_ALL_DEPENDENCIES="mmtf-python nose=1.3.7 mock six biopython networkx cython joblib nose-timer matplotlib netcdf4 scikit-learn scipy seaborn coveralls clustalw=2.1 pytest=3.1.2 pytest-cov=2.5.1" + - CONDA_DEPENDENCIES="mmtf-python nose=1.3.7 mock six biopython networkx cython joblib nose-timer matplotlib scipy griddataformats pytest pytest-cov pytest-raises" + - CONDA_ALL_DEPENDENCIES="mmtf-python nose=1.3.7 mock six biopython networkx cython joblib nose-timer matplotlib netcdf4 scikit-learn scipy griddataformats seaborn coveralls clustalw=2.1 pytest pytest-cov pytest-raises" # Install griddataformats from PIP so that scipy is only installed in the full build (#1147) - - PIP_DEPENDENCIES='griddataformats' + - PIP_DEPENDENCIES='' - CONDA_CHANNELS='biobuilds conda-forge' - CONDA_CHANNEL_PRIORITY=True - NUMPY_VERSION=stable diff --git a/package/CHANGELOG b/package/CHANGELOG index f1aba435b37..6859b765b25 100644 --- a/package/CHANGELOG +++ b/package/CHANGELOG @@ -29,6 +29,9 @@ Fixes * Fixed dtype of numpy arrays to accomodate 32 bit architectures (Issue #1362) * Groups are hashable on python 3 (Issue #1397) +Changes + * scipy and matplotlib are now required dependencies (Issue #1159) + 06/03/17 utkbansal, kain88-de, xiki-tempula, kaplajon, wouterboomsma, richardjgowers, Shtkddud123, QuantumEntangledAndy, orbeckst, diff --git a/package/MDAnalysis/analysis/distances.py b/package/MDAnalysis/analysis/distances.py index ae4ad699141..d524a993625 100644 --- a/package/MDAnalysis/analysis/distances.py +++ b/package/MDAnalysis/analysis/distances.py @@ -42,6 +42,7 @@ 'contact_matrix', 'dist', 'between'] import numpy as np +import scipy.sparse from MDAnalysis.lib.distances import distance_array, self_distance_array from MDAnalysis.lib.c_distances import contact_matrix_no_pbc, contact_matrix_pbc @@ -51,15 +52,6 @@ import logging logger = logging.getLogger("MDAnalysis.analysis.distances") -try: - from scipy import sparse -except ImportError: - sparse = None - msg = "scipy.sparse could not be imported: some functionality will " \ - "not be available in contact_matrix()" - warnings.warn(msg, category=ImportWarning) - logger.warn(msg) - del msg def contact_matrix(coord, cutoff=15.0, returntype="numpy", box=None): '''Calculates a matrix of contacts. @@ -93,12 +85,6 @@ def contact_matrix(coord, cutoff=15.0, returntype="numpy", box=None): The contact matrix is returned in a format determined by the `returntype` keyword. - - Note - ---- - :mod:`scipy.sparse` is require for using *sparse* matrices; if it cannot - be imported then an `ImportError` is raised. - See Also -------- :mod:`MDAnalysis.analysis.contacts` for native contact analysis @@ -112,14 +98,9 @@ def contact_matrix(coord, cutoff=15.0, returntype="numpy", box=None): adj = (distance_array(coord, coord, box=box) < cutoff) return adj elif returntype == "sparse": - if sparse is None: - # hack: if we are running with minimal dependencies then scipy was - # not imported and we have to bail here (see scipy import at top) - raise ImportError("For sparse matrix functionality you need to " - "import scipy.") # Initialize square List of Lists matrix of dimensions equal to number # of coordinates passed - sparse_contacts = sparse.lil_matrix((len(coord), len(coord)), dtype='bool') + sparse_contacts = scipy.sparse.lil_matrix((len(coord), len(coord)), dtype='bool') if box is not None: # with PBC contact_matrix_pbc(coord, sparse_contacts, box, cutoff) diff --git a/package/MDAnalysis/analysis/encore/similarity.py b/package/MDAnalysis/analysis/encore/similarity.py index bd7e9e5e0ea..a56b3c0c1e5 100644 --- a/package/MDAnalysis/analysis/encore/similarity.py +++ b/package/MDAnalysis/analysis/encore/similarity.py @@ -172,21 +172,13 @@ from __future__ import print_function, division, absolute_import from six.moves import range, zip -import MDAnalysis as mda -import numpy as np import warnings import logging -try: - from scipy.stats import gaussian_kde -except ImportError: - gaussian_kde = None - msg = "scipy.stats.gaussian_kde could not be imported. " \ - "Dimensionality reduction ensemble comparisons will not " \ - "be available." - warnings.warn(msg, - category=ImportWarning) - logging.warn(msg) - del msg + +import numpy as np +import scipy.stats + +import MDAnalysis as mda from ...coordinates.memory import MemoryReader from .confdistmatrix import get_distance_matrix @@ -460,18 +452,11 @@ def gen_kde_pdfs(embedded_space, ensemble_assignment, nensembles, embedded_ensembles = [] resamples = [] - if gaussian_kde is None: - # hack: if we are running with minimal dependencies then scipy was - # not imported and we have to bail here (see scipy import at top) - raise ImportError("For Kernel Density Estimation functionality you" - "need to import scipy") - for i in range(1, nensembles + 1): this_embedded = embedded_space.transpose()[ np.where(np.array(ensemble_assignment) == i)].transpose() embedded_ensembles.append(this_embedded) - kdes.append(gaussian_kde( - this_embedded)) + kdes.append(scipy.stats.gaussian_kde(this_embedded)) # # Set number of samples # if not nsamples: @@ -623,12 +608,6 @@ def cumulative_gen_kde_pdfs(embedded_space, ensemble_assignment, nensembles, """ - if gaussian_kde is None: - # hack: if we are running with minimal dependencies then scipy was - # not imported and we have to bail here (see scipy import at top) - raise ImportError("For Kernel Density Estimation functionality you" - "need to import scipy") - kdes = [] embedded_ensembles = [] resamples = [] @@ -639,8 +618,7 @@ def cumulative_gen_kde_pdfs(embedded_space, ensemble_assignment, nensembles, np.logical_and(ensemble_assignment >= ens_id_min, ensemble_assignment <= i))].transpose() embedded_ensembles.append(this_embedded) - kdes.append( - gaussian_kde(this_embedded)) + kdes.append(scipy.stats.gaussian_kde(this_embedded)) # Resample according to probability distributions for this_kde in kdes: diff --git a/package/MDAnalysis/analysis/hbonds/hbond_autocorrel.py b/package/MDAnalysis/analysis/hbonds/hbond_autocorrel.py index a0bf6bb5451..296100addd8 100644 --- a/package/MDAnalysis/analysis/hbonds/hbond_autocorrel.py +++ b/package/MDAnalysis/analysis/hbonds/hbond_autocorrel.py @@ -155,6 +155,8 @@ from __future__ import division, absolute_import from six.moves import zip import numpy as np +import scipy.optimize + import warnings from MDAnalysis.lib.log import ProgressMeter @@ -162,7 +164,7 @@ class HydrogenBondAutoCorrel(object): - """Perform a time autocorrelation of the hydrogen bonds in the system. + """Perform a time autocorrelation of the hydrogen bonds in the system. Parameters ---------- @@ -421,8 +423,10 @@ def solve(self, p_guess=None): Initial guess for the leastsq fit, must match the shape of the expected coefficients - Continuous defition results are fitted to a double exponential, - intermittent definition are fit to a triple exponential. + + Continuous defition results are fitted to a double exponential with + :func:`scipy.optimize.leastsq`, intermittent definition are fit to a + triple exponential. The results of this fitting procedure are saved into the *fit*, *tau* and *estimate* keywords in the solution dict. @@ -434,14 +438,14 @@ def solve(self, p_guess=None): - *estimate* contains the estimate provided by the fit of the time autocorrelation function - In addition, the output of the leastsq function is saved into the - solution dict + In addition, the output of the :func:`~scipy.optimize.leastsq` function + is saved into the solution dict - *infodict* - *mesg* - *ier* + """ - from scipy.optimize import leastsq if self.solution['results'] is None: raise ValueError( @@ -498,9 +502,8 @@ def triple(x, A1, A2, tau1, tau2, tau3): if p_guess is None: p_guess = (0.5, 10 * self.sample_time, self.sample_time) - p, cov, infodict, mesg, ier = leastsq(err, p_guess, - args=(time, results), - full_output=True) + p, cov, infodict, mesg, ier = scipy.optimize.leastsq( + err, p_guess, args=(time, results), full_output=True) self.solution['fit'] = p A1, tau1, tau2 = p A2 = 1 - A1 @@ -512,9 +515,8 @@ def triple(x, A1, A2, tau1, tau2, tau3): p_guess = (0.33, 0.33, 10 * self.sample_time, self.sample_time, 0.1 * self.sample_time) - p, cov, infodict, mesg, ier = leastsq(err, p_guess, - args=(time, results), - full_output=True) + p, cov, infodict, mesg, ier = scipy.optimize.leastsq( + err, p_guess, args=(time, results), full_output=True) self.solution['fit'] = p A1, A2, tau1, tau2, tau3 = p A3 = 1 - A1 - A2 diff --git a/package/MDAnalysis/analysis/hole.py b/package/MDAnalysis/analysis/hole.py index e43316a868c..7088e5f8a7c 100644 --- a/package/MDAnalysis/analysis/hole.py +++ b/package/MDAnalysis/analysis/hole.py @@ -245,7 +245,6 @@ from six.moves import zip, cPickle import six -import numpy as np import glob import os import errno @@ -258,6 +257,10 @@ import logging from itertools import cycle +import numpy as np +import matplotlib +import matplotlib.pyplot as plt + from MDAnalysis import Universe from MDAnalysis.exceptions import ApplicationError from MDAnalysis.lib.util import which, realpath, asiterable @@ -370,8 +373,6 @@ def save(self, filename="hole.pickle"): cPickle.dump(self.profiles, open(filename, "wb"), cPickle.HIGHEST_PROTOCOL) def _process_plot_kwargs(self, kwargs): - import matplotlib.colors - kw = {} frames = kwargs.pop('frames', None) if frames is None: @@ -448,9 +449,6 @@ def plot(self, **kwargs): Returns ``ax``. """ - - import matplotlib.pyplot as plt - kw, kwargs = self._process_plot_kwargs(kwargs) ax = kwargs.pop('ax', None) @@ -517,8 +515,7 @@ def plot3D(self, **kwargs): Returns ``ax``. """ - - import matplotlib.pyplot as plt + # installed with matplotlib; imported here to enable 3D axes from mpl_toolkits.mplot3d import Axes3D kw, kwargs = self._process_plot_kwargs(kwargs) @@ -540,8 +537,7 @@ def plot3D(self, **kwargs): rxncoord = profile.rxncoord else: # does not seem to work with masked arrays but with nan hack! - # http://stackoverflow.com/questions/4913306/python-matplotlib-mplot3d-how-do-i-set-a-maximum-value - # -for-the-z-axis + # http://stackoverflow.com/questions/4913306/python-matplotlib-mplot3d-how-do-i-set-a-maximum-value-for-the-z-axis #radius = np.ma.masked_greater(profile.radius, rmax) #rxncoord = np.ma.array(profile.rxncoord, mask=radius.mask) rxncoord = profile.rxncoord diff --git a/package/MDAnalysis/analysis/legacy/x3dna.py b/package/MDAnalysis/analysis/legacy/x3dna.py index f960cd1fc5d..99cb4a8fc65 100644 --- a/package/MDAnalysis/analysis/legacy/x3dna.py +++ b/package/MDAnalysis/analysis/legacy/x3dna.py @@ -132,13 +132,15 @@ import errno import shutil import warnings -import numpy as np import os.path import subprocess import tempfile import textwrap from collections import OrderedDict +import numpy as np +import matplotlib.pyplot as plt + from MDAnalysis import ApplicationError from MDAnalysis.lib.util import which, realpath, asiterable @@ -413,7 +415,6 @@ def plot(self, **kwargs): Provide `ax` to have all plots plotted in the same axes. """ - import matplotlib.pyplot as plt na_avg, na_std = self.mean_std() for k in range(len(na_avg[0])): diff --git a/package/MDAnalysis/analysis/pca.py b/package/MDAnalysis/analysis/pca.py index 18d729ecc08..b9920a27ed3 100644 --- a/package/MDAnalysis/analysis/pca.py +++ b/package/MDAnalysis/analysis/pca.py @@ -106,6 +106,7 @@ import warnings import numpy as np +import scipy.integrate from MDAnalysis import Universe from MDAnalysis.analysis.align import _fit_to @@ -357,9 +358,9 @@ def cosine_content(pca_space, i): .. [BerkHess1] Berk Hess. Convergence of sampling in protein simulations. Phys. Rev. E 65, 031910 (2002). """ - from scipy.integrate import simps + t = np.arange(len(pca_space)) T = len(pca_space) cos = np.cos(np.pi * t * (i + 1) / T) - return ((2.0 / T) * (simps(cos*pca_space[:, i])) ** 2 / - simps(pca_space[:, i] ** 2)) + return ((2.0 / T) * (scipy.integrate.simps(cos*pca_space[:, i])) ** 2 / + scipy.integrate.simps(pca_space[:, i] ** 2)) diff --git a/package/MDAnalysis/analysis/polymer.py b/package/MDAnalysis/analysis/polymer.py index 645055e1878..b7babb204ab 100644 --- a/package/MDAnalysis/analysis/polymer.py +++ b/package/MDAnalysis/analysis/polymer.py @@ -36,6 +36,8 @@ from six.moves import range import numpy as np +import scipy.optimize + import logging from .. import NoDataError @@ -165,13 +167,10 @@ def fit_exponential_decay(x, y): ----- This function assumes that data starts at 1.0 and decays to 0.0 - Requires scipy """ - from scipy.optimize import curve_fit - def expfunc(x, a): return np.exp(-x/a) - a = curve_fit(expfunc, x, y)[0][0] + a = scipy.optimize.curve_fit(expfunc, x, y)[0][0] return a diff --git a/package/MDAnalysis/analysis/psa.py b/package/MDAnalysis/analysis/psa.py index 6c002241a40..f4562ee4e36 100644 --- a/package/MDAnalysis/analysis/psa.py +++ b/package/MDAnalysis/analysis/psa.py @@ -216,7 +216,11 @@ from six.moves import range, cPickle import numpy as np -import warnings,numbers +from scipy import spatial, cluster +import matplotlib + +import warnings +import numbers import MDAnalysis import MDAnalysis.analysis.align @@ -396,13 +400,15 @@ def hausdorff(P, Q): Notes ----- - - The Hausdorff distance is calculated in a brute force manner from the - distance matrix without further optimizations, essentially following - [Huttenlocher1993]_. - - :func:`scipy.spatial.distance.directed_hausdorff` is an optimized - implementation of the early break algorithm of [Taha2015]_; note that - one still has to calculate the *symmetric* Hausdorff distance as - `max(directed_hausdorff(P, Q)[0], directed_hausdorff(Q, P)[0])`. + The Hausdorff distance is calculated in a brute force manner from the + distance matrix without further optimizations, essentially following + [Huttenlocher1993]_. + + :func:`scipy.spatial.distance.directed_hausdorff` is an optimized + implementation of the early break algorithm of [Taha2015]_; note that one + still has to calculate the *symmetric* Hausdorff distance as + `max(directed_hausdorff(P, Q)[0], directed_hausdorff(Q, P)[0])`. + References ---------- @@ -410,16 +416,20 @@ def hausdorff(P, Q): W. J. Rucklidge. Comparing images using the Hausdorff distance. IEEE Transactions on Pattern Analysis and Machine Intelligence, 15(9):850–863, 1993. - .. [Taha2015] A. A. Taha and A. Hanbury. An efficient algorithm for calculating the exact Hausdorff distance. IEEE Transactions On Pattern Analysis And Machine Intelligence, 37:2153-63, 2015. + + See Also + -------- + scipy.spatial.distance.directed_hausdorff + """ N, axis = get_coord_axes(P) d = get_msd_matrix(P, Q, axis=axis) - return ( max( np.amax(np.amin(d, axis=0)), \ - np.amax(np.amin(d, axis=1)) ) / N )**0.5 + return (max(np.amax(np.amin(d, axis=0)), + np.amax(np.amin(d, axis=1))) / N)**0.5 def hausdorff_wavg(P, Q): @@ -1650,7 +1660,7 @@ def plot(self, filename=None, linkage='ward', count_sort=False, If `filename` is supplied then the figure is also written to file (the suffix determines the file type, e.g. pdf, png, eps, ...). All other - keyword arguments are passed on to :func:`matplotlib.pyplot.imshow`. + keyword arguments are passed on to :func:`matplotlib.pyplot.matshow`. Parameters @@ -1669,6 +1679,15 @@ def plot(self, filename=None, linkage='ward', count_sort=False, set the font size for colorbar labels; font size for path labels on dendrogram default to 3 points smaller [``12``] + Returns + ------- + Z + `Z` from :meth:`cluster` + dgram + `dgram` from :meth:`cluster` + dist_matrix_clus + clustered distance matrix (reordered) + """ from matplotlib.pyplot import figure, colorbar, cm, savefig, clf @@ -1770,6 +1789,23 @@ def plot_annotated_heatmap(self, filename=None, linkage='ward', \ annot_size : float font size of annotation labels on heat map [``6.5``] + Returns + ------- + Z + `Z` from :meth:`cluster` + dgram + `dgram` from :meth:`cluster` + dist_matrix_clus + clustered distance matrix (reordered) + + + Note + ---- + This function requires the seaborn_ package, which can be installed + with `pip install seaborn` or `conda install seaborn`. + + .. _seaborn: https://seaborn.pydata.org/ + """ from matplotlib.pyplot import figure, colorbar, cm, savefig, clf @@ -1870,6 +1906,17 @@ def plot_nearest_neighbors(self, filename=None, idx=0, \ set the font size for colorbar labels; font size for path labels on dendrogram default to 3 points smaller [``12``] + Returns + ------- + ax : axes + + Note + ---- + This function requires the seaborn_ package, which can be installed + with `pip install seaborn` or `conda install seaborn`. + + .. _seaborn: https://seaborn.pydata.org/ + """ from matplotlib.pyplot import figure, savefig, tight_layout, clf, show try: @@ -1927,7 +1974,8 @@ def plot_nearest_neighbors(self, filename=None, idx=0, \ head = self.targetdir + self.datadirs['plots'] outfile = os.path.join(head, filename) savefig(outfile, dpi=300, bbox_inches='tight') - show() + + return ax def cluster(self, distArray, method='ward', count_sort=False, \ @@ -1955,22 +2003,28 @@ def cluster(self, distArray, method='ward', count_sort=False, \ Returns ------- - list + Z + output from :func:`scipy.cluster.hierarchy.linkage`; list of indices representing the row-wise order of the objects after clustering + dgram + output from :func:`scipy.cluster.hierarchy.dendrogram` """ - import matplotlib - from scipy.cluster.hierarchy import linkage, dendrogram - + # perhaps there is a better way to manipulate the plot... or perhaps it + # is not even necessary? In any case, the try/finally makes sure that + # we are not permanently changing the user's global state + orig_linewidth = matplotlib.rcParams['lines.linewidth'] matplotlib.rcParams['lines.linewidth'] = 0.5 - - Z = linkage(distArray, method=method) - dgram = dendrogram(Z, no_labels=no_labels, orientation='left', \ - count_sort=count_sort, distance_sort=distance_sort, \ - no_plot=no_plot, color_threshold=color_threshold) + try: + Z = cluster.hierarchy.linkage(distArray, method=method) + dgram = cluster.hierarchy.dendrogram( + Z, no_labels=no_labels, orientation='left', + count_sort=count_sort, distance_sort=distance_sort, + no_plot=no_plot, color_threshold=color_threshold) + finally: + matplotlib.rcParams['lines.linewidth'] = orig_linewidth return Z, dgram - def _get_plot_obj_locs(self): """Find and return coordinates for dendrogram, heat map, and colorbar. @@ -2005,7 +2059,8 @@ def get_num_atoms(self): Returns ------- - the number of atoms + int + the number of atoms Note ---- @@ -2077,8 +2132,7 @@ def get_pairwise_distances(self, vectorform=False): err_str = "No distance data; do 'PSAnalysis.run(store=True)' first." raise ValueError(err_str) if vectorform: - from scipy.spatial.distance import squareform - return squareform(self.D) + return spatial.distance.squareform(self.D) else: return self.D diff --git a/package/setup.py b/package/setup.py index 5aecb4889c5..04b7fb23e1e 100755 --- a/package/setup.py +++ b/package/setup.py @@ -494,11 +494,12 @@ def dynamic_author_list(): classifiers=CLASSIFIERS, cmdclass=cmdclass, requires=['numpy (>=1.10.4)', 'biopython', 'mmtf (>=1.0.0)', - 'networkx (>=1.0)', 'GridDataFormats (>=0.3.2)', 'joblib'], + 'networkx (>=1.0)', 'GridDataFormats (>=0.3.2)', 'joblib', + 'scipy', 'matplotlib (>=1.5.1)'], # all standard requirements are available through PyPi and # typically can be installed without difficulties through setuptools setup_requires=[ - 'numpy>=1.9.3', + 'numpy>=1.10.4', ], install_requires=[ 'numpy>=1.10.4', @@ -508,6 +509,8 @@ def dynamic_author_list(): 'six>=1.4.0', 'mmtf-python>=1.0.0', 'joblib', + 'scipy', + 'matplotlib>=1.5.1', ], # extras can be difficult to install through setuptools and/or # you might prefer to use the version available through your @@ -516,8 +519,6 @@ def dynamic_author_list(): 'AMBER': ['netCDF4>=1.0'], # for AMBER netcdf, also needs HDF5 # and netcdf-4 'analysis': [ - 'matplotlib>=1.5.1', - 'scipy', 'seaborn', # for annotated heat map and nearest neighbor # plotting in PSA 'sklearn', # For clustering and dimensionality reduction diff --git a/testsuite/MDAnalysisTests/analysis/test_density.py b/testsuite/MDAnalysisTests/analysis/test_density.py index 75502b71380..adc51445244 100644 --- a/testsuite/MDAnalysisTests/analysis/test_density.py +++ b/testsuite/MDAnalysisTests/analysis/test_density.py @@ -30,9 +30,6 @@ assert_raises) import MDAnalysis as mda -# imported inside a skipif-protected method so that it can -# be tested in the absence of scipy -## import MDAnalysis.analysis.density from MDAnalysisTests.datafiles import TPR, XTC, GRO from MDAnalysisTests import module_not_found, tempdir @@ -45,8 +42,6 @@ class TestDensity(TestCase): counts = 100 Lmax = 10. - @dec.skipif(module_not_found('scipy'), - "Test skipped because scipy is not available.") def setUp(self): import MDAnalysis.analysis.density @@ -123,8 +118,6 @@ class Test_density_from_Universe(TestCase): cutoffs = {'notwithin': 4.0, } precision = 5 - @dec.skipif(module_not_found('scipy'), - "Test skipped because scipy is not available.") def setUp(self): self.outfile = 'density.dx' self.universe = mda.Universe(self.topology, self.trajectory) diff --git a/testsuite/MDAnalysisTests/analysis/test_distances.py b/testsuite/MDAnalysisTests/analysis/test_distances.py index a217f6142d8..91562f00f6f 100644 --- a/testsuite/MDAnalysisTests/analysis/test_distances.py +++ b/testsuite/MDAnalysisTests/analysis/test_distances.py @@ -21,22 +21,25 @@ # from __future__ import print_function, absolute_import +import scipy +import scipy.spatial + import MDAnalysis from MDAnalysisTests import module_not_found from MDAnalysisTests.datafiles import GRO from MDAnalysisTests.util import block_import +import MDAnalysis.analysis.distances + from numpy.testing import TestCase, assert_equal, dec import numpy as np + import warnings -from mock import Mock, patch import sys class TestContactMatrix(TestCase): - @dec.skipif(module_not_found('scipy'), - "Test skipped because scipy is not available.") def setUp(self): import MDAnalysis.analysis.distances self.coord = np.array([[1, 1, 1], @@ -87,17 +90,7 @@ def test_box_sparse(self): assert_equal(contacts.toarray(), self.res_pbc) class TestDist(TestCase): - '''Tests for MDAnalysis.analysis.distances.dist(). - Imports do not happen at the top level of the module - because of the scipy dependency.''' - - @dec.skipif(module_not_found('scipy'), - "Test skipped because scipy is not available.") - def setUp(self): - import MDAnalysis.analysis.distances - import scipy - import scipy.spatial self.u = MDAnalysis.Universe(GRO) self.ag = self.u.atoms[:20] self.u2 = MDAnalysis.Universe(GRO) @@ -142,17 +135,7 @@ def test_mismatch_exception(self): MDAnalysis.analysis.distances.dist(self.ag[:19], self.ag2) class TestBetween(TestCase): - '''Tests for MDAnalysis.analysis.distances.between(). - Imports do not happen at the top level of the module - because of the scipy dependency.''' - - @dec.skipif(module_not_found('scipy'), - "Test skipped because scipy is not available.") - def setUp(self): - import MDAnalysis.analysis.distances - import scipy - import scipy.spatial self.u = MDAnalysis.Universe(GRO) self.ag = self.u.atoms[:10] self.ag2 = self.u.atoms[12:33] @@ -190,41 +173,3 @@ def test_between_simple_case_indices_only(self): self.ag2, self.distance).indices) assert_equal(actual, self.expected) - -class TestImportWarnings(TestCase): - # see unit testing for warnings: - # http://stackoverflow.com/a/3892301 - - def setUp(self): - sys.modules.pop('MDAnalysis.analysis.distances', None) - - @block_import('scipy') - def test_warning_raised_no_scipy_module_level(self): - # an appropriate warning rather than an exception should be - # raised if scipy is absent when importing - # MDAnalysis.analysis.distances - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always") - import MDAnalysis.analysis.distances - assert issubclass(w[-1].category, ImportWarning) - - def test_silent_success_scipy_present_module_level(self): - # if scipy is present no module level ImportWarning should be - # raised when importing MDAnalysis.analysis.distances - mock = Mock() # mock presence of scipy - with patch.dict('sys.modules', {'scipy':mock}): - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always") - import MDAnalysis.analysis.distances - assert w == [] - - @block_import('scipy') - def test_import_error_contact_matrix_no_scipy(self): - # contact_matrix should raise an ImportError if returntype is - # "sparse" and scipy is not available - with self.assertRaises(ImportError): - np.random.seed(321) - points = np.random.random_sample((10, 3)) - import MDAnalysis.analysis.distances - MDAnalysis.analysis.distances.contact_matrix(points, - returntype="sparse") diff --git a/testsuite/MDAnalysisTests/analysis/test_encore.py b/testsuite/MDAnalysisTests/analysis/test_encore.py index 884545cf94d..3348d2e289c 100644 --- a/testsuite/MDAnalysisTests/analysis/test_encore.py +++ b/testsuite/MDAnalysisTests/analysis/test_encore.py @@ -118,18 +118,18 @@ def test_triangular_matrix(): incremented_triangular_matrix = triangular_matrix + scalar assert_equal(incremented_triangular_matrix[0,1], expected_value + scalar, - err_msg="Error in TriangularMatrix: addition of scalar gave\ -inconsistent results") + err_msg="Error in TriangularMatrix: addition of scalar gave" + "inconsistent results") triangular_matrix += scalar assert_equal(triangular_matrix[0,1], expected_value + scalar, - err_msg="Error in TriangularMatrix: addition of scalar gave\ -inconsistent results") + err_msg="Error in TriangularMatrix: addition of scalar gave" + "inconsistent results") multiplied_triangular_matrix_2 = triangular_matrix_2 * scalar assert_equal(multiplied_triangular_matrix_2[0,1], expected_value * scalar, - err_msg="Error in TriangularMatrix: multiplication by scalar gave\ -inconsistent results") + err_msg="Error in TriangularMatrix: multiplication by scalar gave" + "inconsistent results") triangular_matrix_2 *= scalar assert_equal(triangular_matrix_2[0,1], expected_value * scalar, @@ -299,8 +299,6 @@ def test_ces(self): assert_almost_equal(result_value, expected_value, decimal=2, err_msg="Unexpected value for Cluster Ensemble Similarity: {0:f}. Expected {1:f}.".format(result_value, expected_value)) - @dec.skipif(module_not_found('scipy'), - "Test skipped because scipy is not available.") def test_dres_to_self(self): results, details = encore.dres([self.ens1, self.ens1]) result_value = results[0,1] @@ -308,8 +306,6 @@ def test_dres_to_self(self): assert_almost_equal(result_value, expected_value, decimal=2, err_msg="Dim. Reduction Ensemble Similarity to itself not zero: {0:f}".format(result_value)) - @dec.skipif(module_not_found('scipy'), - "Test skipped because scipy is not available.") def test_dres(self): results, details = encore.dres([self.ens1, self.ens2], selection="name CA and resnum 1-10") result_value = results[0,1] @@ -317,8 +313,6 @@ def test_dres(self): self.assertLess(result_value, upper_bound, msg="Unexpected value for Dim. reduction Ensemble Similarity: {0:f}. Expected {1:f}.".format(result_value, upper_bound)) - @dec.skipif(module_not_found('scipy'), - "Test skipped because scipy is not available.") def test_dres_without_superimposition(self): distance_matrix = encore.get_distance_matrix( encore.merge_universes([self.ens1, self.ens2]), @@ -338,8 +332,6 @@ def test_ces_convergence(self): assert_almost_equal(ev, results[i], decimal=2, err_msg="Unexpected value for Clustering Ensemble similarity in convergence estimation") - @dec.skipif(module_not_found('scipy'), - "Test skipped because scipy is not available.") def test_dres_convergence(self): expected_values = [ 0.3, 0.] results = encore.dres_convergence(self.ens1, 10) @@ -399,8 +391,6 @@ def test_ces_error_estimation_ensemble_bootstrap(self): err_msg="Unexpected standard daviation for bootstrapped samples in Clustering Ensemble similarity") @dec.slow - @dec.skipif(module_not_found('scipy'), - "Test skipped because scipy is not available.") def test_dres_error_estimation(self): average_upper_bound = 0.3 stdev_upper_bound = 0.2 @@ -847,18 +837,9 @@ def _check_sklearn_import_warns(self, package): warnings.simplefilter('always') assert_warns(ImportWarning, importlib.import_module, package) - @block_import('scipy') - def _check_scipy_import_warns(self, package): - warnings.simplefilter('always') - assert_warns(ImportWarning, importlib.import_module, package) - def test_import_warnings(self): for pkg in ( 'MDAnalysis.analysis.encore.dimensionality_reduction.DimensionalityReductionMethod', 'MDAnalysis.analysis.encore.clustering.ClusteringMethod', ): yield self._check_sklearn_import_warns, pkg - for pkg in ( - 'MDAnalysis.analysis.encore.similarity', - ): - yield self._check_scipy_import_warns, pkg diff --git a/testsuite/MDAnalysisTests/analysis/test_hole.py b/testsuite/MDAnalysisTests/analysis/test_hole.py index e001b265d4f..22d891daca3 100644 --- a/testsuite/MDAnalysisTests/analysis/test_hole.py +++ b/testsuite/MDAnalysisTests/analysis/test_hole.py @@ -32,6 +32,9 @@ assert_array_equal, assert_array_almost_equal, assert_) import numpy as np +import matplotlib +import mpl_toolkits.mplot3d + import nose from nose.plugins.attrib import attr @@ -150,27 +153,21 @@ def test_min_radius(self): @attr('slow') @dec.skipif(executable_not_found("hole"), msg="Test skipped because HOLE not found") - @dec.skipif(module_not_found("matplotlib")) def test_plot(self): - import matplotlib.axes ax = self.H.plot(label=True) assert_(isinstance(ax, matplotlib.axes.Axes), msg="H.plot() did not produce an Axes instance") @attr('slow') @dec.skipif(executable_not_found("hole"), msg="Test skipped because HOLE not found") - @dec.skipif(module_not_found("matplotlib")) def test_plot3D(self): - import mpl_toolkits.mplot3d ax = self.H.plot3D() assert_(isinstance(ax, mpl_toolkits.mplot3d.Axes3D), msg="H.plot3D() did not produce an Axes3D instance") @attr('slow') @dec.skipif(executable_not_found("hole"), msg="Test skipped because HOLE not found") - @dec.skipif(module_not_found("matplotlib")) def test_plot3D_rmax(self): - import mpl_toolkits.mplot3d ax = self.H.plot3D(rmax=2.5) assert_(isinstance(ax, mpl_toolkits.mplot3d.Axes3D), msg="H.plot3D(rmax=float) did not produce an Axes3D instance") diff --git a/testsuite/MDAnalysisTests/analysis/test_hydrogenbondautocorrel.py b/testsuite/MDAnalysisTests/analysis/test_hydrogenbondautocorrel.py index 59ca9509e6a..49d3b80c040 100644 --- a/testsuite/MDAnalysisTests/analysis/test_hydrogenbondautocorrel.py +++ b/testsuite/MDAnalysisTests/analysis/test_hydrogenbondautocorrel.py @@ -142,7 +142,6 @@ def test_intermittent_excl(self): # For `solve` the test trajectories aren't long enough # So spoof the results and check that solver finds solution - @dec.skipif(module_not_found('scipy')) def test_solve_continuous(self): hbond = HBAC(self.u, hydrogens=self.H, @@ -168,7 +167,6 @@ def actual_function_cont(t): np.array([0.75, 0.5, 0.1]), ) - @dec.skipif(module_not_found('scipy')) def test_solve_intermittent(self): hbond = HBAC(self.u, hydrogens=self.H, @@ -248,7 +246,6 @@ def test_bond_type_VE(self): sample_time=0.06, ) - @dec.skipif(module_not_found('scipy')) def test_solve_before_run_VE(self): hbond = HBAC(self.u, hydrogens=self.H, diff --git a/testsuite/MDAnalysisTests/analysis/test_leaflet.py b/testsuite/MDAnalysisTests/analysis/test_leaflet.py index e83ec0fec69..3f358034f6e 100644 --- a/testsuite/MDAnalysisTests/analysis/test_leaflet.py +++ b/testsuite/MDAnalysisTests/analysis/test_leaflet.py @@ -29,8 +29,6 @@ from MDAnalysisTests.datafiles import Martini_membrane_gro class TestLeafletFinder(TestCase): - @dec.skipif(module_not_found('scipy'), - "Test skipped because scipy is not available.") def setUp(self): self.universe = MDAnalysis.Universe(Martini_membrane_gro, Martini_membrane_gro) self.lipid_heads = self.universe.select_atoms("name PO4") diff --git a/testsuite/MDAnalysisTests/analysis/test_pca.py b/testsuite/MDAnalysisTests/analysis/test_pca.py index e182d3d99a4..2d410365a35 100644 --- a/testsuite/MDAnalysisTests/analysis/test_pca.py +++ b/testsuite/MDAnalysisTests/analysis/test_pca.py @@ -87,8 +87,6 @@ def test_transform_universe(): pca_test.transform(u2) @staticmethod - @dec.skipif(module_not_found('scipy'), - "Test skipped because scipy is not available.") def test_cosine_content(): rand = MDAnalysis.Universe(RANDOM_WALK_TOPO, RANDOM_WALK) pca_random = pca.PCA(rand).run() diff --git a/testsuite/MDAnalysisTests/analysis/test_persistencelength.py b/testsuite/MDAnalysisTests/analysis/test_persistencelength.py index fc023ba95fc..4c72a8dee9c 100644 --- a/testsuite/MDAnalysisTests/analysis/test_persistencelength.py +++ b/testsuite/MDAnalysisTests/analysis/test_persistencelength.py @@ -24,7 +24,10 @@ import MDAnalysis from MDAnalysis.analysis import polymer from MDAnalysis.exceptions import NoDataError + import numpy as np +import matplotlib + from numpy.testing import ( assert_, assert_almost_equal, @@ -61,8 +64,6 @@ def test_run(self): assert_(len(p.results) == 280) assert_almost_equal(p.lb, 1.485, 3) - @dec.skipif(module_not_found('scipy'), - "Test skipped because scipy is not available.") def test_fit(self): p = self._make_p() p.run() @@ -71,14 +72,9 @@ def test_fit(self): assert_almost_equal(p.lp, 6.504, 3) assert_(len(p.fit) == len(p.results)) - @dec.skipif(module_not_found('matplotlib'), - "Test skipped because matplotlib is not available.") - @dec.skipif(module_not_found('scipy'), - "Test skipped because scipy is not available.") def test_plot_ax_return(self): '''Ensure that a matplotlib axis object is returned when plot() is called.''' - import matplotlib p = self._make_p() p.run() p.perform_fit() @@ -104,14 +100,10 @@ def tearDown(self): del self.a_ref del self.y - @dec.skipif(module_not_found('scipy'), - "Test skipped because scipy is not available.") def test_fit_simple(self): a = polymer.fit_exponential_decay(self.x, self.y) assert_(a == self.a_ref) - @dec.skipif(module_not_found('scipy'), - "Test skipped because scipy is not available.") def test_fit_noisy(self): noise = np.sin(self.x) * 0.01 y2 = noise + self.y diff --git a/testsuite/MDAnalysisTests/analysis/test_psa.py b/testsuite/MDAnalysisTests/analysis/test_psa.py index f4c3b234873..e62621aa2db 100644 --- a/testsuite/MDAnalysisTests/analysis/test_psa.py +++ b/testsuite/MDAnalysisTests/analysis/test_psa.py @@ -28,6 +28,8 @@ assert_array_almost_equal, assert_, assert_almost_equal, assert_equal) import numpy as np +import scipy +import scipy.spatial from MDAnalysisTests.datafiles import PSF, DCD, DCD2 from MDAnalysisTests import parser_not_found, tempdir, module_not_found @@ -36,10 +38,6 @@ class TestPSAnalysis(TestCase): @dec.skipif(parser_not_found('DCD'), 'DCD parser not available. Are you using python 3?') - @dec.skipif(module_not_found('matplotlib'), - "Test skipped because matplotlib is not available.") - @dec.skipif(module_not_found('scipy'), - "Test skipped because scipy is not available.") def setUp(self): self.tmpdir = tempdir.TempDir() self.iu1 = np.triu_indices(3, k=1) @@ -187,9 +185,6 @@ class _BaseHausdorffDistance(TestCase): for various Hausdorff distance calculation properties.''' - @dec.skipif(module_not_found('scipy'), - 'scipy not available') - def setUp(self): self.random_angles = np.random.random((100,)) * np.pi * 2 self.random_columns = np.column_stack((self.random_angles, @@ -247,10 +242,9 @@ def setUp(self): class TestWeightedAvgHausdorffSymmetric(_BaseHausdorffDistance): '''Tests for weighted average and symmetric (undirected) Hausdorff distance between point sets in 3D.''' + def setUp(self): super(TestWeightedAvgHausdorffSymmetric, self).setUp() - import scipy - import scipy.spatial self.h = PSA.hausdorff_wavg self.distance_matrix = scipy.spatial.distance.cdist(self.path_1, self.path_2) @@ -270,10 +264,9 @@ def test_asymmetric_weight(self): class TestAvgHausdorffSymmetric(_BaseHausdorffDistance): '''Tests for unweighted average and symmetric (undirected) Hausdorff distance between point sets in 3D.''' + def setUp(self): super(TestAvgHausdorffSymmetric, self).setUp() - import scipy - import scipy.spatial self.h = PSA.hausdorff_avg self.distance_matrix = scipy.spatial.distance.cdist(self.path_1, self.path_2) diff --git a/testsuite/MDAnalysisTests/utils/test_distances.py b/testsuite/MDAnalysisTests/utils/test_distances.py index 0acef18c720..83708446c46 100644 --- a/testsuite/MDAnalysisTests/utils/test_distances.py +++ b/testsuite/MDAnalysisTests/utils/test_distances.py @@ -24,6 +24,7 @@ import MDAnalysis.lib.distances import numpy as np +import pytest from numpy.testing import (TestCase, dec, raises, assert_, assert_almost_equal, assert_equal, assert_raises,) @@ -33,43 +34,49 @@ from MDAnalysis.lib import mdamath from MDAnalysisTests import parser_not_found -class _TestDistanceArray(TestCase): - # override backend in test classes - - __test__ = False - - backend = None - - def setUp(self): - self.box = np.array([1., 1., 2.], dtype=np.float32) - self.points = np.array( - [ - [0, 0, 0], [1, 1, 2], [1, 0, 2], # identical under PBC - [0.5, 0.5, 1.5], - ], dtype=np.float32) - self.ref = self.points[0:1] - self.conf = self.points[1:] - - def _dist(self, n, ref=None): - if ref is None: - ref = self.ref[0] - else: - ref = np.asarray(ref, dtype=np.float32) - x = self.points[n] + +@pytest.fixture() +def ref_system(): + box = np.array([1., 1., 2.], dtype=np.float32) + points = np.array( + [ + [0, 0, 0], [1, 1, 2], [1, 0, 2], # identical under PBC + [0.5, 0.5, 1.5], + ], dtype=np.float32) + ref = points[0:1] + conf = points[1:] + + return box, points, ref, conf + + +@pytest.mark.parametrize('backend', ['serial', 'openmp']) +class TestDistanceArray(object): + @staticmethod + def _dist(x, ref): + ref = np.asarray(ref, dtype=np.float32) r = x - ref return np.sqrt(np.dot(r, r)) - def test_noPBC(self): - d = MDAnalysis.lib.distances.distance_array(self.ref, self.points, - backend=self.backend) - assert_almost_equal(d, np.array([[self._dist(0), self._dist(1), self._dist(2), self._dist(3)]])) + def test_noPBC(self, backend, ref_system): + box, points, ref, conf = ref_system + + d = MDAnalysis.lib.distances.distance_array(ref, points, backend=backend) + + assert_almost_equal(d, np.array([[ + self._dist(points[0], ref[0]), + self._dist(points[1], ref[0]), + self._dist(points[2], ref[0]), + self._dist(points[3], ref[0])] + ])) + + def test_PBC(self, backend, ref_system): + box, points, ref, conf = ref_system + + d = MDAnalysis.lib.distances.distance_array(ref, points, box=box, backend=backend) - def test_PBC(self): - d = MDAnalysis.lib.distances.distance_array(self.ref, self.points, - box=self.box, backend=self.backend) - assert_almost_equal(d, np.array([[0., 0., 0., self._dist(3, ref=[1, 1, 2])]])) + assert_almost_equal(d, np.array([[0., 0., 0., self._dist(points[3], ref=[1, 1, 2])]])) - def test_PBC2(self): + def test_PBC2(self, backend): a = np.array([7.90146923, -13.72858524, 3.75326586], dtype=np.float32) b = np.array([-1.36250901, 13.45423985, -0.36317623], dtype=np.float32) box = np.array([5.5457325, 5.5457325, 5.5457325], dtype=np.float32) @@ -80,85 +87,68 @@ def mindist(a, b, box): ref = mindist(a, b, box) val = MDAnalysis.lib.distances.distance_array(np.array([a]), np.array([b]), - box=box, backend=self.backend)[0, 0] + box=box, backend=backend)[0, 0] assert_almost_equal(val, ref, decimal=6, err_msg="Issue 151 not correct (PBC in distance array)") - -class TestDistanceArray_Serial(_TestDistanceArray): - __test__ = True - - backend = "serial" - - -class TestDistanceArray_OpenMP(_TestDistanceArray): - __test__ = True - - backend = "OpenMP" - - -class _TestDistanceArrayDCD(TestCase): - __test__ = False - - backend = None - - @dec.skipif(parser_not_found('DCD'), - 'DCD parser not available. Are you using python 3?') - def setUp(self): - self.universe = MDAnalysis.Universe(PSF, DCD) - self.trajectory = self.universe.trajectory - self.ca = self.universe.select_atoms('name CA') - # reasonable precision so that tests succeed on 32 and 64 bit machines - # (the reference values were obtained on 64 bit) - # Example: - # Items are not equal: wrong maximum distance value - # ACTUAL: 52.470254967456412 - # DESIRED: 52.470257062419059 - self.prec = 5 - - def tearDown(self): - del self.universe - del self.trajectory - del self.ca - - def test_simple(self): - U = self.universe - self.trajectory.rewind() +@pytest.fixture() +def DCD_Universe(): + universe = MDAnalysis.Universe(PSF, DCD) + trajectory = universe.trajectory + + return universe, trajectory + +@pytest.mark.skipif(parser_not_found('DCD'), + reason='DCD parser not available. Are you using python 3?') +@pytest.mark.parametrize('backend', ['serial', 'openmp']) +class TestDistanceArrayDCD(object): + # reasonable precision so that tests succeed on 32 and 64 bit machines + # (the reference values were obtained on 64 bit) + # Example: + # Items are not equal: wrong maximum distance value + # ACTUAL: 52.470254967456412 + # DESIRED: 52.470257062419059 + prec = 5 + + @attr('issue') + def test_simple(self, DCD_Universe, backend): + U, trajectory = DCD_Universe + trajectory.rewind() x0 = U.atoms.positions - self.trajectory[10] + trajectory[10] x1 = U.atoms.positions - d = MDAnalysis.lib.distances.distance_array(x0, x1, backend=self.backend) + d = MDAnalysis.lib.distances.distance_array(x0, x1, backend=backend) assert_equal(d.shape, (3341, 3341), "wrong shape (should be (Natoms,Natoms))") assert_almost_equal(d.min(), 0.11981228170520701, self.prec, err_msg="wrong minimum distance value") assert_almost_equal(d.max(), 53.572192429459619, self.prec, err_msg="wrong maximum distance value") - def test_outarray(self): - U = self.universe - self.trajectory.rewind() + def test_outarray(self, DCD_Universe, backend): + U, trajectory = DCD_Universe + trajectory.rewind() x0 = U.atoms.positions - self.trajectory[10] + trajectory[10] x1 = U.atoms.positions natoms = len(U.atoms) d = np.zeros((natoms, natoms), np.float64) - MDAnalysis.lib.distances.distance_array(x0, x1, result=d, backend=self.backend) + MDAnalysis.lib.distances.distance_array(x0, x1, result=d, backend=backend) assert_equal(d.shape, (natoms, natoms), "wrong shape, shoud be (Natoms,Natoms) entries") assert_almost_equal(d.min(), 0.11981228170520701, self.prec, err_msg="wrong minimum distance value") assert_almost_equal(d.max(), 53.572192429459619, self.prec, err_msg="wrong maximum distance value") - def test_periodic(self): + def test_periodic(self, DCD_Universe, backend): # boring with the current dcd as that has no PBC - U = self.universe - self.trajectory.rewind() + U, trajectory = DCD_Universe + trajectory.rewind() x0 = U.atoms.positions - self.trajectory[10] + trajectory[10] x1 = U.atoms.positions d = MDAnalysis.lib.distances.distance_array(x0, x1, box=U.coord.dimensions, - backend=self.backend) + backend=backend) assert_equal(d.shape, (3341, 3341), "should be square matrix with Natoms entries") assert_almost_equal(d.min(), 0.11981228170520701, self.prec, err_msg="wrong minimum distance value with PBC") @@ -166,42 +156,18 @@ def test_periodic(self): err_msg="wrong maximum distance value with PBC") -class TestDistanceArrayDCD_Serial(_TestDistanceArrayDCD): - __test__ = True - - backend = "serial" +@pytest.mark.skipif(parser_not_found('DCD'), + reason='DCD parser not available. Are you using python 3?') +@pytest.mark.parametrize('backend', ['serial', 'openmp']) +class TestSelfDistanceArrayDCD(object): + prec = 5 -class TestDistanceArrayDCD_OpenMP(_TestDistanceArrayDCD): - __test__ = True - - backend = "OpenMP" - - -class _TestSelfDistanceArrayDCD(TestCase): - __test__ = False - - backend = None - - @dec.skipif(parser_not_found('DCD'), - 'DCD parser not available. Are you using python 3?') - def setUp(self): - self.universe = MDAnalysis.Universe(PSF, DCD) - self.trajectory = self.universe.trajectory - self.ca = self.universe.select_atoms('name CA') - # see comments above on precision - self.prec = 5 - - def tearDown(self): - del self.universe - del self.trajectory - del self.ca - - def test_simple(self): - U = self.universe - self.trajectory.rewind() + def test_simple(self, DCD_Universe, backend): + U, trajectory = DCD_Universe + trajectory.rewind() x0 = U.atoms.positions - d = MDAnalysis.lib.distances.self_distance_array(x0, backend=self.backend) + d = MDAnalysis.lib.distances.self_distance_array(x0, backend=backend) N = 3341 * (3341 - 1) / 2 assert_equal(d.shape, (N,), "wrong shape (should be (Natoms*(Natoms-1)/2,))") assert_almost_equal(d.min(), 0.92905562402529318, self.prec, @@ -209,29 +175,29 @@ def test_simple(self): assert_almost_equal(d.max(), 52.4702570624190590, self.prec, err_msg="wrong maximum distance value") - def test_outarray(self): - U = self.universe - self.trajectory.rewind() + def test_outarray(self, DCD_Universe, backend): + U, trajectory = DCD_Universe + trajectory.rewind() x0 = U.atoms.positions natoms = len(U.atoms) N = natoms * (natoms - 1) // 2 d = np.zeros((N,), np.float64) - MDAnalysis.lib.distances.self_distance_array(x0, result=d, backend=self.backend) + MDAnalysis.lib.distances.self_distance_array(x0, result=d, backend=backend) assert_equal(d.shape, (N,), "wrong shape (should be (Natoms*(Natoms-1)/2,))") assert_almost_equal(d.min(), 0.92905562402529318, self.prec, err_msg="wrong minimum distance value") assert_almost_equal(d.max(), 52.4702570624190590, self.prec, err_msg="wrong maximum distance value") - def test_periodic(self): + def test_periodic(self, DCD_Universe, backend): # boring with the current dcd as that has no PBC - U = self.universe - self.trajectory.rewind() + U, trajectory = DCD_Universe + trajectory.rewind() x0 = U.atoms.positions natoms = len(U.atoms) N = natoms * (natoms - 1) / 2 d = MDAnalysis.lib.distances.self_distance_array(x0, box=U.coord.dimensions, - backend=self.backend) + backend=backend) assert_equal(d.shape, (N,), "wrong shape (should be (Natoms*(Natoms-1)/2,))") assert_almost_equal(d.min(), 0.92905562402529318, self.prec, err_msg="wrong minimum distance value with PBC") @@ -239,19 +205,8 @@ def test_periodic(self): err_msg="wrong maximum distance value with PBC") -class TestSelfDistanceArrayDCD_Serial(_TestSelfDistanceArrayDCD): - __test__ = True - - backend = "serial" - - -class TestSelfDistanceArrayDCD_OpenMP(_TestSelfDistanceArrayDCD): - __test__ = True - - backend = "OpenMP" - - -class _TestTriclinicDistances(TestCase): +@pytest.mark.parametrize('backend', ['serial', 'openmp']) +class TestTriclinicDistances(object): """Unit tests for the Triclinic PBC functions. Tests: # transforming to and from S space (fractional coords) @@ -262,69 +217,74 @@ class _TestTriclinicDistances(TestCase): mda.lib.distances.distance_array """ - __test__ = False + prec = 2 - backend = None + @staticmethod + @pytest.fixture() + def TRIC(): + return MDAnalysis.Universe(TRIC) - def setUp(self): - self.universe = MDAnalysis.Universe(TRIC) - self.prec = 2 + @staticmethod + @pytest.fixture() + def box(TRIC): + return MDAnalysis.coordinates.core.triclinic_vectors(TRIC.dimensions) - self.box = MDAnalysis.coordinates.core.triclinic_vectors(self.universe.dimensions) - self.boxV = MDAnalysis.coordinates.core.triclinic_box(self.box[0], self.box[1], self.box[2]) + @staticmethod + @pytest.fixture() + def boxV(box): + return MDAnalysis.coordinates.core.triclinic_box(box[0], box[1], box[2]) - self.S_mol1 = np.array([self.universe.atoms[383].position]) - self.S_mol2 = np.array([self.universe.atoms[390].position]) + @staticmethod + @pytest.fixture() + def S_mol(TRIC): + S_mol1 = np.array([TRIC.atoms[383].position]) + S_mol2 = np.array([TRIC.atoms[390].position]) - def tearDown(self): - del self.universe - del self.boxV - del self.box - del self.S_mol1 - del self.S_mol2 - del self.prec + return S_mol1, S_mol2 - def test_transforms(self): + def test_transforms(self, S_mol, box, boxV, backend): from MDAnalysis.lib.distances import transform_StoR, transform_RtoS # To check the cython coordinate transform, the same operation is done in numpy # Is a matrix multiplication of Coords x Box = NewCoords, so can use np.dot - + S_mol1, S_mol2 = S_mol # Test transformation - R_mol1 = transform_StoR(self.S_mol1, self.box, backend=self.backend) - R_np1 = np.dot(self.S_mol1, self.box) + R_mol1 = transform_StoR(S_mol1, box, backend=backend) + R_np1 = np.dot(S_mol1, box) # Test transformation when given box in different form - R_mol2 = transform_StoR(self.S_mol2, self.boxV, backend=self.backend) - R_np2 = np.dot(self.S_mol2, self.box) + R_mol2 = transform_StoR(S_mol2, boxV, backend=backend) + R_np2 = np.dot(S_mol2, box) assert_almost_equal(R_mol1, R_np1, self.prec, err_msg="StoR transform failed with box") assert_almost_equal(R_mol2, R_np2, self.prec, err_msg="StoR transform failed with boxV") # Round trip test # boxV here althought initial transform with box - S_test1 = transform_RtoS(R_mol1, self.boxV, backend=self.backend) + S_test1 = transform_RtoS(R_mol1, boxV, backend=backend) # and vice versa, should still work - S_test2 = transform_RtoS(R_mol2, self.box, backend=self.backend) + S_test2 = transform_RtoS(R_mol2, box, backend=backend) - assert_almost_equal(S_test1, self.S_mol1, self.prec, err_msg="Round trip failed in transform") - assert_almost_equal(S_test2, self.S_mol2, self.prec, err_msg="Round trip failed in transform") + assert_almost_equal(S_test1, S_mol1, self.prec, err_msg="Round trip failed in transform") + assert_almost_equal(S_test2, S_mol2, self.prec, err_msg="Round trip failed in transform") - def test_selfdist(self): + def test_selfdist(self, S_mol, box, boxV, backend): from MDAnalysis.lib.distances import self_distance_array from MDAnalysis.lib.distances import transform_StoR - R_coords = transform_StoR(self.S_mol1, self.box, backend=self.backend) + S_mol1, S_mol2 = S_mol + + R_coords = transform_StoR(S_mol1, box, backend=backend) # Transform functions are tested elsewhere so taken as working here - dists = self_distance_array(R_coords, box=self.box, backend=self.backend) + dists = self_distance_array(R_coords, box=box, backend=backend) # Manually calculate self_distance_array manual = np.zeros(len(dists), dtype=np.float64) distpos = 0 for i, Ri in enumerate(R_coords): for Rj in R_coords[i + 1:]: Rij = Rj - Ri - Rij -= round(Rij[2] / self.box[2][2]) * self.box[2] - Rij -= round(Rij[1] / self.box[1][1]) * self.box[1] - Rij -= round(Rij[0] / self.box[0][0]) * self.box[0] + Rij -= round(Rij[2] / box[2][2]) * box[2] + Rij -= round(Rij[1] / box[1][1]) * box[1] + Rij -= round(Rij[0] / box[0][0]) * box[0] Rij = np.linalg.norm(Rij) # find norm of Rij vector manual[distpos] = Rij # and done, phew distpos += 1 @@ -334,18 +294,18 @@ def test_selfdist(self): # Do it again for input 2 (has wider separation in points) # Also use boxV here in self_dist calculation - R_coords = transform_StoR(self.S_mol2, self.box, backend=self.backend) + R_coords = transform_StoR(S_mol2, box, backend=backend) # Transform functions are tested elsewhere so taken as working here - dists = self_distance_array(R_coords, box=self.boxV, backend=self.backend) + dists = self_distance_array(R_coords, box=boxV, backend=backend) # Manually calculate self_distance_array manual = np.zeros(len(dists), dtype=np.float64) distpos = 0 for i, Ri in enumerate(R_coords): for Rj in R_coords[i + 1:]: Rij = Rj - Ri - Rij -= round(Rij[2] / self.box[2][2]) * self.box[2] - Rij -= round(Rij[1] / self.box[1][1]) * self.box[1] - Rij -= round(Rij[0] / self.box[0][0]) * self.box[0] + Rij -= round(Rij[2] / box[2][2]) * box[2] + Rij -= round(Rij[1] / box[1][1]) * box[1] + Rij -= round(Rij[0] / box[0][0]) * box[0] Rij = np.linalg.norm(Rij) # find norm of Rij vector manual[distpos] = Rij # and done, phew distpos += 1 @@ -353,23 +313,25 @@ def test_selfdist(self): assert_almost_equal(dists, manual, self.prec, err_msg="self_distance_array failed with input 2") - def test_distarray(self): + def test_distarray(self, S_mol, box, boxV, backend): from MDAnalysis.lib.distances import distance_array from MDAnalysis.lib.distances import transform_StoR - R_mol1 = transform_StoR(self.S_mol1, self.box, backend=self.backend) - R_mol2 = transform_StoR(self.S_mol2, self.box, backend=self.backend) + S_mol1, S_mol2 = S_mol + + R_mol1 = transform_StoR(S_mol1, box, backend=backend) + R_mol2 = transform_StoR(S_mol2, box, backend=backend) # Try with box - dists = distance_array(R_mol1, R_mol2, box=self.box, backend=self.backend) + dists = distance_array(R_mol1, R_mol2, box=box, backend=backend) # Manually calculate distance_array manual = np.zeros((len(R_mol1), len(R_mol2))) for i, Ri in enumerate(R_mol1): for j, Rj in enumerate(R_mol2): Rij = Rj - Ri - Rij -= round(Rij[2] / self.box[2][2]) * self.box[2] - Rij -= round(Rij[1] / self.box[1][1]) * self.box[1] - Rij -= round(Rij[0] / self.box[0][0]) * self.box[0] + Rij -= round(Rij[2] / box[2][2]) * box[2] + Rij -= round(Rij[1] / box[1][1]) * box[1] + Rij -= round(Rij[0] / box[0][0]) * box[0] Rij = np.linalg.norm(Rij) # find norm of Rij vector manual[i][j] = Rij @@ -377,75 +339,73 @@ def test_distarray(self): err_msg="distance_array failed with box") # Now check using boxV - dists = distance_array(R_mol1, R_mol2, box=self.boxV, backend=self.backend) + dists = distance_array(R_mol1, R_mol2, box=boxV, backend=backend) assert_almost_equal(dists, manual, self.prec, err_msg="distance_array failed with boxV") - def test_pbc_dist(self): + def test_pbc_dist(self, S_mol, boxV, backend): from MDAnalysis.lib.distances import distance_array + S_mol1, S_mol2 = S_mol results = np.array([[37.629944]]) - - dists = distance_array(self.S_mol1, self.S_mol2, box=self.boxV, - backend=self.backend) + dists = distance_array(S_mol1, S_mol2, box=boxV, + backend=backend) assert_almost_equal(dists, results, self.prec, err_msg="distance_array failed to retrieve PBC distance") -class TestTriclinicDistances_Serial(_TestTriclinicDistances): - __test__ = True - - backend = "serial" - - -class TestTriclinicDistances_OpenMP(_TestTriclinicDistances): - __test__ = True - backend = "OpenMP" - - -class _TestCythonFunctions(TestCase): +@pytest.mark.parametrize('backend', ['serial', 'openmp']) +class TestCythonFunctions(object): # Unit tests for calc_bonds calc_angles and calc_dihedrals in lib.distances # Tests both numerical results as well as input types as Cython will silently # produce nonsensical results if given wrong data types otherwise. + prec = 5 - __test__ = False + @staticmethod + @pytest.fixture() + def box(): + return np.array([10., 10., 10.], dtype=np.float32) - backend = None + @staticmethod + @pytest.fixture() + def triclinic_box(): + return np.array([[10., 0., 0.], [1., 10., 0., ], [1., 0., 10.]], dtype=np.float32) - def setUp(self): - self.prec = 5 - self.box = np.array([10., 10., 10.], dtype=np.float32) - self.box2 = np.array([[10., 0., 0.], [1., 10., 0., ], [1., 0., 10.]], dtype=np.float32) + @staticmethod + @pytest.fixture() + def positions(): # dummy atom data - self.a = np.array([[0., 0., 0.], [0., 0., 0.], [0., 11., 0.], [1., 1., 1.]], dtype=np.float32) - self.b = np.array([[0., 0., 0.], [1., 1., 1.], [0., 0., 0.], [29., -21., 99.]], dtype=np.float32) - self.c = np.array([[0., 0., 0.], [2., 2., 2.], [11., 0., 0.], [1., 9., 9.]], dtype=np.float32) - self.d = np.array([[0., 0., 0.], [3., 3., 3.], [11., -11., 0.], [65., -65., 65.]], dtype=np.float32) - self.wrongtype = np.array([[0., 0., 0.], [3., 3., 3.], [3., 3., 3.], [3., 3., 3.]], - dtype=np.float64) # declared as float64 and should raise TypeError - self.wronglength = np.array([[0., 0., 0.], [3., 3., 3.]], - dtype=np.float32) # has a different length to other inputs and should raise - # ValueError - - def tearDown(self): - del self.box - del self.box2 - del self.a - del self.b - del self.c - del self.d - del self.wrongtype - del self.wronglength - - def test_bonds(self): - dists = MDAnalysis.lib.distances.calc_bonds(self.a, self.b, - backend=self.backend) + + a = np.array([[0., 0., 0.], [0., 0., 0.], [0., 11., 0.], [1., 1., 1.]], dtype=np.float32) + b = np.array([[0., 0., 0.], [1., 1., 1.], [0., 0., 0.], [29., -21., 99.]], dtype=np.float32) + c = np.array([[0., 0., 0.], [2., 2., 2.], [11., 0., 0.], [1., 9., 9.]], dtype=np.float32) + d = np.array([[0., 0., 0.], [3., 3., 3.], [11., -11., 0.], [65., -65., 65.]], dtype=np.float32) + return a, b, c, d + + @staticmethod + @pytest.fixture() + def wrongtype(): + # declared as float64 and should raise TypeError + return np.array([[0., 0., 0.], [3., 3., 3.], [3., 3., 3.], [3., 3., 3.]], + dtype=np.float64) + + @staticmethod + @pytest.fixture() + def wronglength(): + # has a different length to other inputs and should raise ValueError + return np.array([[0., 0., 0.], [3., 3., 3.]], + dtype=np.float32) + + def test_bonds(self, positions, box, backend): + a, b, c, d = positions + dists = MDAnalysis.lib.distances.calc_bonds(a, b, + backend=backend) assert_equal(len(dists), 4, err_msg="calc_bonds results have wrong length") - dists_pbc = MDAnalysis.lib.distances.calc_bonds(self.a, self.b, box=self.box, - backend=self.backend) - # tests 0 length + dists_pbc = MDAnalysis.lib.distances.calc_bonds(a, b, box=box, + backend=backend) + #tests 0 length assert_almost_equal(dists[0], 0.0, self.prec, err_msg="Zero length calc_bonds fail") assert_almost_equal(dists[1], 1.7320508075688772, self.prec, err_msg="Standard length calc_bonds fail") # arbitrary length check @@ -458,41 +418,46 @@ def test_bonds(self): err_msg="PBC check #2 w/o box") # lengths in all directions assert_almost_equal(dists_pbc[3], 3.46410072, self.prec, err_msg="PBC check #w with box") - # Bad input checking - def test_bonds_wrongtype(self): - assert_raises(TypeError, MDAnalysis.lib.distances.calc_bonds, self.a, - self.wrongtype, backend=self.backend) + #Bad input checking + def test_bonds_wrongtype(self, positions, wrongtype, wronglength, backend): + a, b, c, d = positions + assert_raises(TypeError, MDAnalysis.lib.distances.calc_bonds, a, + wrongtype, backend=backend) assert_raises(TypeError, MDAnalysis.lib.distances.calc_bonds, - self.wrongtype, self.b, backend=self.backend) - assert_raises(ValueError, MDAnalysis.lib.distances.calc_bonds, self.a, - self.wronglength, backend=self.backend) + wrongtype, b, backend=backend) + assert_raises(ValueError, MDAnalysis.lib.distances.calc_bonds, a, + wronglength, backend=backend) assert_raises(ValueError, MDAnalysis.lib.distances.calc_bonds, - self.wronglength, self.b, backend=self.backend) + wronglength, b, backend=backend) - def test_bonds_badbox(self): + def test_bonds_badbox(self, positions, backend): + a, b, c, d = positions badboxtype = np.array([10., 10., 10.], dtype=np.float64) badboxsize = np.array([[10., 10.], [10., 10., ]], dtype=np.float32) - assert_raises(ValueError, MDAnalysis.lib.distances.calc_bonds, self.a, - self.b, box=badboxsize, backend=self.backend) # Bad box data - assert_raises(TypeError, MDAnalysis.lib.distances.calc_bonds, self.a, - self.b, box=badboxtype, backend=self.backend) # Bad box type + assert_raises(ValueError, MDAnalysis.lib.distances.calc_bonds, a, + b, box=badboxsize, backend=backend) # Bad box data + assert_raises(TypeError, MDAnalysis.lib.distances.calc_bonds, a, + b, box=badboxtype, backend=backend) # Bad box type - def test_bonds_badresult(self): - badresult = np.zeros(len(self.a) - 1) + def test_bonds_badresult(self, positions, backend): + a, b, c, d = positions + badresult = np.zeros(len(a) - 1) assert_raises(ValueError, MDAnalysis.lib.distances.calc_bonds, - self.a, self.b, result=badresult, backend=self.backend) # Bad result array + a, b, result=badresult, backend=backend) # Bad result array - def test_bonds_triclinic(self): - dists = MDAnalysis.lib.distances.calc_bonds(self.a, self.b, - box=self.box2, backend=self.backend) + def test_bonds_triclinic(self, positions, triclinic_box, backend): + a, b, c, d = positions + dists = MDAnalysis.lib.distances.calc_bonds(a, b, + box=triclinic_box, backend=backend) reference = np.array([0.0, 1.7320508, 1.4142136, 2.82842712]) assert_almost_equal(dists, reference, self.prec, err_msg="calc_bonds with triclinic box failed") - def test_angles(self): - angles = MDAnalysis.lib.distances.calc_angles(self.a, self.b, self.c, - backend=self.backend) + def test_angles(self, positions, backend): + a, b, c, d = positions + angles = MDAnalysis.lib.distances.calc_angles(a, b, c, + backend=backend) # Check calculated values assert_equal(len(angles), 4, err_msg="calc_angles results have wrong length") # assert_almost_equal(angles[0], 0.0, self.prec, @@ -505,32 +470,35 @@ def test_angles(self): err_msg="Small angle failed in calc_angles") # Check data type checks - def test_angles_wrongtype(self): + def test_angles_wrongtype(self, positions, wrongtype, wronglength, backend): + a, b, c, d = positions assert_raises(TypeError, MDAnalysis.lib.distances.calc_angles, - self.a, self.wrongtype, self.c, backend=self.backend) # try inputting float64 values + a, wrongtype, c, backend=backend) # try inputting float64 values assert_raises(TypeError, MDAnalysis.lib.distances.calc_angles, - self.wrongtype, self.b, self.c, backend=self.backend) + wrongtype, b, c, backend=backend) assert_raises(TypeError, MDAnalysis.lib.distances.calc_angles, - self.a, self.b, self.wrongtype, backend=self.backend) + a, b, wrongtype, backend=backend) assert_raises(ValueError, MDAnalysis.lib.distances.calc_angles, - self.a, self.wronglength, self.c, - backend=self.backend) # try inputting arrays of different length + a, wronglength, c, + backend=backend) # try inputting arrays of different length assert_raises(ValueError, MDAnalysis.lib.distances.calc_angles, - self.wronglength, self.b, self.c, - backend=self.backend) + wronglength, b, c, + backend=backend) assert_raises(ValueError, MDAnalysis.lib.distances.calc_angles, - self.a, self.b, self.wronglength, - backend=self.backend) + a, b, wronglength, + backend=backend) - def test_angles_bad_result(self): - badresult = np.zeros(len(self.a) - 1) + def test_angles_bad_result(self, positions, backend): + a, b, c, d = positions + badresult = np.zeros(len(a) - 1) assert_raises(ValueError, MDAnalysis.lib.distances.calc_angles, - self.a, self.b, self.c, result=badresult, backend=self.backend) # Bad result array + a, b, c, result=badresult, backend=backend) # Bad result array - def test_dihedrals(self): - dihedrals = MDAnalysis.lib.distances.calc_dihedrals(self.a, self.b, - self.c, self.d, - backend=self.backend) + def test_dihedrals(self, positions, backend): + a, b, c, d = positions + dihedrals = MDAnalysis.lib.distances.calc_dihedrals(a, b, + c, d, + backend=backend) # Check calculated values assert_equal(len(dihedrals), 4, err_msg="calc_dihedrals results have wrong length") # assert_almost_equal(dihedrals[0], 0.0, self.prec, err_msg="Zero length dihedral failed") @@ -540,58 +508,62 @@ def test_dihedrals(self): err_msg="arbitrary dihedral angle failed") # Check data type checks - def test_dihedrals_wrongtype(self): + def test_dihedrals_wrongtype(self, positions, wrongtype, backend): + a, b, c, d = positions assert_raises(TypeError, MDAnalysis.lib.distances.calc_dihedrals, - self.a, self.wrongtype, self.c, self.d, - backend=self.backend) # try inputting float64 values + a, wrongtype, c, d, + backend=backend) # try inputting float64 values assert_raises(TypeError, MDAnalysis.lib.distances.calc_dihedrals, - self.wrongtype, self.b, self.c, self.d, - backend=self.backend) + wrongtype, b, c, d, + backend=backend) assert_raises(TypeError, MDAnalysis.lib.distances.calc_dihedrals, - self.a, self.b, self.wrongtype, self.d, - backend=self.backend) + a, b, wrongtype, d, + backend=backend) assert_raises(TypeError, MDAnalysis.lib.distances.calc_dihedrals, - self.a, self.b, self.c, self.wrongtype, - backend=self.backend) + a, b, c, wrongtype, + backend=backend) - def test_dihedrals_wronglength(self): + def test_dihedrals_wronglength(self, positions, wronglength, backend): + a, b, c, d = positions assert_raises(ValueError, MDAnalysis.lib.distances.calc_dihedrals, - self.a, self.wronglength, self.c, self.d, - backend=self.backend) + a, wronglength, c, d, + backend=backend) assert_raises(ValueError, MDAnalysis.lib.distances.calc_dihedrals, - self.wronglength, self.b, self.c, self.d, - backend=self.backend) + wronglength, b, c, d, + backend=backend) assert_raises(ValueError, MDAnalysis.lib.distances.calc_dihedrals, - self.a, self.b, self.wronglength, self.d, - backend=self.backend) + a, b, wronglength, d, + backend=backend) assert_raises(ValueError, MDAnalysis.lib.distances.calc_dihedrals, - self.a, self.b, self.c, self.wronglength, - backend=self.backend) + a, b, c, wronglength, + backend=backend) - def test_dihedrals_bad_result(self): - badresult = np.zeros(len(self.a) - 1) + def test_dihedrals_bad_result(self, positions, backend): + a, b, c, d = positions + badresult = np.zeros(len(a) - 1) assert_raises(ValueError, MDAnalysis.lib.distances.calc_dihedrals, - self.a, self.b, self.c, self.d, result=badresult, - backend=self.backend) # Bad result array + a, b, c, d, result=badresult, + backend=backend) # Bad result array - def test_numpy_compliance(self): + def test_numpy_compliance(self, positions, backend): + a, b, c, d = positions # Checks that the cython functions give identical results to the numpy versions - bonds = MDAnalysis.lib.distances.calc_bonds(self.a, self.b, - backend=self.backend) - angles = MDAnalysis.lib.distances.calc_angles(self.a, self.b, self.c, - backend=self.backend) - dihedrals = MDAnalysis.lib.distances.calc_dihedrals(self.a, self.b, - self.c, self.d, - backend=self.backend) - - bonds_numpy = np.array([mdamath.norm(y - x) for x, y in zip(self.a, self.b)]) - vec1 = self.a - self.b - vec2 = self.c - self.b + bonds = MDAnalysis.lib.distances.calc_bonds(a, b, + backend=backend) + angles = MDAnalysis.lib.distances.calc_angles(a, b, c, + backend=backend) + dihedrals = MDAnalysis.lib.distances.calc_dihedrals(a, b, + c, d, + backend=backend) + + bonds_numpy = np.array([mdamath.norm(y - x) for x, y in zip(a, b)]) + vec1 = a - b + vec2 = c - b angles_numpy = np.array([mdamath.angle(x, y) for x, y in zip(vec1, vec2)]) - ab = self.b - self.a - bc = self.c - self.b - cd = self.d - self.c + ab = b - a + bc = c - b + cd = d - c dihedrals_numpy = np.array([mdamath.dihedral(x, y, z) for x, y, z in zip(ab, bc, cd)]) assert_almost_equal(bonds, bonds_numpy, self.prec, @@ -604,32 +576,13 @@ def test_numpy_compliance(self): err_msg="Cython dihedrals didn't match numpy calculations") -class TestCythonFunctions_Serial(_TestCythonFunctions): - __test__ = True - - backend = "serial" - - -class TestCythonFunctions_OpenMP(_TestCythonFunctions): - __test__ = True - - backend = "OpenMP" - - -class _Test_apply_PBC(TestCase): - __test__ = False - - backend = None - - def setUp(self): - self.prec = 6 - - def tearDown(self): - del self.prec +@pytest.mark.parametrize('backend', ['serial', 'openmp']) +class Test_apply_PBC(object): + prec = 6 - @dec.skipif(parser_not_found('DCD'), - 'DCD parser not available. Are you using python 3?') - def test_ortho_PBC(self): + @pytest.mark.skipif(parser_not_found('DCD'), + reason='DCD parser not available. Are you using python 3?') + def test_ortho_PBC(self, backend): from MDAnalysis.lib.distances import apply_PBC U = MDAnalysis.Universe(PSF, DCD) @@ -637,8 +590,8 @@ def test_ortho_PBC(self): box1 = np.array([2.5, 2.5, 3.5], dtype=np.float32) box2 = np.array([2.5, 2.5, 3.5, 90., 90., 90.], dtype=np.float32) - cyth1 = apply_PBC(atoms, box1, backend=self.backend) - cyth2 = apply_PBC(atoms, box2, backend=self.backend) + cyth1 = apply_PBC(atoms, box1, backend=backend) + cyth2 = apply_PBC(atoms, box2, backend=backend) reference = atoms - np.floor(atoms / box1) * box1 assert_almost_equal(cyth1, reference, self.prec, @@ -646,7 +599,7 @@ def test_ortho_PBC(self): assert_almost_equal(cyth2, reference, self.prec, err_msg="Ortho apply_PBC #2 failed comparison with np") - def test_tric_PBC(self): + def test_tric_PBC(self, backend): from MDAnalysis.lib.distances import apply_PBC U = MDAnalysis.Universe(TRIC) @@ -664,8 +617,8 @@ def numpy_PBC(coords, box): return coords - cyth1 = apply_PBC(atoms, box1, backend=self.backend) - cyth2 = apply_PBC(atoms, box2, backend=self.backend) + cyth1 = apply_PBC(atoms, box1, backend=backend) + cyth2 = apply_PBC(atoms, box2, backend=backend) reference = numpy_PBC(atoms, box2) assert_almost_equal(cyth1, reference, self.prec, @@ -674,120 +627,91 @@ def numpy_PBC(coords, box): err_msg="Trlclinic apply_PBC failed comparison with np") -class _Test_apply_PBC_Serial(_Test_apply_PBC): - __test__ = True - - backend = "serial" - - -class _Test_apply_PBC_OpenMP(_Test_apply_PBC): - __test__ = True - - backend = "OpenMP" - - -class _TestPeriodicAngles(TestCase): +@pytest.mark.parametrize('backend', ['serial', 'openmp']) +class TestPeriodicAngles(object): """Test case for properly considering minimum image convention when calculating angles and dihedrals (Issue 172) """ - - __test__ = False - - def setUp(self): - self.prec = 5 - self.a = np.array([[0.0, 1.0, 0.0]], dtype=np.float32) - self.b = np.array([[0.0, 0.0, 0.0]], dtype=np.float32) - self.c = np.array([[1.0, 0.0, 0.0]], dtype=np.float32) - self.d = np.array([[1.0, 0.0, 1.0]], dtype=np.float32) - self.box = np.array([10.0, 10.0, 10.0], dtype=np.float32) - - def tearDown(self): - del self.prec - del self.a - del self.b - del self.c - del self.d - del self.box - - def test_angles(self): + @staticmethod + @pytest.fixture() + def positions(): + a = np.array([[0.0, 1.0, 0.0]], dtype=np.float32) + b = np.array([[0.0, 0.0, 0.0]], dtype=np.float32) + c = np.array([[1.0, 0.0, 0.0]], dtype=np.float32) + d = np.array([[1.0, 0.0, 1.0]], dtype=np.float32) + box = np.array([10.0, 10.0, 10.0], dtype=np.float32) + return a, b, c, d, box + + prec = 5 + + def test_angles(self, positions, backend): from MDAnalysis.lib.distances import calc_angles # Shift atom coordinates a few box lengths in random directions and see if we still get same results - a2 = (self.a + self.box * (-1, 0, 0)).astype(np.float32) # seem to get converted to float64 otherwise - b2 = (self.b + self.box * (1, 0, 1)).astype(np.float32) - c2 = (self.c + self.box * (-2, 5, -7)).astype(np.float32) + a, b, c, d, box = positions + a2 = (a + box * (-1, 0, 0)).astype(np.float32) # seem to get converted to float64 otherwise + b2 = (b + box * (1, 0, 1)).astype(np.float32) + c2 = (c + box * (-2, 5, -7)).astype(np.float32) - ref = calc_angles(self.a, self.b, self.c, backend=self.backend) + ref = calc_angles(a, b, c, backend=backend) - test1 = calc_angles(a2, self.b, self.c, box=self.box, backend=self.backend) - test2 = calc_angles(self.a, b2, self.c, box=self.box, backend=self.backend) - test3 = calc_angles(self.a, self.b, c2, box=self.box, backend=self.backend) - test4 = calc_angles(a2, b2, c2, box=self.box, backend=self.backend) + test1 = calc_angles(a2, b, c, box=box, backend=backend) + test2 = calc_angles(a, b2, c, box=box, backend=backend) + test3 = calc_angles(a, b, c2, box=box, backend=backend) + test4 = calc_angles(a2, b2, c2, box=box, backend=backend) for val in [test1, test2, test3, test4]: assert_almost_equal(ref, val, self.prec, err_msg="Min image in angle calculation failed") - def test_dihedrals(self): + def test_dihedrals(self, positions, backend): from MDAnalysis.lib.distances import calc_dihedrals - - a2 = (self.a + self.box * (-1, 0, 0)).astype(np.float32) - b2 = (self.b + self.box * (1, 0, 1)).astype(np.float32) - c2 = (self.c + self.box * (-2, 5, -7)).astype(np.float32) - d2 = (self.d + self.box * (0, -5, 0)).astype(np.float32) - - ref = calc_dihedrals(self.a, self.b, self.c, self.d, backend=self.backend) - - test1 = calc_dihedrals(a2, self.b, self.c, self.d, box=self.box, - backend=self.backend) - test2 = calc_dihedrals(self.a, b2, self.c, self.d, box=self.box, - backend=self.backend) - test3 = calc_dihedrals(self.a, self.b, c2, self.d, box=self.box, - backend=self.backend) - test4 = calc_dihedrals(self.a, self.b, self.c, d2, box=self.box, - backend=self.backend) - test5 = calc_dihedrals(a2, b2, c2, d2, box=self.box, - backend=self.backend) + a, b, c, d, box = positions + a2 = (a + box * (-1, 0, 0)).astype(np.float32) + b2 = (b + box * (1, 0, 1)).astype(np.float32) + c2 = (c + box * (-2, 5, -7)).astype(np.float32) + d2 = (d + box * (0, -5, 0)).astype(np.float32) + + ref = calc_dihedrals(a, b, c, d, backend=backend) + + test1 = calc_dihedrals(a2, b, c, d, box=box, + backend=backend) + test2 = calc_dihedrals(a, b2, c, d, box=box, + backend=backend) + test3 = calc_dihedrals(a, b, c2, d, box=box, + backend=backend) + test4 = calc_dihedrals(a, b, c, d2, box=box, + backend=backend) + test5 = calc_dihedrals(a2, b2, c2, d2, box=box, + backend=backend) for val in [test1, test2, test3, test4, test5]: assert_almost_equal(ref, val, self.prec, err_msg="Min image in dihedral calculation failed") -class TestPeriodicAngles_Serial(_TestPeriodicAngles): - __test__ = True - - backend = "serial" - -class TestPeriodicAngles_OpenMP(_TestPeriodicAngles): - __test__ = True +class TestDistanceBackendSelection(object): + @staticmethod + @pytest.fixture() + def backend_selection_pos(): + positions = np.random.rand(10, 3) + N = positions.shape[0] + result = np.empty(N * (N - 1) // 2, dtype=np.float64) - backend = "OpenMP" + return positions, result - -class TestDistanceBackendSelection(TestCase): - def setUp(self): - self.positions = np.random.rand(10, 3) - N = self.positions.shape[0] - self.result = np.empty(N * (N - 1) // 2, dtype=np.float64) - - def _case_insensitivity_test(self, backend): + @pytest.mark.parametrize('backend', [ + "serial", "Serial", "SeRiAL", "SERIAL", + "openmp", "OpenMP", "oPENmP", "OPENMP", + pytest.mark.raises('not_implemented_stuff', exception=ValueError), + ]) + def test_case_insensitivity(self, backend, backend_selection_pos): + positions, result = backend_selection_pos try: MDAnalysis.lib.distances._run("calc_self_distance_array", - args=(self.positions, self.result), + args=(positions, result), backend=backend) except RuntimeError: raise AssertionError("Failed to understand backend {0}".format(backend)) - def test_case_insensitivity(self): - for backend in ("serial", "Serial", "SeRiAL", "SERIAL", - "openmp", "OpenMP", "oPENmP", "OPENMP"): - yield self._case_insensitivity_test, backend - - @raises(ValueError) - def test_missing_backend_raises_ValueError(self): - MDAnalysis.lib.distances._run("calc_self_distance_array", - args=(self.positions, self.result), - backend="not_implemented_stuff") - def test_used_openmpflag(): assert_(isinstance(MDAnalysis.lib.distances.USED_OPENMP, bool)) diff --git a/testsuite/MDAnalysisTests/utils/test_transformations.py b/testsuite/MDAnalysisTests/utils/test_transformations.py index b834c0fb633..14c6af04489 100644 --- a/testsuite/MDAnalysisTests/utils/test_transformations.py +++ b/testsuite/MDAnalysisTests/utils/test_transformations.py @@ -25,7 +25,7 @@ from itertools import permutations import numpy as np -import unittest +import pytest from numpy.testing import (assert_allclose, assert_equal, assert_almost_equal, assert_array_equal) @@ -38,29 +38,8 @@ Testing transformations is weird because there are 2 versions of many of these functions. This is because both python and Cython versions of these functions exist. To test therefore, each test has to be done twice, -once for each backend. - -The general pattern for this is, - -1) Create tests which call self.f (the function) - -2) Create mixins which define self.f (one of the two backends) - -Eg: - -class _ClipMatrix(object): - def test_this(self): - result = self.f(stuff) - assert_awesome(me) - -class TestClipMatrixNP(_ClipMatrix): - f = staticmethod(MDAnalysis.lib.transformations._py_clip_matrix) - -class TestClipMatrixCY(_ClipMatrix): - f = staticmethod(MDAnalysis.lib.transformations.clip_matrix) - -Note that the function to be tested needs to be defined as a static method! - +once for each backend. This is done through parametrizing and passing +in both versions of the function as an argument. This should ensure that both versions work and are covered! """ @@ -69,34 +48,24 @@ class TestClipMatrixCY(_ClipMatrix): _ATOL = 1e-06 -class _IdentityMatrix(object): - def test_identity_matrix(self): - I = self.f() - assert_allclose(I, np.dot(I, I)) - assert_equal(np.sum(I), np.trace(I)) - assert_allclose(I, np.identity(4, dtype=np.float64)) - - -class TestIdentityMatrixNP(_IdentityMatrix): - f = staticmethod(t._py_identity_matrix) - - -class TestIdentityMatrixCy(_IdentityMatrix): - f = staticmethod(t.identity_matrix) - - -class _TranslationMatrix(object): - def test_translation_matrix(self): - v = np.array([0.2, 0.2, 0.2]) - assert_allclose(v, self.f(v)[:3, 3]) - - -class TestTranslationMatrixNP(_TranslationMatrix): - f = staticmethod(t._py_translation_matrix) +@pytest.mark.parametrize('f', [ + t._py_identity_matrix, + t.identity_matrix +]) +def test_identity_matrix(f): + I = f() + assert_allclose(I, np.dot(I, I)) + assert_equal(np.sum(I), np.trace(I)) + assert_allclose(I, np.identity(4, dtype=np.float64)) -class TestTranslationMatrixCy(_TranslationMatrix): - f = staticmethod(t.translation_matrix) +@pytest.mark.parametrize('f', [ + t._py_translation_matrix, + t.translation_matrix, +]) +def test_translation_matrix(f): + v = np.array([0.2, 0.2, 0.2]) + assert_allclose(v, f(v)[:3, 3]) def test_translation_from_matrix(): @@ -106,26 +75,21 @@ def test_translation_from_matrix(): assert_allclose(v0, v1) -class _ReflectionMatrix(object): - def test_reflection_matrix(self): - v0 = np.array([0.2, 0.2, 0.2, 1.0]) # arbitrary values - v1 = np.array([0.4, 0.4, 0.4]) - R = self.f(v0, v1) - assert_allclose(2., np.trace(R)) - assert_allclose(v0, np.dot(R, v0)) - v2 = v0.copy() - v2[:3] += v1 - v3 = v0.copy() - v2[:3] -= v1 - assert_allclose(v2, np.dot(R, v3)) - - -class TestReflectionMatrixNP(_ReflectionMatrix): - f = staticmethod(t._py_reflection_matrix) - - -class TestReflectionMatrixCy(_ReflectionMatrix): - f = staticmethod(t.reflection_matrix) +@pytest.mark.parametrize('f', [ + t._py_reflection_matrix, + t.reflection_matrix, +]) +def test_reflection_matrix(f): + v0 = np.array([0.2, 0.2, 0.2, 1.0]) # arbitrary values + v1 = np.array([0.4, 0.4, 0.4]) + R = f(v0, v1) + assert_allclose(2., np.trace(R)) + assert_allclose(v0, np.dot(R, v0)) + v2 = v0.copy() + v2[:3] += v1 + v3 = v0.copy() + v2[:3] -= v1 + assert_allclose(v2, np.dot(R, v3)) def test_reflection_from_matrix(): @@ -137,30 +101,25 @@ def test_reflection_from_matrix(): assert_equal(t.is_same_transform(M0, M1), True) -class _RotationMatrix(object): - def test_rotation_matrix(self): - R = self.f(np.pi / 2.0, [0, 0, 1], [1, 0, 0]) - assert_allclose(np.dot(R, [0, 0, 0, 1]), [1., -1., 0., 1.]) - angle = 0.2 * 2 * np.pi # arbitrary value - direc = np.array([0.2, 0.2, 0.2]) - point = np.array([0.4, 0.4, 0.4]) - R0 = self.f(angle, direc, point) - R1 = self.f(angle - 2 * np.pi, direc, point) - assert_equal(t.is_same_transform(R0, R1), True) - R0 = self.f(angle, direc, point) - R1 = self.f(-angle, -direc, point) - assert_equal(t.is_same_transform(R0, R1), True) - I = np.identity(4, np.float64) - assert_allclose(I, self.f(np.pi * 2, direc), atol=_ATOL) - assert_allclose(2., np.trace(self.f(np.pi / 2, direc, point))) - - -class TestRotationMatrixNP(_RotationMatrix): - f = staticmethod(t._py_rotation_matrix) - - -class TestRotationMatrixCy(_RotationMatrix): - f = staticmethod(t.rotation_matrix) +@pytest.mark.parametrize('f', [ + t._py_rotation_matrix, + t.rotation_matrix, +]) +def test_rotation_matrix(f): + R = f(np.pi / 2.0, [0, 0, 1], [1, 0, 0]) + assert_allclose(np.dot(R, [0, 0, 0, 1]), [1., -1., 0., 1.]) + angle = 0.2 * 2 * np.pi # arbitrary value + direc = np.array([0.2, 0.2, 0.2]) + point = np.array([0.4, 0.4, 0.4]) + R0 = f(angle, direc, point) + R1 = f(angle - 2 * np.pi, direc, point) + assert_equal(t.is_same_transform(R0, R1), True) + R0 = f(angle, direc, point) + R1 = f(-angle, -direc, point) + assert_equal(t.is_same_transform(R0, R1), True) + I = np.identity(4, np.float64) + assert_allclose(I, f(np.pi * 2, direc), atol=_ATOL) + assert_allclose(2., np.trace(f(np.pi / 2, direc, point))) def test_rotation_from_matrix(): @@ -173,19 +132,14 @@ def test_rotation_from_matrix(): assert_equal(t.is_same_transform(R0, R1), True) -class _ScaleMatrix(object): - def test_scale_matrix(self): - v = np.array([14.1, 15.1, 16.1, 1]) - S = self.f(-1.234) - assert_allclose(np.dot(S, v)[:3], -1.234 * v[:3]) - - -class TestScaleMatrixNP(_ScaleMatrix): - f = staticmethod(t._py_scale_matrix) - - -class TestScaleMatrixCy(_ScaleMatrix): - f = staticmethod(t.scale_matrix) +@pytest.mark.parametrize('f', [ + t._py_scale_matrix, + t.scale_matrix, +]) +def test_scale_matrix(f): + v = np.array([14.1, 15.1, 16.1, 1]) + S = f(-1.234) + assert_allclose(np.dot(S, v)[:3], -1.234 * v[:3]) def test_scale_from_matrix(): @@ -201,84 +155,90 @@ def test_scale_from_matrix(): S1 = t.scale_matrix(factor, origin, direction) assert_equal(t.is_same_transform(S0, S1), True) - -class _ProjectionMatrix(object): - def test_projection_matrix_1(self): - P = self.f((0, 0, 0), (1, 0, 0)) +@pytest.mark.parametrize('f', [ + t._py_projection_matrix, + t.projection_matrix, +]) +class TestProjectionMatrix(object): + def test_projection_matrix_1(self, f): + P = f((0, 0, 0), (1, 0, 0)) assert_allclose(P[1:, 1:], np.identity(4)[1:, 1:], atol=_ATOL) - def test_projection_matrix_2(self): + def test_projection_matrix_2(self, f): point = np.array([0.2, 0.2, 0.2]) # arbitrary values normal = np.array([0.4, 0.4, 0.4]) direct = np.array([0.6, 0.6, 0.6]) persp = np.array([0.8, 0.8, 0.8]) - P0 = self.f(point, normal) + P0 = f(point, normal) # TODO: why isn't this used anymore? - P1 = self.f(point, normal, direction=direct) - P2 = self.f(point, normal, perspective=persp) - P3 = self.f(point, normal, perspective=persp, pseudo=True) + P1 = f(point, normal, direction=direct) + P2 = f(point, normal, perspective=persp) + P3 = f(point, normal, perspective=persp, pseudo=True) assert_equal(t.is_same_transform(P2, np.dot(P0, P3)), True) - def test_projection_matrix_3(self): - P = self.f((3, 0, 0), (1, 1, 0), (1, 0, 0)) + def test_projection_matrix_3(self, f): + P = f((3, 0, 0), (1, 1, 0), (1, 0, 0)) v0 = np.array([14.1, 15.1, 16.1, 1]) # arbitrary values v1 = np.dot(P, v0) assert_allclose(v1[1], v0[1], atol=_ATOL) assert_allclose(v1[0], 3.0 - v1[1], atol=_ATOL) -class TestProjectionMatrixNP(_ProjectionMatrix): - f = staticmethod(t._py_projection_matrix) +class TestProjectionFromMatrix(object): + @staticmethod + @pytest.fixture() + def data(): + point = np.array([0.2, 0.2, 0.2]) # arbitrary values + normal = np.array([0.4, 0.4, 0.4]) + direct = np.array([0.6, 0.6, 0.6]) + persp = np.array([0.8, 0.8, 0.8]) + return point, normal, direct, persp -class TestProjectionMatrixCy(_ProjectionMatrix): - f = staticmethod(t.projection_matrix) - - -class TestProjectionFromMatrix(TestCase): - def setUp(self): - self.point = np.array([0.2, 0.2, 0.2]) # arbitrary values - self.normal = np.array([0.4, 0.4, 0.4]) - self.direct = np.array([0.6, 0.6, 0.6]) - self.persp = np.array([0.8, 0.8, 0.8]) - - def test_projection_from_matrix_1(self): - P0 = t.projection_matrix(self.point, self.normal) + def test_projection_from_matrix_1(self, data): + point, normal, direct, persp = data + P0 = t.projection_matrix(point, normal) result = t.projection_from_matrix(P0) P1 = t.projection_matrix(*result) assert_equal(t.is_same_transform(P0, P1), True) - def test_projection_from_matrix_2(self): - P0 = t.projection_matrix(self.point, self.normal, self.direct) + def test_projection_from_matrix_2(self, data): + point, normal, direct, persp = data + P0 = t.projection_matrix(point, normal, direct) result = t.projection_from_matrix(P0) P1 = t.projection_matrix(*result) assert_equal(t.is_same_transform(P0, P1), True) - def test_projection_from_matrix_3(self): + def test_projection_from_matrix_3(self, data): + point, normal, direct, persp = data P0 = t.projection_matrix( - self.point, self.normal, perspective=self.persp, pseudo=False) + point, normal, perspective=persp, pseudo=False) result = t.projection_from_matrix(P0, pseudo=False) P1 = t.projection_matrix(*result) assert_equal(t.is_same_transform(P0, P1), True) - def test_projection_from_matrix_4(self): + def test_projection_from_matrix_4(self, data): + point, normal, direct, persp = data P0 = t.projection_matrix( - self.point, self.normal, perspective=self.persp, pseudo=True) + point, normal, perspective=persp, pseudo=True) result = t.projection_from_matrix(P0, pseudo=True) P1 = t.projection_matrix(*result) assert_equal(t.is_same_transform(P0, P1), True) -class _ClipMatrix(unittest.TestCase): - __test__ = False - def test_clip_matrix_1(self): +@pytest.mark.parametrize('f', [ + t._py_clip_matrix, + t.clip_matrix, +]) +class TestClipMatrix(object): + def test_clip_matrix_1(self, f): frustrum = np.array([0.1, 0.2, 0.3, 0.4, 0.5, 0.6]) # arbitrary values frustrum[1] += frustrum[0] frustrum[3] += frustrum[2] frustrum[5] += frustrum[4] - M = self.f(perspective=False, *frustrum) + M = f(perspective=False, *frustrum) assert_allclose( np.dot(M, [frustrum[0], frustrum[2], frustrum[4], 1.0]), np.array([-1., -1., -1., 1.])) @@ -286,64 +246,47 @@ def test_clip_matrix_1(self): np.dot(M, [frustrum[1], frustrum[3], frustrum[5], 1.0]), np.array([1., 1., 1., 1.])) - def test_clip_matrix_2(self): + def test_clip_matrix_2(self, f): frustrum = np.array([0.1, 0.2, 0.3, 0.4, 0.5, 0.6]) # arbitrary values frustrum[1] += frustrum[0] frustrum[3] += frustrum[2] frustrum[5] += frustrum[4] - M = self.f(perspective=True, *frustrum) + M = f(perspective=True, *frustrum) v = np.dot(M, [frustrum[0], frustrum[2], frustrum[4], 1.0]) assert_allclose(v / v[3], np.array([-1., -1., -1., 1.])) v = np.dot(M, [frustrum[1], frustrum[3], frustrum[4], 1.0]) assert_allclose(v / v[3], np.array([1., 1., -1., 1.])) - def test_clip_matrix_frustrum_left_right_bounds(self): + def test_clip_matrix_frustrum_left_right_bounds(self, f): '''ValueError should be raised if left > right.''' frustrum = np.array([0.4, 0.3, 0.3, 0.7, 0.5, 1.1]) - with self.assertRaises(ValueError): - self.f(*frustrum) + with pytest.raises(ValueError): + f(*frustrum) - def test_clip_matrix_frustrum_bottom_top_bounds(self): + def test_clip_matrix_frustrum_bottom_top_bounds(self, f): '''ValueError should be raised if bottom > top.''' frustrum = np.array([0.1, 0.3, 0.71, 0.7, 0.5, 1.1]) - with self.assertRaises(ValueError): - self.f(*frustrum) + with pytest.raises(ValueError): + f(*frustrum) - def test_clip_matrix_frustrum_near_far_bounds(self): + def test_clip_matrix_frustrum_near_far_bounds(self, f): '''ValueError should be raised if near > far.''' frustrum = np.array([0.1, 0.3, 0.3, 0.7, 1.5, 1.1]) - with self.assertRaises(ValueError): - self.f(*frustrum) - - -class TestClipMatrixNP(_ClipMatrix): - __test__ = True + with pytest.raises(ValueError): + f(*frustrum) - f = staticmethod(t._py_clip_matrix) - -class TestClipMatrixCy(_ClipMatrix): - __test__ = True - - f = staticmethod(t.clip_matrix) - - -class _ShearMatrix(object): - def test_shear_matrix(self): - angle = 0.2 * 4 * np.pi # arbitrary values - direct = np.array([0.2, 0.2, 0.2]) - point = np.array([0.3, 0.4, 0.5]) - normal = np.cross(direct, np.array([0.8, 0.6, 0.4])) - S = self.f(angle, direct, point, normal) - assert_allclose(1.0, np.linalg.det(S), atol=_ATOL) - - -class TestShearMatrixNP(_ShearMatrix): - f = staticmethod(t._py_shear_matrix) - - -class TestShearMatrixCy(_ShearMatrix): - f = staticmethod(t.shear_matrix) +@pytest.mark.parametrize('f', [ + t._py_shear_matrix, + t.shear_matrix, +]) +def test_shear_matrix(f): + angle = 0.2 * 4 * np.pi # arbitrary values + direct = np.array([0.2, 0.2, 0.2]) + point = np.array([0.3, 0.4, 0.5]) + normal = np.cross(direct, np.array([0.8, 0.6, 0.4])) + S = f(angle, direct, point, normal) + assert_allclose(1.0, np.linalg.det(S), atol=_ATOL) def test_shear_from_matrix(): @@ -400,254 +343,201 @@ def test_compose_matrix(): assert_equal(t.is_same_transform(M0, M1), True) -class _OrthogonalizationMatrix(object): - def test_orthogonalization_matrix_1(self): - O = self.f((10., 10., 10.), (90., 90., 90.)) +@pytest.mark.parametrize('f', [ + t._py_orthogonalization_matrix, + t.orthogonalization_matrix, +]) +class TestOrthogonalizationMatrix(object): + def test_orthogonalization_matrix_1(self, f): + O = f((10., 10., 10.), (90., 90., 90.)) assert_allclose(O[:3, :3], np.identity(3, float) * 10, atol=_ATOL) - def test_orthogonalization_matrix_2(self): - O = self.f([9.8, 12.0, 15.5], [87.2, 80.7, 69.7]) + def test_orthogonalization_matrix_2(self, f): + O = f([9.8, 12.0, 15.5], [87.2, 80.7, 69.7]) assert_allclose(np.sum(O), 43.063229, atol=_ATOL) -class TestOrthogonalizationMatrixNP(_OrthogonalizationMatrix): - f = staticmethod(t._py_orthogonalization_matrix) - - -class TestOrthogonalizationMatrixCy(_OrthogonalizationMatrix): - f = staticmethod(t.orthogonalization_matrix) - - -class _SuperimpositionMatrix(object): - def test_superimposition_matrix(self): - v0 = np.sin(np.linspace(0, 0.99, 30)).reshape(3, - 10) # arbitrary values - M = self.f(v0, v0) - assert_allclose(M, np.identity(4), atol=_ATOL) - - R = t.random_rotation_matrix(np.array([0.3, 0.4, 0.5])) - v0 = ((1, 0, 0), (0, 1, 0), (0, 0, 1), (1, 1, 1)) - v1 = np.dot(R, v0) - M = self.f(v0, v1) - assert_allclose(v1, np.dot(M, v0), atol=_ATOL) - - v0 = np.sin(np.linspace(-1, 1, 400)).reshape(4, 100) - v0[3] = 1.0 - v1 = np.dot(R, v0) - M = self.f(v0, v1) - assert_allclose(v1, np.dot(M, v0), atol=_ATOL) - - S = t.scale_matrix(0.45) - T = t.translation_matrix(np.array([0.2, 0.2, 0.2]) - 0.5) - M = t.concatenate_matrices(T, R, S) - v1 = np.dot(M, v0) - v0[:3] += np.sin(np.linspace(0.0, 1e-9, 300)).reshape(3, -1) - M = self.f(v0, v1, scaling=True) - assert_allclose(v1, np.dot(M, v0), atol=_ATOL) - - M = self.f(v0, v1, scaling=True, usesvd=False) - assert_allclose(v1, np.dot(M, v0), atol=_ATOL) - - v = np.empty((4, 100, 3), dtype=np.float64) - v[:, :, 0] = v0 - M = self.f(v0, v1, scaling=True, usesvd=False) - assert_allclose(v1, np.dot(M, v[:, :, 0]), atol=_ATOL) - - -class TestSuperimpositionMatrixNP(_SuperimpositionMatrix): - f = staticmethod(t._py_superimposition_matrix) - - -class TestSuperimpositionMatrixCy(_SuperimpositionMatrix): - f = staticmethod(t.superimposition_matrix) - - -class _EulerMatrix(object): - def test_euler_matrix_1(self): - R = self.f(1, 2, 3, 'syxz') +@pytest.mark.parametrize('f', [ + t._py_superimposition_matrix, + t.superimposition_matrix, +]) +def test_superimposition_matrix(f): + v0 = np.sin(np.linspace(0, 0.99, 30)).reshape(3, 10) # arbitrary values + M = f(v0, v0) + assert_allclose(M, np.identity(4), atol=_ATOL) + + R = t.random_rotation_matrix(np.array([0.3, 0.4, 0.5])) + v0 = ((1, 0, 0), (0, 1, 0), (0, 0, 1), (1, 1, 1)) + v1 = np.dot(R, v0) + M = f(v0, v1) + assert_allclose(v1, np.dot(M, v0), atol=_ATOL) + + v0 = np.sin(np.linspace(-1, 1, 400)).reshape(4, 100) + v0[3] = 1.0 + v1 = np.dot(R, v0) + M = f(v0, v1) + assert_allclose(v1, np.dot(M, v0), atol=_ATOL) + + S = t.scale_matrix(0.45) + T = t.translation_matrix(np.array([0.2, 0.2, 0.2]) - 0.5) + M = t.concatenate_matrices(T, R, S) + v1 = np.dot(M, v0) + v0[:3] += np.sin(np.linspace(0.0, 1e-9, 300)).reshape(3, -1) + M = f(v0, v1, scaling=True) + assert_allclose(v1, np.dot(M, v0), atol=_ATOL) + + M = f(v0, v1, scaling=True, usesvd=False) + assert_allclose(v1, np.dot(M, v0), atol=_ATOL) + + v = np.empty((4, 100, 3), dtype=np.float64) + v[:, :, 0] = v0 + M = f(v0, v1, scaling=True, usesvd=False) + assert_allclose(v1, np.dot(M, v[:, :, 0]), atol=_ATOL) + + +@pytest.mark.parametrize('f', [ + t._py_euler_matrix, + t.euler_matrix, +]) +class TestEulerMatrix(object): + def test_euler_matrix_1(self, f): + R = f(1, 2, 3, 'syxz') assert_allclose(np.sum(R[0]), -1.34786452) - def test_euler_matrix_2(self): - R = self.f(1, 2, 3, (0, 1, 0, 1)) + def test_euler_matrix_2(self, f): + R = f(1, 2, 3, (0, 1, 0, 1)) assert_allclose(np.sum(R[0]), -0.383436184) -class TestEulerMatrixNP(_EulerMatrix): - f = staticmethod(t._py_euler_matrix) - - -class TestEulerMatrixCy(_EulerMatrix): - f = staticmethod(t.euler_matrix) - - -class _EulerFromMatrix(object): - def test_euler_from_matrix_1(self): +@pytest.mark.parametrize('f', [ + t._py_euler_from_matrix, + t.euler_from_matrix, +]) +class TestEulerFromMatrix(object): + def test_euler_from_matrix_1(self, f): R0 = t.euler_matrix(1, 2, 3, 'syxz') - al, be, ga = self.f(R0, 'syxz') + al, be, ga = f(R0, 'syxz') R1 = t.euler_matrix(al, be, ga, 'syxz') assert_allclose(R0, R1) - def test_euler_from_matrix_2(self): + def test_euler_from_matrix_2(self, f): angles = 4.0 * np.pi * np.array([-0.3, -0.3, -0.3]) # arbitrary values for axes in t._AXES2TUPLE.keys(): R0 = t.euler_matrix(axes=axes, *angles) - R1 = t.euler_matrix(axes=axes, *self.f(R0, axes)) + R1 = t.euler_matrix(axes=axes, *f(R0, axes)) assert_allclose(R0, R1, err_msg=("{0} failed".format(axes))) -class TestEulerFromMatrixNP(_EulerFromMatrix): - f = staticmethod(t._py_euler_from_matrix) - - -class TestEulerFromMatrixCy(_EulerFromMatrix): - f = staticmethod(t.euler_from_matrix) - - def test_euler_from_quaternion(): angles = t.euler_from_quaternion([0.99810947, 0.06146124, 0, 0]) assert_allclose(angles, [0.123, 0, 0], atol=_ATOL) -class _QuaternionFromEuler(object): - def test_quaternion_from_euler(self): - q = self.f(1, 2, 3, 'ryxz') - assert_allclose( - q, [0.435953, 0.310622, -0.718287, 0.444435], atol=_ATOL) - - -class TestQuaternionFromEulerNP(_QuaternionFromEuler): - f = staticmethod(t._py_quaternion_from_euler) - +@pytest.mark.parametrize('f', [ + t._py_quaternion_from_euler, + t.quaternion_from_euler, +]) +def test_quaternion_from_euler(f): + q = f(1, 2, 3, 'ryxz') + assert_allclose(q, [0.435953, 0.310622, -0.718287, 0.444435], atol=_ATOL) -class TestQuaternionFromEulerCy(_QuaternionFromEuler): - f = staticmethod(t.quaternion_from_euler) +@pytest.mark.parametrize('f', [ + t._py_quaternion_about_axis, + t.quaternion_about_axis, +]) +def test_quaternion_about_axis(f): + q = f(0.123, (1, 0, 0)) + assert_allclose(q, [0.99810947, 0.06146124, 0, 0], atol=_ATOL) -class _QuaternionAboutAxis(object): - def test_quaternion_about_axis(self): - q = self.f(0.123, (1, 0, 0)) - assert_allclose(q, [0.99810947, 0.06146124, 0, 0], atol=_ATOL) - -class TestQuaternionAboutAxisNP(_QuaternionAboutAxis): - f = staticmethod(t._py_quaternion_about_axis) - - -class TestQuaternionAboutAxisCy(_QuaternionAboutAxis): - f = staticmethod(t.quaternion_about_axis) - - -class _QuaternionMatrix(object): - def test_quaternion_matrix_1(self): - M = self.f([0.99810947, 0.06146124, 0, 0]) +@pytest.mark.parametrize('f', [ + t._py_quaternion_matrix, + t.quaternion_matrix, +]) +class TestQuaternionMatrix(object): + def test_quaternion_matrix_1(self, f): + M = f([0.99810947, 0.06146124, 0, 0]) assert_allclose(M, t.rotation_matrix(0.123, (1, 0, 0)), atol=_ATOL) - def test_quaternion_matrix_2(self): - M = self.f([1, 0, 0, 0]) + def test_quaternion_matrix_2(self, f): + M = f([1, 0, 0, 0]) assert_allclose(M, t.identity_matrix(), atol=_ATOL) - def test_quaternion_matrix_3(self): - M = self.f([0, 1, 0, 0]) + def test_quaternion_matrix_3(self, f): + M = f([0, 1, 0, 0]) assert_allclose(M, np.diag([1, -1, -1, 1]), atol=_ATOL) -class TestQuaternionMatrixNP(_QuaternionMatrix): - f = staticmethod(t._py_quaternion_matrix) - - -class TestQuaternionMatrixCy(_QuaternionMatrix): - f = staticmethod(t.quaternion_matrix) - - -class _QuaternionFromMatrix(object): - def test_quaternion_from_matrix_1(self): - q = self.f(t.identity_matrix(), True) +@pytest.mark.parametrize('f', [ + t._py_quaternion_from_matrix, + t.quaternion_from_matrix, +]) +class TestQuaternionFromMatrix(object): + def test_quaternion_from_matrix_1(self, f): + q = f(t.identity_matrix(), True) assert_allclose(q, [1., 0., 0., 0.], atol=_ATOL) - def test_quaternion_from_matrix_2(self): - q = self.f(np.diag([1., -1., -1., 1.])) + def test_quaternion_from_matrix_2(self, f): + q = f(np.diag([1., -1., -1., 1.])) check = (np.allclose( q, [0, 1, 0, 0], atol=_ATOL) or np.allclose( q, [0, -1, 0, 0], atol=_ATOL)) assert_equal(check, True) - def test_quaternion_from_matrix_3(self): + def test_quaternion_from_matrix_3(self, f): R = t.rotation_matrix(0.123, (1, 2, 3)) - q = self.f(R, True) + q = f(R, True) assert_allclose( q, [0.9981095, 0.0164262, 0.0328524, 0.0492786], atol=_ATOL) - def test_quaternion_from_matrix_4(self): + def test_quaternion_from_matrix_4(self, f): R = [[-0.545, 0.797, 0.260, 0], [0.733, 0.603, -0.313, 0], [-0.407, 0.021, -0.913, 0], [0, 0, 0, 1]] - q = self.f(R) + q = f(R) assert_allclose(q, [0.19069, 0.43736, 0.87485, -0.083611], atol=_ATOL) - def test_quaternion_from_matrix_5(self): + def test_quaternion_from_matrix_5(self, f): R = [[0.395, 0.362, 0.843, 0], [-0.626, 0.796, -0.056, 0], [-0.677, -0.498, 0.529, 0], [0, 0, 0, 1]] - q = self.f(R) + q = f(R) assert_allclose( q, [0.82336615, -0.13610694, 0.46344705, -0.29792603], atol=_ATOL) - def test_quaternion_from_matrix_6(self): + def test_quaternion_from_matrix_6(self, f): R = t.random_rotation_matrix() - q = self.f(R) + q = f(R) assert_equal(t.is_same_transform(R, t.quaternion_matrix(q)), True) -class TestQuaternionFromMatrixNP(_QuaternionFromMatrix): - f = staticmethod(t._py_quaternion_from_matrix) - - -class TestQuaternionFromMatrixCy(_QuaternionFromMatrix): - f = staticmethod(t.quaternion_from_matrix) - - -class _QuaternionMultiply(object): - def test_quaternion_multiply(self): - q = self.f([4, 1, -2, 3], [8, -5, 6, 7]) - assert_allclose(q, [28, -44, -14, 48]) - - -class TestQuaternionMultiplyNP(_QuaternionMultiply): - f = staticmethod(t._py_quaternion_multiply) - - -class TestQuaternionMultiplyCy(_QuaternionMultiply): - f = staticmethod(t.quaternion_multiply) - - -class _QuaternionConjugate(object): - def test_quaternion_conjugate(self): - q0 = t.random_quaternion() - q1 = self.f(q0) - check = q1[0] == q0[0] and all(q1[1:] == -q0[1:]) - assert_equal(check, True) - - -class TestQuaternionConjugateNP(_QuaternionConjugate): - f = staticmethod(t._py_quaternion_conjugate) - - -class TestQuaternionConjugateCy(_QuaternionConjugate): - f = staticmethod(t.quaternion_conjugate) - - -class _QuaternionInverse(object): - def test_quaternion_inverse(self): - q0 = t.random_quaternion() - q1 = self.f(q0) - assert_allclose( - t.quaternion_multiply(q0, q1), [1, 0, 0, 0], atol=_ATOL) +@pytest.mark.parametrize('f', [ + t._py_quaternion_multiply, + t.quaternion_multiply, +]) +def test_quaternion_multiply(f): + q = f([4, 1, -2, 3], [8, -5, 6, 7]) + assert_allclose(q, [28, -44, -14, 48]) -class TestQuaternionInverseNP(_QuaternionInverse): - f = staticmethod(t._py_quaternion_inverse) +@pytest.mark.parametrize('f', [ + t._py_quaternion_conjugate, + t.quaternion_conjugate, +]) +def test_quaternion_conjugate(f): + q0 = t.random_quaternion() + q1 = f(q0) + check = q1[0] == q0[0] and all(q1[1:] == -q0[1:]) + assert_equal(check, True) -class TestQuaternionInverseCy(_QuaternionInverse): - f = staticmethod(t.quaternion_inverse) +@pytest.mark.parametrize('f', [ + t._py_quaternion_inverse, + t.quaternion_inverse, +]) +def test_quaternion_inverse(f): + q0 = t.random_quaternion() + q1 = f(q0) + assert_allclose(t.quaternion_multiply(q0, q1), [1, 0, 0, 0], atol=_ATOL) def test_quaternion_real(): @@ -658,203 +548,167 @@ def test_quaternion_imag(): assert_allclose(t.quaternion_imag([3.0, 0.0, 1.0, 2.0]), [0.0, 1.0, 2.0]) -class _QuaternionSlerp(object): - def test_quaternion_slerp(self): - q0 = t.random_quaternion() - q1 = t.random_quaternion() - q = self.f(q0, q1, 0.0) - assert_allclose(q, q0, atol=_ATOL) - - q = self.f(q0, q1, 1.0, 1) - assert_allclose(q, q1, atol=_ATOL) - - q = self.f(q0, q1, 0.5) - angle = np.arccos(np.dot(q0, q)) +@pytest.mark.parametrize('f', [ + t._py_quaternion_slerp, + t.quaternion_slerp, +]) +def test_quaternion_slerp(f): + q0 = t.random_quaternion() + q1 = t.random_quaternion() + q = f(q0, q1, 0.0) + assert_allclose(q, q0, atol=_ATOL) - check = (np.allclose(2.0, np.arccos(np.dot(q0, q1)) / angle) or - np.allclose(2.0, np.arccos(-np.dot(q0, q1)) / angle)) - - assert_equal(check, True) + q = f(q0, q1, 1.0, 1) + assert_allclose(q, q1, atol=_ATOL) + q = f(q0, q1, 0.5) + angle = np.arccos(np.dot(q0, q)) -class TestQuaternionSlerpNP(_QuaternionSlerp): - f = staticmethod(t._py_quaternion_slerp) + check = (np.allclose(2.0, np.arccos(np.dot(q0, q1)) / angle) or + np.allclose(2.0, np.arccos(-np.dot(q0, q1)) / angle)) + assert_equal(check, True) -class TestQuaternionSlerpCy(_QuaternionSlerp): - f = staticmethod(t.quaternion_slerp) - -class _RandomQuaternion(object): - def test_random_quaternion_1(self): - q = self.f() +@pytest.mark.parametrize('f', [ + t._py_random_quaternion, + t.random_quaternion, +]) +class TestRandomQuaternion(object): + def test_random_quaternion_1(self, f): + q = f() assert_allclose(1.0, t.vector_norm(q)) - def test_random_quaternion_2(self): - q = self.f(np.array([0.2, 0.2, 0.2])) + def test_random_quaternion_2(self, f): + q = f(np.array([0.2, 0.2, 0.2])) assert_equal(len(q.shape), 1) assert_equal(q.shape[0] == 4, True) -class TestRandomQuaternionNP(_RandomQuaternion): - f = staticmethod(t._py_random_quaternion) - - -class TestRandomQuaternionCy(_RandomQuaternion): - f = staticmethod(t.random_quaternion) - +@pytest.mark.parametrize('f', [ + t._py_random_rotation_matrix, + t.random_rotation_matrix, +]) +def test_random_rotation_matrix(f): + R = f() + assert_allclose(np.dot(R.T, R), np.identity(4), atol=_ATOL) -class _RandomRotationMatrix(object): - def test_random_rotation_matrix(self): - R = self.f() - assert_allclose(np.dot(R.T, R), np.identity(4), atol=_ATOL) - -class TestRandomRotationMatrixNP(_RandomRotationMatrix): - f = staticmethod(t._py_random_rotation_matrix) - - -class TestRandomRotationMatrixCy(_RandomRotationMatrix): - f = staticmethod(t.random_rotation_matrix) - - -class _InverseMatrix(object): - def _check_inverse(self, size): +@pytest.mark.parametrize('f', [ + t._py_inverse_matrix, + t.inverse_matrix, +]) +class TestInverseMatrix(object): + @pytest.mark.parametrize('size', list(range(1, 7))) + def test_inverse(self, size, f): # Create a known random state to generate numbers from # these numbers will then be uncorrelated but deterministic rs = np.random.RandomState(1234) M0 = rs.randn(size, size) - M1 = self.f(M0) + M1 = f(M0) assert_allclose(M1, np.linalg.inv(M0), err_msg=str(size), atol=_ATOL) - def test_inverse_matrix(self): + def test_inverse_matrix(self, f): M0 = t.random_rotation_matrix() - M1 = self.f(M0.T) + M1 = f(M0.T) assert_allclose(M1, np.linalg.inv(M0.T)) - for size in range(1, 7): - yield self._check_inverse, size - - -class TestInverseMatrixNP(_InverseMatrix): - f = staticmethod(t._py_inverse_matrix) - - -class TestInverseMatrixCy(_InverseMatrix): - f = staticmethod(t.inverse_matrix) +@pytest.mark.parametrize('f', [ + t._py_is_same_transform, + t.is_same_transform, +]) +class TestIsSameTransform(object): + def test_is_same_transform_1(self, f): + assert_equal(f(np.identity(4), np.identity(4)), True) -class _IsSameTransform(object): - def test_is_same_transform_1(self): - assert_equal(self.f(np.identity(4), np.identity(4)), True) + def test_is_same_transform_2(self, f): + assert_equal(f(t.random_rotation_matrix(), np.identity(4)), False) - def test_is_same_transform_2(self): - assert_equal(self.f(t.random_rotation_matrix(), np.identity(4)), False) - -class TestIsSameTransformNP(_IsSameTransform): - f = staticmethod(t._py_is_same_transform) - - -class TestIsSameTransformCy(_IsSameTransform): - f = staticmethod(t.is_same_transform) - - -class _RandomVector(object): - def test_random_vector_1(self): - v = self.f(1000) +@pytest.mark.parametrize('f', [ + t._py_random_vector, + t.random_vector, +]) +class TestRandomVector(object): + def test_random_vector_1(self, f): + v = f(1000) check = np.all(v >= 0.0) and np.all(v < 1.0) assert_equal(check, True) - def test_random_vector_2(self): - v0 = self.f(10) - v1 = self.f(10) + def test_random_vector_2(self, f): + v0 = f(10) + v1 = f(10) assert_equal(np.any(v0 == v1), False) -class TestRandomVectorNP(_RandomVector): - f = staticmethod(t._py_random_vector) - - -class TestRandomVectorCy(_RandomVector): - f = staticmethod(t.random_vector) - - -class _UnitVector(object): - def test_unit_vector_1(self): +@pytest.mark.parametrize('f', [ + t._py_unit_vector, + t.unit_vector, +]) +class TestUnitVector(object): + def test_unit_vector_1(self, f): v0 = np.array([0.2, 0.2, 0.2]) - v1 = self.f(v0) + v1 = f(v0) assert_allclose(v1, v0 / np.linalg.norm(v0), atol=_ATOL) - def test_unit_vector_2(self): + def test_unit_vector_2(self, f): v0 = np.sin(np.linspace(0, 10, 5 * 4 * 3)).reshape(5, 4, 3) - v1 = self.f(v0, axis=-1) + v1 = f(v0, axis=-1) v2 = v0 / np.expand_dims(np.sqrt(np.sum(v0 * v0, axis=2)), 2) assert_allclose(v1, v2, atol=_ATOL) - def test_unit_vector_3(self): + def test_unit_vector_3(self, f): v0 = np.sin(np.linspace(0, 10, 5 * 4 * 3)).reshape(5, 4, 3) - v1 = self.f(v0, axis=1) + v1 = f(v0, axis=1) v2 = v0 / np.expand_dims(np.sqrt(np.sum(v0 * v0, axis=1)), 1) assert_allclose(v1, v2, atol=_ATOL) - def test_unit_vector_4(self): + def test_unit_vector_4(self, f): v0 = np.sin(np.linspace(0, 10, 5 * 4 * 3)).reshape(5, 4, 3) v1 = np.empty((5, 4, 3), dtype=np.float64) v2 = v0 / np.expand_dims(np.sqrt(np.sum(v0 * v0, axis=1)), 1) - self.f(v0, axis=1, out=v1) + f(v0, axis=1, out=v1) assert_allclose(v1, v2, atol=_ATOL) - def test_unit_vector_5(self): - assert_equal(list(self.f([])), []) + def test_unit_vector_5(self, f): + assert_equal(list(f([])), []) - def test_unit_vector_6(self): - assert_equal(list(self.f([1.0])), [1.0]) + def test_unit_vector_6(self, f): + assert_equal(list(f([1.0])), [1.0]) -class TestUnitVectorNP(_UnitVector): - f = staticmethod(t._py_unit_vector) - - -class TestUnitVectorCy(_UnitVector): - f = staticmethod(t.unit_vector) - - -class _VectorNorm(object): - def test_vector_norm_1(self): +@pytest.mark.parametrize('f', [ + t._py_vector_norm, + t.vector_norm, +]) +class TestVectorNorm(object): + def test_vector_norm_1(self, f): v = np.array([0.2, 0.2, 0.2]) - n = self.f(v) + n = f(v) assert_allclose(n, np.linalg.norm(v), atol=_ATOL) - def test_vector_norm_2(self): + def test_vector_norm_2(self, f): v = np.sin(np.linspace(0, 10, 6 * 5 * 3)).reshape(6, 5, 3) - n = self.f(v, axis=-1) + n = f(v, axis=-1) assert_allclose(n, np.sqrt(np.sum(v * v, axis=2)), atol=_ATOL) - def test_vector_norm_3(self): + def test_vector_norm_3(self, f): v = np.sin(np.linspace(0, 10, 6 * 5 * 3)).reshape(6, 5, 3) - n = self.f(v, axis=1) + n = f(v, axis=1) assert_allclose(n, np.sqrt(np.sum(v * v, axis=1)), atol=_ATOL) - def test_vector_norm_4(self): + def test_vector_norm_4(self, f): v = np.sin(np.linspace(0, 10, 5 * 4 * 3)).reshape(5, 4, 3) n = np.empty((5, 3), dtype=np.float64) - self.f(v, axis=1, out=n) + f(v, axis=1, out=n) assert_allclose(n, np.sqrt(np.sum(v * v, axis=1)), atol=_ATOL) - def test_vector_norm_5(self): - assert_equal(self.f([]), 0.0) - - def test_vector_norm_6(self): - assert_equal(self.f([1.0]), 1.0) - - -class TestVectorNormNP(_VectorNorm): - f = staticmethod(t._py_vector_norm) - + def test_vector_norm_5(self, f): + assert_equal(f([]), 0.0) -class TestVectorNormCy(_VectorNorm): - f = staticmethod(t.vector_norm) + def test_vector_norm_6(self, f): + assert_equal(f([1.0]), 1.0) class TestArcBall(object): diff --git a/testsuite/MDAnalysisTests/utils/test_units.py b/testsuite/MDAnalysisTests/utils/test_units.py index 6e010d1e485..f2ae6218952 100644 --- a/testsuite/MDAnalysisTests/utils/test_units.py +++ b/testsuite/MDAnalysisTests/utils/test_units.py @@ -21,15 +21,16 @@ # from __future__ import unicode_literals, absolute_import import six +import pytest import numpy as np -from numpy.testing import assert_equal, assert_almost_equal, assert_raises,TestCase +from numpy.testing import assert_equal, assert_almost_equal, assert_raises from MDAnalysis import units from MDAnalysis.core import flags -class TestDefaultUnits(TestCase): +class TestDefaultUnits(object): def test_length(self): assert_equal(flags['length_unit'], 'Angstrom', u"The default length unit should be Angstrom (in core.flags)") @@ -43,7 +44,8 @@ def test_convert_gromacs_trajectories(self): u"The default behaviour should be to auto-convert Gromacs trajectories") -class TestUnitEncoding(TestCase): + +class TestUnitEncoding(object): def test_unicode(self): try: assert_equal(units.lengthUnit_factor[u"\u212b"], 1.0) @@ -68,9 +70,9 @@ class TestConstants(object): 'calorie': 4.184, # J } - def test_constant(self): - for name, value in six.iteritems(self.constants_reference): - yield self.check_physical_constant, name, value + @pytest.mark.parametrize('name, value', constants_reference.items()) + def test_constant(self, name, value): + self.check_physical_constant(name, value) @staticmethod def check_physical_constant(name, reference): @@ -80,33 +82,49 @@ def check_physical_constant(name, reference): class TestConversion(object): @staticmethod def _assert_almost_equal_convert(value, u1, u2, ref): - assert_almost_equal(units.convert(value, u1, u2), ref, + val = units.convert(value, u1, u2) + assert_almost_equal(val, ref, err_msg="Conversion {0} --> {1} failed".format(u1, u2)) - # generate individual test cases using nose's test generator mechanism - def test_length(self): - nm = 12.34567 - A = nm * 10. - yield self._assert_almost_equal_convert, nm, 'nm', 'A', A - yield self._assert_almost_equal_convert, A, 'Angstrom', 'nm', nm - - def test_time(self): - yield self._assert_almost_equal_convert, 1, 'ps', 'AKMA', 20.45482949774598 - yield self._assert_almost_equal_convert, 1, 'AKMA', 'ps', 0.04888821 - - def test_energy(self): - yield self._assert_almost_equal_convert, 1, 'kcal/mol', 'kJ/mol', 4.184 - yield self._assert_almost_equal_convert, 1, 'kcal/mol', 'eV', 0.0433641 - - def test_force(self): - yield self._assert_almost_equal_convert, 1, 'kJ/(mol*A)', 'J/m', 1.66053892103219e-11 - yield self._assert_almost_equal_convert, 2.5, 'kJ/(mol*nm)', 'kJ/(mol*A)', 0.25 - yield self._assert_almost_equal_convert, 1, 'kcal/(mol*Angstrom)', 'kJ/(mol*Angstrom)', 4.184 - - def test_unit_unknown(self): - nm = 12.34567 - assert_raises(ValueError, units.convert, nm, 'Stone', 'nm') - assert_raises(ValueError, units.convert, nm, 'nm', 'Stone') + nm = 12.34567 + A = nm * 10. + @pytest.mark.parametrize('quantity, unit1, unit2, ref', ( + (nm, 'nm', 'A', A), + (A, 'Angstrom', 'nm', nm), + )) + def test_length(self, quantity, unit1, unit2, ref): + self._assert_almost_equal_convert(quantity, unit1, unit2, ref) + + @pytest.mark.parametrize('quantity, unit1, unit2, ref', ( + (1, 'ps', 'AKMA', 20.45482949774598), + (1, 'AKMA', 'ps', 0.04888821), + )) + def test_time(self, quantity, unit1, unit2, ref): + self._assert_almost_equal_convert(quantity, unit1, unit2, ref) + + @pytest.mark.parametrize('quantity, unit1, unit2, ref', ( + (1, 'kcal/mol', 'kJ/mol', 4.184), + (1, 'kcal/mol', 'eV', 0.0433641), + )) + def test_energy(self, quantity, unit1, unit2, ref): + self._assert_almost_equal_convert(quantity, unit1, unit2, ref) + + @pytest.mark.parametrize('quantity, unit1, unit2, ref', ( + (1, 'kJ/(mol*A)', 'J/m', 1.66053892103219e-11), + (2.5, 'kJ/(mol*nm)', 'kJ/(mol*A)', 0.25), + (1, 'kcal/(mol*Angstrom)', 'kJ/(mol*Angstrom)', 4.184), + )) + def test_force(self, quantity, unit1, unit2, ref): + self._assert_almost_equal_convert(quantity, unit1, unit2, ref) + + @pytest.mark.parametrize('quantity, unit1, unit2, ref', ( + pytest.mark.raises((nm, 'Stone', 'nm', None), exception=ValueError), + pytest.mark.raises((nm, 'nm', 'Stone', None), exception=ValueError), + )) + def test_unit_unknown(self, quantity, unit1, unit2, ref): + val = units.convert(quantity, unit1, unit2) + assert_almost_equal(val, ref, + err_msg="Conversion {0} --> {1} failed".format(unit1, unit2)) def test_unit_unconvertable(self): nm = 12.34567