def update(self, Tij, Pi=None): r""" Updates the transition matrix and recomputes all derived quantities """ # EMMA imports from pyemma.msm import analysis as msmana # save a copy of the transition matrix self._Tij = np.array(Tij) assert msmana.is_transition_matrix(self._Tij), "Given transition matrix is not a stochastic matrix" assert self._Tij.shape[0] == self._nstates, "Given transition matrix has unexpected number of states " # initial / stationary distribution if Pi is not None: assert np.all(Pi >= 0), "Given initial distribution contains negative elements." Pi = np.array(Pi) / np.sum(Pi) # ensure normalization and make a copy if self._stationary: pT = msmana.stationary_distribution(self._Tij) if Pi is None: # stationary and no stationary distribution fixed, so computing it from trans. mat. self._Pi = pT else: # stationary but stationary distribution is fixed, so the transition matrix must be consistent assert np.allclose(Pi, pT), ( "Stationary HMM requested, but given distribution is not the " "stationary distribution of the given transition matrix." ) self._Pi = Pi else: if Pi is None: # no initial distribution given, so use stationary distribution anyway self._Pi = msmana.stationary_distribution(self._Tij) else: self._Pi = Pi # reversible if self._reversible: assert msmana.is_reversible(Tij), "Reversible HMM requested, but given transition matrix is not reversible." # try to do eigendecomposition by default, because it's very cheap for hidden transition matrices from scipy.linalg import LinAlgError try: if self._reversible: self._R, self._D, self._L = msmana.rdl_decomposition(self._Tij, norm="reversible") # everything must be real-valued self._R = self._R.real self._D = self._D.real self._L = self._L.real else: self._R, self._D, self._L = msmana.rdl_decomposition(self._Tij, norm="standard") self._eigenvalues = np.diag(self._D) self._spectral_decomp_available = True except LinAlgError: logger().warn( "Eigendecomposition failed for transition matrix\n" + str(self._Tij) + "\nspectral properties will not be available" ) self._spectral_decomp_available = False
def test_transition_matrix_samples(self): Psamples = self.sampled_hmm_lag10.transition_matrix_samples # shape assert np.array_equal(Psamples.shape, (self.nsamples, self.nstates, self.nstates)) # consistency import pyemma.msm.analysis as msmana for P in Psamples: assert msmana.is_transition_matrix(P) assert msmana.is_reversible(P)
def test_discrete_6_3(self): # 4x4 transition matrix nstates = 3 P = np.array([[0.90, 0.10, 0.00, 0.00, 0.00, 0.00], [0.20, 0.79, 0.01, 0.00, 0.00, 0.00], [0.00, 0.01, 0.84, 0.15, 0.00, 0.00], [0.00, 0.00, 0.05, 0.94, 0.01, 0.00], [0.00, 0.00, 0.00, 0.02, 0.78, 0.20], [0.00, 0.00, 0.00, 0.00, 0.10, 0.90]]) # generate realization import pyemma.msm.generation as msmgen T = 10000 dtrajs = [msmgen.generate_traj(P, T)] # estimate initial HMM with 2 states - should be identical to P hmm = initdisc.initial_model_discrete(dtrajs, nstates) # Test stochasticity and reversibility Tij = hmm.transition_matrix B = hmm.output_model.output_probabilities import pyemma.msm.analysis as msmana msmana.is_transition_matrix(Tij) msmana.is_reversible(Tij) np.allclose(B.sum(axis=1), np.ones(B.shape[0]))
def _transition_matrix(self, msm): P = msm.transition_matrix # should be ndarray by default assert (isinstance(P, np.ndarray)) # shape assert (np.all(P.shape == (msm.nstates, msm.nstates))) # test transition matrix properties import pyemma.msm.analysis as msmana assert (msmana.is_transition_matrix(P)) assert (msmana.is_connected(P)) # REVERSIBLE if msm.is_reversible: assert (msmana.is_reversible(P))
def test_IsReversible(self): # create a reversible matrix self.assertTrue(is_reversible(self.T, self.mu), "T should be reversible")
def test_is_reversible(self): self.assertTrue(is_reversible(self.T, tol=self.tol), 'matrix should be reversible')
def rdl_decomposition(T, k=None, norm='auto', ncv=None): r"""Compute the decomposition into left and right eigenvectors. Parameters ---------- T : sparse matrix Transition matrix k : int (optional) Number of eigenvector/eigenvalue pairs norm: {'standard', 'reversible', 'auto'} standard: (L'R) = Id, L[:,0] is a probability distribution, the stationary distribution mu of T. Right eigenvectors R have a 2-norm of 1. reversible: R and L are related via L=L[:,0]*R. auto: will be reversible if T is reversible, otherwise standard. ncv : int (optional) The number of Lanczos vectors generated, `ncv` must be greater than k; it is recommended that ncv > 2*k Returns ------- R : (M, M) ndarray The normalized ("unit length") right eigenvectors, such that the column ``R[:,i]`` is the right eigenvector corresponding to the eigenvalue ``w[i]``, ``dot(T,R[:,i])``=``w[i]*R[:,i]`` D : (M, M) ndarray A diagonal matrix containing the eigenvalues, each repeated according to its multiplicity L : (M, M) ndarray The normalized (with respect to `R`) left eigenvectors, such that the row ``L[i, :]`` is the left eigenvector corresponding to the eigenvalue ``w[i]``, ``dot(L[i, :], T)``=``w[i]*L[i, :]`` """ if k is None: raise ValueError("Number of eigenvectors required for decomposition of sparse matrix") # auto-set norm if norm == 'auto': from pyemma.msm.analysis import is_reversible if (is_reversible(T)): norm = 'reversible' else: norm = 'standard' # Standard norm: Euclidean norm is 1 for r and LR = I. if norm == 'standard': v, R = scipy.sparse.linalg.eigs(T, k=k, which='LM', ncv=ncv) r, L = scipy.sparse.linalg.eigs(T.transpose(), k=k, which='LM', ncv=ncv) """Sort right eigenvectors""" ind = np.argsort(np.abs(v))[::-1] v = v[ind] R = R[:, ind] """Sort left eigenvectors""" ind = np.argsort(np.abs(r))[::-1] r = r[ind] L = L[:, ind] """l1-normalization of L[:, 0]""" L[:, 0] = L[:, 0] / np.sum(L[:, 0]) """Standard normalization L'R=Id""" ov = np.diag(np.dot(np.transpose(L), R)) R = R / ov[np.newaxis, :] """Diagonal matrix with eigenvalues""" D = np.diag(v) return R, D, np.transpose(L) # Reversible norm: elif norm == 'reversible': v, R = scipy.sparse.linalg.eigs(T, k=k, which='LM', ncv=ncv) mu = stationary_distribution_from_backward_iteration(T) """Sort right eigenvectors""" ind = np.argsort(np.abs(v))[::-1] v = v[ind] R = R[:, ind] """Ensure that R[:,0] is positive""" R[:, 0] = R[:, 0] / np.sign(R[0, 0]) """Diagonal matrix with eigenvalues""" D = np.diag(v) """Compute left eigenvectors from right ones""" L = mu[:, np.newaxis] * R """Compute overlap""" s = np.diag(np.dot(np.transpose(L), R)) """Renormalize left-and right eigenvectors to ensure L'R=Id""" R = R / np.sqrt(s[np.newaxis, :]) L = L / np.sqrt(s[np.newaxis, :]) return R, D, np.transpose(L) else: raise ValueError("Keyword 'norm' has to be either 'standard' or 'reversible'")
def test_transition_matrix(self): import pyemma.msm.analysis as msmana for P in [self.hmm_lag1.transition_matrix, self.hmm_lag1.transition_matrix]: assert msmana.is_transition_matrix(P) assert msmana.is_reversible(P)
def tpt(T, A, B, mu=None, qminus=None, qplus=None, rate_matrix=False): r""" Computes the A->B reactive flux using transition path theory (TPT) Parameters ---------- T : (M, M) ndarray or scipy.sparse matrix Transition matrix (default) or Rate matrix (if rate_matrix=True) A : array_like List of integer state labels for set A B : array_like List of integer state labels for set B mu : (M,) ndarray (optional) Stationary vector qminus : (M,) ndarray (optional) Backward committor for A->B reaction qplus : (M,) ndarray (optional) Forward committor for A-> B reaction rate_matrix = False : boolean By default (False), T is a transition matrix. If set to True, T is a rate matrix. Returns ------- tpt: pyemma.msm.flux.ReactiveFlux object A python object containing the reactive A->B flux network and several additional quantities, such as stationary probability, committors and set definitions. Notes ----- The central object used in transition path theory is the forward and backward comittor function. TPT (originally introduced in [1]) for continous systems has a discrete version outlined in [2]. Here, we use the transition matrix formulation described in [3]. See also -------- pyemma.msm.analysis.committor, ReactiveFlux References ---------- .. [1] W. E and E. Vanden-Eijnden. Towards a theory of transition paths. J. Stat. Phys. 123: 503-523 (2006) .. [2] P. Metzner, C. Schuette and E. Vanden-Eijnden. Transition Path Theory for Markov Jump Processes. Multiscale Model Simul 7: 1192-1219 (2009) .. [3] F. Noe, Ch. Schuette, E. Vanden-Eijnden, L. Reich and T. Weikl: Constructing the Full Ensemble of Folding Pathways from Short Off-Equilibrium Simulations. Proc. Natl. Acad. Sci. USA, 106, 19011-19016 (2009) """ import pyemma.msm.analysis as msmana if len(A) == 0 or len(B) == 0: raise ValueError('set A or B is empty') n = T.shape[0] if len(A) > n or len(B) > n or max(A) > n or max(B) > n: raise ValueError('set A or B defines more states, than given transition matrix.') if (rate_matrix is False) and (not msmana.is_transition_matrix(T)): raise ValueError('given matrix T is not a transition matrix') if (rate_matrix is True): raise NotImplementedError( 'TPT with rate matrix is not yet implemented - But it is very simple, so feel free to do it.') # we can compute the following properties from either dense or sparse T # stationary dist if mu is None: mu = msmana.stationary_distribution(T) # forward committor if qplus is None: qplus = msmana.committor(T, A, B, forward=True) # backward committor if qminus is None: if msmana.is_reversible(T, mu=mu): qminus = 1.0 - qplus else: qminus = msmana.committor(T, A, B, forward=False, mu=mu) # gross flux grossflux = flux_matrix(T, mu, qminus, qplus, netflux=False) # net flux netflux = to_netflux(grossflux) # construct flux object from reactive_flux import ReactiveFlux F = ReactiveFlux(A, B, netflux, mu=mu, qminus=qminus, qplus=qplus, gross_flux=grossflux) # done return F
def _pcca_connected(P, n, return_rot=False): """ PCCA+ spectral clustering method with optimized memberships [1]_ Clusters the first n_cluster eigenvectors of a transition matrix in order to cluster the states. This function assumes that the transition matrix is fully connected. Parameters ---------- P : ndarray (n,n) Transition matrix. n : int Number of clusters to group to. Returns ------- chi by default, or (chi,rot) if return_rot = True chi : ndarray (n x m) A matrix containing the probability or membership of each state to be assigned to each cluster. The rows sum to 1. rot_mat : ndarray (m x m) A rotation matrix that rotates the dominant eigenvectors to yield the PCCA memberships, i.e.: chi = np.dot(evec, rot_matrix References ---------- [1] S. Roeblitz and M. Weber, Fuzzy spectral clustering by PCCA+: application to Markov state models and data classification. Adv Data Anal Classif 7, 147-179 (2013). """ # test connectivity from pyemma.msm.estimation import connected_sets labels = connected_sets(P) n_components = len( labels ) # (n_components, labels) = connected_components(P, connection='strong') if (n_components > 1): raise ValueError( "Transition matrix is disconnected. Cannot use pcca_connected.") from pyemma.msm.analysis import stationary_distribution pi = stationary_distribution(P) # print "statdist = ",pi from pyemma.msm.analysis import is_reversible if not is_reversible(P, mu=pi): raise ValueError( "Transition matrix does not fulfill detailed balance. " "Make sure to call pcca with a reversible transition matrix estimate" ) # TODO: Susanna mentioned that she has a potential fix for nonreversible matrices by replacing each complex conjugate # pair by the real and imaginary components of one of the two vectors. We could use this but would then need to # orthonormalize all eigenvectors e.g. using Gram-Schmidt orthonormalization. Currently there is no theoretical # foundation for this, so I'll skip it for now. # right eigenvectors, ordered from pyemma.msm.analysis import eigenvectors evecs = eigenvectors(P, n) # orthonormalize for i in range(n): evecs[:, i] /= math.sqrt(np.dot(evecs[:, i] * pi, evecs[:, i])) # make first eigenvector positive evecs[:, 0] = np.abs(evecs[:, 0]) # Is there a significant complex component? if not np.alltrue(np.isreal(evecs)): raise Warning( "The given transition matrix has complex eigenvectors, so it doesn't exactly fulfill detailed balance " + "forcing eigenvectors to be real and continuing. Be aware that this is not theoretically solid." ) evecs = np.real(evecs) # create initial solution using PCCA+. This could have negative memberships (chi, rot_matrix) = _pcca_connected_isa(evecs, n) #print "initial chi = \n",chi # optimize the rotation matrix with PCCA++. rot_matrix = _opt_soft(evecs, rot_matrix, n) # These memberships should be nonnegative memberships = np.dot(evecs[:, :], rot_matrix) # We might still have numerical errors. Force memberships to be in [0,1] # print "memberships unnormalized: ",memberships memberships = np.maximum(0.0, memberships) memberships = np.minimum(1.0, memberships) # print "memberships unnormalized: ",memberships for i in range(0, np.shape(memberships)[0]): memberships[i] /= np.sum(memberships[i]) # print "final chi = \n",chi return memberships
def rdl_decomposition(T, k=None, norm='auto', ncv=None): r"""Compute the decomposition into left and right eigenvectors. Parameters ---------- T : sparse matrix Transition matrix k : int (optional) Number of eigenvector/eigenvalue pairs norm: {'standard', 'reversible', 'auto'} standard: (L'R) = Id, L[:,0] is a probability distribution, the stationary distribution mu of T. Right eigenvectors R have a 2-norm of 1. reversible: R and L are related via L=L[:,0]*R. auto: will be reversible if T is reversible, otherwise standard. ncv : int (optional) The number of Lanczos vectors generated, `ncv` must be greater than k; it is recommended that ncv > 2*k Returns ------- R : (M, M) ndarray The normalized ("unit length") right eigenvectors, such that the column ``R[:,i]`` is the right eigenvector corresponding to the eigenvalue ``w[i]``, ``dot(T,R[:,i])``=``w[i]*R[:,i]`` D : (M, M) ndarray A diagonal matrix containing the eigenvalues, each repeated according to its multiplicity L : (M, M) ndarray The normalized (with respect to `R`) left eigenvectors, such that the row ``L[i, :]`` is the left eigenvector corresponding to the eigenvalue ``w[i]``, ``dot(L[i, :], T)``=``w[i]*L[i, :]`` """ if k is None: raise ValueError( "Number of eigenvectors required for decomposition of sparse matrix" ) # auto-set norm if norm == 'auto': from pyemma.msm.analysis import is_reversible if (is_reversible(T)): norm = 'reversible' else: norm = 'standard' # Standard norm: Euclidean norm is 1 for r and LR = I. if norm == 'standard': v, R = scipy.sparse.linalg.eigs(T, k=k, which='LM', ncv=ncv) r, L = scipy.sparse.linalg.eigs(T.transpose(), k=k, which='LM', ncv=ncv) """Sort right eigenvectors""" ind = np.argsort(np.abs(v))[::-1] v = v[ind] R = R[:, ind] """Sort left eigenvectors""" ind = np.argsort(np.abs(r))[::-1] r = r[ind] L = L[:, ind] """l1-normalization of L[:, 0]""" L[:, 0] = L[:, 0] / np.sum(L[:, 0]) """Standard normalization L'R=Id""" ov = np.diag(np.dot(np.transpose(L), R)) R = R / ov[np.newaxis, :] """Diagonal matrix with eigenvalues""" D = np.diag(v) return R, D, np.transpose(L) # Reversible norm: elif norm == 'reversible': v, R = scipy.sparse.linalg.eigs(T, k=k, which='LM', ncv=ncv) mu = stationary_distribution_from_backward_iteration(T) """Sort right eigenvectors""" ind = np.argsort(np.abs(v))[::-1] v = v[ind] R = R[:, ind] """Ensure that R[:,0] is positive""" R[:, 0] = R[:, 0] / np.sign(R[0, 0]) """Diagonal matrix with eigenvalues""" D = np.diag(v) """Compute left eigenvectors from right ones""" L = mu[:, np.newaxis] * R """Compute overlap""" s = np.diag(np.dot(np.transpose(L), R)) """Renormalize left-and right eigenvectors to ensure L'R=Id""" R = R / np.sqrt(s[np.newaxis, :]) L = L / np.sqrt(s[np.newaxis, :]) return R, D, np.transpose(L) else: raise ValueError( "Keyword 'norm' has to be either 'standard' or 'reversible'")
def _pcca_connected(P, n, return_rot=False): """ PCCA+ spectral clustering method with optimized memberships [1]_ Clusters the first n_cluster eigenvectors of a transition matrix in order to cluster the states. This function assumes that the transition matrix is fully connected. Parameters ---------- P : ndarray (n,n) Transition matrix. n : int Number of clusters to group to. Returns ------- chi by default, or (chi,rot) if return_rot = True chi : ndarray (n x m) A matrix containing the probability or membership of each state to be assigned to each cluster. The rows sum to 1. rot_mat : ndarray (m x m) A rotation matrix that rotates the dominant eigenvectors to yield the PCCA memberships, i.e.: chi = np.dot(evec, rot_matrix References ---------- [1] S. Roeblitz and M. Weber, Fuzzy spectral clustering by PCCA+: application to Markov state models and data classification. Adv Data Anal Classif 7, 147-179 (2013). """ # test connectivity from pyemma.msm.estimation import connected_sets labels = connected_sets(P) n_components = len(labels) # (n_components, labels) = connected_components(P, connection='strong') if (n_components > 1): raise ValueError("Transition matrix is disconnected. Cannot use pcca_connected.") from pyemma.msm.analysis import stationary_distribution pi = stationary_distribution(P) # print "statdist = ",pi from pyemma.msm.analysis import is_reversible if not is_reversible(P, mu=pi): raise ValueError("Transition matrix does not fulfill detailed balance. " "Make sure to call pcca with a reversible transition matrix estimate") # TODO: Susanna mentioned that she has a potential fix for nonreversible matrices by replacing each complex conjugate # pair by the real and imaginary components of one of the two vectors. We could use this but would then need to # orthonormalize all eigenvectors e.g. using Gram-Schmidt orthonormalization. Currently there is no theoretical # foundation for this, so I'll skip it for now. # right eigenvectors, ordered from pyemma.msm.analysis import eigenvectors evecs = eigenvectors(P, n) # orthonormalize for i in range(n): evecs[:, i] /= math.sqrt(np.dot(evecs[:, i] * pi, evecs[:, i])) # make first eigenvector positive evecs[:, 0] = np.abs(evecs[:, 0]) # Is there a significant complex component? if not np.alltrue(np.isreal(evecs)): raise Warning( "The given transition matrix has complex eigenvectors, so it doesn't exactly fulfill detailed balance " + "forcing eigenvectors to be real and continuing. Be aware that this is not theoretically solid.") evecs = np.real(evecs) # create initial solution using PCCA+. This could have negative memberships (chi, rot_matrix) = _pcca_connected_isa(evecs, n) #print "initial chi = \n",chi # optimize the rotation matrix with PCCA++. rot_matrix = _opt_soft(evecs, rot_matrix, n) # These memberships should be nonnegative memberships = np.dot(evecs[:, :], rot_matrix) # We might still have numerical errors. Force memberships to be in [0,1] # print "memberships unnormalized: ",memberships memberships = np.maximum(0.0, memberships) memberships = np.minimum(1.0, memberships) # print "memberships unnormalized: ",memberships for i in range(0, np.shape(memberships)[0]): memberships[i] /= np.sum(memberships[i]) # print "final chi = \n",chi return memberships
def tpt(T, A, B, mu=None, qminus=None, qplus=None, rate_matrix=False): r""" Computes the A->B reactive flux using transition path theory (TPT) Parameters ---------- T : (M, M) ndarray or scipy.sparse matrix Transition matrix (default) or Rate matrix (if rate_matrix=True) A : array_like List of integer state labels for set A B : array_like List of integer state labels for set B mu : (M,) ndarray (optional) Stationary vector qminus : (M,) ndarray (optional) Backward committor for A->B reaction qplus : (M,) ndarray (optional) Forward committor for A-> B reaction rate_matrix = False : boolean By default (False), T is a transition matrix. If set to True, T is a rate matrix. Returns ------- tpt: pyemma.msm.flux.ReactiveFlux object A python object containing the reactive A->B flux network and several additional quantities, such as stationary probability, committors and set definitions. Notes ----- The central object used in transition path theory is the forward and backward comittor function. TPT (originally introduced in [1]) for continous systems has a discrete version outlined in [2]. Here, we use the transition matrix formulation described in [3]. See also -------- pyemma.msm.analysis.committor, ReactiveFlux References ---------- .. [1] W. E and E. Vanden-Eijnden. Towards a theory of transition paths. J. Stat. Phys. 123: 503-523 (2006) .. [2] P. Metzner, C. Schuette and E. Vanden-Eijnden. Transition Path Theory for Markov Jump Processes. Multiscale Model Simul 7: 1192-1219 (2009) .. [3] F. Noe, Ch. Schuette, E. Vanden-Eijnden, L. Reich and T. Weikl: Constructing the Full Ensemble of Folding Pathways from Short Off-Equilibrium Simulations. Proc. Natl. Acad. Sci. USA, 106, 19011-19016 (2009) """ import pyemma.msm.analysis as msmana if len(A) == 0 or len(B) == 0: raise ValueError('set A or B is empty') n = T.shape[0] if len(A) > n or len(B) > n or max(A) > n or max(B) > n: raise ValueError('set A or B defines more states, than given transition matrix.') if (rate_matrix is False) and (not msmana.is_transition_matrix(T)): raise ValueError('given matrix T is not a transition matrix') if (rate_matrix is True): raise NotImplementedError('TPT with rate matrix is not yet implemented - But it is very simple, so feel free to do it.') # we can compute the following properties from either dense or sparse T # stationary dist if mu is None: mu = msmana.stationary_distribution(T) # forward committor if qplus is None: qplus = msmana.committor(T, A, B, forward=True) # backward committor if qminus is None: if msmana.is_reversible(T, mu=mu): qminus = 1.0-qplus else: qminus = msmana.committor(T, A, B, forward=False, mu=mu) # gross flux grossflux = flux_matrix(T, mu, qminus, qplus, netflux = False) # net flux netflux = to_netflux(grossflux) # construct flux object from reactive_flux import ReactiveFlux F = ReactiveFlux(A, B, netflux, mu=mu, qminus=qminus, qplus=qplus, gross_flux=grossflux) # done return F
def rdl_decomposition(T, k=None, norm='standard'): r"""Compute the decomposition into left and right eigenvectors. Parameters ---------- T : (M, M) ndarray Transition matrix k : int (optional) Number of eigenvector/eigenvalue pairs norm: {'standard', 'reversible'} standard: (L'R) = Id, L[:,0] is a probability distribution, the stationary distribution mu of T. Right eigenvectors R have a 2-norm of 1. reversible: R and L are related via L=L[:,0]*R. auto: will be reversible if T is reversible, otherwise standard. Returns ------- R : (M, M) ndarray The normalized (with respect to L) right eigenvectors, such that the column R[:,i] is the right eigenvector corresponding to the eigenvalue w[i], dot(T,R[:,i])=w[i]*R[:,i] D : (M, M) ndarray A diagonal matrix containing the eigenvalues, each repeated according to its multiplicity L : (M, M) ndarray The normalized (with respect to `R`) left eigenvectors, such that the row ``L[i, :]`` is the left eigenvector corresponding to the eigenvalue ``w[i]``, ``dot(L[i, :], T)``=``w[i]*L[i, :]`` """ d = T.shape[0] w, R = eig(T) """Sort by decreasing magnitude of eigenvalue""" ind = np.argsort(np.abs(w))[::-1] w = w[ind] R = R[:, ind] """Diagonal matrix containing eigenvalues""" D = np.diag(w) # auto-set norm if norm == 'auto': from pyemma.msm.analysis import is_reversible if (is_reversible(T)): norm = 'reversible' else: norm = 'standard' # Standard norm: Euclidean norm is 1 for r and LR = I. if norm == 'standard': L = solve(np.transpose(R), np.eye(d)) """l1- normalization of L[:, 0]""" R[:, 0] = R[:, 0] * np.sum(L[:, 0]) L[:, 0] = L[:, 0] / np.sum(L[:, 0]) if k is None: return R, D, np.transpose(L) else: return R[:, 0:k], D[0:k, 0:k], np.transpose(L[:, 0:k]) # Reversible norm: elif norm == 'reversible': b = np.zeros(d) b[0] = 1.0 A = np.transpose(R) nu = solve(A, b) mu = nu / np.sum(nu) """Ensure that R[:,0] is positive""" R[:, 0] = R[:, 0] / np.sign(R[0, 0]) """Use mu to connect L and R""" L = mu[:, np.newaxis] * R """Compute overlap""" s = np.diag(np.dot(np.transpose(L), R)) """Renormalize left-and right eigenvectors to ensure L'R=Id""" R = R / np.sqrt(s[np.newaxis, :]) L = L / np.sqrt(s[np.newaxis, :]) if k is None: return R, D, np.transpose(L) else: return R[:, 0:k], D[0:k, 0:k], np.transpose(L[:, 0:k]) else: raise ValueError( "Keyword 'norm' has to be either 'standard' or 'reversible'")
def rdl_decomposition(T, k=None, norm='standard'): r"""Compute the decomposition into left and right eigenvectors. Parameters ---------- T : (M, M) ndarray Transition matrix k : int (optional) Number of eigenvector/eigenvalue pairs norm: {'standard', 'reversible'} standard: (L'R) = Id, L[:,0] is a probability distribution, the stationary distribution mu of T. Right eigenvectors R have a 2-norm of 1. reversible: R and L are related via L=L[:,0]*R. auto: will be reversible if T is reversible, otherwise standard. Returns ------- R : (M, M) ndarray The normalized (with respect to L) right eigenvectors, such that the column R[:,i] is the right eigenvector corresponding to the eigenvalue w[i], dot(T,R[:,i])=w[i]*R[:,i] D : (M, M) ndarray A diagonal matrix containing the eigenvalues, each repeated according to its multiplicity L : (M, M) ndarray The normalized (with respect to `R`) left eigenvectors, such that the row ``L[i, :]`` is the left eigenvector corresponding to the eigenvalue ``w[i]``, ``dot(L[i, :], T)``=``w[i]*L[i, :]`` """ d = T.shape[0] w, R = eig(T) """Sort by decreasing magnitude of eigenvalue""" ind = np.argsort(np.abs(w))[::-1] w = w[ind] R = R[:, ind] """Diagonal matrix containing eigenvalues""" D = np.diag(w) # auto-set norm if norm == 'auto': from pyemma.msm.analysis import is_reversible if (is_reversible(T)): norm = 'reversible' else: norm = 'standard' # Standard norm: Euclidean norm is 1 for r and LR = I. if norm == 'standard': L = solve(np.transpose(R), np.eye(d)) """l1- normalization of L[:, 0]""" R[:, 0] = R[:, 0] * np.sum(L[:, 0]) L[:, 0] = L[:, 0] / np.sum(L[:, 0]) if k is None: return R, D, np.transpose(L) else: return R[:, 0:k], D[0:k, 0:k], np.transpose(L[:, 0:k]) # Reversible norm: elif norm == 'reversible': b = np.zeros(d) b[0] = 1.0 A = np.transpose(R) nu = solve(A, b) mu = nu / np.sum(nu) """Ensure that R[:,0] is positive""" R[:, 0] = R[:, 0] / np.sign(R[0, 0]) """Use mu to connect L and R""" L = mu[:, np.newaxis] * R """Compute overlap""" s = np.diag(np.dot(np.transpose(L), R)) """Renormalize left-and right eigenvectors to ensure L'R=Id""" R = R / np.sqrt(s[np.newaxis, :]) L = L / np.sqrt(s[np.newaxis, :]) if k is None: return R, D, np.transpose(L) else: return R[:, 0:k], D[0:k, 0:k], np.transpose(L[:, 0:k]) else: raise ValueError("Keyword 'norm' has to be either 'standard' or 'reversible'")