Example #1
0
 def test_eigenvalues(self):
     P = self.bdc.transition_matrix
     ev = eigvals(P)
     """Sort with decreasing magnitude"""
     ev = ev[np.argsort(np.abs(ev))[::-1]]
     """k=None"""
     evn = eigenvalues(P)
     assert_allclose(ev, evn)
     """k is not None"""
     evn = eigenvalues(P, k=self.k)
     assert_allclose(ev[0:self.k], evn)
Example #2
0
    def _compute_eigenvalues(self, neig):
        """ Conducts the eigenvalue decomposition and stores k eigenvalues, left and right eigenvectors """
        from deeptime.markov.tools.analysis import eigenvalues
        if self.reversible:
            self._eigenvalues = eigenvalues(self.transition_matrix, k=neig, ncv=self.ncv,
                                            reversible=True, mu=self.stationary_distribution)
        else:
            self._eigenvalues = eigenvalues(self.transition_matrix, k=neig, ncv=self.ncv, reversible=False)

        if _np.all(self._eigenvalues.imag == 0):
            self._eigenvalues = self._eigenvalues.real
Example #3
0
 def test_eigenvalues_reversible(self):
     P = self.bdc.transition_matrix
     ev = eigvals(P)
     """Sort with decreasing magnitude"""
     ev = ev[np.argsort(np.abs(ev))[::-1]]
     """reversible without given mu"""
     evn = eigenvalues(P, reversible=True)
     assert_allclose(ev, evn)
     """reversible with given mu"""
     evn = eigenvalues(P,
                       reversible=True,
                       mu=self.bdc.stationary_distribution)
     assert_allclose(ev, evn)
Example #4
0
 def test_eigenvalues(self):
     P = self.bdc.transition_matrix_sparse
     P_dense = self.bdc.transition_matrix
     ev = eigvals(P_dense)
     """Sort with decreasing magnitude"""
     ev = ev[np.argsort(np.abs(ev))[::-1]]
     """k=None"""
     with self.assertRaises(ValueError):
         evn = eigenvalues(P)
     """k is not None"""
     evn = eigenvalues(P, k=self.k)
     assert_allclose(ev[0:self.k], evn)
     """k is not None and ncv is not None"""
     evn = eigenvalues(P, k=self.k, ncv=self.ncv)
     assert_allclose(ev[0:self.k], evn)
def test_eigenvalues_reversible(scenario, ncv_values):
    k, bdc = scenario
    P = bdc.transition_matrix
    ev = eigvals(P)
    """Sort with decreasing magnitude"""
    ev = ev[np.argsort(np.abs(ev))[::-1]]

    with assert_raises(ValueError) if bdc.sparse and k is None else nullcontext():
        """reversible without given mu"""
        evn = eigenvalues(P, reversible=True, k=k, ncv=ncv_values)
        assert_allclose(ev[:k], evn)

        """reversible with given mu"""
        evn = eigenvalues(P, reversible=True, mu=bdc.stationary_distribution, k=k, ncv=ncv_values)
        assert_allclose(ev[:k], evn)
def test_eigenvalues(scenario, ncv_values):
    k, bdc = scenario
    P = bdc.transition_matrix
    ev = eigvals(P)
    """Sort with decreasing magnitude"""
    ev = ev[np.argsort(np.abs(ev))[::-1]]

    """k=None"""
    with assert_raises(ValueError) if bdc.sparse and k is None else nullcontext():
        evn = eigenvalues(P, ncv=ncv_values, k=k)
        assert_allclose(ev[:k], evn)
Example #7
0
 def test_eigenvalues_rev(self):
     P = self.bdc.transition_matrix_sparse
     P_dense = self.bdc.transition_matrix
     ev = eigvals(P_dense)
     """Sort with decreasing magnitude"""
     ev = ev[np.argsort(np.abs(ev))[::-1]]
     """k=None"""
     with self.assertRaises(ValueError):
         evn = eigenvalues(P, reversible=True)
     """k is not None"""
     evn = eigenvalues(P, k=self.k, reversible=True)
     assert_allclose(ev[0:self.k], evn)
     """k is not None and ncv is not None"""
     evn = eigenvalues(P, k=self.k, ncv=self.ncv, reversible=True)
     assert_allclose(ev[0:self.k], evn)
     """mu is not None"""
     mu = self.bdc.stationary_distribution
     """k=None"""
     with self.assertRaises(ValueError):
         evn = eigenvalues(P, reversible=True, mu=mu)
     """k is not None"""
     evn = eigenvalues(P, k=self.k, reversible=True, mu=mu)
     assert_allclose(ev[0:self.k], evn)
     """k is not None and ncv is not None"""
     evn = eigenvalues(P, k=self.k, ncv=self.ncv, reversible=True, mu=mu)
     assert_allclose(ev[0:self.k], evn)
