def test_eigenvectors(self): P = self.bdc.transition_matrix # k==None ev = eigvals(P) ev = ev[np.argsort(np.abs(ev))[::-1]] Dn = np.diag(ev) # right eigenvectors Rn = eigenvectors(P) assert_allclose(np.dot(P, Rn), np.dot(Rn, Dn)) # left eigenvectors Ln = eigenvectors(P, right=False).T assert_allclose(np.dot(Ln.T, P), np.dot(Dn, Ln.T)) # orthogonality Xn = np.dot(Ln.T, Rn) di = np.diag_indices(Xn.shape[0]) Xn[di] = 0.0 assert_allclose(Xn, 0) # k!=None Dnk = Dn[:, 0:self.k][0:self.k, :] # right eigenvectors Rn = eigenvectors(P, k=self.k) assert_allclose(np.dot(P, Rn), np.dot(Rn, Dnk)) # left eigenvectors Ln = eigenvectors(P, right=False, k=self.k).T assert_allclose(np.dot(Ln.T, P), np.dot(Dnk, Ln.T)) # orthogonality Xn = np.dot(Ln.T, Rn) di = np.diag_indices(self.k) Xn[di] = 0.0 assert_allclose(Xn, 0)
def test_eigenvectors_reversible(scenario, ncv_values): k, bdc = scenario P = bdc.transition_matrix ev = eigvals(P) ev = ev[np.argsort(np.abs(ev))[::-1]] Dn = np.diag(ev) Dnk = Dn[:, :k][:k, :] with assert_raises(ValueError) if bdc.sparse and k is None else nullcontext(): # right eigenvectors Rn = eigenvectors(P, k=k, reversible=True, ncv=ncv_values) assert_allclose(P @ Rn, Rn @ Dnk) # left eigenvectors Ln = eigenvectors(P, right=False, k=k, reversible=True, ncv=ncv_values).T assert_allclose(Ln.T @ P, Dnk @ Ln.T) # orthogonality Xn = Ln.T @ Rn di = np.diag_indices(Xn.shape[0] if k is None else k) Xn[di] = 0.0 assert_allclose(Xn, 0) Rn = eigenvectors(P, k=k, ncv=ncv_values, reversible=True, mu=bdc.stationary_distribution) assert_allclose(ev[:k][np.newaxis, :] * Rn, P.dot(Rn)) Ln = eigenvectors(P, right=False, k=k, ncv=ncv_values, reversible=True, mu=bdc.stationary_distribution).T assert_allclose(P.transpose().dot(Ln), ev[:k][np.newaxis, :] * Ln)
def test_eigenvectors(scenario, ncv_values): k, bdc = scenario P = bdc.transition_matrix ev = eigvals(P) ev = ev[np.argsort(np.abs(ev))[::-1]] Dn = np.diag(ev) Dnk = Dn[:, :k][:k, :] with assert_raises(ValueError) if bdc.sparse and k is None else nullcontext(): # right eigenvectors Rn = eigenvectors(P, k=k, ncv=ncv_values) assert_allclose(P @ Rn, Rn @ Dnk) # left eigenvectors Ln = eigenvectors(P, right=False, k=k, ncv=ncv_values).T assert_allclose(Ln.T @ P, Dnk @ Ln.T) # orthogonality Xn = Ln.T @ Rn di = np.diag_indices(Xn.shape[0] if k is None else k) Xn[di] = 0.0 assert_allclose(Xn, 0)
def test_eigenvectors(self): P_dense = self.bdc.transition_matrix P = self.bdc.transition_matrix_sparse ev, L, R = eig(P_dense, left=True, right=True) ind = np.argsort(np.abs(ev))[::-1] ev = ev[ind] R = R[:, ind] L = L[:, ind] vals = ev[0:self.k] """k=None""" with self.assertRaises(ValueError): Rn = eigenvectors(P) with self.assertRaises(ValueError): Ln = eigenvectors(P, right=False) """k is not None""" Rn = eigenvectors(P, k=self.k) assert_allclose(vals[np.newaxis, :] * Rn, P.dot(Rn)) Ln = eigenvectors(P, right=False, k=self.k).T assert_allclose(P.transpose().dot(Ln), vals[np.newaxis, :] * Ln) """k is not None and ncv is not None""" Rn = eigenvectors(P, k=self.k, ncv=self.ncv) assert_allclose(vals[np.newaxis, :] * Rn, P.dot(Rn)) Ln = eigenvectors(P, right=False, k=self.k, ncv=self.ncv).T assert_allclose(P.transpose().dot(Ln), vals[np.newaxis, :] * Ln)
def _pcca_connected(P, n, pi=None): r"""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. pi: ndarray(n,), optional, default=None Stationary distribution if available. Returns ------- 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. 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 deeptime.markov.tools.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.") if pi is None: from deeptime.markov.tools.analysis import stationary_distribution pi = stationary_distribution(P) else: if pi.shape[0] != P.shape[0]: raise ValueError( f"Stationary distribution must span entire state space but got {pi.shape[0]} states " f"instead of {P.shape[0]}.") pi /= pi.sum() # make sure it is normalized from deeptime.markov.tools.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 deeptime.markov.tools.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)): warnings.warn( "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) # 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] memberships = np.clip(memberships, 0., 1.) for i in range(0, np.shape(memberships)[0]): memberships[i] /= np.sum(memberships[i]) return memberships