def _compute_probability_distribution_eigenvector(self): """ Find the eigenvector (s) of the transition matrix :param transition_count: :return: """ transition_probability = np.zeros(self.transition_count.shape) transition_count = self._remove_transitions_to_isolated_bins( self.transition_count) for rowidx, row in enumerate(transition_count): # transition_probability[rowidx, rowidx] = 0 rowsum = np.sum(row) if rowsum > 0: transition_probability[rowidx] = row / rowsum eigenvalues, eigenvectors = np.linalg.eig(transition_probability.T) stationary_solution = None unit_eigenval = None # The eigenvalue closest to 1 for idx, eigenval in enumerate(eigenvalues): vec = eigenvectors[:, idx] # logger.debug("Eigenvec for eigenvalue %s:\n%s", eigenval, vec) if np.isclose(1.0, eigenval, rtol=1e-2): neg_vec, pos_vec = vec[vec < 0], vec[vec > 0] if len(pos_vec) == 0: # No positive entries. All must be negative. We can multiply the eigenvector by a factor of -1 vec = -1 * vec elif len(neg_vec) > 0: logger.warning( "Found a vector with eigenvalue ~1(%s) but with negative entries in its eigenvector", eigenval, ) continue if stationary_solution is not None: raise Exception( "Multiple stationary solutions found. Perhaps there were no transitions between states. Eigenvalues:\n%s" % eigenvalues) vec = np.real(vec) stationary_solution = vec / np.sum(vec) unit_eigenval = eigenval relaxation_eigenval = ( None # corresponds to the largest eigenvalue less than 1 ) for idx, eigenval in enumerate(eigenvalues): if eigenval < 1 and eigenval != unit_eigenval: if (relaxation_eigenval is None or eigenval > relaxation_eigenval): relaxation_eigenval = eigenval if stationary_solution is None: raise Exception("No stationary solution found. Eigenvalues:\n%s", eigenvalues) if relaxation_eigenval is not None: logger.info( "Relaxation time for system: %s [units of lag time]. Eigenval=%s", -np.log(relaxation_eigenval), relaxation_eigenval, ) return stationary_solution
def compute_transition_count(self) -> np.array: n_cvs = self.cv_coordinates.shape[2] nbins = self.n_grid_points**n_cvs transition_count = np.zeros((nbins, nbins)) # Transition per bin for t in self.cv_coordinates: if np.any(np.isnan(t) | np.isinf(t)): logger.warning("Found NaN or Inf transition. Ignoring it.") continue start_grid = self._find_grid_coordinates(t[0]) end_grid = self._find_grid_coordinates(t[-1]) start_bin = self._index_converter.convert_to_bin_idx(start_grid) end_bin = self._index_converter.convert_to_bin_idx(end_grid) transition_count[start_bin, end_bin] += 1 return transition_count
def _remove_transitions_to_isolated_bins(self, transition_count): """Remove all transitions which moves from a bin with no starting points""" last_inaccessible_states, last_nonstarting_states = -1, -1 inaccessible_states, nonstarting_states = 1, 1 while (inaccessible_states != last_inaccessible_states or nonstarting_states != last_nonstarting_states): last_inaccessible_states, last_nonstarting_states = ( inaccessible_states, nonstarting_states, ) inaccessible_states, nonstarting_states, accessible_states = ( 0, 0, 0, ) for rowidx, row in enumerate(transition_count): rowsum = np.sum(row) if rowsum == 0: # transition_probability[rowidx] = 0 if np.sum(transition_count[:, rowidx]) == 0: # logger.warning("Found inaccessible state at index %s ", rowidx) inaccessible_states += 1 # rho[rowidx] = np.nan else: # logger.warning("Found non-starting states") nonstarting_states += 1 # TODO see if this makes sense: to set all transition into this state to zero to completely isolate it! transition_count[:, rowidx] = 0 else: accessible_states += 1 if inaccessible_states > 0 or nonstarting_states > 0: logger.warning( "Found %s accessible states, %s inaccessible states and %s states with no starting points.", accessible_states, inaccessible_states, nonstarting_states, ) return transition_count
def _compute_probability_distribution_detailed_balance(self): nbins = self.transition_count.shape[0] transition_probability = np.zeros(self.transition_count.shape) rho = np.zeros((nbins, )) inaccessible_states, nonstarting_states = 0, 0 transition_count = self._remove_transitions_to_isolated_bins( self.transition_count) for rowidx, row in enumerate(transition_count): # transition_probability[rowidx, rowidx] = 0 rowsum = np.sum(row) if rowsum > 0: # transition_probability[rowidx, rowidx] = 0 transition_probability[rowidx] = row / rowsum # transition_probability[rowidx, rowidx] = -rowsum else: rho[rowidx] = np.nan if inaccessible_states > 0 or nonstarting_states > 0: logger.warning( "Found %s inaccessible states and %s states with no starting points.", inaccessible_states, nonstarting_states, ) # Set up starting guess for distribution: all accessible states equal for i, rhoi in enumerate(rho): if np.isnan(rhoi): rho[i] = 0 else: rho[i] = 1 rho = rho / np.sum(rho) convergences = [] convergence = 100 while convergence > 1e-6: last = rho # np.copy(rho) for k, rhok in enumerate(rho): if rhok == 0: continue crossterm = np.dot(rho, transition_probability[:, k]) - np.sum( rhok * transition_probability[k, :]) # crossterm = 0 # for l, rhol in enumerate(rho): # if l != k: # crossterm += rhol * transition_probability[l, k] - rhok * transition_probability[k, l] if rhok == 0 and crossterm > 0: logger.warning( "NOOOO for index %s. Crossterm %s rhok %s", k, crossterm, rhok, ) rho[k] = rhok + crossterm rho = rho / np.sum(rho) if last is not None: convergence = np.linalg.norm(rho - last) convergences.append(convergence) logger.debug( "Converged with master equation after %s iterations", len(convergences), ) # plt.plot(convergences, label="Convergence") # plt.show() if len(rho[rho == 0]) < inaccessible_states: raise Exception( "Something went wrong. Number inaccessible states differ %s vs. %s" % (len(rho[rho == 0]), inaccessible_states)) return rho