Example #8
0
def pcca(P: np.ndarray,
         m: int,
         pi: np.ndarray = None,
         transition_matrix_tol: float = 1e-12):
    """
    PCCA+ spectral clustering method with optimized memberships [1]_

    Clusters the first m eigenvectors of a transition matrix in order to cluster the states.
    This function does not assume that the transition matrix is fully connected. Disconnected sets
    will automatically define the first metastable states, with perfect membership assignments.

    Parameters
    ----------
    P : ndarray (n,n)
        Transition matrix.
    m : int
        Number of clusters to group to.
    pi : ndarray(n,), optional, default=None
        Stationary distribution if available. Should be defined piecewise over the connected sets.
    transition_matrix_tol : float, optional, default=1e-12
        Tolerance under which P is checked to be a transition matrix.

    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).
    [2] F. Noe, multiset PCCA and HMMs, in preparation.
    """
    # imports
    from deeptime.markov.tools.estimation import connected_sets
    from deeptime.markov.tools.analysis import eigenvalues, is_transition_matrix, hitting_probability

    # validate input
    n = np.shape(P)[0]
    if m > n:
        raise ValueError(
            f"Number of metastable states m={m} exceeds number of states of transition matrix n={n}"
        )
    if not is_transition_matrix(P, tol=transition_matrix_tol):
        raise ValueError("Input matrix is not a transition matrix.")
    if pi is not None and pi.ndim > 1:
        raise ValueError(
            "Stationary distribution must be given as one-dimensional array or left None."
        )
    if pi is not None and pi.shape[0] != n:
        raise ValueError(
            f"Stationary distribution must be defined on entire space, piecewise if the transition matrix "
            f"has multiple connected components. It covered {pi.shape[0]} != {n} states."
        )

    # prepare output
    chi = np.zeros((n, m))

    # test connectivity
    components = connected_sets(P)
    n_components = len(
        components
    )  # (n_components, labels) = connected_components(P, connection='strong')

    # store components as closed (with positive equilibrium distribution)
    # or as transition states (with vanishing equilibrium distribution)
    closed_components = []
    transition_states = []
    for i in range(n_components):
        component = components[i]  # np.argwhere(labels==i).flatten()
        rest = list(set(range(n)) - set(component))
        # is component closed?
        if np.sum(P[component, :][:, rest]) == 0:
            closed_components.append(component)
        else:
            transition_states.append(component)
    n_closed_components = len(closed_components)
    closed_states = np.concatenate(closed_components)
    if len(transition_states) == 0:
        transition_states = np.array([], dtype=int)
    else:
        transition_states = np.concatenate(transition_states)

    # check if we have enough clusters to support the disconnected sets
    if m < len(closed_components):
        raise ValueError(
            f"Number of metastable states m={m} is too small. Transition matrix "
            f"has {len(closed_components)} disconnected components.")

    # We collect eigenvalues in order to decide which
    closed_components_Psub = []
    closed_components_ev = []
    closed_components_enum = []
    for i in range(n_closed_components):
        component = closed_components[i]

        # compute eigenvalues in submatrix
        Psub = P[component, :][:, component]
        closed_components_Psub.append(Psub)
        closed_components_ev.append(eigenvalues(Psub))
        closed_components_enum.append(i * np.ones(
            (component.size, ), dtype=int))

    # flatten
    closed_components_ev_flat = np.hstack(closed_components_ev)
    closed_components_enum_flat = np.hstack(closed_components_enum)
    # which components should be clustered?
    component_indexes = closed_components_enum_flat[np.argsort(
        closed_components_ev_flat)][0:m]
    # cluster each component
    ipcca = 0
    for i in range(n_closed_components):
        component = closed_components[i]
        # how many PCCA states in this component?
        m_by_component = np.shape(np.argwhere(component_indexes == i))[0]

        # if 1, then the result is trivial
        if m_by_component == 1:
            chi[component, ipcca] = 1.0
            ipcca += 1
        elif m_by_component > 1:
            # print "submatrix: ",closed_components_Psub[i]
            chi[component, ipcca:ipcca + m_by_component] = _pcca_connected(
                closed_components_Psub[i],
                m_by_component,
                pi=None if pi is None else pi[closed_components[i]])
            ipcca += m_by_component
        else:
            raise RuntimeError(
                f"Component {i} spuriously has {m_by_component} pcca sets")

    # finally assign all transition states
    if transition_states.size > 0:
        # make all closed states absorbing, so we can see which closed state we hit first
        Pabs = P.copy()
        Pabs[closed_states, :] = 0.0
        Pabs[closed_states, closed_states] = 1.0
        for i in range(closed_states.size):
            # hitting probability to each closed state
            h = hitting_probability(Pabs, closed_states[i])
            for j in range(transition_states.size):
                # transition states belong to closed states with the hitting probability, and inherit their chi
                chi[transition_states[j]] += h[transition_states[j]] * chi[
                    closed_states[i]]

    # check if we have m metastable sets. If less than m, we must raise
    nmeta = np.count_nonzero(chi.sum(axis=0))
    if nmeta < m:
        raise RuntimeError(
            f"{m} metastable states requested, but transition matrix only has {nmeta}. "
            f"Consider using a prior or request less metastable states.")

    return chi