def test_is_connected(sparse_mode): C1 = 1.0 * np.array([[1, 4, 3], [3, 2, 4], [4, 5, 1]]) C2 = 1.0 * np.array([[0, 1], [1, 0]]) C3 = 1.0 * np.array([[7]]) C = scipy.sparse.block_diag((C1, C2, C3)) C = C.toarray() """Forward transition block 1 -> block 2""" C[2, 3] = 1 """Forward transition block 2 -> block 3""" C[4, 5] = 1 T_connected = scipy.sparse.csr_matrix(C1 / C1.sum(axis=1)[:, np.newaxis]) T_not_connected = scipy.sparse.csr_matrix(C / C.sum(axis=1)[:, np.newaxis]) if not sparse_mode: T_connected = T_connected.toarray() T_not_connected = T_not_connected.toarray() """Directed""" assert_(not is_connected(T_not_connected)) assert_(is_connected(T_connected)) """Undirected""" assert_(is_connected(T_not_connected, directed=False))
def test_connected_count_matrix(self): """Directed""" is_c = is_connected(self.T_not_connected) self.assertFalse(is_c) is_c = is_connected(self.T_connected) self.assertTrue(is_c) """Undirected""" is_c = is_connected(self.T_not_connected, directed=False) self.assertTrue(is_c)
def test_transition_matrix(oom_msm_scenario): for msm in oom_msm_scenario.msms: P = msm.transition_matrix # should be ndarray by default np.testing.assert_( isinstance(P, np.ndarray) or isinstance(P, scipy.sparse.csr_matrix)) # shape np.testing.assert_equal(P.shape, (msm.n_states, msm.n_states)) # test transition matrix properties import deeptime.markov.tools.analysis as msmana np.testing.assert_(msmana.is_transition_matrix(P)) np.testing.assert_(msmana.is_connected(P)) # REVERSIBLE if msm.reversible: np.testing.assert_(msmana.is_reversible(P)) # Test equality with model: from scipy.sparse import issparse if issparse(P): P = P.toarray() if msm.reversible: np.testing.assert_allclose( P, oom_msm_scenario.rmsmrev.transition_matrix) else: np.testing.assert_allclose(P, oom_msm_scenario.rmsm.transition_matrix)
def _transition_matrix(self, msm): P = msm.transition_matrix # should be ndarray by default # assert (isinstance(P, np.ndarray)) assert (isinstance(P, np.ndarray) or isinstance(P, scipy.sparse.csr_matrix)) # shape assert (np.all(P.shape == (msm.nstates, msm.nstates))) # test transition matrix properties assert (is_transition_matrix(P)) assert (is_connected(P)) # REVERSIBLE if msm.is_reversible: assert (is_reversible(P))
def _update_transition_matrix(model: _SampleStorage, transition_matrix_prior, initial_distribution_prior, reversible: bool = True, stationary: bool = False, n_sampling_steps: int = 1000): """ Updates the hidden-state transition matrix and the initial distribution """ C = _bd_util.count_matrix(model.hidden_trajs, 1, model.transition_matrix.shape[0]) model.counts[...] = C C = C + transition_matrix_prior # check if we work with these options if reversible and not is_connected(C, directed=True): raise NotImplementedError( 'Encountered disconnected count matrix with sampling option reversible:\n ' f'{C}\nUse prior to ensure connectivity or use reversible=False.' ) # ensure consistent sparsity pattern (P0 might have additional zeros because of underflows) # TODO: these steps work around a bug in msmtools. Should be fixed there P0 = transition_matrix(C, reversible=reversible, maxiter=10000, warn_not_converged=False) zeros = np.where(P0 + P0.T == 0) C[zeros] = 0 # run sampler Tij = sample_tmatrix(C, nsample=1, nsteps=n_sampling_steps, reversible=reversible) # INITIAL DISTRIBUTION if stationary: # p0 is consistent with P p0 = stationary_distribution(Tij, C=C) else: n0 = BayesianHMM._count_init(model.hidden_trajs, model.transition_matrix.shape[0]) first_timestep_counts_with_prior = n0 + initial_distribution_prior positive = first_timestep_counts_with_prior > 0 p0 = np.zeros(n0.shape, dtype=model.transition_matrix.dtype) p0[positive] = np.random.dirichlet( first_timestep_counts_with_prior[positive] ) # sample p0 from posterior # update HMM with new sample model.transition_matrix[...] = Tij model.stationary_distribution[...] = p0
def test_transition_matrix(self, setting): scenario = make_double_well(setting) msm = scenario.msm P = msm.transition_matrix # should be ndarray by default # assert (isinstance(P, np.ndarray)) assert_(isinstance(P, np.ndarray) or isinstance(P, scipy.sparse.csr_matrix)) # shape assert_equal(P.shape, (msm.n_states, msm.n_states)) # test transition matrix properties import deeptime.markov.tools.analysis as msmana assert_(msmana.is_transition_matrix(P)) assert_(msmana.is_connected(P)) # REVERSIBLE if msm.reversible: assert_(msmana.is_reversible(P))
def check_input(self): from deeptime.markov.tools.analysis import is_connected if self.C.dtype not in (np.float32, np.float64, np.longdouble): raise ValueError( "Only supports float32, float64, and longdouble dtype.") if not np.all(self.C >= 0): raise ValueError("Count matrix contains negative elements") if not is_connected(self.C): raise ValueError("Count matrix is not connected") if not np.all(self.V >= 0.0): raise ValueError("P0 contains negative entries") if not np.allclose(self.V, self.V.T, atol=1e-6): raise ValueError("P0 is not reversible") """Check sparsity pattern""" iC, jC = np.where((self.C + self.C.T + np.eye(self.C.shape[0])) > 0) iV, jV = np.where((self.V + self.V.T + np.eye(self.V.shape[0])) > 0) if not np.array_equal(iC, iV): raise ValueError('Sparsity patterns of C and X are different.') if not np.array_equal(jC, jV): raise ValueError('Sparsity patterns of C and X are different.')
def check_input(self): from deeptime.markov.tools.analysis import is_connected if not np.all(self.C >= 0): raise ValueError("Count matrix contains negative elements") if not is_connected(self.C, directed=False): raise ValueError("Count matrix is not connected") if not np.all(self.X >= 0.0): raise ValueError("P0 contains negative entries") if not np.allclose(self.X, self.X.T): raise ValueError("P0 is not reversible") """Check sparsity pattern - ignore diagonal""" C_sym = self.C + self.C.T X_sym = self.X + self.X.T ind = np.diag_indices(C_sym.shape[0]) C_sym[ind] = 0.0 X_sym[ind] = 0.0 iC, jC = np.where(C_sym > 0.0) iV, jV = np.where(X_sym > 0.0) if not np.array_equal(iC, iV): raise ValueError('Sparsity patterns of C and X are different.') if not np.array_equal(jC, jV): raise ValueError('Sparsity patterns of C and X are different.')
def test_is_connected(self): self.assertTrue(is_connected(self.T)) self.assertTrue(is_connected(self.T, directed=False))
def fit(self, data, n_burn_in: int = 0, n_thin: int = 1, **kwargs): r""" Sample from the posterior. Parameters ---------- data : array_like or list of array_like Input time series data. n_burn_in : int, optional, default=0 The number of samples to discard to burn-in, following which :attr:`n_samples` samples will be generated. n_thin : int, optional, default=1 The number of Gibbs sampling updates used to generate each returned sample. **kwargs Ignored kwargs for scikit-learn compatibility. Returns ------- self : BayesianHMM Reference to self. """ dtrajs = ensure_dtraj_list(data) # fetch priors tmat = self.initial_hmm.transition_model.transition_matrix transition_matrix_prior = self._transition_matrix_prior_np initial_distribution_prior = self._initial_distribution_prior_np model = BayesianHMMPosterior() # update HMM Model model.prior = self.initial_hmm.copy() prior = model.prior # check if we are strongly connected in the reversible case (plus prior) if self.reversible and not is_connected(tmat + transition_matrix_prior, directed=True): raise NotImplementedError( 'Trying to sample disconnected HMM with option reversible:\n ' f'{tmat}\n Use prior to connect, select connected subset, ' f'or use reversible=False.') # EVALUATE STRIDE dtrajs_lagged_strided = compute_dtrajs_effective( dtrajs, lagtime=prior.lagtime, n_states=prior.n_hidden_states, stride=self.stride) # if stride is different to init_hmm, check if microstates in lagged-strided trajs are compatible if self.stride != self.initial_hmm.stride: symbols = np.unique(np.concatenate(dtrajs_lagged_strided)) if not len( np.intersect1d(self.initial_hmm.observation_symbols, symbols)) == len(symbols): raise ValueError( 'Choice of stride has excluded a different set of microstates than in ' 'init_hmm. Set of observed microstates in time-lagged strided trajectories ' 'must match to the one used for init_hmm estimation.') # here we blow up the output matrix (if needed) to the FULL state space because we want to use dtrajs in the # Bayesian HMM sampler. This is just an initialization. n_states_full = number_of_states(dtrajs_lagged_strided) if prior.n_observation_states < n_states_full: eps = 0.01 / n_states_full # default output probability, in order to avoid zero columns # full state space output matrix. make sure there are no zero columns full_obs_probabilities = eps * np.ones( (prior.n_hidden_states, n_states_full), dtype=np.float64) # fill active states full_obs_probabilities[:, prior.observation_symbols] = np.maximum( eps, prior.output_probabilities) # renormalize B to make it row-stochastic full_obs_probabilities /= full_obs_probabilities.sum(axis=1)[:, None] else: full_obs_probabilities = prior.output_probabilities maxT = max(len(o) for o in dtrajs_lagged_strided) # pre-construct hidden variables temp_alpha = np.zeros((maxT, prior.n_hidden_states)) has_all_obs_symbols = model.prior.n_observation_states == len( model.prior.observation_symbols_full) try: # sample model is basically copy of prior sample_model = BayesianHMM._SampleStorage( transition_matrix=prior.transition_model.transition_matrix. copy(), output_model=DiscreteOutputModel( full_obs_probabilities.copy()), initial_distribution=prior.initial_distribution.copy(), stationary_distribution=prior.transition_model. stationary_distribution.copy(), counts=prior.count_model.count_matrix.copy(), hidden_trajs=[]) # Run burn-in. for _ in range(n_burn_in): self._update(sample_model, dtrajs_lagged_strided, temp_alpha, transition_matrix_prior, initial_distribution_prior) # Collect data. models = [] for _ in range(self.n_samples): # Run a number of Gibbs sampling updates to generate each sample. for _ in range(n_thin): self._update(sample_model, dtrajs_lagged_strided, temp_alpha, transition_matrix_prior, initial_distribution_prior) sample_model.output_model.normalize() self._append_sample(models, prior, sample_model) if not has_all_obs_symbols: models = [ m.submodel(states=None, obs=model.prior.observation_symbols) for m in models ] model.samples = models finally: del temp_alpha # set new model self._model = model return self
def _estimate(self, dtrajs): # ensure right format dtrajs = ensure_dtraj_list(dtrajs) if self.init_hmsm is None: # estimate using maximum-likelihood superclass # memorize the observation state for bhmm and reset # TODO: more elegant solution is to set Estimator params only temporarily in estimate(X, **kwargs) default_connectivity = self.connectivity default_mincount_connectivity = self.mincount_connectivity default_observe_nonempty = self.observe_nonempty self.connectivity = None self.observe_nonempty = False self.mincount_connectivity = 0 self.accuracy = 1e-2 # this is sufficient for an initial guess super(BayesianHMSM, self)._estimate(dtrajs) self.connectivity = default_connectivity self.mincount_connectivity = default_mincount_connectivity self.observe_nonempty = default_observe_nonempty else: # if given another initialization, must copy its attributes copy_attributes = [ '_nstates', '_reversible', '_pi', '_observable_set', 'likelihoods', 'likelihood', 'hidden_state_probabilities', 'hidden_state_trajectories', 'count_matrix', 'initial_count', 'initial_distribution', '_active_set' ] check_user_choices = ['lag', '_nstates'] # check if nstates and lag are compatible for attr in check_user_choices: if not getattr(self, attr) == getattr(self.init_hmsm, attr): raise UserWarning( 'BayesianHMSM cannot be initialized with init_hmsm with ' 'incompatible lag or nstates.') if (len(dtrajs) != len(self.init_hmsm.dtrajs_full) or not all( (_np.array_equal(d1, d2) for d1, d2 in zip(dtrajs, self.init_hmsm.dtrajs_full)))): raise NotImplementedError( 'Bayesian HMM estimation with init_hmsm is currently only implemented ' + 'if applied to the same data.') # TODO: implement more elegant solution to copy-pasting effective stride evaluation from ML HMM. # EVALUATE STRIDE if self.stride == 'effective': # by default use lag as stride (=lag sampling), because we currently have no better theory for deciding # how many uncorrelated counts we can make self.stride = self.lag # get a quick estimate from the spectral radius of the nonreversible from pyemma.msm import estimate_markov_model msm_nr = estimate_markov_model(dtrajs, lag=self.lag, reversible=False, sparse=False, connectivity='largest', dt_traj=self.timestep_traj) # if we have more than nstates timescales in our MSM, we use the next (neglected) timescale as an # estimate of the decorrelation time if msm_nr.nstates > self.nstates: corrtime = max(1, msm_nr.timescales()[self.nstates - 1]) # use the smaller of these two pessimistic estimates self.stride = int(min(self.lag, 2 * corrtime)) # if stride is different to init_hmsm, check if microstates in lagged-strided trajs are compatible if self.stride != self.init_hmsm.stride: from deeptime.markov import compute_dtrajs_effective dtrajs_lagged_strided = compute_dtrajs_effective( dtrajs, lagtime=self.lag, n_states=-1, stride=self.stride) _nstates_obs = number_of_states(dtrajs_lagged_strided, only_used=True) _nstates_obs_full = number_of_states(dtrajs) if _np.setxor1d(_np.concatenate(dtrajs_lagged_strided), _np.concatenate( self.init_hmsm._dtrajs_lagged)).size != 0: raise UserWarning( 'Choice of stride has excluded a different set of microstates than in ' 'init_hmsm. Set of observed microstates in time-lagged strided trajectories ' 'must match to the one used for init_hmsm estimation.') self._dtrajs_full = dtrajs self._dtrajs_lagged = dtrajs_lagged_strided self._nstates_obs_full = _nstates_obs_full self._nstates_obs = _nstates_obs self._observable_set = _np.arange(self._nstates_obs) self._dtrajs_obs = dtrajs else: copy_attributes += [ '_dtrajs_full', '_dtrajs_lagged', '_nstates_obs_full', '_nstates_obs', '_observable_set', '_dtrajs_obs' ] # update self with estimates from init_hmsm self.__dict__.update({ k: i for k, i in self.init_hmsm.__dict__.items() if k in copy_attributes }) # as mentioned in the docstring, take init_hmsm observed set observation probabilities self.observe_nonempty = False # update HMM Model self.update_model_params( P=self.init_hmsm.transition_matrix, pobs=self.init_hmsm.observation_probabilities, dt_model=TimeUnit(self.dt_traj).get_scaled(self.lag)) # check if we have a valid initial model if self.reversible and not is_connected(self.count_matrix): raise NotImplementedError( 'Encountered disconnected count matrix:\n{count_matrix} ' 'with reversible Bayesian HMM sampler using lag={lag}' ' and stride={stride}. Consider using shorter lag, ' 'or shorter stride (to use more of the data), ' 'or using a lower value for mincount_connectivity.'.format( count_matrix=self.count_matrix, lag=self.lag, stride=self.stride)) # here we blow up the output matrix (if needed) to the FULL state space because we want to use dtrajs in the # Bayesian HMM sampler. This is just an initialization. nstates_full = number_of_states(dtrajs) if self.nstates_obs < nstates_full: eps = 0.01 / nstates_full # default output probability, in order to avoid zero columns # full state space output matrix. make sure there are no zero columns B_init = eps * _np.ones( (self.nstates, nstates_full), dtype=_np.float64) # fill active states B_init[:, self.observable_set] = _np.maximum( eps, self.observation_probabilities) # renormalize B to make it row-stochastic B_init /= B_init.sum(axis=1)[:, None] else: B_init = self.observation_probabilities # HMM sampler if self.show_progress: self._progress_register(self.nsamples, description='Sampling HMSMs', stage=0) from deeptime.util.callbacks import ProgressCallback outer_self = self class BHMMCallback(ProgressCallback): def __call__(self, inc=1, *args, **kw): super().__call__(inc, *args, **kw) outer_self._progress_update(1, stage=0) progress = BHMMCallback else: progress = None from deeptime.markov.hmm import BayesianHMM if self.init_hmsm is not None: hmm_mle = self.init_hmsm.hmm estimator = BayesianHMM( hmm_mle, n_samples=self.nsamples, stride=self.stride, initial_distribution_prior=self.p0_prior, transition_matrix_prior=self.transition_matrix_prior, store_hidden=self.store_hidden, reversible=self.reversible, stationary=self.stationary) else: estimator = BayesianHMM.default( dtrajs, n_hidden_states=self.nstates, lagtime=self.lag, n_samples=self.nsamples, stride=self.stride, initial_distribution_prior=self.p0_prior, transition_matrix_prior=self.transition_matrix_prior, store_hidden=self.store_hidden, reversible=self.reversible, stationary=self.stationary, prior_submodel=True, separate=self.separate) estimator.fit(dtrajs, n_burn_in=0, n_thin=1, progress=progress) model = estimator.fetch_model() if self.show_progress: self._progress_force_finish(stage=0) # Samples sample_inp = [(m.transition_model.transition_matrix, m.transition_model.stationary_distribution, m.output_probabilities) for m in model.samples] samples = [] for P, pi, pobs in sample_inp: # restrict to observable set if necessary Bobs = pobs[:, self.observable_set] pobs = Bobs / Bobs.sum(axis=1)[:, None] # renormalize samples.append(_HMSM(P, pobs, pi=pi, dt_model=self.dt_model)) # store results self.sampled_trajs = [ model.samples[i].hidden_state_trajectories for i in range(self.nsamples) ] self.update_model_params(samples=samples) # deal with connectivity states_subset = None if self.connectivity == 'largest': states_subset = 'largest-strong' elif self.connectivity == 'populous': states_subset = 'populous-strong' # OBSERVATION SET if self.observe_nonempty: observe_subset = 'nonempty' else: observe_subset = None # return submodel (will return self if all None) return self.submodel(states=states_subset, obs=observe_subset, mincount_connectivity=self.mincount_connectivity, inplace=True)