def test_extrema_wf(lim=1e-10): """ For small mu, the Wright-Fisher process is minimal in the center. Test that this happens. """ for n, N, mins in [(2, 40, [(20, 20)]), (3, 30, [(10, 10, 10)])]: mu = 1. / N**3 m = numpy.ones((n, n)) # neutral landscape fitness_landscape = linear_fitness_landscape(m) incentive = replicator(fitness_landscape) edge_func = wright_fisher.multivariate_transitions(N, incentive, mu=mu, num_types=n) states = list(simplex_generator(N, d=n - 1)) s = stationary_distribution(edge_func, states=states, iterations=4 * N, lim=lim) s2 = expected_divergence(edge_func, states=states, q_d=0) assert_equal(find_local_minima(s), set(mins)) er = entropy_rate(edge_func, s, states=states) assert_greater_equal(er, 0)
def test_extrema_moran_5(lim=1e-16): """ Test for extrema of the stationary distribution. """ n = 3 N = 60 mu = (3. / 2) * 1. / N m = [[0, 1, 1], [1, 0, 1], [1, 1, 0]] maxes = set([(20, 20, 20), (0, 0, 60), (0, 60, 0), (60, 0, 0), (30, 0, 30), (0, 30, 30), (30, 30, 0)]) fitness_landscape = linear_fitness_landscape(m) incentive = fermi(fitness_landscape, beta=0.1) edges = incentive_process.multivariate_transitions(N, incentive, num_types=n, mu=mu) s = stationary_distribution(edges, lim=lim) s2 = expected_divergence(edges, q_d=0) flow = inflow_outflow(edges) # These sets should all correspond assert_equal(find_local_maxima(s), set(maxes)) assert_equal(find_local_minima(s2), set(maxes)) assert_equal(find_local_minima(flow), set(maxes)) # The minima are pathological assert_equal(find_local_minima(s), set([(3, 3, 54), (3, 54, 3), (54, 3, 3)])) assert_equal(find_local_maxima(s2), set([(4, 52, 4), (4, 4, 52), (52, 4, 4)])) assert_equal(find_local_maxima(flow), set([(1, 58, 1), (1, 1, 58), (58, 1, 1)]))
def test_extrema_moran_5(lim=1e-16): """ Test for extrema of the stationary distribution. """ n = 3 N = 60 mu = (3./2) * 1./N m = [[0, 1, 1], [1, 0, 1], [1, 1, 0]] maxes = set([(20, 20, 20), (0, 0, 60), (0, 60, 0), (60, 0, 0), (30, 0, 30), (0, 30, 30), (30, 30, 0)]) fitness_landscape = linear_fitness_landscape(m) incentive = fermi(fitness_landscape, beta=0.1) edges = incentive_process.multivariate_transitions( N, incentive, num_types=n, mu=mu) s = stationary_distribution(edges, lim=lim) s2 = expected_divergence(edges, q_d=0) flow = inflow_outflow(edges) # These sets should all correspond assert_equal(find_local_maxima(s), set(maxes)) assert_equal(find_local_minima(s2), set(maxes)) assert_equal(find_local_minima(flow), set(maxes)) # The minima are pathological assert_equal(find_local_minima(s), set([(3, 3, 54), (3, 54, 3), (54, 3, 3)])) assert_equal(find_local_maxima(s2), set([(4, 52, 4), (4, 4, 52), (52, 4, 4)])) assert_equal(find_local_maxima(flow), set([(1, 58, 1), (1, 1, 58), (58, 1, 1)]))
def test_wright_fisher(N=20, lim=1e-10, n=2): """Test 2 dimensional Wright-Fisher process.""" for n in [2, 3]: mu = (n - 1.) / n * 1. / (N + 1) m = numpy.ones((n, n)) # neutral landscape fitness_landscape = linear_fitness_landscape(m) incentive = replicator(fitness_landscape) # Wright-Fisher for low_memory in [True, False]: edge_func = wright_fisher.multivariate_transitions( N, incentive, mu=mu, num_types=n, low_memory=low_memory) states = list(simplex_generator(N, d=n-1)) for logspace in [False, True]: s = stationary_distribution( edge_func, states=states, iterations=200, lim=lim, logspace=logspace) wf_edges = edge_func_to_edges(edge_func, states) er = entropy_rate(wf_edges, s) assert_greater_equal(er, 0) # Check that the stationary distribution satistifies balance # conditions check_detailed_balance(wf_edges, s, places=2) check_global_balance(wf_edges, s, places=4) check_eigenvalue(wf_edges, s, places=2)
def test_incentive_process_k(lim=1e-14): """ Compare stationary distribution computations to known analytic form for neutral landscape for the Moran process. """ for k in [1, 2, 10,]: for n, N in [(2, 20), (2, 50), (3, 10), (3, 20)]: mu = (n-1.)/n * 1./(N+1) m = numpy.ones((n, n)) # neutral landscape fitness_landscape = linear_fitness_landscape(m) incentive = replicator(fitness_landscape) # Neutral landscape is the default edges = incentive_process.k_fold_incentive_transitions( N, incentive, num_types=n, mu=mu, k=k) stationary_1 = stationary_distribution(edges, lim=lim) # Check that the stationary distribution satisfies balance # conditions check_detailed_balance(edges, stationary_1) check_global_balance(edges, stationary_1) check_eigenvalue(edges, stationary_1) # Also check edge_func calculation edges = incentive_process.multivariate_transitions( N, incentive, num_types=n, mu=mu) states = states_from_edges(edges) edge_func = power_transitions(edges, k) stationary_2 = stationary_distribution( edge_func, states=states, lim=lim) for key in stationary_1.keys(): assert_almost_equal( stationary_1[key], stationary_2[key], places=5)
def compute_entropy_rate(N=30, n=2, m=None, incentive_func=None, beta=1., mu=None, exact=False, lim=1e-13, logspace=False): if not m: m = np.ones((n, n)) if not incentive_func: incentive_func = incentives.fermi if not mu: # mu = (n-1.)/n * 1./(N+1) mu = 1. / N fitness_landscape = incentives.linear_fitness_landscape(m) incentive = incentive_func(fitness_landscape, beta=beta, q=1) edges = incentive_process.multivariate_transitions(N, incentive, num_types=n, mu=mu) s = stationary.stationary_distribution(edges, exact=exact, lim=lim, logspace=logspace) e = stationary.entropy_rate(edges, s) return e, s
def test_wright_fisher(N=20, lim=1e-10, n=2): """Test 2 dimensional Wright-Fisher process.""" for n in [2, 3]: mu = (n - 1.) / n * 1. / (N + 1) m = numpy.ones((n, n)) # neutral landscape fitness_landscape = linear_fitness_landscape(m) incentive = replicator(fitness_landscape) # Wright-Fisher for low_memory in [True, False]: edge_func = wright_fisher.multivariate_transitions( N, incentive, mu=mu, num_types=n, low_memory=low_memory) states = list(simplex_generator(N, d=n - 1)) for logspace in [False, True]: s = stationary_distribution(edge_func, states=states, iterations=200, lim=lim, logspace=logspace) wf_edges = edge_func_to_edges(edge_func, states) er = entropy_rate(wf_edges, s) assert_greater_equal(er, 0) # Check that the stationary distribution satistifies balance conditions check_detailed_balance(wf_edges, s, places=2) check_global_balance(wf_edges, s, places=4) check_eigenvalue(wf_edges, s, places=2)
def test_extrema_moran_3(lim=1e-12): """ Test for extrema of the stationary distribution. """ n = 2 N = 100 mu = 6. / 25 m = [[1, 0], [0, 1]] maxes = set([(38, 62), (62, 38)]) mins = set([(50, 50), (100, 0), (0, 100)]) fitness_landscape = linear_fitness_landscape(m) incentive = replicator(fitness_landscape) edges = incentive_process.multivariate_transitions(N, incentive, num_types=n, mu=mu) s = stationary_distribution(edges, lim=lim) flow = inflow_outflow(edges) for q_d in [0, 1]: s2 = incentive_process.kl(edges, q_d=1) assert_equal(find_local_maxima(s), set(maxes)) assert_equal(find_local_minima(s), set(mins)) assert_equal(find_local_minima(s2), set([(50, 50), (40, 60), (60, 40)])) assert_equal(find_local_maxima(flow), set(mins))
def cycle_stationary_example(N, m, mu, incentive_func=replicator): graph = cycle(N) fitness_landscape = linear_fitness_landscape(m) incentive = incentive_func(fitness_landscape) edge_dict = multivariate_graph_transitions(N, graph, incentive, num_types=2, mu=mu) print "There are %s configurations and %s transitions" % (len(set([x[0] for x in edge_dict.keys()])), len(edge_dict)) # Compute stationary distribution edges = [(v1, v2, t) for ((v1,v2),t) in edge_dict.items()] s = stationary_distribution(edges, lim=1e-8) return s
def full_example(N=60, m=None, mu=None, pickle_filename="inv_enum.pickle", beta=1., filename="enumerated_edges.csv"): """ Full example of exporting the stationary calculation to C++. """ print "Computing graph of the Markov process." if not mu: mu = 3. / 2 * 1. / N if m is None: m = [[0, 1, 1], [1, 0, 1], [1, 1, 0]] iterations = 5 * N num_types = len(m[0]) fitness_landscape = linear_fitness_landscape(m, normalize=True) incentive = fermi(fitness_landscape, beta=beta) edges_gen = incentive_process.multivariate_transitions_gen( N, incentive, num_types=num_types, mu=mu) print "Outputting graph to %s" % filename inv_enum = output_enumerated_edges(N, num_types, edges_gen, filename=filename) print "Saving inverse enumeration to %s" % pickle_filename pickle_inv_enumeration(inv_enum, pickle_filename="inv_enum.pickle") print "Running C++ Calculation" cwd = os.getcwd() executable = os.path.join(cwd, "a.out") subprocess.call([executable, filename, str(iterations)]) print "Loading stationary distribution" s = stationary_gen(filename="enumerated_stationary.txt", pickle_filename="inv_enum.pickle") print "Rendering stationary to SVG" vmax, vmin = stationary_max_min() s = stationary_gen(filename="enumerated_stationary.txt", pickle_filename="inv_enum.pickle") ternary.svg_heatmap(s, N, "stationary.svg", vmax=vmax, vmin=vmin, style='h')
def test_extrema_moran_2(lim=1e-16): """ Test for extrema of the stationary distribution. """ n = 2 N = 100 mu = 1. / 1000 m = [[1, 2], [3, 1]] maxes = set([(33, 67), (100,0), (0, 100)]) fitness_landscape = linear_fitness_landscape(m) incentive = replicator(fitness_landscape) edges = incentive_process.multivariate_transitions(N, incentive, num_types=n, mu=mu) s = stationary_distribution(edges, lim=lim) s2 = expected_divergence(edges, q_d=0) assert_equal(find_local_maxima(s), set(maxes)) assert_equal(find_local_minima(s2), set(maxes))
def compute_entropy_rate(N=30, n=2, m=None, incentive_func=None, beta=1., mu=None, exact=False, lim=1e-13, logspace=False): if not m: m = np.ones((n, n)) if not incentive_func: incentive_func = incentives.fermi if not mu: # mu = (n-1.)/n * 1./(N+1) mu = 1. / N fitness_landscape = incentives.linear_fitness_landscape(m) incentive = incentive_func(fitness_landscape, beta=beta, q=1) edges = incentive_process.multivariate_transitions( N, incentive, num_types=n, mu=mu) s = stationary.stationary_distribution(edges, exact=exact, lim=lim, logspace=logspace) e = stationary.entropy_rate(edges, s) return e, s
def wright_fisher(N, game_matrix=None, mu=None, incentive_func=replicator, logspace=False): """ A convenience function for the Moran process with mutation. Computes the transition probabilities and the stationary distribution. The number of types is determined from the dimensions of the game_matrix. Parameters ---------- N: int The population size game_matrix: list of lists or numpy matrix, None The game matrix of the process, e.g. [[1, 2], [2, 1]] for the two-type Hawk-Dove game. If not specified, the 2-type neutral landscape is used. mu: float, None The mutation rate, if None then `mu` is set to 1 / N incentive_func: function, replicator A function defining the process, e.g. the Moran process, logit, Fermi, Incentives functions are in stationary.processes.incentives logspace: bool, False Compute in log-space or not Returns ------- edges, s, er: the list of transitions, the stationary distribution, and the entropy rate. """ if not game_matrix: game_matrix = [[1, 1], [1, 1]] if not mu: mu = 1. / N num_types = len(game_matrix[0]) fitness_landscape = linear_fitness_landscape(game_matrix) incentive = incentive_func(fitness_landscape) edge_func = wright_fisher.multivariate_transitions(N, incentive, mu=mu, num_types=num_types) states = list(simplex_generator(N, d=num_types-1)) s = stationary_distribution(edge_func, states=states, iterations=4*N, logspace=logspace) er = entropy_rate(edge_func, s) return edge_func, s, er
def full_example(N, m, mu, beta=1., pickle_filename="inv_enum.pickle", filename="enumerated_edges.csv"): """ Full example of exporting the stationary calculation to C++. """ print("Computing graph of the Markov process.") if not mu: mu = 3. / 2 * 1. / N if m is None: m = [[0, 1, 1], [1, 0, 1], [1, 1, 0]] iterations = 200 * N num_types = len(m[0]) fitness_landscape = linear_fitness_landscape(m) incentive = fermi(fitness_landscape, beta=beta) edges_gen = multivariate_transitions_gen( N, incentive, num_types=num_types, mu=mu) print("Outputting graph to %s" % filename) inv_enum = output_enumerated_edges( N, num_types, edges_gen, filename=filename) print("Saving inverse enumeration to %s" % pickle_filename) pickle_inv_enumeration(inv_enum, pickle_filename="inv_enum.pickle") print("Running C++ Calculation") cwd = os.getcwd() executable = os.path.join(cwd, "a.out") subprocess.call([executable, filename, str(iterations)]) print("Rendering stationary to SVG") vmax, vmin = stationary_max_min() s = list(stationary_gen( filename="enumerated_stationary.txt", pickle_filename="inv_enum.pickle")) ternary.svg_heatmap(s, N, "stationary.svg", vmax=vmax, vmin=vmin, style='h') print("Rendering stationary") tax = render_stationary(s) tax.ticks(axis='lbr', linewidth=1, multiple=N//3, offset=0.015) tax.clear_matplotlib_ticks() plt.show()
def test_extrema_moran_4(lim=1e-16): """ Test for extrema of the stationary distribution. """ n = 3 N = 60 mu = 3./ (2 * N) m = [[0, 1, 1], [1, 0, 1], [1, 1, 0]] maxes = set([(20,20,20)]) mins = set([(0, 0, 60), (0, 60, 0), (60, 0, 0)]) fitness_landscape = linear_fitness_landscape(m) incentive = logit(fitness_landscape, beta=0.1) edges = incentive_process.multivariate_transitions(N, incentive, num_types=n, mu=mu) s = stationary_distribution(edges, lim=lim) s2 = expected_divergence(edges, q_d=0) assert_equal(find_local_maxima(s), set(maxes)) assert_equal(find_local_minima(s), set(mins)) assert_equal(find_local_minima(s2), set(maxes)) assert_equal(find_local_maxima(s2), set(mins))
def test_extrema_wf(lim=1e-10): """ For small mu, the Wright-Fisher process is minimal in the center. Test that this happens. """ for n, N, mins in [(2, 40, [(20, 20)]), (3, 30, [(10, 10, 10)])]: mu = 1. / N ** 3 m = numpy.ones((n, n)) # neutral landscape fitness_landscape = linear_fitness_landscape(m) incentive = replicator(fitness_landscape) edge_func = wright_fisher.multivariate_transitions( N, incentive, mu=mu, num_types=n) states = list(simplex_generator(N, d=n-1)) s = stationary_distribution( edge_func, states=states, iterations=4*N, lim=lim) assert_equal(find_local_minima(s), set(mins)) er = entropy_rate(edge_func, s, states=states) assert_greater_equal(er, 0)
def test_extrema_moran_3(lim=1e-12): """ Test for extrema of the stationary distribution. """ n = 2 N = 100 mu = 6./ 25 m = [[1, 0], [0, 1]] maxes = set([(38, 62), (62, 38)]) mins = set([(50, 50), (100, 0), (0, 100)]) fitness_landscape = linear_fitness_landscape(m) incentive = replicator(fitness_landscape) edges = incentive_process.multivariate_transitions(N, incentive, num_types=n, mu=mu) s = stationary_distribution(edges, lim=lim) flow = inflow_outflow(edges) for q_d in [0, 1]: s2 = expected_divergence(edges, q_d=1) assert_equal(find_local_maxima(s), set(maxes)) assert_equal(find_local_minima(s), set(mins)) assert_equal(find_local_minima(s2), set([(50,50), (40, 60), (60, 40)])) assert_equal(find_local_maxima(flow), set(mins))
def full_example(N=60, m=None, mu=None, pickle_filename="inv_enum.pickle", beta=1., filename="enumerated_edges.csv"): """ Full example of exporting the stationary calculation to C++. """ print "Computing graph of the Markov process." if not mu: mu = 3. / 2 * 1. / N if m is None: m = [[0,1,1],[1,0,1],[1,1,0]] iterations = 5 * N num_types = len(m[0]) fitness_landscape = linear_fitness_landscape(m, normalize=True) incentive = fermi(fitness_landscape, beta=beta) edges_gen = incentive_process.multivariate_transitions_gen(N, incentive, num_types=num_types, mu=mu) print "Outputting graph to %s" % filename inv_enum = output_enumerated_edges(N, num_types, edges_gen, filename=filename) print "Saving inverse enumeration to %s" % pickle_filename pickle_inv_enumeration(inv_enum, pickle_filename="inv_enum.pickle") print "Running C++ Calculation" cwd = os.getcwd() executable = os.path.join(cwd, "a.out") subprocess.call([executable, filename, str(iterations)]) print "Loading stationary distribution" s = stationary_gen(filename="enumerated_stationary.txt", pickle_filename="inv_enum.pickle") print "Rendering stationary to SVG" vmax, vmin = stationary_max_min() s = stationary_gen(filename="enumerated_stationary.txt", pickle_filename="inv_enum.pickle") ternary.svg_heatmap(s, N, "stationary.svg", vmax=vmax, vmin=vmin, style='h')
def compute_everything(N, m, mu, initial_state, num_trajectories=100, trajectory_length=100, incentive_func=replicator, beta=1.): # Get the edges of the Markov process edges = incentive_process.compute_edges(N=N, m=m, mu=mu, beta=beta, incentive_func=incentive_func) transition_dict = edges_to_dictionary(edges) # Generate some trajectories trajectories = list(generate_trajectories(edges, initial_state, iterations=num_trajectories, max_iterations=trajectory_length)) # Compute yens along the trajectories yens = [compute_yen(trajectory, transition_dict) for trajectory in trajectories] # Compute the fitness flux fitness_landscape = linear_fitness_landscape(m) try: incentive = incentive_func(fitness_landscape, beta=beta) except TypeError : incentive = incentive_func(fitness_landscape) fluxes = [compute_fitness_flux(trajectory, incentive) for trajectory in trajectories] # Compute the self-informations self_infos = [compute_self_info(trajectory, edges) for trajectory in trajectories] return (yens, fluxes, self_infos)
def graph_test(): """Test yen search on large process -- a two type neutral Moran process on a cycle.""" def neighbor_function(state): """States in this case are a list of ones and zeroes, i.e. a graph coloring.""" neighbors = [] for i, t in enumerate(state): neighbor = copy.copy(list(state)) if t == 0: neighbor[i] = 1 else: neighbor[i] = 0 neighbors.append(tuple(neighbor)) return neighbors # print(neighbor_function([0, 1, 0])) def transition_function(source, target, mu=0.05): # Find the state that differs for i, (s, t) in enumerate(zip(source, target)): if s != t: break # i is now the index of the state that differs # Look at neighboring states (within the cycle) to determine transition N = len(source) indices = [i-1, i+1] transition = 0. for index in indices: if source[index % N] == s: transition += 1 - mu else: transition += mu return transition state = [0, 1, 0, 1, 0, 0] e = yen_search(state, neighbor_function, transition_function) print(e) state = [0, 1, 0, 1, 1, 1] e = yen_search(state, neighbor_function, transition_function) print(e) # state = [0, 0, 0, 1, 1, 1] # e = yen_search(state, neighbor_function, transition_function) # print(e) # Non-neutral landscape test m = [[1,2], [2,1]] # Hawk-Dove fitness_landscape = linear_fitness_landscape(m) incentive = replicator(fitness_landscape) def transition_function2(source, target, mu=0.01): """Non-neutral landscape specified by a game matrix (above)""" # Find the state that differs for i, (s, t) in enumerate(zip(source, target)): if s != t: break # i is now the index of the state that differs s = sum(source) N = len(source) population_state = (N - s, s) inc = incentive(population_state) denom = float(sum(inc)) indices = [i-1, i+1] transition = 0. for index in indices: rep_type = source[index % N] r = float(inc[rep_type]) / denom if rep_type == s: transition += r * (1 - mu) else: transition += r * mu return transition state = [0, 1, 0, 1, 0, 1, 0, 1, 1, 0] e = yen_search(state, neighbor_function, transition_function2) print(e) state = [0, 1, 1, 0, 1, 1, 0, 0, 0, 1] e = yen_search(state, neighbor_function, transition_function2) print(e) state = [0, 1] * 8 e = yen_search(state, neighbor_function, transition_function2) print(e) state = [0] * 8 + [1] * 8 + [0] * 8 + [1] * 8 e = yen_search(state, neighbor_function, transition_function2) print(e) state = [0, 1] * 64 e = yen_search(state, neighbor_function, transition_function2) print(e) state = [0] * 256 + [1] * 256 e = yen_search(state, neighbor_function, transition_function2) print(e) state = [0, 1] * 64 + [1] * 4 e = yen_search(state, neighbor_function, transition_function2, extrema='min') print(e) state = [0] * 32 + [1, 0] + [1] * 32 e = yen_search(state, neighbor_function, transition_function2, extrema='min') print(e) # transition_function3() def transition_function3(source, target, mu=0.6): """Non-neutral landscape specified by a game matrix (above)""" # Find the state that differs for i, (s, t) in enumerate(zip(source, target)): if s != t: break # i is now the index of the state that differs s = sum(source) N = len(source) population_state = (N - s, s) inc = incentive(population_state) denom = float(sum(inc)) indices = [i-1, i+1] transition = 0. for index in indices: rep_type = source[index % N] r = float(inc[rep_type]) / denom if rep_type == s: transition += r * (1 - mu) else: transition += r * mu return transition state = [0, 1, 0, 1, 0, 1, 0, 1, 1, 0] e = yen_search(state, neighbor_function, transition_function3) print(e) state = [0, 1, 1, 0, 1, 1, 0, 0, 0, 1] e = yen_search(state, neighbor_function, transition_function3) print(e) state = [0, 1] * 8 e = yen_search(state, neighbor_function, transition_function3) print(e) state = [0, 1] * 64 e = yen_search(state, neighbor_function, transition_function3) print(e) state = [0] * 128 + [1] * 128 e = yen_search(state, neighbor_function, transition_function3) print(e)
except IndexError: N = 10 try: mu = sys.argv[2] except IndexError: mu = 1. / N #m = [[1,1], [1,1]] #m = [[1,2], [2,1]] #m = [[2,1],[1,2]] #m = [[2,2],[2,1]] m = [[2, 2], [1, 1]] print(N, m, mu) graph = cycle(N) fitness_landscape = linear_fitness_landscape(m) incentive = replicator(fitness_landscape) edge_dict = multivariate_graph_transitions(N, graph, incentive, num_types=2, mu=mu) edges = [(v1, v2, t) for ((v1, v2), t) in edge_dict.items()] g = Graph(edges) print("There are %s configurations and %s transitions" % (len(set([x[0] for x in edge_dict.keys()])), len(edge_dict))) print("Local Maxima:", len(find_extrema_yen(g, extrema="max"))) print("Local Minima:", len(find_extrema_yen(g, extrema="min"))) print("Total States:", 2**N)
except IndexError: N = 10 try: mu = sys.argv[2] except IndexError: mu = 1./N #m = [[1,1], [1,1]] #m = [[1,2], [2,1]] #m = [[2,1],[1,2]] #m = [[2,2],[2,1]] m = [[2,2],[1,1]] print(N, m, mu) graph = cycle(N) fitness_landscape = linear_fitness_landscape(m) incentive = replicator(fitness_landscape) edge_dict = multivariate_graph_transitions(N, graph, incentive, num_types=2, mu=mu) edges = [(v1, v2, t) for ((v1, v2), t) in edge_dict.items()] g = Graph(edges) print("There are %s configurations and %s transitions" % (len(set([x[0] for x in edge_dict.keys()])), len(edge_dict))) print("Local Maxima:", len(find_extrema_yen(g, extrema="max"))) print("Local Minima:", len(find_extrema_yen(g, extrema="min"))) print("Total States:", 2**N) exit() print("Computing stationary") s = stationary_distribution(edges, lim=1e-8, iterations=1000) print("Local Maxima:", len(find_extrema_stationary(s, g, extrema="max")))