From 846131cb4a16cea48f3ccc56dce9a82995c8be27 Mon Sep 17 00:00:00 2001 From: Irfan Alibay Date: Thu, 11 Jan 2024 16:50:48 +0000 Subject: [PATCH] Deprecate waterdynamics and replace with waterdynamics mdakit (#4404) * towards #4310 * removed waterdynamics code and replaced with import of waterdynamics MDAKit * Add waterdynamics dep * Add deprecation docstring notice and see also * Add deprecationwarning test * remove unit tests * update changelog * Add removal version in changelog --- .github/actions/setup-deps/action.yaml | 3 + package/CHANGELOG | 2 + package/MDAnalysis/analysis/waterdynamics.py | 1034 +---------------- package/doc/sphinx/source/conf.py | 1 + package/pyproject.toml | 1 + package/setup.py | 1 + .../analysis/test_waterdynamics.py | 255 +--- 7 files changed, 30 insertions(+), 1267 deletions(-) diff --git a/.github/actions/setup-deps/action.yaml b/.github/actions/setup-deps/action.yaml index 1c5495429f..392aeeb428 100644 --- a/.github/actions/setup-deps/action.yaml +++ b/.github/actions/setup-deps/action.yaml @@ -51,6 +51,8 @@ inputs: default: 'threadpoolctl' tqdm: default: 'tqdm>=4.43.0' + waterdynamics: + default: 'waterdynamics' # conda-installed optional dependencies biopython: default: 'biopython>=1.80' @@ -124,6 +126,7 @@ runs: ${{ inputs.scipy }} ${{ inputs.threadpoolctl }} ${{ inputs.tqdm }} + ${{ inputs.waterdynamics }} CONDA_OPT_DEPS: | ${{ inputs.biopython }} ${{ inputs.chemfiles-python }} diff --git a/package/CHANGELOG b/package/CHANGELOG index 142db2aa8c..c5ae43a945 100644 --- a/package/CHANGELOG +++ b/package/CHANGELOG @@ -29,6 +29,8 @@ Changes is used from python 3.9 to 3.11 (Issue #4401, PR #4402) Deprecations + * The MDAnalysis.analysis.waterdynamics module has been deprecated in favour + of the waterdynamics MDAKit and will be removed in version 3.0.0 (PR #4404) * The MDAnalysis.analysis.psa module has been deprecated in favour of the PathSimAnalysis MDAKit and will be removed in version 3.0.0 (PR #4403) diff --git a/package/MDAnalysis/analysis/waterdynamics.py b/package/MDAnalysis/analysis/waterdynamics.py index 70769f6d19..df6867a3bf 100644 --- a/package/MDAnalysis/analysis/waterdynamics.py +++ b/package/MDAnalysis/analysis/waterdynamics.py @@ -30,1029 +30,27 @@ .. versionadded:: 0.11.0 -This module provides functions to analyze water dynamics trajectories and water -interactions with other molecules. The functions in this module are: water -orientational relaxation (WOR) :footcite:p:`Yeh1999`, hydrogen bond lifetimes (HBL) -:footcite:p:`Rapaport1983`, angular distribution (AD) :footcite:p:`Grigera1995`, mean -square displacement (MSD) :footcite:p:`Brodka1994` and survival probability (SP) -:footcite:p:`Liu2004`. - -For more information about this type of analysis please refer to -:footcite:p:`ArayaSecchi2014` (water in a protein cavity) and -:footcite:p:`Milischuk2011` (water in a nanopore). - -.. rubric:: References - -.. footbibliography:: - - -Example use of the analysis classes ------------------------------------ - -HydrogenBondLifetimes -~~~~~~~~~~~~~~~~~~~~~ - -To analyse hydrogen bond lifetime, use -:meth:`MDAnalysis.analysis.hydrogenbonds.hbond_analysis.HydrogenBondAnalysis.lifetime`. +.. deprecated:: 2.8.0 + This module is deprecated in favour of the mdakit + `waterdynamics `_ and + will be removed in MDAnalysis 3.0.0. See Also -------- -:mod:`MDAnalysis.analysis.hydrogenbonds.hbond_analysis` - - -WaterOrientationalRelaxation -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Analyzing water orientational relaxation (WOR) -:class:`WaterOrientationalRelaxation`. In this case we are analyzing "how fast" -water molecules are rotating/changing direction. If WOR is very stable we can -assume that water molecules are rotating/changing direction very slow, on the -other hand, if WOR decay very fast, we can assume that water molecules are -rotating/changing direction very fast:: - - import MDAnalysis - from MDAnalysis.analysis.waterdynamics import WaterOrientationalRelaxation as WOR - - u = MDAnalysis.Universe(pdb, trajectory) - select = "byres name OH2 and sphzone 6.0 protein and resid 42" - WOR_analysis = WOR(universe, select, 0, 1000, 20) - WOR_analysis.run() - time = 0 - #now we print the data ready to plot. The first two columns are WOR_OH vs t plot, - #the second two columns are WOR_HH vs t graph and the third two columns are WOR_dip vs t graph - for WOR_OH, WOR_HH, WOR_dip in WOR_analysis.timeseries: - print("{time} {WOR_OH} {time} {WOR_HH} {time} {WOR_dip}".format(time=time, WOR_OH=WOR_OH, WOR_HH=WOR_HH,WOR_dip=WOR_dip)) - time += 1 - - #now, if we want, we can plot our data - plt.figure(1,figsize=(18, 6)) - - #WOR OH - plt.subplot(131) - plt.xlabel('time') - plt.ylabel('WOR') - plt.title('WOR OH') - plt.plot(range(0,time),[column[0] for column in WOR_analysis.timeseries]) - - #WOR HH - plt.subplot(132) - plt.xlabel('time') - plt.ylabel('WOR') - plt.title('WOR HH') - plt.plot(range(0,time),[column[1] for column in WOR_analysis.timeseries]) - - #WOR dip - plt.subplot(133) - plt.xlabel('time') - plt.ylabel('WOR') - plt.title('WOR dip') - plt.plot(range(0,time),[column[2] for column in WOR_analysis.timeseries]) - - plt.show() - -where t0 = 0, tf = 1000 and dtmax = 20. In this way we create 20 windows -timesteps (20 values in the x axis), the first window is created with 1000 -timestep average (1000/1), the second window is created with 500 timestep -average(1000/2), the third window is created with 333 timestep average (1000/3) -and so on. - -AngularDistribution -~~~~~~~~~~~~~~~~~~~ - -Analyzing angular distribution (AD) :class:`AngularDistribution` for OH vector, -HH vector and dipole vector. It returns a line histogram with vector -orientation preference. A straight line in the output plot means no -preferential orientation in water molecules. In this case we are analyzing if -water molecules have some orientational preference, in this way we can see if -water molecules are under an electric field or if they are interacting with -something (residue, protein, etc):: - - import MDAnalysis - from MDAnalysis.analysis.waterdynamics import AngularDistribution as AD - - u = MDAnalysis.Universe(pdb, trajectory) - selection = "byres name OH2 and sphzone 6.0 (protein and (resid 42 or resid 26) )" - bins = 30 - AD_analysis = AD(universe,selection,bins) - AD_analysis.run() - #now we print data ready to graph. The first two columns are P(cos(theta)) vs cos(theta) for OH vector , - #the seconds two columns are P(cos(theta)) vs cos(theta) for HH vector and thirds two columns - #are P(cos(theta)) vs cos(theta) for dipole vector - for bin in range(bins): - print("{AD_analysisOH} {AD_analysisHH} {AD_analysisDip}".format(AD_analysis.graph0=AD_analysis.graph[0][bin], AD_analysis.graph1=AD_analysis.graph[1][bin],AD_analysis.graph2=AD_analysis.graph[2][bin])) - - #and if we want to graph our results - plt.figure(1,figsize=(18, 6)) - - #AD OH - plt.subplot(131) - plt.xlabel('cos theta') - plt.ylabel('P(cos theta)') - plt.title('PDF cos theta for OH') - plt.plot([float(column.split()[0]) for column in AD_analysis.graph[0][:-1]],[float(column.split()[1]) for column in AD_analysis.graph[0][:-1]]) - - #AD HH - plt.subplot(132) - plt.xlabel('cos theta') - plt.ylabel('P(cos theta)') - plt.title('PDF cos theta for HH') - plt.plot([float(column.split()[0]) for column in AD_analysis.graph[1][:-1]],[float(column.split()[1]) for column in AD_analysis.graph[1][:-1]]) - - #AD dip - plt.subplot(133) - plt.xlabel('cos theta') - plt.ylabel('P(cos theta)') - plt.title('PDF cos theta for dipole') - plt.plot([float(column.split()[0]) for column in AD_analysis.graph[2][:-1]],[float(column.split()[1]) for column in AD_analysis.graph[2][:-1]]) - - plt.show() - - -where `P(cos(theta))` is the angular distribution or angular probabilities. - - -MeanSquareDisplacement -~~~~~~~~~~~~~~~~~~~~~~ - -Analyzing mean square displacement (MSD) :class:`MeanSquareDisplacement` for -water molecules. In this case we are analyzing the average distance that water -molecules travels inside protein in XYZ direction (cylindric zone of radius -11[nm], Zmax 4.0[nm] and Zmin -8.0[nm]). A strong rise mean a fast movement of -water molecules, a weak rise mean slow movement of particles:: - - import MDAnalysis - from MDAnalysis.analysis.waterdynamics import MeanSquareDisplacement as MSD - - u = MDAnalysis.Universe(pdb, trajectory) - select = "byres name OH2 and cyzone 11.0 4.0 -8.0 protein" - MSD_analysis = MSD(universe, select, 0, 1000, 20) - MSD_analysis.run() - #now we print data ready to graph. The graph - #represents MSD vs t - time = 0 - for msd in MSD_analysis.timeseries: - print("{time} {msd}".format(time=time, msd=msd)) - time += 1 - - #Plot - plt.xlabel('time') - plt.ylabel('MSD') - plt.title('MSD') - plt.plot(range(0,time),MSD_analysis.timeseries) - plt.show() - - -.. _SP-examples: - -SurvivalProbability -~~~~~~~~~~~~~~~~~~~ - -Analyzing survival probability (SP) :class:`SurvivalProbability` of molecules. -In this case we are analyzing how long water molecules remain in a -sphere of radius 12.3 centered in the geometrical center of resid 42 and 26. -A slow decay of SP means a long permanence time of water molecules in -the zone, on the other hand, a fast decay means a short permanence time:: - - import MDAnalysis - from MDAnalysis.analysis.waterdynamics import SurvivalProbability as SP - import matplotlib.pyplot as plt - - universe = MDAnalysis.Universe(pdb, trajectory) - select = "byres name OH2 and sphzone 12.3 (resid 42 or resid 26) " - sp = SP(universe, select, verbose=True) - sp.run(start=0, stop=101, tau_max=20) - tau_timeseries = sp.tau_timeseries - sp_timeseries = sp.sp_timeseries - - # print in console - for tau, sp in zip(tau_timeseries, sp_timeseries): - print("{time} {sp}".format(time=tau, sp=sp)) - - # plot - plt.xlabel('Time') - plt.ylabel('SP') - plt.title('Survival Probability') - plt.plot(tau_timeseries, sp_timeseries) - plt.show() - -One should note that the `stop` keyword as used in the above example has an -`exclusive` behaviour, i.e. here the final frame used will be 100 not 101. -This behaviour is aligned with :class:`AnalysisBase` but currently differs from -other :mod:`MDAnalysis.analysis.waterdynamics` classes, which all exhibit -`inclusive` behaviour for their final frame selections. - -Another example applies to the situation where you work with many different "residues". -Here we calculate the SP of a potassium ion around each lipid in a membrane and -average the results. In this example, if the SP analysis were run without treating each lipid -separately, potassium ions may hop from one lipid to another and still be counted as remaining -in the specified region. That is, the survival probability of the potassium ion around the -entire membrane will be calculated. - -Note, for this example, it is advisable to use `Universe(in_memory=True)` to ensure that the -simulation is not being reloaded into memory for each lipid:: - - import MDAnalysis as mda - from MDAnalysis.analysis.waterdynamics import SurvivalProbability as SP - import numpy as np - - u = mda.Universe("md.gro", "md100ns.xtc", in_memory=True) - lipids = u.select_atoms('resname LIPIDS') - joined_sp_timeseries = [[] for _ in range(20)] - for lipid in lipids.residues: - print("Lipid ID: %d" % lipid.resid) - - select = "resname POTASSIUM and around 3.5 (resid %d and name O13 O14) " % lipid.resid - sp = SP(u, select, verbose=True) - sp.run(tau_max=20) - - # Raw SP points for each tau: - for sps, new_sps in zip(joined_sp_timeseries, sp.sp_timeseries_data): - sps.extend(new_sps) - - # average all SP datapoints - sp_data = [np.mean(sp) for sp in joined_sp_timeseries] - - for tau, sp in zip(range(1, tau_max + 1), sp_data): - print("{time} {sp}".format(time=tau, sp=sp)) - -.. _Output: - -Output ------- - -WaterOrientationalRelaxation -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Water orientational relaxation (WOR) data is returned per window timestep, -which is stored in :attr:`WaterOrientationalRelaxation.timeseries`:: - - results = [ - [ # time t0 - , , - ], - [ # time t1 - , , - ], - ... - ] - -AngularDistribution -~~~~~~~~~~~~~~~~~~~ - -Angular distribution (AD) data is returned per vector, which is stored in -:attr:`AngularDistribution.graph`. In fact, AngularDistribution returns a -histogram:: - - results = [ - [ # OH vector values - # the values are order in this way: - , , ... - ], - [ # HH vector values - , , ... - ], - [ # dip vector values - , , ... - ], - ] - -MeanSquareDisplacement -~~~~~~~~~~~~~~~~~~~~~~ - -Mean Square Displacement (MSD) data is returned in a list, which each element -represents a MSD value in its respective window timestep. Data is stored in -:attr:`MeanSquareDisplacement.timeseries`:: - - results = [ - #MSD values orders by window timestep - , , ... - ] - -SurvivalProbability -~~~~~~~~~~~~~~~~~~~ - -Survival Probability (SP) computes two lists: a list of taus (:attr:`SurvivalProbability.tau_timeseries`) and a list of -the corresponding survival probabilities (:attr:`SurvivalProbability.sp_timeseries`). - - results = [ tau1, tau2, ..., tau_n ], [ sp_tau1, sp_tau2, ..., sp_tau_n] - -Additionally, a list :attr:`SurvivalProbability.sp_timeseries_data`, is provided which contains -a list of all SPs calculated for each tau. This can be used to compute the distribution or time dependence of SP, etc. - -Classes --------- - -.. autoclass:: WaterOrientationalRelaxation - :members: - :inherited-members: - -.. autoclass:: AngularDistribution - :members: - :inherited-members: - -.. autoclass:: MeanSquareDisplacement - :members: - :inherited-members: - -.. autoclass:: SurvivalProbability - :members: - :inherited-members: - +:mod:`waterdynamics.waterdynamics` """ -from MDAnalysis.lib.correlations import autocorrelation, correct_intermittency -import MDAnalysis.analysis.hbonds -from itertools import zip_longest -import logging import warnings -import numpy as np - - -logger = logging.getLogger('MDAnalysis.analysis.waterdynamics') -from MDAnalysis.lib.log import ProgressBar - - -class WaterOrientationalRelaxation(object): - r"""Water orientation relaxation analysis - - Function to evaluate the Water Orientational Relaxation proposed by Yu-ling - Yeh and Chung-Yuan Mou :footcite:p:`Yeh1999`. WaterOrientationalRelaxation - indicates "how fast" water molecules are rotating or changing direction. - This is a time correlation function given by: - - .. math:: - C_{\hat u}(\tau)=\langle \mathit{P}_2[\mathbf{\hat{u}}(t_0)\cdot\mathbf{\hat{u}}(t_0+\tau)]\rangle - - where :math:`P_2=(3x^2-1)/2` is the second-order Legendre polynomial and :math:`\hat{u}` is - a unit vector along HH, OH or dipole vector. - - - Parameters - ---------- - universe : Universe - Universe object - selection : str - Selection string for water [‘byres name OH2’]. - t0 : int - frame where analysis begins - tf : int - frame where analysis ends - dtmax : int - Maximum dt size, `dtmax` < `tf` or it will crash. - - - .. versionadded:: 0.11.0 - - .. versionchanged:: 1.0.0 - Changed `selection` keyword to `select` - """ - - def __init__(self, universe, select, t0, tf, dtmax, nproc=1): - self.universe = universe - self.selection = select - self.t0 = t0 - self.tf = tf - self.dtmax = dtmax - self.nproc = nproc - self.timeseries = None - - def _repeatedIndex(self, selection, dt, totalFrames): - """ - Indicates the comparation between all the t+dt. - The results is a list of list with all the repeated index per frame - (or time). - Ex: dt=1, so compare frames (1,2),(2,3),(3,4)... - Ex: dt=2, so compare frames (1,3),(3,5),(5,7)... - Ex: dt=3, so compare frames (1,4),(4,7),(7,10)... - """ - rep = [] - for i in range(int(round((totalFrames - 1) / float(dt)))): - if (dt * i + dt < totalFrames): - rep.append(self._sameMolecTandDT( - selection, dt * i, (dt * i) + dt)) - return rep - - def _getOneDeltaPoint(self, universe, repInd, i, t0, dt): - """ - Gives one point to calculate the mean and gets one point of the plot - C_vect vs t. - Ex: t0=1 and tau=1 so calculate the t0-tau=1-2 intervale. - Ex: t0=5 and tau=3 so calcultate the t0-tau=5-8 intervale. - i = come from getMeanOnePoint (named j) (int) - """ - valOH = 0 - valHH = 0 - valdip = 0 - n = 0 - for j in range(len(repInd[i]) // 3): - begj = 3 * j - universe.trajectory[t0] - Ot0 = repInd[i][begj] - H1t0 = repInd[i][begj + 1] - H2t0 = repInd[i][begj + 2] - OHVector0 = H1t0.position - Ot0.position - HHVector0 = H1t0.position - H2t0.position - dipVector0 = ((H1t0.position + H2t0.position) * 0.5) - Ot0.position - - universe.trajectory[t0 + dt] - Otp = repInd[i][begj] - H1tp = repInd[i][begj + 1] - H2tp = repInd[i][begj + 2] - - OHVectorp = H1tp.position - Otp.position - HHVectorp = H1tp.position - H2tp.position - dipVectorp = ((H1tp.position + H2tp.position) * 0.5) - Otp.position - - normOHVector0 = np.linalg.norm(OHVector0) - normOHVectorp = np.linalg.norm(OHVectorp) - normHHVector0 = np.linalg.norm(HHVector0) - normHHVectorp = np.linalg.norm(HHVectorp) - normdipVector0 = np.linalg.norm(dipVector0) - normdipVectorp = np.linalg.norm(dipVectorp) - - unitOHVector0 = [OHVector0[0] / normOHVector0, - OHVector0[1] / normOHVector0, - OHVector0[2] / normOHVector0] - unitOHVectorp = [OHVectorp[0] / normOHVectorp, - OHVectorp[1] / normOHVectorp, - OHVectorp[2] / normOHVectorp] - unitHHVector0 = [HHVector0[0] / normHHVector0, - HHVector0[1] / normHHVector0, - HHVector0[2] / normHHVector0] - unitHHVectorp = [HHVectorp[0] / normHHVectorp, - HHVectorp[1] / normHHVectorp, - HHVectorp[2] / normHHVectorp] - unitdipVector0 = [dipVector0[0] / normdipVector0, - dipVector0[1] / normdipVector0, - dipVector0[2] / normdipVector0] - unitdipVectorp = [dipVectorp[0] / normdipVectorp, - dipVectorp[1] / normdipVectorp, - dipVectorp[2] / normdipVectorp] - - valOH += self.lg2(np.dot(unitOHVector0, unitOHVectorp)) - valHH += self.lg2(np.dot(unitHHVector0, unitHHVectorp)) - valdip += self.lg2(np.dot(unitdipVector0, unitdipVectorp)) - n += 1 - return (valOH/n, valHH/n, valdip/n) if n > 0 else (0, 0, 0) - - - def _getMeanOnePoint(self, universe, selection1, selection_str, dt, - totalFrames): - """ - This function gets one point of the plot C_vec vs t. It uses the - _getOneDeltaPoint() function to calculate the average. - - """ - repInd = self._repeatedIndex(selection1, dt, totalFrames) - sumsdt = 0 - n = 0.0 - sumDeltaOH = 0.0 - sumDeltaHH = 0.0 - sumDeltadip = 0.0 - - for j in range(totalFrames // dt - 1): - a = self._getOneDeltaPoint(universe, repInd, j, sumsdt, dt) - sumDeltaOH += a[0] - sumDeltaHH += a[1] - sumDeltadip += a[2] - sumsdt += dt - n += 1 - - # if no water molecules remain in selection, there is nothing to get - # the mean, so n = 0. - return (sumDeltaOH / n, sumDeltaHH / n, sumDeltadip / n) if n > 0 else (0, 0, 0) - - def _sameMolecTandDT(self, selection, t0d, tf): - """ - Compare the molecules in the t0d selection and the t0d+dt selection and - select only the particles that are repeated in both frame. This is to - consider only the molecules that remains in the selection after the dt - time has elapsed. - The result is a list with the indices of the atoms. - """ - a = set(selection[t0d]) - b = set(selection[tf]) - sort = sorted(list(a.intersection(b))) - return sort - - def _selection_serial(self, universe, selection_str): - selection = [] - for ts in ProgressBar(universe.trajectory, verbose=True, - total=universe.trajectory.n_frames): - selection.append(universe.select_atoms(selection_str)) - return selection - - @staticmethod - def lg2(x): - """Second Legendre polynomial""" - return (3*x*x - 1)/2 - - def run(self, **kwargs): - """Analyze trajectory and produce timeseries""" - - # All the selection to an array, this way is faster than selecting - # later. - if self.nproc == 1: - selection_out = self._selection_serial( - self.universe, self.selection) - else: - # selection_out = self._selection_parallel(self.universe, - # self.selection, self.nproc) - # parallel selection to be implemented - selection_out = self._selection_serial( - self.universe, self.selection) - self.timeseries = [] - for dt in list(range(1, self.dtmax + 1)): - output = self._getMeanOnePoint( - self.universe, selection_out, self.selection, dt, self.tf) - self.timeseries.append(output) - - -class AngularDistribution(object): - r"""Angular distribution function analysis - - The angular distribution function (AD) is defined as the distribution - probability of the cosine of the :math:`\theta` angle formed by the OH - vector, HH vector or dipolar vector of water molecules and a vector - :math:`\hat n` parallel to chosen axis (z is the default value). The cosine - is define as :math:`\cos \theta = \hat u \cdot \hat n`, where :math:`\hat - u` is OH, HH or dipole vector. It creates a histogram and returns a list - of lists, see Output_. The AD is also know as Angular Probability (AP). - - - Parameters - ---------- - universe : Universe - Universe object - select : str - Selection string to evaluate its angular distribution ['byres name OH2'] - bins : int (optional) - Number of bins to create the histogram by means of :func:`numpy.histogram` - axis : {'x', 'y', 'z'} (optional) - Axis to create angle with the vector (HH, OH or dipole) and calculate - cosine theta ['z']. - - - .. versionadded:: 0.11.0 - - .. versionchanged:: 1.0.0 - Changed `selection` keyword to `select` - """ - - def __init__(self, universe, select, bins=40, nproc=1, axis="z"): - self.universe = universe - self.selection_str = select - self.bins = bins - self.nproc = nproc - self.axis = axis - self.graph = None - - def _getCosTheta(self, universe, selection, axis): - valOH = [] - valHH = [] - valdip = [] - - i = 0 - while i <= (len(selection) - 1): - universe.trajectory[i] - line = selection[i].positions - - Ot0 = line[::3] - H1t0 = line[1::3] - H2t0 = line[2::3] - - OHVector0 = H1t0 - Ot0 - HHVector0 = H1t0 - H2t0 - dipVector0 = (H1t0 + H2t0) * 0.5 - Ot0 - - unitOHVector0 = OHVector0 / \ - np.linalg.norm(OHVector0, axis=1)[:, None] - unitHHVector0 = HHVector0 / \ - np.linalg.norm(HHVector0, axis=1)[:, None] - unitdipVector0 = dipVector0 / \ - np.linalg.norm(dipVector0, axis=1)[:, None] - - j = 0 - while j < len(line) / 3: - if axis == "z": - valOH.append(unitOHVector0[j][2]) - valHH.append(unitHHVector0[j][2]) - valdip.append(unitdipVector0[j][2]) - - elif axis == "x": - valOH.append(unitOHVector0[j][0]) - valHH.append(unitHHVector0[j][0]) - valdip.append(unitdipVector0[j][0]) - - elif axis == "y": - valOH.append(unitOHVector0[j][1]) - valHH.append(unitHHVector0[j][1]) - valdip.append(unitdipVector0[j][1]) - - j += 1 - i += 1 - return (valOH, valHH, valdip) - - def _getHistogram(self, universe, selection, bins, axis): - """ - This function gets a normalized histogram of the cos(theta) values. It - return a list of list. - """ - a = self._getCosTheta(universe, selection, axis) - cosThetaOH = a[0] - cosThetaHH = a[1] - cosThetadip = a[2] - lencosThetaOH = len(cosThetaOH) - lencosThetaHH = len(cosThetaHH) - lencosThetadip = len(cosThetadip) - histInterval = bins - histcosThetaOH = np.histogram(cosThetaOH, histInterval, density=True) - histcosThetaHH = np.histogram(cosThetaHH, histInterval, density=True) - histcosThetadip = np.histogram(cosThetadip, histInterval, density=True) - - return (histcosThetaOH, histcosThetaHH, histcosThetadip) - - def _hist2column(self, aList): - """ - This function transform from the histogram format - to a column format. - """ - a = [] - for x in zip_longest(*aList, fillvalue="."): - a.append(" ".join(str(i) for i in x)) - return a - - def run(self, **kwargs): - """Function to evaluate the angular distribution of cos(theta)""" - - if self.nproc == 1: - selection = self._selection_serial( - self.universe, self.selection_str) - else: - # not implemented yet - # selection = self._selection_parallel(self.universe, - # self.selection_str,self.nproc) - selection = self._selection_serial( - self.universe, self.selection_str) - - self.graph = [] - output = self._getHistogram( - self.universe, selection, self.bins, self.axis) - # this is to format the exit of the file - # maybe this output could be improved - listOH = [list(output[0][1]), list(output[0][0])] - listHH = [list(output[1][1]), list(output[1][0])] - listdip = [list(output[2][1]), list(output[2][0])] - - self.graph.append(self._hist2column(listOH)) - self.graph.append(self._hist2column(listHH)) - self.graph.append(self._hist2column(listdip)) - - def _selection_serial(self, universe, selection_str): - selection = [] - for ts in ProgressBar(universe.trajectory, verbose=True, - total=universe.trajectory.n_frames): - selection.append(universe.select_atoms(selection_str)) - return selection - - -class MeanSquareDisplacement(object): - r"""Mean square displacement analysis - - Function to evaluate the Mean Square Displacement (MSD_). The MSD gives the - average distance that particles travels. The MSD is given by: - - .. math:: - \langle\Delta r(t)^2\rangle = 2nDt - - where :math:`r(t)` is the position of particle in time :math:`t`, - :math:`\Delta r(t)` is the displacement after time lag :math:`t`, - :math:`n` is the dimensionality, in this case :math:`n=3`, - :math:`D` is the diffusion coefficient and :math:`t` is the time. - - .. _MSD: http://en.wikipedia.org/wiki/Mean_squared_displacement - - - Parameters - ---------- - universe : Universe - Universe object - select : str - Selection string for water [‘byres name OH2’]. - t0 : int - frame where analysis begins - tf : int - frame where analysis ends - dtmax : int - Maximum dt size, `dtmax` < `tf` or it will crash. - - - .. versionadded:: 0.11.0 - - .. versionchanged:: 1.0.0 - Changed `selection` keyword to `select` - """ - - def __init__(self, universe, select, t0, tf, dtmax, nproc=1): - self.universe = universe - self.selection = select - self.t0 = t0 - self.tf = tf - self.dtmax = dtmax - self.nproc = nproc - self.timeseries = None - - def _repeatedIndex(self, selection, dt, totalFrames): - """ - Indicate the comparation between all the t+dt. - The results is a list of list with all the repeated index per frame - (or time). - - - Ex: dt=1, so compare frames (1,2),(2,3),(3,4)... - - Ex: dt=2, so compare frames (1,3),(3,5),(5,7)... - - Ex: dt=3, so compare frames (1,4),(4,7),(7,10)... - """ - rep = [] - for i in range(int(round((totalFrames - 1) / float(dt)))): - if (dt * i + dt < totalFrames): - rep.append(self._sameMolecTandDT( - selection, dt * i, (dt * i) + dt)) - return rep - - def _getOneDeltaPoint(self, universe, repInd, i, t0, dt): - """ - Gives one point to calculate the mean and gets one point of the plot - C_vect vs t. - - - Ex: t0=1 and dt=1 so calculate the t0-dt=1-2 interval. - - Ex: t0=5 and dt=3 so calcultate the t0-dt=5-8 interval - - i = come from getMeanOnePoint (named j) (int) - """ - valO = 0 - n = 0 - for j in range(len(repInd[i]) // 3): - begj = 3 * j - universe.trajectory[t0] - # Plus zero is to avoid 0to be equal to 0tp - Ot0 = repInd[i][begj].position + 0 - - universe.trajectory[t0 + dt] - # Plus zero is to avoid 0to be equal to 0tp - Otp = repInd[i][begj].position + 0 - - # position oxygen - OVector = Ot0 - Otp - # here it is the difference with - # waterdynamics.WaterOrientationalRelaxation - valO += np.dot(OVector, OVector) - n += 1 - - # if no water molecules remain in selection, there is nothing to get - # the mean, so n = 0. - return valO/n if n > 0 else 0 - - def _getMeanOnePoint(self, universe, selection1, selection_str, dt, - totalFrames): - """ - This function gets one point of the plot C_vec vs t. It's uses the - _getOneDeltaPoint() function to calculate the average. - - """ - repInd = self._repeatedIndex(selection1, dt, totalFrames) - sumsdt = 0 - n = 0.0 - sumDeltaO = 0.0 - valOList = [] - - for j in range(totalFrames // dt - 1): - a = self._getOneDeltaPoint(universe, repInd, j, sumsdt, dt) - sumDeltaO += a - valOList.append(a) - sumsdt += dt - n += 1 - - # if no water molecules remain in selection, there is nothing to get - # the mean, so n = 0. - return sumDeltaO/n if n > 0 else 0 - - def _sameMolecTandDT(self, selection, t0d, tf): - """ - Compare the molecules in the t0d selection and the t0d+dt selection and - select only the particles that are repeated in both frame. This is to - consider only the molecules that remains in the selection after the dt - time has elapsed. The result is a list with the indexs of the atoms. - """ - a = set(selection[t0d]) - b = set(selection[tf]) - sort = sorted(list(a.intersection(b))) - return sort - - def _selection_serial(self, universe, selection_str): - selection = [] - for ts in ProgressBar(universe.trajectory, verbose=True, - total=universe.trajectory.n_frames): - selection.append(universe.select_atoms(selection_str)) - return selection - - def run(self, **kwargs): - """Analyze trajectory and produce timeseries""" - - # All the selection to an array, this way is faster than selecting - # later. - if self.nproc == 1: - selection_out = self._selection_serial( - self.universe, self.selection) - else: - # parallel not yet implemented - # selection = selection_parallel(universe, selection_str, nproc) - selection_out = self._selection_serial( - self.universe, self.selection) - self.timeseries = [] - for dt in list(range(1, self.dtmax + 1)): - output = self._getMeanOnePoint( - self.universe, selection_out, self.selection, dt, self.tf) - self.timeseries.append(output) - - -class SurvivalProbability(object): - r""" - Survival Probability (SP) gives the probability for a group of particles to remain - in a certain region. The SP is given by: - - .. math:: - P(\tau) = \langle \frac{ N(t, t + \tau )} { N(t) }\rangle - - where :math:`\tau` is the timestep, :math:`N(t)` the number of particles at time - :math:`t`, and :math:`N(t, t+\tau)` is the number of particles at every frame from - :math:`t` to :math:`t + \tau`. The angular brackets represent an average over all time - origins, :math:`t`. See :func:`MDAnalysis.lib.correlations.autocorrelation` for - technical details. - - - Parameters - ---------- - universe : Universe - Universe object - select : str - Selection string; any selection is allowed. With this selection you - define the region/zone where to analyze, e.g.: "resname SOL and around 5 - (resid 10)". See `SP-examples`_. - verbose : Boolean, optional - When True, prints progress and comments to the console. - - - Notes - ----- - Currently :class:`SurvivalProbability` is the only on in - :mod:`MDAnalysis.analysis.waterdynamics` to support an `exclusive` - behaviour (i.e. similar to the current behaviour of :class:`AnalysisBase` - to the `stop` keyword passed to :meth:`SurvivalProbability.run`. Unlike - other :mod:`MDAnalysis.analysis.waterdynamics` final frame definitions - which are `inclusive`. - - - .. versionadded:: 0.11.0 - .. versionchanged:: 1.0.0 - Using the MDAnalysis.lib.correlations.py to carry out the intermittency - and autocorrelation calculations. - Changed `selection` keyword to `select`. - Removed support for the deprecated `t0`, `tf`, and `dtmax` keywords. - These should instead be passed to :meth:`SurvivalProbability.run` as - the `start`, `stop`, and `tau_max` keywords respectively. - The `stop` keyword as passed to :meth:`SurvivalProbability.run` has now - changed behaviour and will act in an `exclusive` manner (instead of it's - previous `inclusive` behaviour), - .. versionchanged:: 2.7.0 - Updated docs to align with discrete autocorrelation function. - """ - - def __init__(self, universe, select, verbose=False): - self.universe = universe - self.selection = select - self.verbose = verbose - - def run(self, tau_max=20, start=None, stop=None, step=None, residues=False, - intermittency=0, verbose=False): - """ - Computes and returns the Survival Probability (SP) timeseries - - Parameters - ---------- - start : int, optional - Zero-based index of the first frame to be analysed, Default: None - (first frame). - stop : int, optional - Zero-based index of the last frame to be analysed (exclusive), - Default: None (last frame). - step : int, optional - Jump every `step`-th frame. This is compatible but independant of - the taus used, and it is good to consider using the `step` equal - to `tau_max` to remove the overlap. Note that `step` and `tau_max` - work consistently with intermittency. Default: None - (use every frame). - tau_max : int, optional - Survival probability is calculated for the range - 1 <= `tau` <= `tau_max`. - residues : Boolean, optional - If true, the analysis will be carried out on the residues - (.resids) rather than on atom (.ids). A single atom is sufficient - to classify the residue as within the distance. - intermittency : int, optional - The maximum number of consecutive frames for which an atom can - leave but be counted as present if it returns at the next frame. - An intermittency of `0` is equivalent to a continuous survival - probability, which does not allow for the leaving and returning of - atoms. For example, for `intermittency=2`, any given atom may leave - a region of interest for up to two consecutive frames yet be - treated as being present at all frames. The default is continuous - (0). - verbose : Boolean, optional - Print the progress to the console. - - Returns - ------- - tau_timeseries : list - tau from 1 to `tau_max`. Saved in the field tau_timeseries. - sp_timeseries : list - survival probability for each value of `tau`. Saved in the field - sp_timeseries. - sp_timeseries_data: list - raw datapoints from which the average is taken (sp_timeseries). - Time dependancy and distribution can be extracted. - - - .. versionchanged:: 1.0.0 - To math other analysis methods, the `stop` keyword is now exclusive - rather than inclusive. - """ - - start, stop, step = self.universe.trajectory.check_slice_indices( - start, - stop, - step - ) - - if tau_max > (stop - start): - raise ValueError("Too few frames selected for given tau_max.") - - # preload the frames (atom IDs) to a list of sets - self._selected_ids = [] - - # fixme - to parallise: the section should be rewritten so that this loop only creates a list of indices, - # on which the parallel _single_frame can be applied. - - # skip frames that will not be used in order to improve performance - # because AtomGroup.select_atoms is the most expensive part of this calculation - # Example: step 5 and tau 2: LLLSS LLLSS, ... where L = Load, and S = Skip - # Intermittency means that we have to load the extra frames to know if the atom is actually missing. - # Say step=5 and tau=1, intermittency=0: LLSSS LLSSS - # Say step=5 and tau=1, intermittency=1: LLLSL LLLSL - frame_loaded_counter = 0 - # only for the first window (frames before t are not used) - frames_per_window = tau_max + 1 + intermittency - # This number will apply after the first windows was loaded - frames_per_window_subsequent = (tau_max + 1) + (2 * intermittency) - num_frames_to_skip = max(step - frames_per_window_subsequent, 0) - - frame_no = start - while frame_no < stop: # we have already added 1 to stop, therefore < - if num_frames_to_skip != 0 and frame_loaded_counter == frames_per_window: - logger.info("Skipping the next %d frames:", num_frames_to_skip) - frame_no += num_frames_to_skip - frame_loaded_counter = 0 - # Correct the number of frames to be loaded after the first window (which starts at t=0, and - # intermittency does not apply to the frames before) - frames_per_window = frames_per_window_subsequent - continue - - # update the frame number - self.universe.trajectory[frame_no] - - logger.info("Loading frame: %d", self.universe.trajectory.frame) - atoms = self.universe.select_atoms(self.selection) - - # SP of residues or of atoms - ids = atoms.residues.resids if residues else atoms.ids - self._selected_ids.append(set(ids)) - - frame_no += 1 - frame_loaded_counter += 1 - - # adjust for the frames that were not loaded (step>tau_max + 1), - # and for extra frames that were loaded (intermittency) - window_jump = step - num_frames_to_skip - - self._intermittent_selected_ids = correct_intermittency(self._selected_ids, intermittency=intermittency) - tau_timeseries, sp_timeseries, sp_timeseries_data = autocorrelation(self._intermittent_selected_ids, - tau_max, window_jump) - # warn the user if the NaN are found - if all(np.isnan(sp_timeseries[1:])): - logger.warning('NaN Error: Most likely data was not found. Check your atom selections. ') +from waterdynamics.waterdynamics import ( + WaterOrientationalRelaxation, + AngularDistribution, + MeanSquareDisplacement, + SurvivalProbability, +) - # user can investigate the distribution and sample size - self.sp_timeseries_data = sp_timeseries_data - self.tau_timeseries = tau_timeseries - self.sp_timeseries = sp_timeseries - return self +wmsg = ("Deprecation in version 2.8.0\n" + "MDAnalysis.analysis.waterdynamics is deprecated in favour of the " + "MDAKit waterdynamics (https://www.mdanalysis.org/waterdynamics/) " + "and will be removed in MDAnalysis version 3.0.0") +warnings.warn(wmsg, category=DeprecationWarning) diff --git a/package/doc/sphinx/source/conf.py b/package/doc/sphinx/source/conf.py index 657840b757..7c644b122f 100644 --- a/package/doc/sphinx/source/conf.py +++ b/package/doc/sphinx/source/conf.py @@ -348,5 +348,6 @@ class KeyStyle(UnsrtStyle): 'numpy': ('https://numpy.org/doc/stable/', None), 'parmed': ('https://parmed.github.io/ParmEd/html/', None), 'rdkit': ('https://rdkit.org/docs/', None), + 'waterdynamics': ('https://www.mdanalysis.org/waterdynamics/', None), 'pathsimanalysis': ('https://www.mdanalysis.org/PathSimAnalysis/', None), } diff --git a/package/pyproject.toml b/package/pyproject.toml index 74d90abf0e..5b81bc4e00 100644 --- a/package/pyproject.toml +++ b/package/pyproject.toml @@ -40,6 +40,7 @@ dependencies = [ 'packaging', 'fasteners', 'mda-xdrlib', + 'waterdynamics', 'pathsimanalysis', ] keywords = [ diff --git a/package/setup.py b/package/setup.py index c661346995..2c9027be44 100755 --- a/package/setup.py +++ b/package/setup.py @@ -602,6 +602,7 @@ def long_description(readme): 'packaging', 'fasteners', 'mda-xdrlib', + 'waterdynamics', 'pathsimanalysis', ] diff --git a/testsuite/MDAnalysisTests/analysis/test_waterdynamics.py b/testsuite/MDAnalysisTests/analysis/test_waterdynamics.py index d61fb39151..653a04e1fd 100644 --- a/testsuite/MDAnalysisTests/analysis/test_waterdynamics.py +++ b/testsuite/MDAnalysisTests/analysis/test_waterdynamics.py @@ -20,256 +20,13 @@ # MDAnalysis: A Toolkit for the Analysis of Molecular Dynamics Simulations. # J. Comput. Chem. 32 (2011), 2319--2327, doi:10.1002/jcc.21787 # -import MDAnalysis -from MDAnalysis.analysis import waterdynamics -from MDAnalysis.lib.correlations import autocorrelation, correct_intermittency - -from MDAnalysisTests.datafiles import waterPSF, waterDCD - +from importlib import reload import pytest -import numpy as np -from unittest.mock import patch, Mock -from numpy.testing import assert_almost_equal, assert_equal - -SELECTION1 = "byres name OH2" -SELECTION2 = "byres name P1" - - -@pytest.fixture(scope='module') -def universe(): - return MDAnalysis.Universe(waterPSF, waterDCD) - - -def test_WaterOrientationalRelaxation(universe): - wor = waterdynamics.WaterOrientationalRelaxation( - universe, SELECTION1, 0, 5, 2) - wor.run() - assert_almost_equal(wor.timeseries[1][2], 0.35887, - decimal=5) - - -def test_WaterOrientationalRelaxation_zeroMolecules(universe): - wor_zero = waterdynamics.WaterOrientationalRelaxation( - universe, SELECTION2, 0, 5, 2) - wor_zero.run() - assert_almost_equal(wor_zero.timeseries[1], (0.0, 0.0, 0.0)) - - -def test_AngularDistribution(universe): - ad = waterdynamics.AngularDistribution(universe, SELECTION1, 40) - ad.run() - # convert a string with two "floats" into a float array - result = np.array(ad.graph[0][39].split(), dtype=np.float64) - assert_almost_equal(result, (0.951172947884, 0.48313682125)) - - -def test_MeanSquareDisplacement(universe): - msd = waterdynamics.MeanSquareDisplacement(universe, SELECTION1, 0, 10, 2) - msd.run() - assert_almost_equal(msd.timeseries[1], 0.03984, - decimal=5) - - -def test_MeanSquareDisplacement_zeroMolecules(universe): - msd_zero = waterdynamics.MeanSquareDisplacement( - universe, SELECTION2, 0, 10, 2) - msd_zero.run() - assert_almost_equal(msd_zero.timeseries[1], 0.0) - - -def test_SurvivalProbability_intermittency1and2(universe): - """ - Intermittency of 2 means that we still count an atom if it is not present for up to 2 consecutive frames, - but then returns at the following step. - """ - with patch.object(universe, 'select_atoms') as select_atoms_mock: - ids = [(9, 8), (), (8,), (9,), (8,), (), (9, 8), (), (8,), (9, 8)] - select_atoms_mock.side_effect = lambda selection: Mock(ids=ids.pop()) # atom IDs fed set by set - sp = waterdynamics.SurvivalProbability(universe, "") - sp.run(tau_max=3, stop=10, verbose=True, intermittency=2) - assert all(x == {9, 8} for x in sp._intermittent_selected_ids) - assert_almost_equal(sp.sp_timeseries, [1, 1, 1, 1]) - - -def test_SurvivalProbability_intermittency2lacking(universe): - """ - If an atom is not present for more than 2 consecutive frames, - it is considered to have left the region. - """ - with patch.object(universe, 'select_atoms') as select_atoms_mock: - ids = [(9,), (), (), (), (9,), (), (), (), (9,)] - select_atoms_mock.side_effect = lambda selection: Mock(ids=ids.pop()) # atom IDs fed set by set - sp = waterdynamics.SurvivalProbability(universe, "") - sp.run(tau_max=3, stop=9, verbose=True, intermittency=2) - assert_almost_equal(sp.sp_timeseries, [1, 0, 0, 0]) - - -def test_SurvivalProbability_intermittency1_step5_noSkipping(universe): - """ - Step leads to skipping frames if (tau_max + 1) + (intermittency * 2) < step. - No frames should be skipped. - """ - with patch.object(universe, 'select_atoms') as select_atoms_mock: - ids = [(2, 3), (3,), (2, 3), (3,), (2,), (3,), (2, 3), (3,), (2, 3), (2, 3)] - select_atoms_mock.side_effect = lambda selection: Mock(ids=ids.pop()) # atom IDs fed set by set - sp = waterdynamics.SurvivalProbability(universe, "") - sp.run(tau_max=2, stop=10, verbose=True, intermittency=1, step=5) - assert all((x == {2, 3} for x in sp._intermittent_selected_ids)) - assert_almost_equal(sp.sp_timeseries, [1, 1, 1]) - - -def test_SurvivalProbability_intermittency1_step5_Skipping(universe): - """ - Step leads to skipping frames if (tau_max + 1) * (intermittency * 2) < step. - In this case one frame will be skipped per window. - """ - with patch.object(universe, 'select_atoms') as select_atoms_mock: - ids = [(1,), (), (1,), (), (1,), (), (1,), (), (1,), (1,)] - beforepopsing = len(ids) - 2 - select_atoms_mock.side_effect = lambda selection: Mock(ids=ids.pop()) # atom IDs fed set by set - sp = waterdynamics.SurvivalProbability(universe, "") - sp.run(tau_max=1, stop=10, verbose=True, intermittency=1, step=5) - assert all((x == {1} for x in sp._intermittent_selected_ids)) - assert len(sp._selected_ids) == beforepopsing - assert_almost_equal(sp.sp_timeseries, [1, 1]) - - -def test_intermittency_none(): - # No changes asked - returns the same data - input_ids = [{1}, {1}, {1}, set(), set(), {1}, {1}, {1}, set(), set(), {1}, set(), set(), {1}] - corrected = correct_intermittency(input_ids, intermittency=0) - assert all(x == y for x,y in zip(input_ids, corrected)) - - -def test_intermittency_1and2(): - # The maximum gap in the dataset is 2, so the IDs are always present after correction - input_ids = [{9, 8}, set(), {8, }, {9, }, {8, }, set(), {9, 8}, set(), {8, }, {9, 8, }] - corrected = correct_intermittency(input_ids, intermittency=2) - assert all((x == {9, 8} for x in corrected)) - -def test_intermittency_2tooShort(): - #The IDs are abscent for too long/ - input_ids = [{9,}, {}, {}, {}, {9,}, {}, {}, {}, {9,}] - corrected = correct_intermittency(input_ids, intermittency=2) - assert all(x == y for x, y in zip(input_ids, corrected)) - - -def test_intermittency_setsOfSets(): - # Verificaiton for the case of hydrogen bonds (sets of sets) - input_ids = [{frozenset({1,2}), frozenset({3, 4})},set(), set(), - {frozenset({1, 2}), frozenset({3, 4})}, set(), set(), - {frozenset({1, 2}), frozenset({3, 4})}, set(), set(), - {frozenset({1, 2}), frozenset({3, 4})}] - corrected = correct_intermittency(input_ids, intermittency=2) - assert all((x == {frozenset({1, 2}), frozenset({3, 4})} for x in corrected)) - - -def test_autocorrelation_alwaysPresent(): - input = [{1, 2}, {1, 2}, {1, 2}, {1, 2}, {1, 2}, {1, 2}, {1, 2}] - tau_timeseries, sp_timeseries, sp_timeseries_data = autocorrelation(input, tau_max=3) - assert all(np.equal(sp_timeseries, 1)) - - -def test_autocorrelation_definedTaus(): - input_ids = [{9, 8, 7}, {8, 7, 6}, {7, 6, 5}, {6, 5, 4}, {5, 4, 3}, {4, 3, 2}, {3, 2, 1}] - tau_timeseries, sp_timeseries, sp_timeseries_data = autocorrelation(input_ids, tau_max=3) - assert_almost_equal(sp_timeseries, [1, 2/3., 1/3., 0]) - - -def test_autocorrelation_intermittency1_windowJump_intermittencyAll(): - """ - Step leads to skipping frames if (tau_max + 1) + (intermittency * 2) < step. - No frames should be skipped so intermittency should be applied to all. - """ - input_ids = [{2, 3}, {3,}, {2, 3}, {3,}, {2,}, {3,}, {2, 3}, {3,}, {2, 3}, {2, 3}] - corrected = correct_intermittency(input_ids, intermittency=1) - tau_timeseries, sp_timeseries, sp_timeseries_data = autocorrelation(corrected, tau_max=2, - window_step=5) - assert all((x == {2, 3} for x in corrected)) - assert_almost_equal(sp_timeseries, [1, 1, 1]) - - -def test_autocorrelation_windowBigJump(): - #The empty sets are ignored (no intermittency) - input_ids = [{1}, {1}, {1}, set(), set(), {1}, {1}, {1}, set(), set(), {1}, {1}, {1}] - tau_timeseries, sp_timeseries, sp_timeseries_data = autocorrelation(input_ids, tau_max=2, window_step=5) - assert_almost_equal(sp_timeseries, [1, 1, 1]) - - -def test_autocorrelation_windowBigJump_absence(): - # In the last frame the molecules are absent - input_ids = [{1}, {1}, {1}, set(), set(), {1}, {1}, {1}, set(), set(), {1}, set(), set()] - tau_timeseries, sp_timeseries, sp_timeseries_data = autocorrelation(input_ids, tau_max=2, window_step=5) - assert_almost_equal(sp_timeseries, [1, 2/3., 2/3.]) - - -def test_autocorrelation_intermittency1_many(): - input_ids = [{1}, set(), {1}, set(), {1}, set(), {1}, set(), {1}, set(), {1}, set(), {1}, set(), {1}] - corrected = correct_intermittency(input_ids, intermittency=1) - tau_timeseries, sp_timeseries, sp_timeseries_data = autocorrelation(corrected, tau_max=14, - window_step=5) - assert_almost_equal(sp_timeseries, [1] * 15) - - -def test_autocorrelation_intermittency2_windowBigJump(): - # The intermittency corrects the last frame - input_ids = [{1}, {1}, {1}, set(), set(), {1}, {1}, {1}, set(), set(), {1}, set(), set(), {1}] - corrected = correct_intermittency(input_ids, intermittency=2) - tau_timeseries, sp_timeseries, sp_timeseries_data = autocorrelation(corrected, tau_max=2, - window_step=5) - assert_almost_equal(sp_timeseries, [1, 1, 1]) - - -def test_SurvivalProbability_t0tf(universe): - with patch.object(universe, 'select_atoms') as select_atoms_mock: - ids = [(0, ), (0, ), (7, 6, 5), (6, 5, 4), (5, 4, 3), (4, 3, 2), (3, 2, 1), (0, )] - select_atoms_mock.side_effect = lambda selection: Mock(ids=ids.pop(2)) # atom IDs fed set by set - sp = waterdynamics.SurvivalProbability(universe, "") - sp.run(tau_max=3, start=2, stop=7) - assert_almost_equal(sp.sp_timeseries, [1, 2 / 3.0, 1 / 3.0, 0]) - - -def test_SurvivalProbability_definedTaus(universe): - with patch.object(universe, 'select_atoms') as select_atoms_mock: - ids = [(9, 8, 7), (8, 7, 6), (7, 6, 5), (6, 5, 4), (5, 4, 3), (4, 3, 2), (3, 2, 1)] - select_atoms_mock.side_effect = lambda selection: Mock(ids=ids.pop()) # atom IDs fed set by set - sp = waterdynamics.SurvivalProbability(universe, "") - sp.run(tau_max=3, start=0, stop=7, verbose=True) - assert_almost_equal(sp.sp_timeseries, [1, 2 / 3.0, 1 / 3.0, 0]) - - -def test_SurvivalProbability_zeroMolecules(universe): - # no atom IDs found - with patch.object(universe, 'select_atoms', return_value=Mock(ids=[])) as select_atoms_mock: - sp = waterdynamics.SurvivalProbability(universe, "") - sp.run(tau_max=3, start=3, stop=7, verbose=True) - assert all(np.isnan(sp.sp_timeseries[1:])) - - -def test_SurvivalProbability_alwaysPresent(universe): - # always the same atom IDs found, 7 and 8 - with patch.object(universe, 'select_atoms', return_value=Mock(ids=[7, 8])) as select_atoms_mock: - sp = waterdynamics.SurvivalProbability(universe, "") - sp.run(tau_max=3, start=0, stop=7, verbose=True) - assert all(np.equal(sp.sp_timeseries, 1)) - - -def test_SurvivalProbability_stepLargerThanDtmax(universe): - # Testing if the frames are skipped correctly - with patch.object(universe, 'select_atoms', return_value=Mock(ids=(1,))) as select_atoms_mock: - sp = waterdynamics.SurvivalProbability(universe, "") - sp.run(tau_max=2, step=5, stop=10, verbose=True) - assert_equal(sp.sp_timeseries, [1, 1, 1]) - # with tau_max=2 for all the frames we only read 6 of them - # this is because the frames which are not used are skipped, and therefore 'select_atoms' - assert universe.trajectory.n_frames > 6 - assert_equal(select_atoms_mock.call_count, 6) +from MDAnalysis.analysis import waterdynamics -def test_SurvivalProbability_stepEqualDtMax(universe): - with patch.object(universe, 'select_atoms', return_value=Mock(ids=(1,))) as select_atoms_mock: - sp = waterdynamics.SurvivalProbability(universe, "") - sp.run(tau_max=4, step=5, stop=10, verbose=True) - # all frames from 0, with 9 inclusive - assert_equal(select_atoms_mock.call_count, 10) +def test_moved_to_mdakit_warning(): + wmsg = "MDAnalysis.analysis.waterdynamics is deprecated" + with pytest.warns(DeprecationWarning, match=wmsg): + reload(waterdynamics)