Source code for msmbuilder.msm_analysis

# This file is part of MSMBuilder.
#
# Copyright 2011 Stanford University
#
# MSMBuilder is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

from __future__ import print_function, absolute_import, division
from mdtraj.utils.six.moves import xrange
import sys
import scipy.sparse
import scipy.linalg
import scipy.sparse.linalg
import scipy
import numpy as np
import multiprocessing
import warnings
from mdtraj import io
from msmbuilder.utils import uneven_zip
import logging
logger = logging.getLogger(__name__)

# Set this value to true (msm_analysis.DisableErrorChecking=True) to ignore
# Eigenvector calculation errors.  Useful if you need to process disconnected data.
DisableErrorChecking = False

[docs]def get_reversible_eigenvectors(t_matrix, k, populations=None, right=False, dense_cutoff=50, normalized=False, **kwargs): r"""Find the k largest left eigenvalues and eigenvectors of a reversible row-stochastic matrix, sorted by eigenvalue magnitude Parameters ---------- t_matrix : sparse or dense matrix The row-stochastic transition probability matrix k : int The number of eigenpairs to calculate. The `k` eigenpairs with the highest eigenvalues will be returned. populations : np.ndarray, optional The equilibrium, stationary distribution of t_matrix. If not supplied, it can be re-computed from `t_matrix`. But this substantially increases the runtime of the routine, so if the stationary distribution is known, it's more efficient to supply it. right : bool, optional The default behavior is to compute the *left* eigenvectors of the transition matrix. This option may be invoked to instead compute the *right* eigenvectors. dense_cutoff : int, optional use dense eigensolver if dimensionality is below this normalized : bool, optional normalize the vectors such that .. math:: \phi_i^T \psi_j = \delta_{ij} where :math:`\phi_i` is the :math:`i^{th}` left eigenvector, and :math:`\psi_j` is the :math:`j^{th}` right eigenvector Other Parameters ---------------- **kwargs Additional keyword arguments are passed directly to ``scipy.sparse.linalg.eigsh``. Refer to the scipy documentation for further details. Returns ------- eigenvalues : ndarray 1D array of eigenvalues eigenvectors : ndarray 2D array of eigenvectors Notes ----- - A reversible transition matrix is one that satisifies the detailed balance condition .. math:: \pi_i T_{i,j} = \pi_j T_{j,i} - A reversible transition matrix satisifies a number of special conditions. In particular, it is similar to a symmetric matrix :math:`S_{i, j} = \sqrt{\frac{pi_i}{\pi_j}} T_{i, j} = S_{j, i}`. This property enables a much more robust solution to the eigenvector problem, because of the superior numerical stability of hermetian eigensolvers. See Also -------- get_eigenvectors : computes the eigenpairs of a general row-stochastic matrix, without requiring that the matrix be reversible. scipy.sparse.linalg.eigsh """ check_dimensions(t_matrix) n = t_matrix.shape[0] if k > n: logger.warning("You cannot calculate %d eigenvectors from a %d x %d matrix." % (k, n, n)) k = n logger.warning("Instead, calculating %d eigenvectors." % k) if n < dense_cutoff and scipy.sparse.issparse(t_matrix): t_matrix = t_matrix.toarray() elif k >= n - 1 and scipy.sparse.issparse(t_matrix): logger.warning("ARPACK cannot calculate %d eigenvectors from a %d x %d matrix." % (k, n, n)) k = n - 2 logger.warning("Instead, calculating %d eigenvectors." % k) if populations is None: populations = get_eigenvectors(t_matrix, 1, **kwargs)[1][:, 0] # This should work for BOTH dense and sparse. # Get the left eigenvectors using the sparse hermetian eigensolver # on the symmetrized transition matrix root_pi = populations**(0.5) root_pi_diag = scipy.sparse.diags(root_pi, offsets=0).tocsr() root_pi_diag_inv = scipy.sparse.diags(1.0 / root_pi, offsets=0).tocsr() symtrans = root_pi_diag.dot(scipy.sparse.csr_matrix(t_matrix)).dot(root_pi_diag_inv) # Force temporary conversion to sparse if scipy.sparse.issparse(t_matrix): values, vectors = scipy.sparse.linalg.eigsh(symtrans.T, k=k, which='LA', **kwargs) else: values, vectors = np.linalg.eigh(symtrans.toarray().T) # Reorder the eigenpairs by descending eigenvalue order = np.argsort(-np.real(values)) values = values[order] vectors = vectors[:, order] # the eigenvectors of symtrans are a rotated version of the eigenvectors # of transmat that we want left_vectors = (root_pi * vectors.T).T # normalize the first eigenvector (populations) left_vectors[:, 0] /= np.sum(left_vectors[:, 0]) # normalize the left eigenvectors if normalized: lengths = np.sum(left_vectors * left_vectors / left_vectors[:, 0:1], axis=0, keepdims=True) left_vectors = left_vectors / np.sqrt(lengths) if right: right_vectors = left_vectors / left_vectors[:, 0:1] return np.real(values[0:k]), np.real(right_vectors[:, 0:k]) else: return np.real(values[0:k]), np.real(left_vectors[:, 0:k])
[docs]def get_eigenvectors(t_matrix, n_eigs, epsilon=.001, dense_cutoff=50, right=False, tol=1E-30, normalized=False): """Get the left eigenvectors of a transition matrix, sorted by eigenvalue magnitude Parameters ---------- t_matrix : sparse or dense matrix transition matrix. if `T` is sparse, the sparse eigensolver will be used n_eigs : int How many eigenvalues to calculate epsilon : float, optional Throw error if `T` is not a stochastic matrix, with tolerance given by `Epsilon` dense_cutoff : int, optional use dense eigensolver if dimensionality is below this right : bool, optional if true, compute the right eigenvectors instead of the left tol : float, optional Convergence criterion for sparse eigenvalue solver. normalized : bool, optional normalize the vectors such that .. math:: \phi_i^T \psi_j = \delta_{ij} where :math:`\phi_i` is the :math`i^{th}` left eigenvector, and :math:`\psi_j` is the :math:`j^{th}` right eigenvector Returns ------- eigenvalues : ndarray 1D array of eigenvalues eigenvectors : ndarray 2D array of eigenvectors Notes ----- - Left eigenvectors satisfy the relation :math:`V \mathbf{T} = \lambda V` - Vectors are returned in columns of matrix. - Too large a value of tol may lead to unstable results. See GitHub issue #174. """ check_transition(t_matrix, epsilon) check_dimensions(t_matrix) n = t_matrix.shape[0] if n_eigs > n: logger.warning("You cannot calculate %d Eigenvectors from a %d x %d matrix." % (n_eigs, n, n)) n_eigs = n logger.warning("Instead, calculating %d Eigenvectors." % n_eigs) if n < dense_cutoff and scipy.sparse.issparse(t_matrix): t_matrix = t_matrix.toarray() elif n_eigs >= n - 1 and scipy.sparse.issparse(t_matrix): logger.warning("ARPACK cannot calculate %d Eigenvectors from a %d x %d matrix." % (n_eigs, n, n)) n_eigs = n - 2 logger.warning("Instead, calculating %d Eigenvectors." % n_eigs) # get the left eigenvectors t_matrix = t_matrix.transpose() if scipy.sparse.issparse(t_matrix): values, vectors = scipy.sparse.linalg.eigs(t_matrix.tocsr(), n_eigs, which="LR", maxiter=100000,tol=tol) else: values, vectors = scipy.linalg.eig(t_matrix) order = np.argsort(-np.real(values)) e_lambda = values[order] left_vectors = vectors[:, order] check_for_bad_eigenvalues(e_lambda, cutoff_value=1 - epsilon) # this is bad IMO --TJL # normalize the first eigenvector (populations) left_vectors[:, 0] /= sum(left_vectors[:, 0]) if normalized: lengths = np.sum(left_vectors * left_vectors / left_vectors[:, 0:1], axis=0, keepdims=True) left_vectors = left_vectors / np.sqrt(lengths) if right: right_vectors = left_vectors / left_vectors[:, 0:1] e_vectors = right_vectors else: e_vectors = left_vectors e_lambda = np.real(e_lambda[0: n_eigs]) e_vectors = np.real(e_vectors[:, 0: n_eigs]) return e_lambda, e_vectors
[docs]def get_implied_timescales(assignments_fn, lag_times, n_implied_times=100, sliding_window=True, trimming=True, symmetrize=None, n_procs=1): """Calculate implied timescales in parallel using multiprocessing library. Does not work in interactive mode. Parameters ---------- AssignmentsFn : str Path to Assignments.h5 file on disk LagTimes : list List of lag times to calculate the timescales at NumImpledTimes : int, optional Number of implied timescales to calculate at each lag time Slide : bool, optional Use sliding window Trim : bool, optional Use ergodic trimming Symmetrize : {'MLE', 'Transpose', None} Symmetrization method nProc : int number of processes to use in parallel (multiprocessing Returns ------- formatedLags : ndarray RTM 6/27 I'm not quite sure what the semantics of the output is. It's not trivial and undocummented. See Also -------- MSMLib.mle_reversible_count_matrix : (MLE symmetrization) MSMLib.build_msm get_eigenvectors """ pool = multiprocessing.Pool(processes=n_procs) # subtle bug possibility; uneven_zip will let strings be iterable, whicj # we dont want inputs = uneven_zip([assignments_fn], lag_times, n_implied_times, sliding_window, trimming, [symmetrize]) result = pool.map_async(get_implied_timescales_helper, inputs) lags = result.get(999999) # reformat formatted_lags = [] for i, (lag_time_array, implied_timescale_array) in enumerate(lags): for j, lag_time in enumerate(lag_time_array): implied_timescale = implied_timescale_array[j] formatted_lags.append([lag_time, implied_timescale]) formatted_lags = np.array(formatted_lags) pool.close() return formatted_lags
def get_implied_timescales_helper(args): """Helper function to compute implied timescales with multiprocessing Does not work in interactive mode Parameters ---------- assignments_fn : str Path to Assignments.h5 file on disk n_states : int Number of states lag_time : list List of lag times to calculate the timescales at n_implied_times : int, optional Number of implied timescales to calculate at each lag time sliding_window : bool, optional Use sliding window trimming : bool, optional Use ergodic trimming symmetrize : {'MLE', 'Transpose', None} Symmetrization method Returns ------- lagTimes : ndarray vector of lag times impTimes : ndarray vector of implied timescales See Also -------- MSMLib.build_msm get_eigenvectors """ assignments_fn, lag_time, n_implied_times, sliding_window, trimming, symmetrize = args logger.info("Calculating implied timescales at lagtime %d" % lag_time) try: assignments = io.loadh(assignments_fn, 'arr_0') except KeyError: assignments = io.loadh(assignments_fn, 'Data') try: from msmbuilder import MSMLib counts = MSMLib.get_count_matrix_from_assignments(assignments, lag_time=lag_time, sliding_window=sliding_window) rev_counts, t_matrix, populations, mapping = MSMLib.build_msm(counts, symmetrize, trimming) except ValueError as e: logger.critical(e) sys.exit(1) n_eigenvectors = n_implied_times + 1 if symmetrize in ['MLE', 'Transpose']: e_values = get_reversible_eigenvectors(t_matrix, n_eigenvectors, populations=populations)[0] else: e_values = get_eigenvectors(t_matrix, n_eigenvectors, epsilon=1)[0] # Correct for possible change in n_eigenvectors from trimming n_eigenvectors = len(e_values) n_implied_times = n_eigenvectors - 1 # make sure to leave off equilibrium distribution lag_times = lag_time * np.ones((n_implied_times)) imp_times = -lag_times / np.log(e_values[1: n_eigenvectors]) return (lag_times, imp_times) def check_for_bad_eigenvalues(eigenvalues, decimal=5, cutoff_value=0.999999): """Ensure that all eigenvalues are less than or equal to one Having multiple eigenvalues of lambda>=1 suggests either non-ergodicity or numerical error. Parameters ---------- Eigenvalues : ndarray 1D array of eigenvalues to check decimal : deprecated (marked 6/27) this doesn't do anything CutoffValue: float, optional Tolerance used Notes ------ Checks that the first eigenvalue is within `CutoffValue` of 1, and that the second eigenvalue is not greater than `CutoffValue` """ if abs(eigenvalues[0] - 1) > 1 - cutoff_value: warnings.warn(("WARNING: the largest eigenvalue is not 1, " "suggesting numerical error. Try using 64 or 128 bit precision.")) if eigenvalues[1] > cutoff_value: warnings.warn(("WARNING: the second largest eigenvalue (x) is close " " to 1, suggesting numerical error or nonergodicity. Try using 64 " "or 128 bit precision. Your data may also be disconnected, in " "which case you cannot simultaneously model both disconnected " "components. Try collecting more data or trimming the " " disconnected pieces."))
[docs]def project_observable_onto_transition_matrix(observable, tprob, num_modes=25): """ Projects an observable vector onto a probability transition matrix's eigenmodes. The function first decomposes the matrix `tprob` into `num_modes` different eigenvectors, sorted by eigenvalue. Then, it returns the amplitude of the projection of the observable onto each of those eigenmodes. This projection gives an estimate of how strong an experimental signal will be see at each timescale - though the actual experimental response will also be modulated by the populations of states at play. Parameters ---------- observable : array_like, float a one-dimensional array of the values of a given observable for each state in the MSM tprob : matrix the transition probability matrix num_modes : int the number of eigenmodes to calculate (the top ones, sorted by mag.) Returns ------- timescales : array_like, float the timescales of each eigenmode, in units of the lagtime of `tprob` amplitudes : array_like, float the amplitudes of the projection of `observable` onto each mode Notes ----- The stationary mode is always discarded The eigenvalues/vectors are calculated from scratch, so this function may take a little while to run """ if num_modes + 1 > tprob.shape[0]: logger.warning("cannot get %d eigenmodes from a rank %d matrix", num_modes + 1, tprob.shape[0]) logger.warning("Getting as many modes as possible...") num_modes = tprob.shape[0] - 1 eigenvalues, eigenvectors = get_eigenvectors(tprob, num_modes + 1, right=True) # discard the stationary eigenmode eigenvalues = np.real(eigenvalues[1:]) eigenvectors = np.real(eigenvectors[:, 1:]) timescales = - 1.0 / np.log(eigenvalues) amplitudes = np.zeros(num_modes) for mode in range(num_modes): amplitudes[mode] = np.dot(observable, eigenvectors[:, mode]) return timescales, amplitudes
[docs]def sample(transition_matrix, state, steps, traj=None, force_dense=False): """Generate a random sequence of states by propogating a transition matrix. Parameters ---------- transition_matrix : sparse or dense matrix A transition matrix State : {int, None, ndarray} Starting state for trajectory. If State is an integer, it will be used as the initial state. If State is None, an initial state will be randomly chosen from an uniform distribution. If State is an array, it represents a probability distribution from which the initial state will be drawn. If a trajectory is specified (see Traj keyword), this variable will be ignored, and the last state of that trajectory will be used. Steps : int How many steps to generate. Traj : list, optional An existing trajectory (python list) can be input; results will be appended to it ForceDense : bool, deprecated Force dense arithmatic. Can speed up results for small models (OBSOLETE). Returns ------- Traj : list Sequence of states as a python list """ check_transition(transition_matrix) check_dimensions(transition_matrix) n_states = transition_matrix.shape[0] if scipy.sparse.isspmatrix(transition_matrix): transition_matrix = transition_matrix.tocsr() # reserve room for the new trajectory (will be appended to an existing trajectory at the end if necessary) newtraj = [-1] * steps # determine initial state if traj is None or len(traj) == 0: if state is None: state = np.random.randint(n_states) elif isinstance(state, np.ndarray): state = np.where(scipy.random.multinomial(1, state / sum(state)) == 1)[0][0] newtraj[0] = state start = 1 else: state = traj[-1] start = 0 if not state < n_states: raise ValueError("Intial state is %s, but should be between 0 and %s." % (state, n_states - 1)) # sample the Markov chain if isinstance(transition_matrix, np.ndarray): for i in xrange(start, steps): p = transition_matrix[state, :] state = np.where(scipy.random.multinomial(1, p) == 1)[0][0] newtraj[i] = state elif isinstance(transition_matrix, scipy.sparse.csr_matrix): if force_dense: # Lutz: this is the old code path that converts the row of transition probabilities to a dense array at each step. # With the optimized handling of sparse matrices below, this can probably be deleted altogether. for i in xrange(start, steps): p = transition_matrix[state, :].toarray().flatten() state = np.where(scipy.random.multinomial(1, p) == 1)[0][0] newtraj[i] = state else: for i in xrange(start, steps): # Lutz: slicing sparse matrices is very slow (compared to slicing ndarrays) # To avoid slicing, use the underlying data structures of the CSR format directly T = transition_matrix vals = T.indices[T.indptr[state]:T.indptr[state + 1]] # the column indices of the non-zero entries are the possible target states p = T.data[T.indptr[state]:T.indptr[state + 1]] # the data values of the non-zero entries are the corresponding probabilities state = vals[np.where(scipy.random.multinomial(1, p) == 1)[0][0]] newtraj[i] = state else: raise RuntimeError("Unknown matrix type: %s" % type(T)) # return the new trajectory, or the concatenation of the old trajectory with the new one if traj is None: return newtraj traj.extend(newtraj) return traj
[docs]def propagate_model(transition_matrix, n_steps, initial_populations, observable_vector=None): """Propogate the time evolution of a population vector. Parameters ---------- T : ndarray or sparse matrix A transition matrix NumSteps : int How many timesteps to iterate initial_populations : ndarray The initial population vector observable_vector : ndarray Vector containing the state-wise averaged property of some observable. Can be used to propagate properties such as fraction folded, ensemble average RMSD, etc. Default: None Returns ------- X : ndarray Final population vector, after propagation obslist : list list of floats of length equal to the number of steps, giving the mean value of the observable (dot product of `ObservableVector` and populations) at each timestep See Also -------- sample scipy.sparse.linalg.aslinearoperator """ check_transition(transition_matrix) if observable_vector == None: check_dimensions(transition_matrix, initial_populations) else: check_dimensions(transition_matrix, initial_populations, observable_vector) X = initial_populations.copy() obslist = [] if scipy.sparse.issparse(transition_matrix): TC = transition_matrix.tocsr() else: TC = transition_matrix Tl = scipy.sparse.linalg.aslinearoperator(TC) for i in xrange(n_steps): X = Tl.rmatvec(X) if observable_vector is not None: obslist.append(sum(observable_vector * X)) return X, obslist
[docs]def calc_expectation_timeseries(tprob, observable, init_pop=None, timepoints=10 ** 6, n_modes=100, lagtime=15.0): """ Calculates the expectation value over time <A(t)> for some `observable` in an MSM. Does this by eigenvalue decomposition, according to the eq math :: \langle A \rangle (t) = \sum_{i=0}^N \langle p(0), \psi^L_i \rangle e^{ - \lambda_i t } \langle \psi^R_i, A \rangle Parameters ---------- tprob : matrix The transition probability matrix (of size N) for the MSM. observable : array_like, float A len N array of the values A of the observable for each state. init_pop : array_like, float A len N array of the initial populations of each state. If None is passed, then the function will start from even populations in each state. timepoints : int The number of timepoints to calculate - the final timeseries will be in length LagTime x `timepoints` n_modes : int The number of eigenmodes to include in the calculation. This number will depend on the timescales involved in the relatation of the observable. Returns ------- timeseries : array_like, float A timeseries of the observable over time, in units of the lag time of the transition matrix. """ # first, perform the eigendecomposition lambd, psi_L = get_eigenvectors(tprob, n_modes, right=False) psi_L = np.real(psi_L) lambd = np.real(lambd) pos_ind = np.where(lambd > 0)[0] lambd = lambd[pos_ind] psi_L = psi_L[:, pos_ind] n_modes = len(lambd) logger.info("Found %d non-negative eigenvalues" % n_modes) # normalize eigenvectors pi = psi_L[:, 0] pi /= pi.sum() np.savetxt('calculated_populations.dat', pi) psi_R = np.zeros(psi_L.shape) for i in range(n_modes): psi_L[:, i] /= np.sqrt(np.sum(np.square(psi_L[:, i]) / pi)) psi_R[:, i] = psi_L[:, i] / pi if lagtime: logger.info("Shortest timescale process included: %s", -lagtime / np.log(np.min(lambd))) # figure out the initial populations if init_pop == None: init_pop = np.ones(tprob.shape[0]) init_pop /= init_pop.sum() assert np.abs(init_pop.sum() - 1.0) < 0.0001 # generate the timeseries timeseries = np.zeros(timepoints) for i in range(n_modes): front = np.dot(init_pop, psi_R[:, i]) back = np.dot(observable, psi_L[:, i]) mode_decay = front * np.power(lambd[i], np.arange(timepoints)) * back timeseries += np.real(mode_decay) logger.info('The equilibrium value is %f, while the last time point calculated is %f', np.dot(pi, observable), timeseries[-1]) return timeseries
[docs]def msm_acf(tprob, observable, timepoints, num_modes=10): """ Calculate an autocorrelation function from an MSM. Rapid calculation of the autocorrelation of an MSM is performed via an eigenmode decomposition. Parameters ---------- tprob : matrix Transition probability matrix observable : ndarray, float Vector representing the observable value for each state timepoints : ndarray, int The timepoints at which to calculate the decay, in units of lag times. num_modes : int (num_modes) The number of eigenmodes to employ. More modes, more accurate, but slower. Returns ------- acf : ndarray, float The autocorrelation function. Notes ----- Use statsmodels.tsa.stattools.acf if you want to calculate an ACF from a raw observable such as an RMSD trace. See Docs/ACF/acf.pdf for a derivation of this calculation. """ eigenvalues, eigenvectors = get_eigenvectors(tprob, num_modes + 1) num_modes = len(eigenvalues) - 1 populations = eigenvectors[:, 0] D = np.diag(populations ** -1.) # discard the stationary eigenmode eigenvalues = np.real(eigenvalues[1:]) eigenvectors = np.real(eigenvectors[:, 1:]) right_eigenvectors = D.dot(eigenvectors) eigenvector_normalizer = np.diag(right_eigenvectors.T.dot(eigenvectors)) eigenvectors /= eigenvector_normalizer S = eigenvectors.T.dot(observable) # Project observable onto left eigenvectors acf = np.array([(eigenvalues ** t).dot(S**2) for t in timepoints]) acf /= (eigenvalues ** 0.).dot(S**2) # Divide by the ACF at time zero. return acf # ======================================================== # # SOME UTILITY FUNCTIONS FOR CHECKING TRANSITION MATRICES # ======================================================== #
def flatten(*args): """Return a generator for a flattened form of all arguments""" for x in args: if hasattr(x, '__iter__'): for y in flatten(*x): yield y else: yield x
[docs]def is_transition_matrix(t_matrix, epsilon=.00001): """Check for row normalization of a matrix Parameters ---------- t_matrix : densee or sparse matrix epsilon : float, optional threshold for how close the row sums need to be to 1 Returns ------- truth : bool True if the 2-norm of error in the row sums is less than Epsilon. """ n = t_matrix.shape[0] row_sums = np.array(t_matrix.sum(1)).flatten() if scipy.linalg.norm(row_sums - np.ones(n)) < epsilon: return True return False
def are_all_dimensions_same(*args): """Are all the supplied arguments the same size Find the shape of every input. Returns ------- truth : boolean True if every matrix and vector is the same size. """ m = len(args) dim_list = [] for i in range(m): dims = scipy.shape(args[i]) dim_list.append(dims) return len(np.unique(flatten(dim_list))) == 1 def check_transition(t_matrix, epsilon=0.00001): """Ensure that matrix is a row normalized stochastic matrix Parameters ---------- t_matrix : dense or sparse matrix epsilon : float, optional Threshold for how close the row sums need to be to one Other Parameters ---------------- DisableErrorChecking : bool If this flag (module scope variable) is set tot True, this function just passes. Raises ------ NormalizationError If T is not a row normalized stochastic matrix See Also -------- check_dimensions : ensures dimensionality is_transition_matrix : does the actual checking """ if not DisableErrorChecking and not is_transition_matrix(t_matrix, epsilon): logger.critical(t_matrix) logger.critical("Transition matrix is not a row normalized" " stocastic matrix. This is often caused by " "either numerical inaccuracies or by having " "states with zero counts.") def check_dimensions(*args): """Ensure that all the dimensions of the inputs are identical Raises ------ DimensionError If some of the supplied arguments have different dimensions from one another """ if are_all_dimensions_same(*args) == False: raise RuntimeError("All dimensions are not the same")
Versions