def __init__(self, class_ids, classes=None, mask=None, fill_empty_classes=False): if classes is not None: self.classes = classes else: self.classes = np.unique(class_ids) k = len(self.classes) class_ids = np.array(class_ids) if not isinstance(class_ids, np.ndarray) else class_ids transitions = np.zeros((k, k)) b = 1 if mask is None else mask.ravel() for ii in range(len(class_ids) - 1): np.add.at(transitions, (class_ids[ii].ravel(), class_ids[ii + 1].ravel()), b) self.transitions = transitions self.p = transitions / np.clip(transitions.sum((-1), keepdims=True), a_min=1, a_max=None) if fill_empty_classes: self.p = fill_empty_diagonals(self.p) p_tmp = self.p p_tmp = fill_empty_diagonals(p_tmp) markovchain = qe.MarkovChain(p_tmp) self.num_cclasses = markovchain.num_communication_classes self.num_rclasses = markovchain.num_recurrent_classes self.cclasses_indices = markovchain.communication_classes_indices self.rclasses_indices = markovchain.recurrent_classes_indices transient = set(list(map(tuple, self.cclasses_indices))).difference( set(list(map(tuple, self.rclasses_indices))) ) self.num_tclasses = len(transient) if len(transient): self.tclasses_indices = [np.asarray(i) for i in transient] else: self.tclasses_indices = None self.astates_indices = list(np.argwhere(np.diag(p_tmp) == 1)) self.num_astates = len(self.astates_indices)
def _calc(self): self.lclass_ids = lag_categorical( self.class_ids, classes=self.classes, kernel_sz=self.kernel_sz, sigma=self.sigma, T=True ) T = np.zeros((self.m, self.k, self.k)) for ii in range(len(self.class_ids) - 1): np.add.at( T, (self.lclass_ids[ii].ravel(), self.class_ids[ii].ravel(), self.class_ids[ii + 1].ravel()), 1, ) P = T.copy() P = P / np.clip(P.sum((-1), keepdims=True), a_min=1, a_max=None) if self.fill_empty_classes: P = fill_empty_diagonals(P) return T, P
def steady_state(P, fill_empty_classes=False): """ Generalized function for calculating the steady state distribution for a regular or reducible Markov transition matrix P. Parameters ---------- P : array (k, k), an ergodic or non-ergodic Markov transition probability matrix. fill_empty_classes: bool, optional If True, assign 1 to diagonal elements which fall in rows full of 0s to ensure the transition probability matrix is a stochastic one. Default is False. Returns ------- : array If the Markov chain is irreducible, meaning that there is only one communicating class, there is one unique steady state distribution towards which the system is converging in the long run. Then steady_state is the same as _steady_state_ergodic (k, ). If the Markov chain is reducible, but only has 1 recurrent class, there will be one steady state distribution as well. If the Markov chain is reducible and there are multiple recurrent classes (num_rclasses), the system could be trapped in any one of these recurrent classes. Then, there will be `num_rclasses` steady state distributions. The returned array will of (num_rclasses, k) dimension. Examples -------- >>> import numpy as np >>> from giddy.ergodic import steady_state Irreducible Markov chain >>> p = np.array([[.5, .25, .25],[.5,0,.5],[.25,.25,.5]]) >>> steady_state(p) array([0.4, 0.2, 0.4]) Reducible Markov chain: two communicating classes >>> p = np.array([[.5, .5, 0],[.2,0.8,0],[0,0,1]]) >>> steady_state(p) array([[0.28571429, 0.71428571, 0. ], [0. , 0. , 1. ]]) Reducible Markov chain: two communicating classes >>> p = np.array([[.5, .5, 0],[.2,0.8,0],[0,0,0]]) >>> steady_state(p, fill_empty_classes = True) array([[0.28571429, 0.71428571, 0. ], [0. , 0. , 1. ]]) >>> steady_state(p, fill_empty_classes = False) Traceback (most recent call last): ... ValueError: Input transition probability matrix has 1 rows full of 0s. Please set fill_empty_classes=True to set diagonal elements for these rows to be 1 to make sure the matrix is stochastic. """ P = np.asarray(P) rows0 = (P.sum(axis=1) == 0).sum() if rows0 > 0: if fill_empty_classes: P = fill_empty_diagonals(P) else: raise ValueError("Input transition probability matrix has " "%d rows full of 0s. Please set " "fill_empty_classes=True to set diagonal " "elements for these rows to be 1 to make " "sure the matrix is stochastic." % rows0) mc = qe.MarkovChain(P) num_classes = mc.num_communication_classes if num_classes == 1: return mc.stationary_distributions[0] else: return mc.stationary_distributions
def fmpt(P, fill_empty_classes=False): """ Generalized function for calculating first mean passage times for an ergodic or non-ergodic transition probability matrix. Parameters ---------- P : array (k, k), an ergodic/non-ergodic Markov transition probability matrix. fill_empty_classes: bool, optional If True, assign 1 to diagonal elements which fall in rows full of 0s to ensure the transition probability matrix is a stochastic one. Default is False. Returns ------- fmpt_all : array (k, k), elements are the expected value for the number of intervals required for a chain starting in state i to first enter state j. If i=j then this is the recurrence time. Examples -------- >>> import numpy as np >>> from giddy.ergodic import fmpt >>> np.set_printoptions(suppress=True) #prevent scientific format Irreducible Markov chain >>> p = np.array([[.5, .25, .25],[.5,0,.5],[.25,.25,.5]]) >>> fm = fmpt(p) >>> fm array([[2.5 , 4. , 3.33333333], [2.66666667, 5. , 2.66666667], [3.33333333, 4. , 2.5 ]]) Thus, if it is raining today in Oz we can expect a nice day to come along in another 4 days, on average, and snow to hit in 3.33 days. We can expect another rainy day in 2.5 days. If it is nice today in Oz, we would experience a change in the weather (either rain or snow) in 2.67 days from today. Reducible Markov chain: two communicating classes (this is an artificial example) >>> p = np.array([[.5, .5, 0],[.2,0.8,0],[0,0,1]]) >>> fmpt(p) array([[3.5, 2. , inf], [5. , 1.4, inf], [inf, inf, 1. ]]) Thus, if it is raining today in Oz we can expect a nice day to come along in another 2 days, on average, and should not expect snow to hit. We can expect another rainy day in 3.5 days. If it is nice today in Oz, we should expect a rainy day in 5 days. >>> p = np.array([[.5, .5, 0],[.2,0.8,0],[0,0,0]]) >>> fmpt(p, fill_empty_classes=True) array([[3.5, 2. , inf], [5. , 1.4, inf], [inf, inf, 1. ]]) >>> p = np.array([[.5, .5, 0],[.2,0.8,0],[0,0,0]]) >>> fmpt(p, fill_empty_classes=False) Traceback (most recent call last): ... ValueError: Input transition probability matrix has 1 rows full of 0s. Please set fill_empty_classes=True to set diagonal elements for these rows to be 1 to make sure the matrix is stochastic. """ P = np.asarray(P) rows0 = (P.sum(axis=1) == 0).sum() if rows0 > 0: if fill_empty_classes: P = fill_empty_diagonals(P) else: raise ValueError("Input transition probability matrix has " "%d rows full of 0s. Please set " "fill_empty_classes=True to set diagonal " "elements for these rows to be 1 to make " "sure the matrix is stochastic." % rows0) mc = qe.MarkovChain(P) num_classes = mc.num_communication_classes if num_classes == 1: fmpt_all = _fmpt_ergodic(P) else: # deal with non-ergodic Markov chains k = P.shape[0] fmpt_all = np.zeros((k, k)) for desti in range(k): b = np.ones(k - 1) p_sub = np.delete(np.delete(P, desti, 0), desti, 1) p_calc = np.eye(k - 1) - p_sub m = np.full(k - 1, np.inf) row0 = (p_calc != 0).sum(axis=1) none0 = np.arange(k - 1) try: m[none0] = np.linalg.solve(p_calc, b) except np.linalg.LinAlgError as err: if "Singular matrix" in str(err): if (row0 == 0).sum() > 0: index0 = set(np.argwhere(row0 == 0).flatten()) x = (p_calc[:, list(index0)] != 0).sum(axis=1) setx = set(np.argwhere(x).flatten()) while not setx.issubset(index0): index0 = index0.union(setx) x = (p_calc[:, list(index0)] != 0).sum(axis=1) setx = set(np.argwhere(x).flatten()) none0 = np.asarray(list(set(none0).difference(index0))) if len(none0) >= 1: p_calc = p_calc[none0, :][:, none0] b = b[none0] m[none0] = np.linalg.solve(p_calc, b) recc = np.nan_to_num( (np.delete(P, desti, 1)[desti] * m), 0, posinf=np.inf).sum() + 1 fmpt_all[:, desti] = np.insert(m, desti, recc) fmpt_all = np.where(fmpt_all < -1e16, np.inf, fmpt_all) fmpt_all = np.where(fmpt_all > 1e16, np.inf, fmpt_all) return fmpt_all