├── LICENSE ├── README.md ├── examples ├── cycle_stationary.py ├── entropic_equilibria_plots.py ├── examples.py ├── fixation_examples.py └── variable_population_stationary.py ├── export_to_cpp.py ├── fixation_example.png ├── setup.py ├── stationary.cpp ├── stationary ├── __init__.py ├── convenience.py ├── entropy_rate_.py ├── processes │ ├── __init__.py │ ├── graph_process.py │ ├── incentive_process.py │ ├── incentives.py │ ├── variable_population_size.py │ └── wright_fisher.py ├── stationary_.py └── utils │ ├── __init__.py │ ├── bomze.py │ ├── bomze.txt │ ├── edges.py │ ├── expected_divergence_.py │ ├── extrema.py │ ├── files.py │ ├── graph.py │ ├── heatmap.py │ ├── math_helpers.py │ ├── matrix_checks.py │ └── plotting.py └── tests ├── test_bomze.py ├── test_edges.py ├── test_math_helpers.py └── test_stationary.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT license 2 | 3 | Copyright (c) 2018 Marc Harper, Google LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Stationary 3 | 4 | This is a python library for computing stationary distributions of finite Markov 5 | process, with a particular emphasis on finite population dynamics. 6 | 7 | The library can approximate solutions for arbitrary finite Markov processes and 8 | compute exact stationary distributions for reversible Markov processes on discretized 9 | simplices. The approximate solutions are very accurate (see below). One need only supply a 10 | list of weighted edges: 11 | 12 | ```python 13 | edges = [(source_state, target_state, transition_probability), ...] 14 | s = stationary_distribution(edges) 15 | ``` 16 | 17 | The states of the process can be any mutable or hashable python object: 18 | integers, strings, etc. The function `stationary_distribution` accepts a few 19 | parameters, including a `logspace=True` option for transition probabilities that 20 | are very small and need to be handle in log space. To save memory for large 21 | state spaces, the library uses a sparse matrix implementation of the graph 22 | associated to the Markov process. A relatively modern computer should be able to 23 | fit a few million states into memory. 24 | 25 | Also included are functions to generate transition probabilities for the Moran 26 | process with mutation and various generalizations, including Fermi processes, 27 | dynamics on graphs, dynamics in populations of varying sizes, and the 28 | Wright-Fisher process. 29 | 30 | For example, the following image (a stationary distribution for a 31 | rock-paper-scissors dynamic on a population of size 560) was created with this 32 | library and a [ternary plotting library](https://github.com/marcharper/python-ternary): 33 | 34 | 35 | 36 | For very large state spaces, the stationary distribution calculation can be 37 | offloaded to an included C++ implementation (faster and smaller memory footprint). 38 | 39 | Calculation of Stationary Distributions 40 | --------------------------------------- 41 | 42 | The library computes stationary distributions in a variety of ways. Transition 43 | probabilities are represented by sparse matrices or by functions on the product 44 | of the state space. The latter is useful when the transition matrix is too large 45 | to fit into memory, such as the Wright-Fisher process for a population of size 46 | N with n-types, which requires 47 | 48 | ![\mathcal{O}\left(N^{2(n-1)}\right)](http://mathurl.com/otljxmb.png) 49 | 50 | floating point values to specify the transition matrix. 51 | 52 | The stationary distribution calculation function `stationary.stationary_distribution` 53 | takes either a list of weighted edges (as above) or a function specifying the 54 | transitions and the collection of states of the process. You can specify the 55 | following: 56 | 57 | - Transitions or a function that computes transitions 58 | - Compute in log-space with `logspace=True`, useful (necessary) for processes with very small 59 | probabilities 60 | - Compute the stationary distribution exactly or approximately with `exact=True` (default is false). If `False`, the library computes large powers of the transition matrix times an initial state. If `exact=True`, the library attempts to use the following formula: 61 | 62 | ![s(v_k) = s(v_0) \prod_{j=1}^{k-1}{ \frac{T(v_j, v_{j+1})}{T(v_{j+1}, v_{j})}}](http://mathurl.com/ossus5f.png) 63 | 64 | This formula only works for reversible processes on the simplex -- a particular encoding 65 | of states and paths is assumed. 66 | 67 | The library can also compute exact solutions for the neutral fitness landscape for the 68 | Moran process. 69 | 70 | Examples 71 | -------- 72 | 73 | Let's walk through a detailed example. For the classical Moran process, we have 74 | a population of two types, A and B. Type B has relative fitness `r` versus the 75 | fitness of type A (which is 1). It is well-known that the fixation probability 76 | of type A is 77 | 78 | ![\rho_A = \frac{1 - r^{-1}}{1 - r^{-N}}](http://mathurl.com/nq99lfn.png) 79 | 80 | It is also known that the stationary distribution of the Moran process with 81 | mutation converges to the following distribution when the mutation rate goes to 82 | zero: 83 | 84 | ![s = \left(\frac{\rho_A}{\rho_A + \rho_B}, 0, \ldots, 0, \frac{\rho_B}{\rho_A + \rho_B}\right)](http://mathurl.com/o6clplh.png) 85 | 86 | where the stationary distribution is over the population states 87 | [(0, N), (1, N-1), ..., (N, 0)]. 88 | 89 | In [fixation_examples.py](https://github.com/marcharper/stationary/blob/master/fixation_examples.py) 90 | we compare the ratio of fixation probabilities with the stationary distribution 91 | for small values of mu, `mu=10^-24`, producing the following plot: 92 | 93 | ![fixation_example.png](https://github.com/marcharper/stationary/blob/master/fixation_example.png) 94 | 95 | In the top plot there is no visual distinction between the two values. The lower 96 | plot has the difference in the two calculations, showing that the error is very 97 | small. 98 | 99 | There are a few convenience functions that make such plots easy. To compute the 100 | stationary distribution of the Moran process is just a few lines of code: 101 | 102 | ```python 103 | from stationary import convenience 104 | r = 2 105 | game_matrix = [[1, 1], [r, r]] 106 | N = 100 107 | mu = 1./ N 108 | # compute the transitions, stationary distribution, and entropy rate 109 | edges, s, er = convenience.moran(N, game_matrix, mu, exact=True, logspace=True) 110 | print s[(0, N)], s[(N, 0)] 111 | >>> 0.247107738567 4.63894759631e-29 112 | ``` 113 | 114 | More Examples 115 | ------------- 116 | 117 | There are many examples in the test suite and some more complex examples in the 118 | [examples](/examples) subdirectory. 119 | 120 | C++ Implementation 121 | ------------------ 122 | 123 | For larger state spaces a more efficient C++ implementation is provided. 124 | Compile with the following command: 125 | 126 | ```bash 127 | g++ -std=c++11 -O3 stationary.cpp 128 | 129 | ``` 130 | 131 | You can still use the python functions to develop the process (e.g. the 132 | transition probabilies) and to plot. See [export_to_cpp](/export_to_cpp.py) for 133 | an example. 134 | 135 | Unit Tests 136 | ---------- 137 | 138 | The library contains a number of tests to ensure that the calculations are 139 | accurate, including comparisons to closed forms when available. 140 | 141 | To run the suite of unit tests, use the command 142 | 143 | ``` 144 | nosetests -s tests 145 | ``` 146 | 147 | Note that there are many tests and some take a considerable amount of time 148 | (several minutes in some cases). 149 | -------------------------------------------------------------------------------- /examples/cycle_stationary.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | from collections import defaultdict 3 | from itertools import product 4 | from operator import itemgetter 5 | import sys 6 | 7 | import numpy 8 | 9 | from stationary.utils.graph import Graph 10 | from stationary import stationary_distribution 11 | from stationary.processes.graph_process import multivariate_graph_transitions 12 | from stationary.processes.incentives import replicator, linear_fitness_landscape 13 | 14 | import faulthandler 15 | faulthandler.enable() 16 | 17 | 18 | def cycle(length, directed=False): 19 | """ 20 | Produces a cycle of length `length`. 21 | 22 | Parameters 23 | ---------- 24 | length: int 25 | Number of vertices in the cycle 26 | directed: bool, False 27 | Is the cycle directed? 28 | 29 | Returns 30 | ------- 31 | a Graph object 32 | """ 33 | 34 | graph = Graph() 35 | edges = [] 36 | for i in range(length - 1): 37 | edges.append((i, i+1)) 38 | if not directed: 39 | edges.append((i+1, i)) 40 | edges.append((length - 1, 0)) 41 | if not directed: 42 | edges.append((0, length - 1)) 43 | graph.add_edges(edges) 44 | return graph 45 | 46 | 47 | def cycle_configurations_consolidation(N): 48 | """ 49 | Consolidates cycle configurations based on rotational symmetry. 50 | """ 51 | config_id = 0 52 | config_mapper = dict() 53 | inverse_mapper = dict() 54 | 55 | for c in product([0, 1], repeat=N): 56 | if c not in config_mapper: 57 | # cycle through the list and add to the mapper 58 | for i in range(len(c)): 59 | b = numpy.roll(c,i) 60 | config_mapper[tuple(b)] = config_id 61 | # Record a representative 62 | inverse_mapper[config_id] = c 63 | config_id += 1 64 | return config_mapper, inverse_mapper 65 | 66 | 67 | def consolidate_stationary(s, N): 68 | """ 69 | Consolidates stationary distribution over cycle rotations. 70 | """ 71 | config_mapper, inverse_mapper = cycle_configurations_consolidation(N) 72 | new_s = defaultdict(float) 73 | for k, v in s.items(): 74 | new_s[config_mapper[k]] += v 75 | return new_s, inverse_mapper 76 | 77 | 78 | def find_extrema_stationary(s, g, extrema="max"): 79 | extreme_states = [] 80 | for state in g.vertices(): 81 | is_extrema = True 82 | v = s[state] 83 | for neighbor in g.out_vertices(state): 84 | if state == neighbor: 85 | continue 86 | if extrema == "max" and s[neighbor] > v: 87 | is_extrema = False 88 | break 89 | elif extrema == "min" and s[neighbor] < v: 90 | is_extrema = False 91 | break 92 | if is_extrema: 93 | extreme_states.append(state) 94 | return extreme_states 95 | 96 | 97 | def find_extrema_yen(graph, extrema="max"): 98 | extreme_states = [] 99 | for state in g.vertices(): 100 | is_extrema = True 101 | for neighbor in g.out_vertices(state): 102 | if state == neighbor: 103 | continue 104 | tout = g[state][neighbor] 105 | tin = g[neighbor][state] 106 | if extrema == "max" and (tout < tin): 107 | is_extrema = False 108 | break 109 | elif extrema == "min" and (tout > tin): 110 | is_extrema = False 111 | break 112 | if is_extrema: 113 | extreme_states.append(state) 114 | return extreme_states 115 | 116 | 117 | if __name__ == '__main__': 118 | try: 119 | N = int(sys.argv[1]) 120 | except IndexError: 121 | N = 10 122 | try: 123 | mu = sys.argv[2] 124 | except IndexError: 125 | mu = 1./N 126 | 127 | #m = [[1,1], [1,1]] 128 | #m = [[1,2], [2,1]] 129 | #m = [[2,1],[1,2]] 130 | #m = [[2,2],[2,1]] 131 | m = [[2,2],[1,1]] 132 | print(N, m, mu) 133 | 134 | graph = cycle(N) 135 | fitness_landscape = linear_fitness_landscape(m) 136 | incentive = replicator(fitness_landscape) 137 | edge_dict = multivariate_graph_transitions(N, graph, incentive, num_types=2, mu=mu) 138 | edges = [(v1, v2, t) for ((v1, v2), t) in edge_dict.items()] 139 | g = Graph(edges) 140 | 141 | print("There are %s configurations and %s transitions" % (len(set([x[0] for x in edge_dict.keys()])), len(edge_dict))) 142 | 143 | print("Local Maxima:", len(find_extrema_yen(g, extrema="max"))) 144 | print("Local Minima:", len(find_extrema_yen(g, extrema="min"))) 145 | print("Total States:", 2**N) 146 | 147 | exit() 148 | print("Computing stationary") 149 | s = stationary_distribution(edges, lim=1e-8, iterations=1000) 150 | print("Local Maxima:", len(find_extrema_stationary(s, g, extrema="max"))) 151 | print("Local Minima:", len(find_extrema_stationary(s, g, extrema="min"))) 152 | 153 | # Print stationary distribution top 20 154 | print("Stationary") 155 | for k, v in sorted(s.items(), key=itemgetter(1), reverse=True)[:20]: 156 | print(k, v) 157 | 158 | print(len([v for v in s.values() if v > 0.001]), sum([v for v in s.values() if v > 0.001])) 159 | 160 | # Consolidate states 161 | s, inverse_mapper = consolidate_stationary(s, N) 162 | # Print stationary distribution top 20 163 | print("Consolidated Stationary") 164 | for k,v in sorted(s.items(), key=itemgetter(1), reverse=True)[:20]: 165 | rep = inverse_mapper[k] 166 | print(rep, sum(rep), v) 167 | 168 | print(len([v for v in s.values() if v > 0.001]), 169 | sum([v for v in s.values() if v > 0.001])) 170 | 171 | -------------------------------------------------------------------------------- /examples/entropic_equilibria_plots.py: -------------------------------------------------------------------------------- 1 | """Figures for the publication 2 | "Entropic Equilibria Selection of Stationary Extrema in Finite Populations" 3 | """ 4 | 5 | from __future__ import print_function 6 | import math 7 | import os 8 | import pickle 9 | import sys 10 | 11 | import matplotlib 12 | from matplotlib import pyplot as plt 13 | import matplotlib.gridspec as gridspec 14 | import numpy as np 15 | import scipy.misc 16 | import ternary 17 | 18 | import stationary 19 | from stationary.processes import incentives, incentive_process 20 | 21 | 22 | ## Global Font config for plots ### 23 | font = {'size': 14} 24 | matplotlib.rc('font', **font) 25 | 26 | 27 | def compute_entropy_rate(N=30, n=2, m=None, incentive_func=None, beta=1., 28 | mu=None, exact=False, lim=1e-13, logspace=False): 29 | if not m: 30 | m = np.ones((n, n)) 31 | if not incentive_func: 32 | incentive_func = incentives.fermi 33 | if not mu: 34 | # mu = (n-1.)/n * 1./(N+1) 35 | mu = 1. / N 36 | 37 | fitness_landscape = incentives.linear_fitness_landscape(m) 38 | incentive = incentive_func(fitness_landscape, beta=beta, q=1) 39 | edges = incentive_process.multivariate_transitions( 40 | N, incentive, num_types=n, mu=mu) 41 | s = stationary.stationary_distribution(edges, exact=exact, lim=lim, 42 | logspace=logspace) 43 | e = stationary.entropy_rate(edges, s) 44 | return e, s 45 | 46 | 47 | # Entropy Characterization Plots 48 | 49 | def dict_max(d): 50 | k0, v0 = list(d.items())[0] 51 | for k, v in d.items(): 52 | if v > v0: 53 | k0, v0 = k, v 54 | return k0, v0 55 | 56 | 57 | def plot_data_sub(domain, plot_data, gs, labels=None, sci=True, use_log=False): 58 | # Plot Entropy Rate 59 | ax1 = plt.subplot(gs[0, 0]) 60 | ax1.plot(domain, [x[0] for x in plot_data[0]], linewidth=2) 61 | 62 | # Plot Stationary Probabilities and entropies 63 | ax2 = plt.subplot(gs[1, 0]) 64 | ax3 = plt.subplot(gs[2, 0]) 65 | 66 | if use_log: 67 | transform = math.log 68 | else: 69 | transform = lambda x: x 70 | 71 | for i, ax, t in [(1, ax2, lambda x: x), (2, ax3, transform)]: 72 | if labels: 73 | for data, label in zip(plot_data, labels): 74 | ys = list(map(t, [x[i] for x in data])) 75 | ax.plot(domain, ys, linewidth=2, label=label) 76 | else: 77 | for data in plot_data: 78 | ys = list(map(t, [x[i] for x in data])) 79 | ax.plot(domain, ys, linewidth=2) 80 | 81 | ax1.set_ylabel("Entropy Rate") 82 | ax2.set_ylabel("Stationary\nExtrema") 83 | if use_log: 84 | ax3.set_ylabel("log RTE $H_v$") 85 | else: 86 | ax3.set_ylabel("RTE $H_v$") 87 | if sci: 88 | ax2.yaxis.get_major_formatter().set_powerlimits((0, 0)) 89 | ax3.yaxis.get_major_formatter().set_powerlimits((0, 0)) 90 | return ax1, ax2, ax3 91 | 92 | 93 | def ER_figure_beta2(N, m, betas): 94 | """Varying Beta, two dimensional example""" 95 | # Beta test 96 | # m = [[1, 4], [4, 1]] 97 | 98 | # Compute the data 99 | ss = [] 100 | plot_data = [[]] 101 | 102 | for beta in betas: 103 | print(beta) 104 | e, s = compute_entropy_rate(N=N, m=m, beta=beta, exact=True) 105 | ss.append(s) 106 | state, s_max = dict_max(s) 107 | plot_data[0].append((e, s_max, e / s_max)) 108 | 109 | gs = gridspec.GridSpec(3, 2) 110 | ax1, ax2, ax3 = plot_data_sub(betas, plot_data, gs, sci=False) 111 | ax3.set_xlabel("Strength of Selection $\\beta$") 112 | 113 | # Plot stationary distribution 114 | ax4 = plt.subplot(gs[:, 1]) 115 | for s in ss[::4]: 116 | ax4.plot(range(0, N+1), [s[(i, N-i)] for i in range(0, N+1)]) 117 | ax4.set_title("Stationary Distributions") 118 | ax4.set_xlabel("Population States $(i , N - i)$") 119 | 120 | 121 | def remove_boundary(s): 122 | s1 = dict() 123 | for k, v in s.items(): 124 | a, b, c = k 125 | if a * b * c != 0: 126 | s1[k] = v 127 | return s1 128 | 129 | 130 | def ER_figure_beta3(N, m, mu, betas, iss_states, labels, stationary_beta=0.35, 131 | pickle_filename="figure_beta3.pickle"): 132 | """Varying Beta, three dimensional example""" 133 | 134 | ss = [] 135 | plot_data = [[] for _ in range(len(iss_states))] 136 | 137 | if os.path.exists(pickle_filename): 138 | with open(pickle_filename, 'rb') as f: 139 | plot_data = pickle.load(f) 140 | else: 141 | for beta in betas: 142 | print(beta) 143 | e, s = compute_entropy_rate( 144 | N=N, m=m, n=3, beta=beta, exact=False, mu=mu, lim=1e-10) 145 | ss.append(s) 146 | for i, iss_state in enumerate(iss_states): 147 | s_max = s[iss_state] 148 | plot_data[i].append((e, s_max, e / s_max)) 149 | with open(pickle_filename, 'wb') as f: 150 | pickle.dump(plot_data, f) 151 | 152 | gs = gridspec.GridSpec(3, 2) 153 | 154 | ax1, ax2, ax3 = plot_data_sub(betas, plot_data, gs, labels=labels, 155 | use_log=True, sci=False) 156 | ax3.set_xlabel("Strength of selection $\\beta$") 157 | ax2.legend(loc="upper right") 158 | 159 | # Plot example stationary 160 | ax4 = plt.subplot(gs[:, 1]) 161 | _, s = compute_entropy_rate( 162 | N=N, m=m, n=3, beta=stationary_beta, exact=False, mu=mu, lim=1e-15) 163 | _, tax = ternary.figure(ax=ax4, scale=N,) 164 | tax.heatmap(s, cmap="jet", style="triangular") 165 | tax.ticks(axis='lbr', linewidth=1, multiple=10, offset=0.015) 166 | tax.clear_matplotlib_ticks() 167 | ax4.set_xlabel("Population States $a_1 + a_2 + a_3 = N$") 168 | 169 | # tax.left_axis_label("$a_1$") 170 | # tax.right_axis_label("$a_2$") 171 | # tax.bottom_axis_label("$a_3$") 172 | 173 | 174 | def ER_figure_N(Ns, m, beta=1, labels=None): 175 | """Varying population size.""" 176 | 177 | ss = [] 178 | plot_data = [[] for _ in range(3)] 179 | n = len(m[0]) 180 | 181 | for N in Ns: 182 | print(N) 183 | mu = 1 / N 184 | norm = float(scipy.misc.comb(N+n, n)) 185 | e, s = compute_entropy_rate( 186 | N=N, m=m, n=3, beta=beta, exact=False, mu=mu, lim=1e-10) 187 | ss.append(s) 188 | iss_states = [(N, 0, 0), (N / 2, N / 2, 0), (N / 3, N / 3, N / 3)] 189 | for i, iss_state in enumerate(iss_states): 190 | s_max = s[iss_state] 191 | plot_data[i].append((e, s_max, e / (s_max * norm))) 192 | # Plot data 193 | gs = gridspec.GridSpec(3, 1) 194 | ax1, ax2, ax3 = plot_data_sub(Ns, plot_data, gs, labels, use_log=True, sci=False) 195 | ax2.legend(loc="upper right") 196 | ax3.set_xlabel("Population Size $N$") 197 | 198 | 199 | def ER_figure_mu(N, mus, m, iss_states, labels, beta=1., 200 | pickle_filename="figure_mu.pickle"): 201 | """ 202 | Plot entropy rates and trajectory entropies for varying mu. 203 | """ 204 | # Compute the data 205 | ss = [] 206 | plot_data = [[] for _ in range(len(iss_states))] 207 | 208 | if os.path.exists(pickle_filename): 209 | with open(pickle_filename, 'rb') as f: 210 | plot_data = pickle.load(f) 211 | else: 212 | for mu in mus: 213 | print(mu) 214 | e, s = compute_entropy_rate( 215 | N=N, m=m, n=3, beta=beta, exact=False, mu=mu, lim=1e-10, 216 | logspace=True) 217 | ss.append(s) 218 | for i, iss_state in enumerate(iss_states): 219 | s_max = s[iss_state] 220 | plot_data[i].append((e, s_max, e / s_max)) 221 | with open(pickle_filename, 'wb') as f: 222 | pickle.dump(plot_data, f) 223 | 224 | # Plot data 225 | gs = gridspec.GridSpec(3, 1) 226 | gs.update(hspace=0.5) 227 | ax1, ax2, ax3 = plot_data_sub(mus, plot_data, gs, labels, use_log=True) 228 | ax2.legend(loc="upper right") 229 | ax3.set_xlabel("Mutation rate $\mu$") 230 | 231 | 232 | if __name__ == '__main__': 233 | fig_num = sys.argv[1] 234 | 235 | if fig_num == "1": 236 | ## Figure 1 237 | # Varying beta, two dimensional 238 | N = 30 239 | m = [[1, 2], [2, 1]] 240 | betas = np.arange(0, 8, 0.2) 241 | ER_figure_beta2(N, m, betas) 242 | plt.tight_layout() 243 | plt.show() 244 | 245 | if fig_num == "2": 246 | ## Figure 2 247 | # # Varying beta, three dimensional 248 | N = 60 249 | mu = 1. / N 250 | m = [[0, 1, 1], [1, 0, 1], [1, 1, 0]] 251 | iss_states = [(N, 0, 0), (N / 2, N / 2, 0), (N / 3, N / 3, N / 3)] 252 | labels = ["$v_0$", "$v_1$", "$v_2$"] 253 | betas = np.arange(0.02, 0.6, 0.02) 254 | ER_figure_beta3(N, m, mu, betas, iss_states, labels) 255 | plt.show() 256 | 257 | if fig_num == "3": 258 | ## Figure 3 259 | # Varying mutation rate figure 260 | N = 42 261 | mus = np.arange(0.0001, 0.015, 0.0005) 262 | m = [[0, 1, 1], [1, 0, 1], [1, 1, 0]] 263 | iss_states = [(N, 0, 0), (N / 2, N / 2, 0), (N / 3, N / 3, N / 3)] 264 | labels = ["$v_0$: (42, 0, 0)", "$v_1$: (21, 21, 0)", "$v_2$: (14, 14, 14)"] 265 | # labels = ["$v_0$", "$v_1$", "$v_2$"] 266 | ER_figure_mu(N, mus, m, iss_states, labels, beta=1.) 267 | plt.show() 268 | 269 | if fig_num == "4": 270 | ## Figure 4 271 | # Note: The RPS landscape takes MUCH longer to converge! 272 | # Consider using the C++ implementation instead for larger N. 273 | N = 120 # Manuscript uses 180 274 | mu = 1. / N 275 | m = incentives.rock_paper_scissors(a=-1, b=-1) 276 | _, s = compute_entropy_rate( 277 | N=N, m=m, n=3, beta=1.5, exact=False, mu=mu, lim=1e-16) 278 | _, tax = ternary.figure(scale=N) 279 | tax.heatmap(remove_boundary(s), cmap="jet", style="triangular") 280 | tax.ticks(axis='lbr', linewidth=1, multiple=60) 281 | tax.clear_matplotlib_ticks() 282 | plt.show() 283 | 284 | if fig_num == "5": 285 | # ## Figure 5 286 | # Varying Population Size 287 | Ns = range(6, 6*6, 6) 288 | m = [[0, 1, 1], [1, 0, 1], [1, 1, 0]] 289 | labels = ["$v_0$", "$v_1$", "$v_2$"] 290 | ER_figure_N(Ns, m, beta=1, labels=labels) 291 | plt.show() 292 | 293 | -------------------------------------------------------------------------------- /examples/examples.py: -------------------------------------------------------------------------------- 1 | import math 2 | import os 3 | import matplotlib 4 | from matplotlib import pyplot 5 | import matplotlib.gridspec as gridspec 6 | 7 | from stationary import stationary_distribution 8 | from stationary.processes.incentives import * 9 | from stationary.processes import incentive_process, wright_fisher 10 | from stationary.utils.math_helpers import simplex_generator, slice_dictionary 11 | from stationary.utils.edges import edges_to_edge_dict 12 | from stationary.utils.plotting import plot_dictionary 13 | from stationary.utils import expected_divergence 14 | 15 | from stationary.utils.bomze import bomze_matrices 16 | from stationary.utils.files import ensure_directory 17 | 18 | import ternary 19 | 20 | # Font config for plots 21 | font = {'size': 20} 22 | matplotlib.rc('font', **font) 23 | 24 | 25 | def bomze_figures(N=60, beta=1, process="incentive", directory=None): 26 | """ 27 | Makes plots of the stationary distribution and expected divergence for each 28 | of the plots in Bomze's classification. 29 | """ 30 | 31 | if not directory: 32 | directory = "bomze_paper_figures_%s" % process 33 | ensure_directory(directory) 34 | for i, m in enumerate(bomze_matrices()): 35 | mu = 3./2 * 1./N 36 | fitness_landscape = linear_fitness_landscape(m) 37 | incentive = fermi(fitness_landscape, beta=beta) 38 | edges = incentive_process.multivariate_transitions(N, incentive, 39 | num_types=3, mu=mu) 40 | d1 = stationary_distribution(edges) 41 | 42 | filename = os.path.join(directory, "%s_%s_stationary.eps" % (i, N)) 43 | figure, tax = ternary.figure(scale = N) 44 | tax.heatmap(d1) 45 | tax.savefig(filename=filename) 46 | pyplot.close(figure) 47 | 48 | for q_d in [0., 1.]: 49 | d2 = expected_divergence(edges, q_d=q_d) 50 | filename = os.path.join(directory, "%s_%s_%s.eps" % (i, N, q_d)) 51 | figure, tax = ternary.figure(scale = N) 52 | tax.heatmap(d2) 53 | tax.savefig(filename=filename) 54 | pyplot.close(figure) 55 | 56 | 57 | def four_dim_figures(N=30, beta=1., q=1.): 58 | """ 59 | Four dimensional example. Three dimensional slices are plotted 60 | for illustation. 61 | """ 62 | 63 | m = [[0, 1, 1, 1], [1, 0, 1, 1], [1, 1, 0, 1], [0, 0, 0, 1]] 64 | num_types = len(m[0]) 65 | fitness_landscape = linear_fitness_landscape(m) 66 | mu = 4. / 3 * 1. / N 67 | 68 | incentive = fermi(fitness_landscape, beta=beta, q=q) 69 | edges = incentive_process.multivariate_transitions(N, incentive, num_types=num_types, mu=mu) 70 | 71 | d1 = expected_divergence(edges, q_d=0, boundary=True) 72 | d2 = stationary_distribution(edges) 73 | 74 | # We need to slice the 4dim dictionary into three-dim slices for plotting. 75 | for slice_index in range(4): 76 | for d in [d1, d2]: 77 | slice_dict = slice_dictionary(d, N, slice_index=3) 78 | figure, tax = ternary.figure(scale=N) 79 | tax.heatmap(slice_dict, style="d") 80 | pyplot.show() 81 | 82 | 83 | def graphical_abstract_figures(N=60, q=1, beta=0.1): 84 | """ 85 | Three dimensional process examples. 86 | """ 87 | 88 | a = 0 89 | b = 1 90 | m = [[a, b, b], [b, a, b], [b, b, a]] 91 | mu = (3. / 2 ) * 1. / N 92 | fitness_landscape = linear_fitness_landscape(m) 93 | incentive = fermi(fitness_landscape, beta=beta, q=q) 94 | edges = incentive_process.multivariate_transitions(N, incentive, num_types=3, mu=mu) 95 | d = stationary_distribution(edges, iterations=None) 96 | 97 | figure, tax = ternary.figure(scale=N) 98 | tax.heatmap(d, scale=N) 99 | tax.savefig(filename="ga_stationary.eps", dpi=600) 100 | 101 | d = expected_divergence(edges, q_d=0) 102 | figure, tax = ternary.figure(scale=N) 103 | tax.heatmap(d, scale=N) 104 | tax.savefig(filename="ga_d_0.eps", dpi=600) 105 | 106 | d = expected_divergence(edges, q_d=1) 107 | figure, tax = ternary.figure(scale=N) 108 | tax.heatmap(d, scale=N) 109 | tax.savefig(filename="ga_d_1.eps", dpi=600) 110 | 111 | 112 | def rps_figures(N=60, q=1, beta=1.): 113 | """ 114 | Three rock-paper-scissors examples. 115 | """ 116 | 117 | m = [[0, -1, 1], [1, 0, -1], [-1, 1, 0]] 118 | num_types = len(m[0]) 119 | fitness_landscape = linear_fitness_landscape(m) 120 | for i, mu in enumerate([1./math.sqrt(N), 1./N, 1./N**(3./2)]): 121 | # Approximate calculation 122 | mu = 3/2. * mu 123 | incentive = fermi(fitness_landscape, beta=beta, q=q) 124 | edges = incentive_process.multivariate_transitions(N, incentive, num_types=num_types, mu=mu) 125 | d = stationary_distribution(edges, lim=1e-10) 126 | 127 | figure, tax = ternary.figure() 128 | tax.heatmap(d, scale=N) 129 | tax.savefig(filename="rsp_mu_" + str(i) + ".eps", dpi=600) 130 | 131 | 132 | def tournament_stationary_3(N, mu=None): 133 | """ 134 | Example for a tournament selection matrix. 135 | """ 136 | 137 | if not mu: 138 | mu = 3./2 * 1./N 139 | m = [[1,1,1], [0,1,1], [0,0,1]] 140 | num_types = len(m[0]) 141 | fitness_landscape = linear_fitness_landscape(m) 142 | incentive = replicator(fitness_landscape) 143 | edges = incentive_process.multivariate_transitions(N, incentive, num_types=num_types, mu=mu) 144 | s = stationary_distribution(edges) 145 | ternary.heatmap(s, scale=N, scientific=True) 146 | d = expected_divergence(edges, q_d=0) 147 | ternary.heatmap(d, scale=N, scientific=True) 148 | pyplot.show() 149 | 150 | 151 | def two_dim_transitions(edges): 152 | """ 153 | Compute the + and - transitions for plotting in two_dim_transitions_figure 154 | """ 155 | 156 | d = edges_to_edge_dict(edges) 157 | N = sum(edges[0][0]) 158 | ups = [] 159 | downs = [] 160 | stays = [] 161 | for i in range(0, N+1): 162 | try: 163 | up = d[((i, N-i), (i+1, N-i-1))] 164 | except KeyError: 165 | up = 0 166 | try: 167 | down = d[((i, N-i), (i-1, N-i+1))] 168 | except KeyError: 169 | down = 0 170 | ups.append(up) 171 | downs.append(down) 172 | stays.append(1 - up - down) 173 | return ups, downs, stays 174 | 175 | 176 | def two_dim_transitions_figure(N, m, mu=0.01, incentive_func=replicator): 177 | """ 178 | Plot transition entropies and stationary distributions. 179 | """ 180 | 181 | n = len(m[0]) 182 | fitness_landscape = linear_fitness_landscape(m) 183 | incentive = incentive_func(fitness_landscape) 184 | if not mu: 185 | mu = 1./ N 186 | edges = incentive_process.multivariate_transitions(N, incentive, num_types=n, mu=mu) 187 | 188 | s = stationary_distribution(edges, exact=True) 189 | d = edges_to_edge_dict(edges) 190 | 191 | # Set up plots 192 | gs = gridspec.GridSpec(3, 1) 193 | ax1 = pyplot.subplot(gs[0, 0]) 194 | ax1.set_title("Transition Probabilities") 195 | ups, downs, _ = two_dim_transitions(edges) 196 | xs = range(0, N+1) 197 | ax1.plot(xs, ups) 198 | ax1.plot(xs, downs) 199 | 200 | ax2 = pyplot.subplot(gs[1, 0]) 201 | ax2.set_title("Relative Entropy") 202 | divs1 = expected_divergence(edges) 203 | divs2 = expected_divergence(edges, q_d=0) 204 | plot_dictionary(divs1, ax=ax2) 205 | plot_dictionary(divs2, ax=ax2) 206 | 207 | ax3 = pyplot.subplot(gs[2, 0]) 208 | ax3.set_title("Stationary Distribution") 209 | plot_dictionary(s, ax=ax3) 210 | ax3.set_xlabel("Number of A individuals (i)") 211 | 212 | 213 | def two_dim_wright_fisher_figure(N, m, mu=0.01, incentive_func=replicator): 214 | """ 215 | Plot relative entropies and stationary distribution for the Wright-Fisher 216 | process. 217 | """ 218 | 219 | n = len(m[0]) 220 | fitness_landscape = linear_fitness_landscape(m) 221 | incentive = incentive_func(fitness_landscape) 222 | if not mu: 223 | mu = 1./ N 224 | 225 | edge_func = wright_fisher.multivariate_transitions(N, incentive, mu=mu, num_types=n) 226 | states = list(simplex_generator(N, d=n-1)) 227 | s = stationary_distribution(edge_func, states=states, iterations=4*N) 228 | s0 = expected_divergence(edge_func, states=states, q_d=0) 229 | s1 = expected_divergence(edge_func, states=states, q_d=1) 230 | 231 | # Set up plots 232 | gs = gridspec.GridSpec(2, 1) 233 | 234 | ax2 = pyplot.subplot(gs[0, 0]) 235 | ax2.set_title("Relative Entropy") 236 | plot_dictionary(s0, ax=ax2) 237 | plot_dictionary(s1, ax=ax2) 238 | 239 | ax3 = pyplot.subplot(gs[1, 0]) 240 | ax3.set_title("Stationary Distribution") 241 | plot_dictionary(s, ax=ax3) 242 | ax3.set_xlabel("Number of A individuals (i)") 243 | 244 | 245 | def two_player_example(N=50): 246 | """ 247 | Two player examples plots. 248 | """ 249 | 250 | m = [[1, 2], [2, 1]] 251 | #m = [[1, 1], [0, 1]] 252 | mu = 1. / N 253 | incentive_func = replicator 254 | figure, ax = pyplot.subplots() 255 | two_dim_transitions_figure(N, m, mu=mu, incentive_func=incentive_func) 256 | figure, ax = pyplot.subplots() 257 | two_dim_wright_fisher_figure(N, m, mu=mu, incentive_func=replicator) 258 | 259 | pyplot.show() 260 | 261 | 262 | if __name__ == '__main__': 263 | # Two-type example 264 | two_player_example() 265 | 266 | ## Three dimensional examples 267 | graphical_abstract_figures() 268 | rps_figures() 269 | tournament_stationary_3(N=60, mu=1./3) 270 | bomze_figures() 271 | 272 | ## Four dimensional examples 273 | four_dim_figures() 274 | -------------------------------------------------------------------------------- /examples/fixation_examples.py: -------------------------------------------------------------------------------- 1 | import math 2 | 3 | import matplotlib 4 | from matplotlib import pyplot 5 | import matplotlib.gridspec as gridspec 6 | from numpy import arange 7 | 8 | from stationary import stationary_distribution, convenience 9 | from stationary.utils.math_helpers import normalize 10 | 11 | # Font config for plots 12 | font = {'size': 20} 13 | matplotlib.rc('font', **font) 14 | 15 | 16 | def fixation_probabilities(N, r): 17 | """ 18 | The fixation probabilities of the classical Moran process. 19 | 20 | Parameters 21 | ---------- 22 | N: int 23 | The population size 24 | r: float 25 | Relative fitness. 26 | """ 27 | 28 | def phi(N, r, i=1.): 29 | return (1. - math.pow(r, -i)) / (1. - math.pow(r, -N)) 30 | 31 | if r == 0: 32 | return (0., 1.) 33 | if r == 1: 34 | return (1. / N, 1. / N) 35 | return (phi(N, r), phi(N, 1. / r)) 36 | 37 | 38 | def fixation_comparison(N=20, r=1.2, mu=1e-24): 39 | """ 40 | Plot the fixation probabilities and the stationary limit. 41 | """ 42 | 43 | fs = [] 44 | ss = [] 45 | diffs = [] 46 | domain = list(arange(0.5, 1.5, 0.01)) 47 | for r in domain: 48 | game_matrix = [[1, 1], [r, r]] 49 | edges, s, er = convenience.moran(N, game_matrix, mu, exact=True, 50 | logspace=True) 51 | fix_1, fix_2 = fixation_probabilities(N, r) 52 | s_1, s_2 = s[(0, N)], s[(N, 0)] 53 | f = normalize([fix_1, fix_2]) 54 | fs.append(f[0]) 55 | ss.append(s_1) 56 | diffs.append(fs[-1] - ss[-1]) 57 | 58 | gs = gridspec.GridSpec(2, 1) 59 | 60 | ax1 = pyplot.subplot(gs[0, 0]) 61 | ax2 = pyplot.subplot(gs[1, 0]) 62 | 63 | ax1.plot(domain, fs) 64 | ax1.plot(domain, ss) 65 | ax1.set_xlabel("Relative fitness $r$") 66 | ax1.set_ylabel("Fixation Probability $\\rho_A / (\\rho_A + \\rho_B)$") 67 | ax1.set_title("Fixation Probabilities and Stationary Distribution") 68 | 69 | ax2.plot(domain, diffs) 70 | ax2.set_xlabel("Relative fitness $r$") 71 | ax2.set_ylabel("$\\rho_A / (\\rho_A + \\rho_B) - s_{(O, N)}$") 72 | 73 | pyplot.show() 74 | 75 | 76 | if __name__ == '__main__': 77 | fixation_comparison(16) 78 | -------------------------------------------------------------------------------- /examples/variable_population_stationary.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | import math 3 | 4 | from matplotlib import pyplot 5 | import ternary 6 | 7 | from stationary.processes.variable_population_size import ( 8 | variable_population_transitions, even_death) 9 | from stationary.processes.incentive_process import ( 10 | linear_fitness_landscape, replicator) 11 | from stationary.utils import expected_divergence 12 | from stationary import stationary_distribution 13 | 14 | 15 | if __name__ == '__main__': 16 | N = 40 17 | mu = 3./2. * 1./N 18 | m = [[1, 2], [2, 1]] 19 | fitness_landscape = linear_fitness_landscape(m, normalize=False) 20 | incentive = replicator(fitness_landscape) 21 | death_probabilities = even_death(N) 22 | 23 | edges = variable_population_transitions( 24 | N, fitness_landscape, death_probabilities, incentive=incentive, mu=mu) 25 | s = stationary_distribution(edges, iterations=10000) 26 | 27 | # Print out the states with the highest stationary probabilities 28 | vs = [(v, k) for (k, v) in s.items()] 29 | vs.sort(reverse=True) 30 | print(vs[:10]) 31 | 32 | # Plot the stationary distribution and expected divergence 33 | figure, tax = ternary.figure(scale=N) 34 | tax.heatmap(s) 35 | 36 | d2 = expected_divergence(edges, q_d=0) 37 | d = dict() 38 | for k, v in d2.items(): 39 | d[k] = math.sqrt(v) 40 | 41 | figure, tax = ternary.figure(scale=N) 42 | tax.heatmap(d) 43 | 44 | pyplot.show() 45 | -------------------------------------------------------------------------------- /export_to_cpp.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pickle 3 | import subprocess 4 | 5 | from matplotlib import pyplot as plt 6 | from scipy.misc import comb 7 | import ternary 8 | 9 | from stationary.utils.edges import enumerate_states_from_edges 10 | from stationary.processes.incentives import linear_fitness_landscape, fermi 11 | from stationary.processes.incentive_process import ( 12 | multivariate_transitions_gen) 13 | 14 | 15 | def num_states(N, n=3): 16 | """ 17 | Returns the number of states in the discretization of the simplex. 18 | """ 19 | 20 | return comb(N+n-1, n-1, exact=True) 21 | 22 | 23 | def pickle_inv_enumeration(inv_enum, pickle_filename="inv_enum.pickle"): 24 | """ 25 | Pickle the inverse enumeration of states, needed to import the exported 26 | stationary calculation. 27 | """ 28 | 29 | with open(pickle_filename, 'wb') as output_file: 30 | pickle.dump(inv_enum, output_file) 31 | 32 | 33 | def output_enumerated_edges(N, n, edges, filename="enumerated_edges.csv"): 34 | """ 35 | Writes the graph underlying to the Markov process to disk. This is used to 36 | export the computation to a C++ implementation if the number of nodes is 37 | very large. 38 | """ 39 | 40 | edges = list(edges) 41 | 42 | # Collect all the states from the list of edges 43 | all_states, enum, inv_enum = enumerate_states_from_edges(edges, inverse=True) 44 | 45 | # Output enumerated_edges 46 | with open(filename, 'w') as outfile: 47 | outfile.write(str(num_states(N, n)) + "\n") 48 | outfile.write(str(n) + "\n") 49 | for (source, target, weight) in list(edges): 50 | row = [str(enum[source]), str(enum[target]), str.format('%.50f' % weight)] 51 | outfile.write(",".join(row) + "\n") 52 | return inv_enum 53 | 54 | 55 | def load_pickled_inv_enum(filename="inv_enum.pickle"): 56 | """ 57 | Load the pickled inverse enumerate to translate the stationary states 58 | from the exported calculation. 59 | """ 60 | 61 | with open(filename, 'rb') as input_file: 62 | inv_enum = pickle.load(input_file) 63 | return inv_enum 64 | 65 | 66 | def load_stationary_gen(filename="enumerated_stationary.txt"): 67 | """ 68 | Loads the computed stationary distribution from the exported calculation. 69 | The states are still enumerated. 70 | """ 71 | 72 | with open(filename) as input_file: 73 | for line in input_file: 74 | line = line.strip() 75 | state, value = line.split(',') 76 | yield (int(state), float(value)) 77 | 78 | 79 | def stationary_gen(filename="enumerated_stationary.txt", 80 | pickle_filename="inv_enum.pickle"): 81 | """ 82 | Loads the stationary distribution computed by the C++ implementation and 83 | reverses the enumeration. 84 | """ 85 | 86 | inv_enum = load_pickled_inv_enum(filename=pickle_filename) 87 | gen = load_stationary_gen(filename=filename) 88 | for enum_state, value in gen: 89 | state = inv_enum[enum_state] 90 | yield (state, value) 91 | 92 | 93 | def remove_boundary(s): 94 | """Removes the boundary, which improves some stationary plots visually.""" 95 | s1 = dict() 96 | for k, v in s.items(): 97 | a, b, c = k 98 | if a * b * c != 0: 99 | s1[k] = v 100 | return s1 101 | 102 | 103 | def render_stationary(s): 104 | """ 105 | Renders a stationary distribution. 106 | """ 107 | 108 | # Put the stationary distribution into a dictionary 109 | d = dict() 110 | for state, value in s: 111 | d[state] = value 112 | N = sum(list(d.keys())[0]) 113 | # Plot it 114 | figure, tax = ternary.figure(scale=N) 115 | tax.heatmap(remove_boundary(d), scientific=True, style='triangular', 116 | cmap="jet") 117 | return tax 118 | 119 | 120 | def stationary_max_min(filename="enumerated_stationary.txt"): 121 | min_ = 1. 122 | max_ = 0. 123 | gen = load_stationary_gen(filename=filename) 124 | for enum_state, value in gen: 125 | if value > max_: 126 | max_ = value 127 | if value < min_: 128 | min_ = value 129 | return max_, min_ 130 | 131 | 132 | def full_example(N, m, mu, beta=1., pickle_filename="inv_enum.pickle", 133 | filename="enumerated_edges.csv"): 134 | """ 135 | Full example of exporting the stationary calculation to C++. 136 | """ 137 | 138 | print("Computing graph of the Markov process.") 139 | if not mu: 140 | mu = 3. / 2 * 1. / N 141 | if m is None: 142 | m = [[0, 1, 1], [1, 0, 1], [1, 1, 0]] 143 | iterations = 200 * N 144 | 145 | num_types = len(m[0]) 146 | fitness_landscape = linear_fitness_landscape(m) 147 | incentive = fermi(fitness_landscape, beta=beta) 148 | edges_gen = multivariate_transitions_gen( 149 | N, incentive, num_types=num_types, mu=mu) 150 | 151 | print("Outputting graph to %s" % filename) 152 | inv_enum = output_enumerated_edges( 153 | N, num_types, edges_gen, filename=filename) 154 | print("Saving inverse enumeration to %s" % pickle_filename) 155 | pickle_inv_enumeration(inv_enum, pickle_filename="inv_enum.pickle") 156 | 157 | print("Running C++ Calculation") 158 | cwd = os.getcwd() 159 | executable = os.path.join(cwd, "a.out") 160 | subprocess.call([executable, filename, str(iterations)]) 161 | 162 | print("Rendering stationary to SVG") 163 | vmax, vmin = stationary_max_min() 164 | s = list(stationary_gen( 165 | filename="enumerated_stationary.txt", 166 | pickle_filename="inv_enum.pickle")) 167 | ternary.svg_heatmap(s, N, "stationary.svg", vmax=vmax, vmin=vmin, style='h') 168 | 169 | print("Rendering stationary") 170 | tax = render_stationary(s) 171 | tax.ticks(axis='lbr', linewidth=1, multiple=N//3, offset=0.015) 172 | tax.clear_matplotlib_ticks() 173 | plt.show() 174 | 175 | 176 | if __name__ == '__main__': 177 | N = 180 178 | mu = 1. / N 179 | m = [[0, 1, -1], [-1, 0, 1], [1, -1, 0]] 180 | full_example(N=N, m=m, mu=mu, beta=1.5) 181 | 182 | 183 | -------------------------------------------------------------------------------- /fixation_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marcharper/stationary/c62d7d4ca98d43b8aa4c1805fdc25fc1da0801fd/fixation_example.png -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup, find_packages 3 | 4 | # Utility function to read the README file. 5 | # Used for the long_description. It's nice, because now 1) we have a top level 6 | # README file and 2) it's easier to type in the README file than to put a raw 7 | # string in below ... 8 | 9 | 10 | def read(fname): 11 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 12 | 13 | 14 | setup( 15 | name="stationary", 16 | version="1.0.0", 17 | author="Marc Harper", 18 | author_email="marc.harper@gmail.com", 19 | description="Stationary distributions for finite Markov processes", 20 | license="MIT", 21 | keywords="markov stationary", 22 | url="https://github.com/marcharper/stationary", 23 | packages=find_packages(), 24 | install_requires=['numpy', 'scipy', 'matplotlib', 'nose', 'python-ternary'], 25 | long_description=read('README.md'), 26 | package_data={'stationary': ['utils/bomze.txt']}, 27 | ) 28 | -------------------------------------------------------------------------------- /stationary.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | // Using C++11 9 | // Compile with >g++ -std=c++11 -O3 stationary.cpp 10 | 11 | int main(int argc, char* argv[]) 12 | { 13 | int iterations = std::stoi(argv[2]); 14 | bool verbose = true; 15 | int N; 16 | int n; 17 | // n(n-1) + 1 = n*n -n + 1 < n*n 18 | 19 | // Read in the values 20 | std::string input_filename(argv[1]); 21 | std::ifstream input_file (input_filename, std::ifstream::in); 22 | if (!input_file.is_open()) 23 | { 24 | std::cout << "Unable to open file\n"; 25 | return 0; 26 | } 27 | 28 | // First line contains the total number of states. 29 | std::string line; 30 | std::getline(input_file, line); 31 | N = std::stoi(line); 32 | // Second line contains the number of types n. 33 | std::getline(input_file, line); 34 | n = std::stoi(line); 35 | if (verbose) { 36 | std::cout << N << std::endl << n << std::endl; 37 | } 38 | 39 | // Load transitions and cache neighbors 40 | std::vector< std::map > transitions(N); 41 | // Subsequent lines are of the form int,int,float 42 | std::string source; 43 | std::string target; 44 | std::string weight; 45 | std::vector< std::vector > in_neighbors(N); 46 | 47 | while (std::getline(input_file,line)) 48 | { 49 | std::stringstream splitstream(line); 50 | getline(splitstream, source, ','); 51 | getline(splitstream, target, ','); 52 | getline(splitstream, weight, '\n'); 53 | int i = std::stoi(source); 54 | int j = std::stoi(target); 55 | double w = std::stod(weight); 56 | transitions[i][j] = w; 57 | in_neighbors[j].push_back(i); 58 | } 59 | 60 | // Initialize vectors 61 | std::vector s(N); 62 | std::vector t(N, 0); 63 | for (int i=0; i < N; i++) 64 | { 65 | s[i] = double(1.0) / double(N); 66 | } 67 | 68 | // Iterate sparse multiplication of the transition matrix 69 | // This converges to the stationary distribution 70 | int in_index; 71 | for (int k=0; k < iterations; k++) 72 | { 73 | if (verbose) { 74 | std::cout << k << std::endl; 75 | } 76 | t.clear(); 77 | t.resize(N, 0); 78 | for (int i = 0; i < N; i++) 79 | { 80 | for ( int j = 0; j < in_neighbors[i].size(); j++) 81 | { 82 | in_index = in_neighbors[i][j]; 83 | t[i] += transitions[in_index][i] * s[in_index]; 84 | } 85 | } 86 | s = t; 87 | } 88 | 89 | // Output stationary distribution to a text file 90 | std::string output_filename("enumerated_stationary.txt"); 91 | std::ofstream output_file (output_filename, std::ofstream::out); 92 | for (int i=0; i < N; i++) 93 | { 94 | output_file << i << ',' << s[i] << std::endl; 95 | } 96 | 97 | return 0; 98 | } 99 | -------------------------------------------------------------------------------- /stationary/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import stationary.utils 4 | import stationary.processes 5 | 6 | from .stationary_ import ( 7 | Cache, stationary_distribution, stationary_generator) 8 | from .entropy_rate_ import entropy_rate 9 | 10 | from . import convenience 11 | -------------------------------------------------------------------------------- /stationary/convenience.py: -------------------------------------------------------------------------------- 1 | """ 2 | Convenience functions for common situations. 3 | """ 4 | 5 | from stationary import stationary_distribution 6 | from stationary.processes import incentive_process 7 | from stationary.processes.incentives import replicator, linear_fitness_landscape 8 | from stationary.utils.math_helpers import simplex_generator 9 | 10 | from .entropy_rate_ import entropy_rate 11 | 12 | 13 | def moran(N, game_matrix=None, mu=None, incentive_func=replicator, 14 | exact=False, logspace=False): 15 | """ 16 | A convenience function for the Moran process with mutation. Computes the 17 | transition probabilities and the stationary distribution. 18 | 19 | The number of types is determined from the dimensions of the game_matrix. 20 | 21 | Parameters 22 | ---------- 23 | N: int 24 | The population size 25 | game_matrix: list of lists or numpy matrix, None 26 | The game matrix of the process, e.g. [[1, 2], [2, 1]] for the two-type 27 | Hawk-Dove game. If not specified, the 2-type neutral landscape is used. 28 | mu: float, None 29 | The mutation rate, if None then `mu` is set to 1 / N 30 | incentive_func: function, replicator 31 | A function defining the process, e.g. the Moran process, logit, Fermi, etc. 32 | Incentives functions are in stationary.processes.incentives 33 | exact: bool, False 34 | Use the approximate or exact calculation function 35 | logspace: bool, False 36 | Compute in log-space or not 37 | 38 | Returns 39 | ------- 40 | edges, s, er: the list of transitions, the stationary distribution, and the 41 | entropy rate. 42 | """ 43 | 44 | if not game_matrix: 45 | game_matrix = [[1, 1], [1, 1]] 46 | if not mu: 47 | mu = 1. / N 48 | num_types = len(game_matrix[0]) 49 | 50 | fitness_landscape = linear_fitness_landscape(game_matrix) 51 | incentive = incentive_func(fitness_landscape) 52 | edges = incentive_process.multivariate_transitions( 53 | N, incentive, num_types=num_types, mu=mu) 54 | s = stationary_distribution(edges, exact=exact, logspace=logspace) 55 | er = entropy_rate(edges, s) 56 | return edges, s, er 57 | 58 | 59 | def wright_fisher(N, game_matrix=None, mu=None, incentive_func=replicator, 60 | logspace=False): 61 | """ 62 | A convenience function for the Moran process with mutation. Computes the 63 | transition probabilities and the stationary distribution. 64 | 65 | The number of types is determined from the dimensions of the game_matrix. 66 | 67 | Parameters 68 | ---------- 69 | N: int 70 | The population size 71 | game_matrix: list of lists or numpy matrix, None 72 | The game matrix of the process, e.g. [[1, 2], [2, 1]] for the two-type 73 | Hawk-Dove game. If not specified, the 2-type neutral landscape is used. 74 | mu: float, None 75 | The mutation rate, if None then `mu` is set to 1 / N 76 | incentive_func: function, replicator 77 | A function defining the process, e.g. the Moran process, logit, Fermi, 78 | Incentives functions are in stationary.processes.incentives 79 | logspace: bool, False 80 | Compute in log-space or not 81 | 82 | Returns 83 | ------- 84 | edges, s, er: the list of transitions, the stationary distribution, and the 85 | entropy rate. 86 | """ 87 | 88 | if not game_matrix: 89 | game_matrix = [[1, 1], [1, 1]] 90 | if not mu: 91 | mu = 1. / N 92 | num_types = len(game_matrix[0]) 93 | 94 | fitness_landscape = linear_fitness_landscape(game_matrix) 95 | incentive = incentive_func(fitness_landscape) 96 | edge_func = wright_fisher.multivariate_transitions(N, incentive, mu=mu, 97 | num_types=num_types) 98 | states = list(simplex_generator(N, d=num_types-1)) 99 | s = stationary_distribution(edge_func, states=states, iterations=4*N, 100 | logspace=logspace) 101 | er = entropy_rate(edge_func, s) 102 | return edge_func, s, er 103 | -------------------------------------------------------------------------------- /stationary/entropy_rate_.py: -------------------------------------------------------------------------------- 1 | """Entropy rate computation.""" 2 | 3 | from collections import defaultdict, Callable 4 | from numpy import log 5 | 6 | 7 | def entropy_rate(edges, stationary, states=None): 8 | """ 9 | Computes the entropy rate given the edges of the process and the stationary distribution. 10 | 11 | Parameters 12 | ---------- 13 | edges: list of tuples or function 14 | The transitions of the process, either a list of (source, target, 15 | transition_probability), or an edge_function that takes two parameters, 16 | the source and target states, to the transition transition probability. 17 | If using an edge_function you must supply the states of the process. 18 | states: list, None 19 | States for use with the edge_func 20 | stationary: dictionary 21 | Precomputed stationary distribution 22 | 23 | Returns 24 | ------- 25 | float, entropy rate of the process 26 | """ 27 | 28 | e = defaultdict(float) 29 | if isinstance(edges, list): 30 | for a, b, v in edges: 31 | e[a] -= stationary[a] * v * log(v) 32 | return sum(e.values()) 33 | elif isinstance(edges, Callable): 34 | if not states: 35 | raise ValueError( 36 | "Keyword argument `states` required with edge_func") 37 | for a in states: 38 | for b in states: 39 | v = edges(a, b) 40 | e[a] -= stationary[a] * v * log(v) 41 | return sum(e.values()) 42 | -------------------------------------------------------------------------------- /stationary/processes/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from . import incentives 3 | from . import incentive_process 4 | from . import graph_process 5 | from . import variable_population_size 6 | from . import wright_fisher 7 | -------------------------------------------------------------------------------- /stationary/processes/graph_process.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | from itertools import product 3 | 4 | import numpy 5 | 6 | from ..utils.graph import Graph 7 | 8 | 9 | ## Don't put N > 20 into this unless you have a lot of RAM and time 10 | def multivariate_graph_transitions(N, graph, incentive, mu=0.001): 11 | """ 12 | Computes transition probabilities of the incentive process on a graph. 13 | Warning: this uses a LOT of RAM (exponential in N typically), keep N small. 14 | 15 | Parameters 16 | ---------- 17 | N: int 18 | Population size / simplex divisor 19 | graph: Graph 20 | The graph that the population occuupies 21 | incentive: function 22 | An incentive function from incentives.py 23 | mu: float, 0.001 24 | The mutation rate of the process 25 | """ 26 | 27 | def population_state(N, config): 28 | """Calculates the population state from a graph configuration, i.e. 29 | if the population were well-mixed on a complete graph.""" 30 | s = sum(config) 31 | population_state_ = (N-s, s) 32 | return numpy.array(population_state_) 33 | 34 | edges = defaultdict(float) 35 | 36 | # Enumerate the graph vertices 37 | enum = dict(enumerate(graph.vertices())) 38 | inv_enum = dict([(y, x) for (x, y) in enumerate(graph.vertices())]) 39 | 40 | # Generate all binary strings (configurations) 41 | for source_config in product([0, 1], repeat=N): 42 | edges[(source_config, source_config)] = 1. 43 | # For each position in the configuration, mutate it and replace one of 44 | # its neighbors, as dictated by the graph. 45 | s = sum(source_config) 46 | population_state = (N - s, s) 47 | inc = incentive(population_state) 48 | denom = float(sum(inc)) 49 | for source_position, source_type in enumerate(source_config): 50 | # Probability that this one was picked to reproduce is 51 | r = 1. / population_state[source_type] * float(inc[source_type]) / denom 52 | # Replace out neighbors 53 | source_vertex = inv_enum[source_position] 54 | out_vertices = graph.out_vertices(source_vertex) 55 | total_out_vertices = float(len(out_vertices)) 56 | for target_vertex in graph.out_vertices(source_vertex): 57 | target_position = enum[target_vertex] 58 | # Replace without mutation 59 | target_config = list(source_config) 60 | target_config[target_position] = source_type 61 | target_config = tuple(target_config) 62 | if source_config != target_config: 63 | t = r * (1. - mu) / total_out_vertices 64 | edges[(source_config, target_config)] += t 65 | edges[(source_config, source_config)] -= t 66 | # Replace with mutation 67 | target_config = list(source_config) 68 | target_config[target_position] = 1 - source_type 69 | target_config = tuple(target_config) 70 | if source_config != target_config: 71 | t = r * mu / total_out_vertices 72 | edges[(source_config, target_config)] += t 73 | edges[(source_config, source_config)] -= t 74 | return edges 75 | 76 | -------------------------------------------------------------------------------- /stationary/processes/incentive_process.py: -------------------------------------------------------------------------------- 1 | """ 2 | Calculates transitions for the Moran process and generalizations. 3 | """ 4 | 5 | from ..utils.math_helpers import ( 6 | simplex_generator, one_step_indicies_generator, logsumexp, 7 | log_factorial, log_inc_factorial, factorial, inc_factorial) 8 | from ..utils.edges import ( 9 | edge_func_to_edges, states_from_edges, power_transitions) 10 | 11 | from numpy import log, exp 12 | 13 | from .incentives import * 14 | 15 | 16 | ## Moran/Incentive Process 17 | 18 | def is_valid_state(state, lower, upper): 19 | """ 20 | Checks the bounds of a state to make sure it is a valid popualation state. 21 | """ 22 | 23 | for i in state: 24 | if i < lower or i > upper: 25 | return False 26 | return True 27 | 28 | 29 | def multivariate_transitions(N, incentive, num_types=3, mu=0.001, 30 | no_boundary=False): 31 | """ 32 | Computes transition probabilities the Incentive process 33 | 34 | Parameters 35 | ---------- 36 | N: int 37 | Population size / simplex divisor 38 | incentive: function 39 | An incentive function from incentives.py 40 | num_types: int, 3 41 | Number of types in population 42 | mu: float, 0.001 43 | The mutation rate of the process 44 | no_boundary: bool, False 45 | Exclude the boundary states 46 | """ 47 | 48 | return list(multivariate_transitions_gen( 49 | N, incentive, num_types=num_types, mu=mu, no_boundary=no_boundary)) 50 | 51 | 52 | def multivariate_transitions_gen(N, incentive, num_types=3, mu=0.001, 53 | no_boundary=False): 54 | """ 55 | Computes transition probabilities the Incentive process (generator), 56 | 57 | Parameters 58 | ---------- 59 | N: int 60 | Population size / simplex divisor 61 | incentive: function 62 | An incentive function from incentives.py 63 | num_types: int, 3 64 | Number of types in population 65 | mu: float, 0.001 66 | The mutation rate of the process 67 | no_boundary: bool, False 68 | Exclude the boundary states 69 | """ 70 | 71 | d = num_types - 1 72 | one_step_indicies = list(one_step_indicies_generator(d)) 73 | if no_boundary: 74 | lower, upper = 1, N-1 75 | else: 76 | lower, upper = 0, N 77 | for state in simplex_generator(N, d): 78 | if no_boundary: 79 | is_boundary = False 80 | for i in state: 81 | if i == 0: 82 | is_boundary = True 83 | break 84 | if is_boundary: 85 | continue 86 | s = 0. 87 | inc = incentive(state) 88 | denom = float(sum(inc)) 89 | # Transition probabilities for each adjacent state. 90 | for plus_index, minus_index in one_step_indicies: 91 | target_state = list(state) 92 | target_state[plus_index] += 1 93 | target_state[minus_index] -= 1 94 | target_state = tuple(target_state) 95 | # Is this a valid state? I.E. Are we on or near the boundary? 96 | if not is_valid_state(target_state, lower, upper): 97 | continue 98 | #mutations = [mu] * num_types 99 | #mutations[plus_index] = 1. - d*mu 100 | mutations = [mu / d] * num_types 101 | mutations[plus_index] = 1. - mu 102 | r = dot_product(inc, mutations) / denom 103 | transition = r * state[minus_index] / float(N) 104 | yield (state, target_state, transition) 105 | s += transition 106 | # Add in the transition probability for staying put. 107 | yield (state, state, 1. - s) 108 | 109 | 110 | def log_multivariate_transitions(N, logincentive, num_types=3, mu=0.001, 111 | no_boundary=False): 112 | """ 113 | Computes transition probabilities the Incentive process in log-space 114 | 115 | Parameters 116 | ---------- 117 | N: int 118 | Population size / simplex divisor 119 | logincentive: function 120 | An incentive function from incentives.py 121 | num_types: int, 3 122 | Number of types in population 123 | mu: float, 0.001 124 | The mutation rate of the process 125 | no_boundary: bool, False 126 | Exclude the boundary states 127 | """ 128 | 129 | d = num_types - 1 130 | edges = [] 131 | one_step_indicies = list(one_step_indicies_generator(d)) 132 | if no_boundary: 133 | lower, upper = 1, N-1 134 | else: 135 | lower, upper = 0, N 136 | for state in simplex_generator(N, d): 137 | if no_boundary: 138 | is_boundary = False 139 | for i in state: 140 | if i == 0: 141 | is_boundary = True 142 | break 143 | if is_boundary: 144 | continue 145 | inc = logincentive(state) 146 | denom = logsumexp(inc) 147 | # Transition probabilities for each adjacent state. 148 | logtransitions = [] 149 | for plus_index, minus_index in one_step_indicies: 150 | target_state = list(state) 151 | target_state[plus_index] += 1 152 | target_state[minus_index] -= 1 153 | target_state = tuple(target_state) 154 | # Is this a valid state? I.E. Are we on or near the boundary? 155 | if not is_valid_state(target_state, lower, upper): 156 | continue 157 | mutations = [mu / d] * num_types 158 | mutations[plus_index] = 1. - mu 159 | r = logsumexp(inc, b=mutations) - denom 160 | logtransition = r + log(state[minus_index]) - log(N) 161 | edges.append((state, target_state, logtransition)) 162 | logtransitions.append(logtransition) 163 | edges.append((state, state, log(1.-exp(logsumexp(logtransitions))))) 164 | return edges 165 | 166 | 167 | def compute_edges(N=30, num_types=None, m=None, incentive_func=logit, beta=1., 168 | q=1., mu=None): 169 | """ 170 | Wrapper function of multivariate_transitions with some reasonable defaults. 171 | """ 172 | if not m: 173 | m = numpy.ones((num_types, num_types)) # neutral landscape 174 | if not num_types: 175 | num_types = len(m[0]) 176 | fitness_landscape = linear_fitness_landscape(m) 177 | incentive = incentive_func(fitness_landscape, beta=beta, q=q) 178 | if not mu: 179 | # mu = (n-1.)/n * 1./(N+1) # Match with Traulsen's form 180 | mu = 1./N 181 | edges = multivariate_transitions(N, incentive, num_types=num_types, mu=mu) 182 | return edges 183 | 184 | 185 | ## k-fold Moran process 186 | 187 | def k_fold_incentive_transitions(N, incentive, num_types, mu=None, k=None): 188 | """ 189 | Computes transition probabilities the k-fold incentive process 190 | 191 | Parameters 192 | ---------- 193 | N: int 194 | Population size / simplex divisor 195 | incentive: function 196 | An incentive function from incentives.py 197 | num_types: int, 3 198 | Number of types in population 199 | mu: float, 0.001 200 | The mutation rate of the process 201 | k: int, N // 2 202 | The power of the process 203 | """ 204 | 205 | if not k: 206 | k = N // 2 207 | if not mu: 208 | mu = 1. / N 209 | edges = multivariate_transitions(N, incentive, num_types=num_types, mu=mu) 210 | edge_func = power_transitions(edges, k) 211 | states = states_from_edges(edges) 212 | new_edges = edge_func_to_edges(edge_func, states) 213 | return new_edges 214 | 215 | 216 | ## Neutral landscape / Dirichlet 217 | 218 | def neutral_stationary(N, alpha, n=3, logspace=False): 219 | """ 220 | Computes the stationary distribution of the neutral landscape. This process 221 | is always reversible and there is an explicit formula. 222 | 223 | Parameters 224 | ---------- 225 | N: int 226 | Population size / simplex divisor 227 | alpha: 228 | Parameter defining the stationary distribution in terms of n and mu 229 | n: int, 3 230 | Simplex dimension - 1, Number of types in population 231 | logspace: bool, False 232 | Use the logspace version 233 | 234 | Returns 235 | ------- 236 | dictionary, stationary distribution of the process 237 | """ 238 | 239 | # Large N is better handled by the log version to avoid underflows 240 | if logspace or (N > 100): 241 | return log_neutral_stationary(N, alpha, n=n) 242 | 243 | # Just compute the distribution directly for each state 244 | d2 = dict() 245 | for state in simplex_generator(N, n-1): 246 | t = 1. 247 | for i in state: 248 | t *= inc_factorial(alpha, i) / factorial(i) 249 | t *= factorial(N) / inc_factorial(n * alpha, N) 250 | d2[state] = t 251 | return d2 252 | 253 | 254 | def log_neutral_stationary(N, alpha, n=3): 255 | """ 256 | Computes the stationary distribution of the neutral landscape. This process 257 | is always reversible and there is an explicit formula. This function is the 258 | same as neutral_stationary in log-space. 259 | 260 | Parameters 261 | ---------- 262 | N: int 263 | Population size / simplex divisor 264 | alpha: 265 | Parameter defining the stationary distribution in terms of n and mu 266 | n: int, 3 267 | Simplex dimension - 1, Number of types in population 268 | 269 | Returns 270 | ------- 271 | dictionary, stationary distribution of the process 272 | """ 273 | 274 | d2 = dict() 275 | for state in simplex_generator(N, n-1): 276 | t = 0. 277 | for i in state: 278 | t += log_inc_factorial(alpha, i) - log_factorial(i) 279 | t += log_factorial(N) - log_inc_factorial(n * alpha, N) 280 | d2[state] = exp(t) 281 | return d2 282 | -------------------------------------------------------------------------------- /stationary/processes/incentives.py: -------------------------------------------------------------------------------- 1 | """ 2 | Necessary mathematical functions for the incentive process. 3 | """ 4 | 5 | from __future__ import absolute_import 6 | 7 | import numpy 8 | from ..utils.math_helpers import multiply_vectors, dot_product 9 | 10 | 11 | # Fitness Landscapes 12 | 13 | def constant_fitness(c): 14 | """ 15 | Returns a function that is constant on the population state space. 16 | """ 17 | 18 | def f(pop): 19 | return numpy.array(pop) * numpy.array(c) 20 | return f 21 | 22 | 23 | def linear_fitness_landscape(m, self_interaction=True, normalize=False): 24 | """ 25 | Computes a fitness landscape from a game matrix given by m and a population 26 | vector (i,j) summing to N. 27 | 28 | Parameters 29 | ---------- 30 | m: matrix or list of lists 31 | The game matrix defining the landscape 32 | self_interaction: bool, True 33 | Whether players can self-interact, which affects the fitness landscape 34 | normalize: bool, False 35 | Whether to normalize the population states (typically not necessary) 36 | 37 | Returns 38 | ------- 39 | A function on population states (the fitness landscape) 40 | """ 41 | 42 | # m = array of rows 43 | def f(pop): 44 | N = sum(pop) 45 | div = N 46 | if not self_interaction: 47 | div = N - 1 48 | # Normalize population vector 49 | pop = [x / float(div) for x in pop] 50 | fitness = [] 51 | for i in range(len(pop)): 52 | # - m[i][i] if individuals do not interact with themselves. 53 | f = dot_product(m[i], pop) 54 | if not self_interaction: 55 | f -= m[i][i] 56 | if normalize: 57 | f = f / float(div) 58 | fitness.append(f) 59 | return fitness 60 | return f 61 | 62 | 63 | def rock_paper_scissors(a=1, b=1): 64 | """ 65 | The game matrix for the rock-paper-scissors game. 66 | """ 67 | 68 | return [[0,- b, a], [a, 0, -b], [-b, a, 0]] 69 | 70 | # Some people call it rock-scissors-paper 71 | rock_scissors_paper = rock_paper_scissors 72 | 73 | 74 | # Incentive Functions 75 | 76 | def replicator(fitness, q=1, **kwargs): 77 | """ 78 | The replicator incentive for a power q. For q=1 this reproduces the Moran 79 | process ratio. 80 | 81 | Parameters 82 | ---------- 83 | fitness: function 84 | A fitness landscape 85 | q: float 86 | Exponent for the population state 87 | 88 | Returns 89 | ------- 90 | a function corresponding to the incentive 91 | """ 92 | 93 | if q == 1: 94 | def f(x): 95 | return multiply_vectors(x, fitness(x)) 96 | return f 97 | 98 | def g(x): 99 | y = numpy.power(x, q) 100 | return y * fitness(x) 101 | return g 102 | 103 | 104 | def logit(fitness, beta=1., q=0.): 105 | """ 106 | The logit incentive for a power q. For q=0 this reproduces the Logit 107 | process ratio. 108 | 109 | Parameters 110 | ---------- 111 | fitness: function 112 | A fitness landscape 113 | q: float 114 | Exponent for the population state 115 | beta: float 116 | An inverse temperature / strength of selection parameter 117 | 118 | Returns 119 | ------- 120 | a function corresponding to the incentive. 121 | """ 122 | 123 | if q == 0: 124 | def f(x): 125 | return numpy.exp(numpy.array(fitness(x)) * beta) 126 | return f 127 | 128 | def g(x): 129 | y = numpy.power(x, q) 130 | return multiply_vectors(y, numpy.exp(numpy.array(fitness(x)) * beta)) 131 | return g 132 | 133 | 134 | def fermi(fitness, beta=1., q=1.): 135 | """ 136 | The Fermi incentive for a power q. For q=0 this reproduces the Fermi process 137 | ratio. Equal to the logit incentive with q=1. 138 | 139 | Parameters 140 | ---------- 141 | fitness: function 142 | A fitness landscape 143 | q: float 144 | Exponent for the population state 145 | beta: float 146 | An inverse temperature / strength of selection parameter 147 | 148 | Returns 149 | ------- 150 | a function corresponding to the incentive. 151 | """ 152 | 153 | return logit(fitness, beta=beta, q=1) 154 | 155 | 156 | def logit2(fitness, beta=1., **kwargs): 157 | """ 158 | The logit incentive for use with large beta, which approximates the 159 | best-reply incentive. Uses a log-space calculation. 160 | 161 | Parameters 162 | ---------- 163 | fitness: function 164 | A fitness landscape 165 | beta: float 166 | An inverse temperature / strength of selection parameter 167 | 168 | Returns 169 | ------- 170 | a function corresponding to the incentive. 171 | """ 172 | 173 | def g(x): 174 | i1, i2 = x 175 | f = fitness(x) 176 | diff = f[1] - f[0] 177 | denom = i1+i2*numpy.exp(beta * diff) 178 | return [i1/denom, i2 * numpy.exp(beta*diff) / denom] 179 | return g 180 | 181 | 182 | def simple_best_reply(fitness): 183 | def g(x): 184 | f = fitness(x) 185 | if f[0] > f[1]: 186 | return [1, 0] 187 | return [0, 1] 188 | return g 189 | -------------------------------------------------------------------------------- /stationary/processes/variable_population_size.py: -------------------------------------------------------------------------------- 1 | from ..utils.math_helpers import normalize, multiply_vectors 2 | 3 | 4 | # Random-death probability distributions 5 | 6 | def moran_death(N): 7 | def p(pop): 8 | s = sum(pop) 9 | if s == N + 1: 10 | return 1 11 | return 0 12 | return p 13 | 14 | 15 | def moran_cascade(N, k=2): 16 | def p(pop): 17 | s = sum(pop) 18 | return math.pow(k, s-N) 19 | return p 20 | 21 | 22 | def discrete_sigmoid(t, k_1=0.1, k_2=-1.1): 23 | # Adapted from 24 | # https://dinodini.wordpress.com/2010/04/05/normalized-tunable-sigmoid-functions/ 25 | k_2 = -1 - k_1 26 | if t < 0: 27 | return 0 28 | if t <= 0.5: 29 | return k_1 * t / (k_1 - 2 * t + 1) 30 | else: 31 | return 0.5 + 0.5 * k_2 * (2 * t - 1.) / (k_2 - (2 * t - 1.) + 1) 32 | 33 | 34 | def sigmoid_death(N, k_1=2, k_2=2): 35 | def p(pop): 36 | s = sum(pop) 37 | return discrete_sigmoid(float(s) / N, k_1=k_1, k_2=k_2) 38 | return p 39 | 40 | 41 | def linear_death(N): 42 | def p(pop): 43 | s = sum(pop) 44 | if s == 1: 45 | return 0 46 | return float(s) / N 47 | return p 48 | 49 | 50 | def even_death(N): 51 | def p(pop): 52 | s = sum(pop) 53 | if s == 1 or s == N: 54 | return 0 55 | return 0.5 56 | return p 57 | 58 | 59 | # 2d moran-like process separating birth and death processes 60 | def variable_population_transitions( 61 | N, fitness_landscape, death_probabilities=None, incentive=None, 62 | mu=0.001): 63 | """ 64 | Computes transition probabilities for the incentive process on two types 65 | for a population of varying size. 66 | 67 | Parameters 68 | ---------- 69 | N: int 70 | Max population size / simplex divisor 71 | fitness_landscape, function 72 | The fitness landscape 73 | death_probabilities, function 74 | A function returning probalities of a death event 75 | incentive: function 76 | An incentive function from incentives.py 77 | mu: float, 0.001 78 | The mutation rate of the process 79 | """ 80 | 81 | if not death_probabilities: 82 | death_probabilities = moran_death(N) 83 | edges = [] 84 | # Possible states are (a, b) with 0 < a + b <= N where a is the number of A 85 | # individuals and B is the number of B individuals. 86 | for a in range(0, N + 1): 87 | for b in range(0, N + 1 - a): 88 | # Death events. 89 | if a + b == 0: 90 | continue 91 | p = death_probabilities((a, b)) 92 | if a > 0 and b > 0: 93 | q = p * float(a) / (a + b) 94 | if q > 0: 95 | edges.append(((a, b), (a - 1, b), q)) 96 | q = p * float(b) / (a + b) 97 | if q > 0: 98 | edges.append(((a, b), (a, b - 1), q)) 99 | # Birth events. 100 | if a + b >= N: 101 | continue 102 | if incentive: 103 | birth_q = normalize(incentive([a,b])) 104 | else: 105 | birth_q = normalize( 106 | multiply_vectors([a, b], fitness_landscape([a, b]))) 107 | if a < N: 108 | q = (1. - p) * (birth_q[0] * (1 - mu) + birth_q[1] * mu) 109 | if q > 0: 110 | edges.append(((a, b), (a + 1, b), q)) 111 | if b < N: 112 | q = (1. - p) * (birth_q[0] * mu + birth_q[1] * (1. - mu)) 113 | if q > 0: 114 | edges.append(((a, b), (a, b + 1), q)) 115 | return edges 116 | -------------------------------------------------------------------------------- /stationary/processes/wright_fisher.py: -------------------------------------------------------------------------------- 1 | """ 2 | The Wright-Fisher process 3 | """ 4 | 5 | import numpy 6 | from numpy import array, log, exp 7 | from scipy.special import gammaln 8 | 9 | from ..utils.math_helpers import simplex_generator, dot_product 10 | 11 | 12 | def cache_multinomial_coefficients(N, num_types=3): 13 | """ 14 | Caches multinomial coefficients. 15 | 16 | Parameters 17 | ---------- 18 | N: int 19 | Population size / simplex divisor 20 | num_types: int, 3 21 | Number of types in population 22 | """ 23 | 24 | if num_types == 2: 25 | M = numpy.zeros(shape=N+1) 26 | for i, j in simplex_generator(N, d=num_types-1): 27 | M[i] = gammaln(N+1) - gammaln(i+1) - gammaln(j+1) 28 | return M 29 | 30 | if num_types == 3: 31 | M = numpy.zeros(shape=(N+1, N+1)) 32 | for i, j, k in simplex_generator(N, d=num_types-1): 33 | M[i][j] = gammaln(N+1) - gammaln(i+1) - gammaln(j+1) - gammaln(k+1) 34 | return M 35 | 36 | 37 | def multivariate_transitions_sub(N, incentive, mu=0.001, low_memory=False): 38 | """ 39 | Computes transitions for dimension n=3 moran process given a game matrix. 40 | 41 | Parameters 42 | ---------- 43 | N: int 44 | Population size / simplex divisor 45 | incentive: function 46 | An incentive function from incentives.py 47 | mu: float, 0.001 48 | The mutation rate of the process 49 | """ 50 | 51 | num_types = 2 52 | 53 | M = cache_multinomial_coefficients(N, num_types=num_types) 54 | 55 | def multinomial_probability(xs, ps): 56 | xs, ps = array(xs), array(ps) 57 | # Log-Binomial 58 | result = M[xs[0]] + sum(xs * log(ps)) 59 | return exp(result) 60 | 61 | def g(current_state, next_state): 62 | inc = incentive(current_state) 63 | ps = [] 64 | s = float(sum(inc)) 65 | if s == 0: 66 | raise ValueError( 67 | "You need to use a Fermi incentive to prevent division by zero." 68 | ) 69 | r = dot_product(inc, [1. - mu, mu]) / s 70 | ps.append(r) 71 | r = dot_product(inc, [mu, 1. - mu]) / s 72 | ps.append(r) 73 | return multinomial_probability(next_state, ps) 74 | 75 | if low_memory: 76 | return g 77 | 78 | # Cache the full edge computation 79 | edges = numpy.zeros(shape=(N+1, N+1)) 80 | for current_state in simplex_generator(N, num_types - 1): 81 | for next_state in simplex_generator(N, num_types - 1): 82 | edges[current_state[0]][next_state[0]] = g(current_state, 83 | next_state) 84 | 85 | def h(current_state, next_state): 86 | return edges[current_state[0]][next_state[0]] 87 | 88 | return h 89 | 90 | 91 | def multivariate_transitions(N, incentive, mu=0.001, num_types=3, 92 | low_memory=False): 93 | """Computes transitions for the Wright-Fisher process. Since this can be a 94 | large matrix, this function returns a function that computes the transitions 95 | for any given two states. This can be converted to a list of edges with 96 | the utils.edges function edge_func_to_edges. 97 | 98 | Parameters 99 | ---------- 100 | N: int 101 | Population size / simplex divisor 102 | incentive: function 103 | An incentive function from incentives.py 104 | num_types: int, 3 105 | Number of types in population 106 | mu: float, 0.001 107 | The mutation rate of the process 108 | low_memory: bool, False 109 | If True, less is cached to save memory 110 | 111 | Returns 112 | ------- 113 | edge_func: function on states x states 114 | """ 115 | 116 | if num_types == 2: 117 | return multivariate_transitions_sub( 118 | N, incentive, mu=mu, low_memory=low_memory) 119 | 120 | M = cache_multinomial_coefficients(N, num_types=num_types) 121 | 122 | def multinomial_probability(xs, ps): 123 | xs, ps = array(xs), array(ps) 124 | result = M[xs[0]][xs[1]] + sum(xs * log(ps)) 125 | return exp(result) 126 | 127 | def g(current_state, next_state): 128 | inc = incentive(current_state) 129 | ps = [] 130 | s = float(sum(inc)) 131 | if s == 0: 132 | raise ValueError( 133 | "You need to use a Fermi incentive to prevent division by zero." 134 | ) 135 | half_mu = mu / 2. 136 | r = dot_product(inc, [1 - mu, half_mu, half_mu]) / s 137 | ps.append(r) 138 | r = dot_product(inc, [half_mu, 1 - mu, half_mu]) / s 139 | ps.append(r) 140 | r = dot_product(inc, [half_mu, half_mu, 1 - mu]) / s 141 | ps.append(r) 142 | return multinomial_probability(next_state, ps) 143 | 144 | if low_memory: 145 | return g 146 | 147 | # Cache the full edge computation 148 | edges = numpy.zeros(shape=(N+1, N+1, N+1, N+1)) 149 | for current_state in simplex_generator(N, num_types-1): 150 | for next_state in simplex_generator(N, num_types-1): 151 | edges[current_state[0]][current_state[1]][next_state[0]][next_state[1]] = g(current_state, next_state) 152 | 153 | def h(current_state, next_state): 154 | return edges[current_state[0]][current_state[1]][next_state[0]][next_state[1]] 155 | 156 | return h 157 | -------------------------------------------------------------------------------- /stationary/stationary_.py: -------------------------------------------------------------------------------- 1 | """ 2 | Stationary distributions and Entropy rates. 3 | """ 4 | 5 | from __future__ import absolute_import 6 | 7 | from collections import Callable 8 | import itertools 9 | 10 | from numpy import log, exp 11 | 12 | from stationary.utils.edges import edges_to_edge_dict 13 | from stationary.utils.graph import Graph 14 | from stationary.utils.math_helpers import ( 15 | simplex_generator, logsumexp, kl_divergence, kl_divergence_dict) 16 | 17 | 18 | def stationary_distribution(edges=None, exact=False, logspace=False, 19 | initial_state=None, iterations=None, lim=1e-8, 20 | states=None): 21 | """ 22 | Convenience function to route to different stationary distribution 23 | computations. 24 | 25 | Parameters 26 | ---------- 27 | edges: list of tuples or function 28 | The transitions of the process, either a list of (source, target, 29 | transition_probability), or an edge_function that takes two parameters, 30 | the source and target states, to the transition transition probability. 31 | If using an edge_function you must supply the states of the process. 32 | exact: bool, False 33 | Try to use the exact formula. Only works for reversible processes (no 34 | check) on a discretization of simplex 35 | logspace: bool False 36 | Carry out the calculation in logspace 37 | iterations: int, None 38 | Maximum number of iterations for stationary approximation 39 | lim: float, 1e-8 40 | Approximate algorithm breaks when successive iterations have a 41 | kl_divergence less than lim 42 | initial_state, None 43 | A distribution over the states of the process. If None, the uniform 44 | distiribution is used. 45 | states: list, None 46 | States for use with the edge_function. 47 | """ 48 | 49 | if isinstance(edges, list): 50 | if not exact: 51 | return approx_stationary( 52 | edges, logspace=logspace, iterations=iterations, lim=lim, 53 | initial_state=initial_state) 54 | else: 55 | return exact_stationary( 56 | edges, initial_state=initial_state, logspace=logspace) 57 | elif isinstance(edges, Callable): 58 | if not states: 59 | raise ValueError( 60 | "Keyword argument `states` required with edge_func") 61 | return approx_stationary_func( 62 | edges, states, iterations=iterations, lim=lim, logspace=logspace) 63 | # Still here? 64 | raise Exception("Parameter combination not implemented") 65 | 66 | 67 | # Stationary Distributions 68 | 69 | class Cache(object): 70 | """ 71 | Caches common calculations for a given graph associated to a Markov process 72 | for efficiency when computing the stationary distribution with the 73 | approximate algorithm. 74 | 75 | Parameters 76 | ---------- 77 | graph: a Graph object 78 | The graph underlying the Markov process. 79 | """ 80 | 81 | def __init__(self, graph): 82 | # Caches vertex enumeration, cumulative sums, absorbing state tests, 83 | # and transition targets. 84 | self.enum = dict() 85 | self.inv_enum = [] 86 | self.in_neighbors = [] 87 | self.terminals = [] 88 | vertices = graph.vertices() 89 | # Enumerate vertices 90 | for (index, vertex) in enumerate(vertices): 91 | self.enum[vertex] = index 92 | self.inv_enum.append(vertex) 93 | # Cache in_neighbors 94 | for vertex in vertices: 95 | in_dict = graph.in_dict(vertex) 96 | self.in_neighbors.append([(self.enum[k], v) for k, v in 97 | in_dict.items()]) 98 | 99 | 100 | def stationary_generator(cache, logspace=False, initial_state=None): 101 | """ 102 | Generator for the stationary distribution of a Markov chain, produced by 103 | iteration of the transition matrix. The iterator yields successive 104 | approximations of the stationary distribution. 105 | 106 | Parameters 107 | ---------- 108 | cache, a Cache object 109 | initial_state: None 110 | A distribution over the states of the process. If None, the uniform 111 | distribution is used. 112 | logspace: bool False 113 | Carry out the calculation in logspace 114 | 115 | Yields 116 | ------ 117 | a list of floats 118 | """ 119 | 120 | N = len(cache.inv_enum) 121 | 122 | if logspace: 123 | sum_func = logsumexp 124 | exp_func = exp 125 | if not initial_state: 126 | initial_state = [-log(N)] * N 127 | else: 128 | sum_func = sum 129 | exp_func = lambda x: x 130 | if not initial_state: 131 | initial_state = [1. / N] * N 132 | 133 | ranks = initial_state 134 | 135 | # This is essentially iterated sparse matrix multiplication. 136 | yield ranks 137 | while True: 138 | new_ranks = [] 139 | for node in range(N): 140 | l = [] 141 | for i, v in cache.in_neighbors[node]: 142 | if logspace: 143 | l.append(log(v) + ranks[i]) 144 | else: 145 | l.append(v * ranks[i]) 146 | new_rank = sum_func(l) 147 | new_ranks.append(new_rank) 148 | ranks = new_ranks 149 | yield exp_func(ranks) 150 | 151 | 152 | ## Approximate stationary distributions computed by by sparse matrix 153 | # multiplications. 154 | 155 | def approx_stationary(edges, logspace=False, iterations=None, lim=1e-8, 156 | initial_state=None): 157 | """ 158 | Approximate stationary distributions computed by by sparse matrix 159 | multiplications. Produces correct results and uses little memory but is 160 | likely not the most CPU efficient implementation in general (e.g. an 161 | eigenvector calculator may be better). 162 | 163 | Essentially raises the transition probabilities matrix to a large power. 164 | 165 | Parameters 166 | ----------- 167 | edges: list of tuples 168 | Transition probabilities of the form [(source, target, 169 | transition_probability 170 | logspace: bool False 171 | Carry out the calculation in logspace 172 | iterations: int, None 173 | Maximum number of iterations 174 | lim: float, 1e-13 175 | Approximate algorithm breaks when successive iterations have a 176 | kl_divergence less than lim 177 | initial_state: None 178 | A distribution over the states of the process. If None, the uniform 179 | distribution is used. 180 | 181 | """ 182 | 183 | g = Graph() 184 | g.add_edges(edges) 185 | cache = Cache(g) 186 | gen = stationary_generator( 187 | cache, logspace=logspace, initial_state=initial_state) 188 | 189 | previous_ranks = None 190 | for i, ranks in enumerate(gen): 191 | if i > 200: 192 | if i % 10: 193 | s = kl_divergence(ranks, previous_ranks) 194 | if s < lim: 195 | break 196 | if iterations: 197 | if i == iterations: 198 | break 199 | previous_ranks = ranks 200 | 201 | # Reverse the enumeration 202 | d = dict() 203 | for m, r in enumerate(ranks): 204 | state = cache.inv_enum[m] 205 | d[(state)] = r 206 | return d 207 | 208 | 209 | def approx_stationary_func(edge_func, states, iterations=100, lim=1e-8, 210 | logspace=False): 211 | """ 212 | Approximate stationary distributions computed by by sparse matrix 213 | multiplications. Produces correct results and uses little memory but is 214 | likely not the most CPU efficient implementation in general (e.g. an 215 | eigenvector calculator may be better). 216 | 217 | Essentially raises the transition probabilities matrix to a large power. 218 | 219 | This function takes a function that computes transitions rather than a list 220 | of edges, to lower the memory footprint (at the cost of efficiency). Needed 221 | for Wright-Fisher. Assumes that the graph is fully connected. 222 | 223 | Parameters 224 | ----------- 225 | edge_func: function 226 | Yields the transition probabilities between two states, edge_func(a,b) 227 | iterations: int, None 228 | Maximum number of iterations 229 | lim: float, 1e-13 230 | Approximate algorithm breaks when successive iterations have a 231 | kl_divergence less than lim 232 | logspace: bool False 233 | Carry out the calculation in logspace 234 | """ 235 | 236 | initial_state = [1./float(len(states))]*(len(states)) 237 | 238 | if logspace: 239 | sum_func = logsumexp 240 | exp_func = exp 241 | initial_state = log(initial_state) 242 | else: 243 | sum_func = sum 244 | exp_func = lambda x: x 245 | 246 | ranks = dict(zip(states, initial_state)) 247 | previous_ranks = None 248 | for iteration in itertools.count(1): 249 | if iterations: 250 | if iteration > iterations: 251 | break 252 | if iteration > 100: 253 | if iteration % 50: 254 | s = kl_divergence_dict(ranks, previous_ranks) 255 | if s < lim: 256 | break 257 | new_ranks = dict() 258 | for x in states: 259 | l = [] 260 | for y in states: 261 | v = edge_func(y,x) 262 | if logspace: 263 | l.append(log(v) + ranks[y]) 264 | else: 265 | l.append(v * ranks[y]) 266 | new_ranks[x] = sum_func(l) 267 | previous_ranks = ranks 268 | ranks = new_ranks 269 | 270 | d = dict() 271 | for m, r in ranks.items(): 272 | d[m] = exp_func(r) 273 | return d 274 | 275 | 276 | # Exact computations for reversible processes. Use at your own risk! No check 277 | # for reversibility is performed 278 | 279 | def exact_stationary(edges, initial_state=None, logspace=False): 280 | """ 281 | Computes the stationary distribution of a reversible process on the simplex 282 | exactly. No check for reversibility. 283 | 284 | Parameters 285 | ---------- 286 | 287 | edges: list or dictionary 288 | The edges or edge_dict of the process 289 | initial_state: tuple, None 290 | The initial state. If not given a suitable state is created. 291 | logspace: bool False 292 | Carry out the calculation in logspace 293 | 294 | returns 295 | ------- 296 | dictionary, the stationary distribution 297 | """ 298 | 299 | # Convert edges to edge_dict if necessary 300 | if isinstance(edges, list): 301 | edges = edges_to_edge_dict(edges) 302 | # Compute population parameters from the edge_dict 303 | state = list(edges)[0][0] 304 | N = sum(state) 305 | num_players = len(state) 306 | # Get an initial state 307 | if not initial_state: 308 | initial_state = [N // num_players]* num_players 309 | initial_state[-1] = N - (num_players -1 ) * (N // num_players) 310 | initial_state = tuple(initial_state) 311 | 312 | # Use the exact form of the stationary distribution. 313 | d = dict() 314 | for state in simplex_generator(N, num_players - 1): 315 | # Take a path from initial to state. 316 | seq = [initial_state] 317 | e = list(seq[-1]) 318 | for i in range(0, num_players): 319 | while e[i] < state[i]: 320 | for j in range(0, num_players): 321 | if e[j] > state[j]: 322 | break 323 | e[j] = e[j] - 1 324 | e[i] = e[i] + 1 325 | seq.append(tuple(e)) 326 | while e[i] > state[i]: 327 | for j in range(0, num_players): 328 | if e[j] < state[j]: 329 | break 330 | e[j] = e[j] + 1 331 | e[i] = e[i] - 1 332 | seq.append(tuple(e)) 333 | if logspace: 334 | s = 0. 335 | else: 336 | s = 1. 337 | for index in range(len(seq)-1): 338 | e, f = seq[index], seq[index+1] 339 | if logspace: 340 | s += log(edges[(e, f)]) - log(edges[(f, e)]) 341 | else: 342 | s *= edges[(e, f)] / edges[(f, e)] 343 | d[state] = s 344 | if logspace: 345 | s0 = logsumexp([v for v in d.values()]) 346 | for key, v in d.items(): 347 | d[key] = exp(v - s0) 348 | else: 349 | s0 = 1. / sum([v for v in d.values()]) 350 | for key, v in d.items(): 351 | d[key] = s0 * v 352 | return d 353 | -------------------------------------------------------------------------------- /stationary/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from . import bomze 2 | from . import edges 3 | from . import extrema 4 | from . import graph 5 | from . import math_helpers 6 | from . import matrix_checks 7 | from . import plotting 8 | from .expected_divergence_ import expected_divergence 9 | -------------------------------------------------------------------------------- /stationary/utils/bomze.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | def bomze_matrices(filename="bomze.txt"): 5 | """ 6 | Yields the 48 matrices from I.M. Bomze's classification of three player phase 7 | portraits. 8 | """ 9 | 10 | this_dir, this_filename = os.path.split(__file__) 11 | 12 | handle = open(os.path.join(this_dir, filename)) 13 | for line in handle: 14 | a, b, c, d, e, f, g, h, i = map(float, line.split()) 15 | yield [[a, b, c], [d, e, f], [g, h, i]] 16 | -------------------------------------------------------------------------------- /stationary/utils/bomze.txt: -------------------------------------------------------------------------------- 1 | 0 0 0 0 0 0 0 0 0 2 | 0 0 1 0 0 1 1 1 0 3 | 0 0 1 0 0 1 1 0 0 4 | 0 0 1 0 0 1 1 -1 0 5 | 0 0 0 0 0 0 1 -1 0 6 | 0 1 2 1 0 1 2 -1 0 7 | 0 1 -1 1 0 -2 -1 2 0 8 | 0 1 1 1 0 1 1 1 0 9 | 0 -1 3 -1 0 3 1 1 0 10 | 0 1 1 -1 0 3 1 1 0 11 | 0 1 3 -1 0 5 1 3 0 12 | 0 1 -1 -1 0 1 -1 1 0 13 | 0 6 -4 -3 0 5 -1 3 0 14 | 0 1 -1 1 0 -3 -1 3 0 15 | 0 3 -1 3 0 -1 1 1 0 16 | 0 3 -1 1 0 1 3 -1 0 17 | 0 -1 1 1 0 -1 -1 1 0 18 | 0 2 -1 -1 0 2 2 -1 0 19 | 0 0 0 0 0 -1 0 1 0 20 | 0 0 0 0 0 -1 0 0 0 21 | 0 0 0 0 0 -1 0 -1 0 22 | 0 0 -2 0 0 -1 -1 -1 0 23 | 0 0 -1 0 0 1 -1 1 0 24 | 0 0 1 0 0 0 1 -1 0 25 | 0 0 1 0 0 0 1 1 0 26 | 0 0 -2 0 0 -1 -1 0 0 27 | 0 0 -1 0 0 1 -1 0 0 28 | 0 0 -1 0 0 -2 -1 0 0 29 | 0 0 -1 0 0 -2 -1 1 0 30 | 0 0 -1 0 0 -1 0 0 0 31 | 0 0 -2 0 0 -1 0 0 0 32 | 0 0 -1 0 0 1 0 0 0 33 | 0 0 -1 0 0 0 0 -1 0 34 | 0 0 -1 0 0 0 1 -1 0 35 | 0 -1 3 -1 0 1 3 1 0 36 | 0 3 1 3 0 1 1 1 0 37 | 0 1 -1 -3 0 1 -1 1 0 38 | 0 1 1 -1 0 1 1 1 0 39 | 0 1 1 -1 0 3 1 3 0 40 | 0 -1 -1 1 0 1 -1 1 0 41 | 0 1 -1 1 0 -1 1 1 0 42 | 0 1 -1 1 0 1 1 -1 0 43 | 0 1 1 1 0 1 -1 -1 0 44 | 0 1 1 -1 0 1 -1 -1 0 45 | 0 1 1 0 0 2 0 3 0 46 | 0 -2 1 0 0 -2 0 -1 0 47 | 0 -1 1 0 0 -1 0 1 0 48 | 0 2 0 2 0 0 1 1 0 49 | 0 1 0 1 0 0 -1 2 0 -------------------------------------------------------------------------------- /stationary/utils/edges.py: -------------------------------------------------------------------------------- 1 | from numpy import zeros 2 | from numpy.linalg import matrix_power 3 | 4 | 5 | def states_from_edges(edges): 6 | """ 7 | Computes the underlying set of states from the list of edges. 8 | 9 | Parameters 10 | ---------- 11 | edges: list of tuples 12 | Transition probabilities of the form [(source, target, transition_probability 13 | 14 | Returns 15 | ------- 16 | states, set 17 | The set of all the vertices of the edge list 18 | """ 19 | states = set() 20 | for (source, target, weight) in edges: 21 | states.add(source) 22 | states.add(target) 23 | return states 24 | 25 | 26 | def enumerate_states(states, inverse=True): 27 | """ 28 | Enumerates a list of states, and possibly with the inverse mapping. 29 | 30 | Parameters 31 | ---------- 32 | states: List 33 | The list of hashable objects to enumerate 34 | inverse: bool True 35 | Include the inverse enumeration 36 | 37 | Returns 38 | ------- 39 | enum, dict 40 | A dictionary mapping states to integers 41 | inv_enum, list 42 | A list mapping integers to states 43 | """ 44 | 45 | if not inverse: 46 | enum = dict(zip(states, range(len(states)))) 47 | return enum 48 | 49 | enum = dict() 50 | inv_enum = [] 51 | for i, state in enumerate(states): 52 | enum[state] = i 53 | inv_enum.append(state) 54 | return enum, inv_enum 55 | 56 | 57 | def enumerate_states_from_edges(edges, inverse=True): 58 | """ 59 | Enumerates the states of a Markov process from the list of edges. 60 | 61 | Parameters 62 | ---------- 63 | edges: list of tuples 64 | Transition probabilities of the form [(source, target, transition_probability 65 | inverse: bool True 66 | Include the inverse enumeration 67 | 68 | Returns 69 | ------- 70 | all_states, set 71 | The set of all states of the process 72 | enum, dict 73 | A dictionary mapping states to integers 74 | inv_enum, list 75 | A list mapping integers to states 76 | """ 77 | 78 | # Collect the states 79 | all_states = states_from_edges(edges) 80 | 81 | if not inverse: 82 | enum = enumerate_states(all_states, inverse=False) 83 | return all_states, enum 84 | 85 | enum, inv_enum = enumerate_states(all_states, inverse=True) 86 | return all_states, enum, inv_enum 87 | 88 | 89 | def edges_to_matrix(edges): 90 | """ 91 | Converts a list of edges to a transition matrix by enumerating the states. 92 | 93 | Parameters 94 | ---------- 95 | edges: list of tuples 96 | Transition probabilities is the form [(source, target, transition 97 | probability 98 | 99 | Returns 100 | ------- 101 | mat, numpy.array 102 | The transition matrix 103 | all_states, list of nodes 104 | The collection of states 105 | enumeration, dictionary 106 | maps states to integers 107 | """ 108 | 109 | # Enumerate states so we can put them in a matrix. 110 | all_states, enumeration = enumerate_states_from_edges(edges, inverse=False) 111 | 112 | # Build a matrix for the transitions 113 | mat = zeros((len(all_states), len(all_states))) 114 | for (a, b, v) in edges: 115 | mat[enumeration[a]][enumeration[b]] = v 116 | return mat, all_states, enumeration 117 | 118 | 119 | def edges_to_edge_dict(edges): 120 | """ 121 | Converts a list of edges to a transition dictionary taking (source, target) 122 | to the transition between the states. 123 | 124 | Parameters 125 | ---------- 126 | edges: list of tuples 127 | Transition probabilities of the form [(source, target, transition 128 | probability 129 | 130 | Returns 131 | ------- 132 | edges, maps 2-tuples of nodes to floats 133 | """ 134 | 135 | edge_dict = dict() 136 | for e1, e2, v in edges: 137 | edge_dict[(e1, e2)] = v 138 | return edge_dict 139 | 140 | 141 | def edge_func_to_edges(edge_func, states): 142 | """ 143 | Convert an edge_func to a list of edges. 144 | """ 145 | 146 | edges = [] 147 | for s1 in states: 148 | for s2 in states: 149 | edges.append((s1, s2, edge_func(s1, s2))) 150 | return edges 151 | 152 | 153 | def power_transitions(edges, k=20): 154 | """ 155 | Raises a transition matrix (specified by edges) to the power `k`, returning 156 | an edge_func. 157 | """ 158 | 159 | mat, all_states, enum = edges_to_matrix(edges) 160 | M_k = matrix_power(mat, k) 161 | 162 | def edge_func(current_state, next_state): 163 | return M_k[enum[current_state]][enum[next_state]] 164 | return edge_func 165 | -------------------------------------------------------------------------------- /stationary/utils/expected_divergence_.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict, Callable 2 | 3 | import numpy 4 | 5 | from .math_helpers import normalize, q_divergence 6 | 7 | 8 | def expected_divergence(edges, states=None, q_d=1, boundary=True): 9 | """ 10 | Computes the KL-div of the expected state with the state, for all states. 11 | 12 | Parameters 13 | ---------- 14 | edges: list of tuples or function 15 | The transitions of the process, either a list of (source, target, 16 | transition_probability), or an edge_function that takes two parameters, 17 | the source and target states, to the transition transition probability. 18 | If using an edge_function you must supply the states of the process. 19 | q_d: float, 1 20 | parameter that specifies which divergence function to use 21 | states: list, None 22 | States for use with the edge_func 23 | boundary: bool, False 24 | Exclude the boundary states 25 | 26 | Returns 27 | ------- 28 | Dictionary mapping states to D(E(state), state) 29 | """ 30 | 31 | dist = q_divergence(q_d) 32 | e = defaultdict(float) 33 | 34 | if isinstance(edges, list): 35 | for x, y, w in edges: 36 | e[x] += numpy.array(y) * w 37 | elif isinstance(edges, Callable): 38 | if not states: 39 | raise ValueError( 40 | "Keyword argument `states` required with edge_func") 41 | for x in states: 42 | e[x] = 0. 43 | for y in states: 44 | w = edges(x, y) 45 | e[x] += numpy.array(y) * w 46 | d = dict() 47 | for state, v in e.items(): 48 | # Some divergences do not play well on the boundary 49 | if not boundary: 50 | p = 1. 51 | for s in state: 52 | p *= s 53 | if p == 0: 54 | continue 55 | d[state] = dist(normalize(v), normalize(list(state))) 56 | return d 57 | -------------------------------------------------------------------------------- /stationary/utils/extrema.py: -------------------------------------------------------------------------------- 1 | import math 2 | import numpy 3 | 4 | from .math_helpers import one_step_generator 5 | from .graph import Graph 6 | 7 | 8 | def find_local_minima(d, comp_func=None): 9 | """ 10 | Finds local minima of distributions on the simplex. 11 | 12 | Parameters 13 | ---------- 14 | d: dict 15 | The dictionary on a simplex discretization to find the extrema of 16 | comp_func: function 17 | Function to compare states 18 | 19 | Returns 20 | ------- 21 | set of minimal states. 22 | """ 23 | 24 | if not comp_func: 25 | comp_func = lambda x, y: (x - y >= 0) 26 | 27 | dim = len(list(d)[0]) - 1 28 | states = [] 29 | for state, value in d.items(): 30 | if value is None: 31 | continue 32 | if math.isnan(value): 33 | continue 34 | is_extremum = True 35 | for one_step in one_step_generator(dim): 36 | adj = tuple(numpy.array(state) + numpy.array(one_step)) 37 | try: 38 | v2 = d[adj] 39 | except KeyError: 40 | continue 41 | if comp_func(value, v2): 42 | is_extremum = False 43 | break 44 | if is_extremum: 45 | states.append(state) 46 | return set(states) 47 | 48 | 49 | def find_local_maxima(d): 50 | """ 51 | Finds local maxima of distributions on the simplex. 52 | 53 | Parameters 54 | ---------- 55 | d: dict 56 | The dictionary on a simplex discretization to find the extrema of 57 | 58 | Returns 59 | ------- 60 | set of maximal states. 61 | """ 62 | 63 | comp_func = lambda x, y: (y - x >= 0) 64 | return find_local_minima(d, comp_func=comp_func) 65 | 66 | 67 | def inflow_outflow(edges): 68 | """ 69 | Computes the inflow - outflow of probability at each state. 70 | """ 71 | 72 | g = Graph(edges) 73 | 74 | flow = dict() 75 | for s1 in g.vertices(): 76 | flow[s1] = sum(g.out_dict(s1).values()) - sum(g.in_dict(s1).values()) 77 | return flow 78 | -------------------------------------------------------------------------------- /stationary/utils/files.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | def ensure_directory(directory): 5 | if not os.path.isdir(directory): 6 | os.mkdir(directory) 7 | 8 | 9 | def ensure_digits(num, s): 10 | """Prepends a string s with zeros to enforce a set num of digits.""" 11 | if len(s) < num: 12 | return "0"*(num - len(s)) + s 13 | return s 14 | -------------------------------------------------------------------------------- /stationary/utils/graph.py: -------------------------------------------------------------------------------- 1 | """ 2 | Labeled and weighted graph class for Markov simulations. Not a full-featured 3 | class, rather an appropriate organizational data structure for handling various 4 | Markov process calculations. 5 | """ 6 | 7 | from collections import defaultdict 8 | import random 9 | 10 | 11 | class Graph(object): 12 | """Directed graph object intended for the graph associated to a Markov process. 13 | Gives easy access to the neighbors of a particular state needed for various 14 | calculations. 15 | 16 | Vertices can be any hashable / immutable python object. 17 | """ 18 | 19 | def __init__(self, edges=None): 20 | self.out_mapping = defaultdict(lambda: defaultdict(float)) 21 | self.in_mapping = defaultdict(lambda: defaultdict(float)) 22 | if edges: 23 | self.add_edges(edges) 24 | 25 | def add_vertex(self, label): 26 | self._vertices.add(label) 27 | 28 | def add_edge(self, source, target, weight=1.): 29 | self.out_mapping[source][target] = weight 30 | self.in_mapping[target][source] = weight 31 | 32 | def add_edges(self, edges): 33 | try: 34 | for source, target, weight in edges: 35 | self.add_edge(source, target, weight) 36 | except ValueError: 37 | for source, target in edges: 38 | self.add_edge(source, target, 1.0) 39 | 40 | def vertices(self): 41 | """Returns the set of vertices of the graph.""" 42 | return self.out_mapping.keys() 43 | 44 | def out_dict(self, source): 45 | """Returns a dictionary of the outgoing edges of source with weights.""" 46 | return self.out_mapping[source] 47 | 48 | def out_vertices(self, source): 49 | """Returns a list of the outgoing vertices.""" 50 | return self.out_mapping[source].keys() 51 | 52 | def in_dict(self, target): 53 | """Returns a dictionary of the incoming edges of source with weights.""" 54 | return self.in_mapping[target] 55 | 56 | def normalize_weights(self): 57 | """Normalizes the weights coming out of each vertex to be probability 58 | distributions.""" 59 | # new_edges = [] 60 | for source in self.out_mapping.keys(): 61 | total = float(sum(self.out_mapping[source].values())) 62 | for target, weight in self.out_mapping.items(): 63 | self.out_mapping[target] = weight / total 64 | 65 | def right_multiply(self, d): 66 | """ 67 | Multiply by a vector (specified as a dict on the vertices) by 68 | viewing the graph as a sparse matrix, i.e. 69 | return G*d 70 | """ 71 | s = defaultdict(float) 72 | for k in d.keys(): 73 | for k2, v2 in self.out_dict(k).items(): 74 | s[k] += d[k2] * v2 75 | return s 76 | 77 | def left_multiply(self, d): 78 | """ 79 | Multiply by a vector (specified as a dict on the vertices) by 80 | viewing the graph as a sparse matrix, i.e. 81 | return d*G 82 | """ 83 | s = defaultdict(float) 84 | for k in d.keys(): 85 | for k2, v2 in self.in_dict(k).items(): 86 | s[k] += d[k2] * v2 87 | return s 88 | 89 | def __getitem__(self, k): 90 | """Returns the dictionary of outgoing edges. You can access the weight 91 | of an edge with g[source][target].""" 92 | return self.out_mapping[k] 93 | 94 | 95 | class RandomGraph(object): 96 | """ 97 | Random Graph class in which there is a probability p of an edge between any 98 | two vertices. Edge existence is drawn on each request (i.e. not determined 99 | once at initiation). 100 | """ 101 | def __init__(self, num_vertices, p): 102 | self._vertices = list(range(num_vertices)) 103 | self.p = p 104 | 105 | def vertices(self): 106 | return self._vertices 107 | 108 | def out_vertices(self, source): 109 | outs = [] 110 | for v in self._vertices: 111 | q = random.random() 112 | if q <= self.p: 113 | outs.append(v) 114 | return outs 115 | 116 | def in_vertices(self, source): 117 | ins = [] 118 | for v in self._vertices: 119 | q = random.random() 120 | if q <= self.p: 121 | ins.append(v) 122 | return ins 123 | -------------------------------------------------------------------------------- /stationary/utils/heatmap.py: -------------------------------------------------------------------------------- 1 | import csv 2 | import sys 3 | 4 | from matplotlib import pyplot as plt 5 | import numpy 6 | 7 | 8 | def get_cmap(cmap_name=None): 9 | if not cmap_name: 10 | cmap = plt.get_cmap('jet') 11 | else: 12 | cmap = plt.get_cmap(cmap_name) 13 | return cmap 14 | 15 | 16 | def load_csv(filename): 17 | with open(filename) as handle: 18 | reader = csv.reader(handle) 19 | data = [row for row in reader] 20 | return data 21 | 22 | 23 | def prepare_heatmap_data(data, xindex=0, yindex=1, cindex=-1, xfunc=float, 24 | yfunc=float, cfunc=float): 25 | # Grab horizontal and vertical coordinates. 26 | xs = list(sorted(set([xfunc(z[xindex]) for z in data]))) 27 | ys = list(sorted(set([yfunc(z[yindex]) for z in data]))) 28 | # Prepare to map to a grid. 29 | x_d = dict(zip(xs, range(len(xs)))) 30 | y_d = dict(zip(ys, range(len(ys)))) 31 | cs = numpy.zeros(shape=(len(ys), len(xs))) 32 | # Extract relevant data and populate color matrix, mapping to proper 33 | # indices. 34 | for row in data: 35 | x = xfunc(row[xindex]) 36 | y = yfunc(row[yindex]) 37 | c = cfunc(row[cindex]) 38 | cs[y_d[y]][x_d[x]] = c 39 | return xs, ys, cs 40 | 41 | 42 | def heatmap(xs, ys, cs, cmap=None, sep=10, offset=0., rounding=True, 43 | round_digits=3): 44 | if not cmap: 45 | cmap = get_cmap() 46 | plot_obj = plt.pcolor(cs, cmap=cmap) 47 | plt.colorbar() 48 | if rounding: 49 | xs = [round(x, round_digits) for x in xs] 50 | ys = [round(y, round_digits) for y in ys] 51 | xticks = [x + offset for x in range(len(xs))] 52 | yticks = [y + offset for y in range(len(ys))] 53 | 54 | plt.xticks(xticks[::sep], xs[::sep]) 55 | plt.yticks(yticks[sep::sep], ys[sep::sep]) 56 | return plot_obj 57 | 58 | 59 | def heatmap_ax(xs, ys, cs, cmap=None, sep=10, offset=0., rounding=True, 60 | round_digits=3, ax=None): 61 | if not cmap: 62 | cmap = get_cmap() 63 | if not ax: 64 | ax = plt.subplot() 65 | plot_obj = ax.pcolor(numpy.array(cs), cmap=cmap) 66 | fig = ax.get_figure() 67 | fig.colorbar(plot_obj) 68 | if rounding: 69 | xs = [round(x, round_digits) for x in xs] 70 | ys = [round(y, round_digits) for y in ys] 71 | 72 | xticks = [x + offset for x in range(len(xs))] 73 | yticks = [y + offset for y in range(len(ys))] 74 | ax.set_xticks(xticks[::sep], xs[::sep]) 75 | ax.set_yticks(yticks[sep::sep], ys[sep::sep]) 76 | return ax 77 | 78 | 79 | def main(data=None, filename=None, xindex=0, yindex=1, cindex=-1, xfunc=float, 80 | yfunc=float, cfunc=float): 81 | if filename: 82 | data = load_csv(filename) 83 | if (not filename) and (not data): 84 | sys.stderr.write('Data or filename is required for heatmap.\n') 85 | xs, ys, cs = prepare_heatmap_data( 86 | data, xindex=xindex, yindex=yindex, 87 | cindex=cindex, xfunc=xfunc, yfunc=yfunc, cfunc=cfunc) 88 | heatmap(xs, ys, cs) 89 | 90 | 91 | if __name__ == '__main__': 92 | filename = sys.argv[1] 93 | main(filename) 94 | plt.show() 95 | -------------------------------------------------------------------------------- /stationary/utils/math_helpers.py: -------------------------------------------------------------------------------- 1 | import numpy 2 | from numpy import log 3 | 4 | try: 5 | from scipy.misc import logsumexp 6 | except ImportError: 7 | from numpy import logaddexp 8 | logsumexp = logaddexp.reduce 9 | 10 | 11 | def slice_dictionary(d, N, slice_index=0, slice_value=0): 12 | """ 13 | Take a three dimensional slice from a four dimensional 14 | dictionary. 15 | """ 16 | 17 | slice_dict = dict() 18 | for state in simplex_generator(N, 2): 19 | new_state = list(state) 20 | new_state.insert(slice_index, slice_value) 21 | slice_dict[state] = d[tuple(new_state)] 22 | return slice_dict 23 | 24 | 25 | def squared_error(d1, d2): 26 | """ 27 | Compute the squared error between two vectors. 28 | """ 29 | 30 | s = 0. 31 | for k in range(len(d1)): 32 | s += (d1[k] - d2[k])**2 33 | return numpy.sqrt(s) 34 | 35 | 36 | def squared_error_dict(d1, d2): 37 | """ 38 | Compute the squared error between two vectors, stored as dictionaries. 39 | """ 40 | 41 | s = 0. 42 | for k in d1.keys(): 43 | s += (d1[k] - d2[k])**2 44 | return numpy.sqrt(s) 45 | 46 | 47 | def multiply_vectors(a, b): 48 | c = [] 49 | for i in range(len(a)): 50 | c.append(a[i]*b[i]) 51 | return c 52 | 53 | 54 | def dot_product(a, b): 55 | c = 0 56 | for i in range(len(a)): 57 | c += a[i] * b[i] 58 | return c 59 | 60 | 61 | def normalize(x): 62 | s = float(sum(x)) 63 | for j in range(len(x)): 64 | x[j] /= s 65 | return x 66 | 67 | 68 | def normalize_dictionary(x): 69 | s = float(sum(x.values())) 70 | for k in x.keys(): 71 | x[k] /= s 72 | return x 73 | 74 | 75 | def inc_factorial(x, n): 76 | p = 1. 77 | for i in range(0, n): 78 | p *= (x + i) 79 | return p 80 | 81 | 82 | def factorial(i): 83 | p = 1. 84 | for j in range(2, i+1): 85 | p *= j 86 | return p 87 | 88 | 89 | def log_inc_factorial(x,n): 90 | p = 1. 91 | for i in range(0, n): 92 | p += log(x + i) 93 | return p 94 | 95 | 96 | def log_factorial(i): 97 | p = 1. 98 | for j in range(2, i+1): 99 | p += log(j) 100 | return p 101 | 102 | 103 | def simplex_generator(N, d=2): 104 | """ 105 | Generates a discretation of the simplex. 106 | 107 | Parameters 108 | ---------- 109 | N: int 110 | The number of subdivsions in each dimension 111 | d: int, 2 112 | The dimension of the simplex (the number of population types is d+1 113 | 114 | Yields 115 | ------ 116 | (d+1)-tuples of numbers summing to N. The total number of yielded tuples is 117 | equal to the simplicial polytopic number corresponding to N and d, 118 | binom{N + d - 1}{d} (see https://en.wikipedia.org/wiki/Figurate_number ) 119 | """ 120 | 121 | if d == 1: 122 | for i in range(N+1): 123 | yield (i, N - i) 124 | if d > 1: 125 | for j in range(N+1): 126 | for s in simplex_generator(N - j, d - 1): 127 | t = [j] 128 | t.extend(s) 129 | yield tuple(t) 130 | 131 | 132 | def one_step_generator(d): 133 | """ 134 | Generates the arrays needed to construct neighboring states one step away 135 | from a state in the dimension d simplex. 136 | """ 137 | 138 | if d == 1: 139 | yield [1, -1] 140 | yield [-1, 1] 141 | return 142 | for plus_index in range(d + 1): 143 | for minus_index in range(d + 1): 144 | if minus_index == plus_index: 145 | continue 146 | step = [0] * (d + 1) 147 | step[plus_index] = 1 148 | step[minus_index] = -1 149 | yield step 150 | 151 | 152 | def one_step_indicies_generator(d): 153 | """ 154 | Generates the indices that form all the neighboring states, by adding +1 in 155 | one index and -1 in another. 156 | """ 157 | if d == 1: 158 | yield [0, 1] 159 | yield [1, 0] 160 | return 161 | for plus_index in range(d + 1): 162 | for minus_index in range(d + 1): 163 | if minus_index == plus_index: 164 | continue 165 | yield (plus_index, minus_index) 166 | 167 | 168 | def kl_divergence(p, q): 169 | """ 170 | Computes the KL-divergence or relative entropy of to input distributions. 171 | 172 | Parameters 173 | ---------- 174 | p, q: lists 175 | The probability distributions to compute the KL-divergence for 176 | 177 | Returns 178 | ------- 179 | float, the KL-divergence of p and q 180 | """ 181 | 182 | s = 0. 183 | for i in range(len(p)): 184 | if p[i] == 0: 185 | continue 186 | if q[i] == 0: 187 | return float('nan') 188 | try: 189 | s += p[i] * log(p[i]) 190 | except (ValueError, ZeroDivisionError): 191 | continue 192 | try: 193 | s -= p[i] * log(q[i]) 194 | except (ValueError, ZeroDivisionError): 195 | continue 196 | return s 197 | 198 | 199 | def kl_divergence_dict(p, q): 200 | """ 201 | Computes the KL-divergence of distributions given as dictionaries. 202 | """ 203 | s = 0. 204 | 205 | p_list = [] 206 | q_list = [] 207 | 208 | for i in p.keys(): 209 | p_list.append(p[i]) 210 | q_list.append(q[i]) 211 | return kl_divergence(p_list, q_list) 212 | 213 | 214 | def q_divergence(q): 215 | """ 216 | Returns the divergence function corresponding to the parameter value q. For 217 | q == 0 this function is one-half the squared Euclidean distance. For q == 1 218 | this function returns the KL-divergence. 219 | """ 220 | 221 | if q == 0: 222 | def d(x, y): 223 | return 0.5 * numpy.dot((x - y), (x - y)) 224 | return d 225 | if q == 1: 226 | return kl_divergence 227 | if q == 2: 228 | def d(x, y): 229 | s = 0. 230 | for i in range(len(x)): 231 | s += log(x[i] / y[i]) + 1 - x[i] / y[i] 232 | return -s 233 | return d 234 | q = float(q) 235 | 236 | def d(x, y): 237 | s = 0. 238 | for i in range(len(x)): 239 | s += (numpy.power(y[i], 2 - q) - numpy.power(x[i], 2 - q)) / (2 - q) 240 | s -= numpy.power(y[i], 1 - q) * (y[i] - x[i]) 241 | s = -s / (1 - q) 242 | return s 243 | return d 244 | 245 | 246 | def shannon_entropy(p): 247 | s = 0. 248 | for i in range(len(p)): 249 | try: 250 | s += p[i] * log(p[i]) 251 | except ValueError: 252 | continue 253 | return -1. * s 254 | 255 | 256 | def binary_entropy(p): 257 | return -p * log(p) - (1 - p) * log(1 - p) 258 | -------------------------------------------------------------------------------- /stationary/utils/matrix_checks.py: -------------------------------------------------------------------------------- 1 | from .math_helpers import kl_divergence_dict 2 | from .graph import Graph 3 | from .edges import edges_to_edge_dict 4 | 5 | from nose.tools import assert_almost_equal 6 | 7 | 8 | def check_detailed_balance(edges, s, places=7): 9 | """ 10 | Check if the detailed balance condition is satisfied. 11 | 12 | Parameters 13 | ---------- 14 | edges: list of tuples 15 | transitions of the Markov process 16 | s: dict 17 | the stationary distribution 18 | places: int 19 | Decimal places of precision to require 20 | """ 21 | 22 | edge_dict = edges_to_edge_dict(edges) 23 | for s1, s2 in edge_dict.keys(): 24 | diff = s[s1] * edge_dict[(s1, s2)] - s[s2] * edge_dict[(s2, s1)] 25 | assert_almost_equal(diff, 0, places=places) 26 | 27 | 28 | def check_global_balance(edges, stationary, places=7): 29 | """ 30 | Checks that the stationary distribution satisfies the global balance 31 | condition. https://en.wikipedia.org/wiki/Balance_equation 32 | 33 | Parameters 34 | ---------- 35 | edges: list of tuples 36 | transitions of the Markov process 37 | stationary: dict 38 | the stationary distribution 39 | places: int 40 | Decimal places of precision to require 41 | """ 42 | 43 | g = Graph(edges) 44 | 45 | for s1 in g.vertices(): 46 | lhs = 0. 47 | rhs = 0. 48 | for s2, v in g.out_dict(s1).items(): 49 | if s1 == s2: 50 | continue 51 | lhs += stationary[s1] * v 52 | for s2, v in g.in_dict(s1).items(): 53 | if s1 == s2: 54 | continue 55 | rhs += stationary[s2] * v 56 | assert_almost_equal(lhs, rhs, places=places) 57 | 58 | 59 | def check_eigenvalue(edges, s, places=3): 60 | """ 61 | Check that the stationary distribution satisfies the eigenvalue condition. 62 | 63 | Parameters 64 | ---------- 65 | edges: list of tuples 66 | transitions of the Markov process 67 | s: dict 68 | the stationary distribution 69 | places: int 70 | Decimal places of precision to require 71 | """ 72 | 73 | g = Graph(edges) 74 | t = g.left_multiply(s) 75 | assert_almost_equal(kl_divergence_dict(s, t), 0, places=places) 76 | -------------------------------------------------------------------------------- /stationary/utils/plotting.py: -------------------------------------------------------------------------------- 1 | from matplotlib import pyplot 2 | import ternary 3 | 4 | 5 | def plot_dictionary(s, ax=None): 6 | """ 7 | Plot two or three dimensional dictionary on a simplex partition, such as a 8 | stationary distribution. 9 | """ 10 | 11 | num_types = len(s.keys()[0]) 12 | N = sum(s.keys()[0]) 13 | 14 | if not ax: 15 | fig, ax = pyplot.subplots() 16 | 17 | if num_types == 2: 18 | domain = list(range(0, N+1)) 19 | values = [] 20 | for i in domain: 21 | state = (i, N-i) 22 | values.append(s[state]) 23 | if not ax: 24 | fig, ax = pyplot.subplots() 25 | pyplot.plot(domain, values) 26 | 27 | if num_types == 3: 28 | fig, tax = ternary.figure(scale=N, ax=ax) 29 | tax.heatmap(s, style='d') 30 | -------------------------------------------------------------------------------- /tests/test_bomze.py: -------------------------------------------------------------------------------- 1 | 2 | from nose.tools import assert_almost_equal, assert_equal, assert_raises, assert_true, assert_less_equal, assert_greater_equal, assert_greater 3 | 4 | from stationary.utils.bomze import bomze_matrices 5 | 6 | def test_bomze_matrices(): 7 | """ 8 | Check that the data file with the Bomze classification matrices is present 9 | and loads the correct number of matrices. 10 | """ 11 | 12 | matrices = list(bomze_matrices()) 13 | assert_equal(len(matrices), 49) 14 | -------------------------------------------------------------------------------- /tests/test_edges.py: -------------------------------------------------------------------------------- 1 | 2 | from nose.tools import assert_almost_equal, assert_equal, assert_raises, assert_true, assert_less_equal, assert_greater_equal, assert_greater 3 | 4 | from stationary.utils.edges import * 5 | 6 | 7 | def test_edge_functions(): 8 | """ 9 | """ 10 | 11 | edges = [(0, 0, 1./3), (0, 1, 1./3), (0, 2, 1./3), 12 | (1, 0, 1./4), (1, 1, 1./2), (1, 2, 1./4), 13 | (2, 0, 1./6), (2, 1, 1./3), (2, 2, 1./2),] 14 | edges.sort() 15 | 16 | states = states_from_edges(edges) 17 | assert_equal(states, set([0, 1, 2])) 18 | -------------------------------------------------------------------------------- /tests/test_math_helpers.py: -------------------------------------------------------------------------------- 1 | 2 | from scipy.misc import comb 3 | 4 | from nose.tools import assert_almost_equal, assert_equal, assert_raises, assert_true, assert_less_equal, assert_greater_equal, assert_greater 5 | 6 | from stationary.utils.math_helpers import simplex_generator 7 | 8 | def test_stationary_generator(): 9 | d = 1 10 | N = 1 11 | states = set(simplex_generator(N, d)) 12 | expected = set([(0, 1), (1, 0)]) 13 | assert_equal(states, expected) 14 | 15 | N = 2 16 | states = set(simplex_generator(N, d)) 17 | expected = set([(0, 2), (1, 1), (2, 0)]) 18 | assert_equal(states, expected) 19 | 20 | N = 3 21 | states = set(simplex_generator(N, d)) 22 | expected = set([(0, 3), (1, 2), (2, 1), (3, 0)]) 23 | assert_equal(states, expected) 24 | 25 | d = 2 26 | N = 1 27 | states = set(simplex_generator(N, d)) 28 | expected = set([(1, 0, 0), (0, 1, 0), (0, 0, 1)]) 29 | assert_equal(states, expected) 30 | 31 | N = 2 32 | states = set(simplex_generator(N, d)) 33 | expected = set([(1, 1, 0), (0, 1, 1), (1, 0, 1), (0, 2, 0), (0, 0, 2), 34 | (2, 0, 0)]) 35 | assert_equal(states, expected) 36 | 37 | for d in range(1, 5): 38 | for N in range(1, 20): 39 | states = set(simplex_generator(N, d)) 40 | size = comb(N + d, d, exact=True) 41 | assert_equal(len(states), size) 42 | -------------------------------------------------------------------------------- /tests/test_stationary.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import numpy 4 | 5 | from nose.tools import ( 6 | assert_almost_equal, assert_equal, assert_raises, assert_true, 7 | assert_less_equal, assert_greater_equal, assert_greater 8 | ) 9 | 10 | from stationary import stationary_distribution, entropy_rate 11 | from stationary.processes import incentive_process, wright_fisher 12 | from stationary.processes.incentives import ( 13 | replicator, logit, fermi, linear_fitness_landscape) 14 | from stationary.utils.matrix_checks import ( 15 | check_detailed_balance, check_global_balance, check_eigenvalue) 16 | 17 | from stationary.utils import expected_divergence 18 | from stationary.utils.math_helpers import simplex_generator 19 | from stationary.utils.edges import ( 20 | states_from_edges, edge_func_to_edges, power_transitions) 21 | from stationary.utils.extrema import ( 22 | find_local_minima, find_local_maxima, inflow_outflow) 23 | 24 | 25 | # Test Generic processes 26 | 27 | def test_stationary(t1=0.4, t2=0.6): 28 | """ 29 | Test the stationary distribution computations a simple Markov process. 30 | """ 31 | 32 | edges = [(0, 1, t1), (0, 0, 1. - t1), (1, 0, t2), (1, 1, 1. - t2)] 33 | s_0 = 1./(1. + t1 / t2) 34 | exact_stationary = {0: s_0, 1: 1 - s_0} 35 | 36 | for logspace in [True, False]: 37 | s = stationary_distribution(edges, logspace=logspace) 38 | # Check that the stationary distribution satisfies balance conditions 39 | check_detailed_balance(edges, s) 40 | check_global_balance(edges, s) 41 | check_eigenvalue(edges, s) 42 | # Check that the approximation converged to the exact distribution 43 | for key in s.keys(): 44 | assert_almost_equal(exact_stationary[key], s[key]) 45 | 46 | 47 | def test_stationary_2(): 48 | """ 49 | Test the stationary distribution computations a simple Markov process. 50 | """ 51 | 52 | edges = [(0, 0, 1./3), (0, 1, 1./3), (0, 2, 1./3), 53 | (1, 0, 1./4), (1, 1, 1./2), (1, 2, 1./4), 54 | (2, 0, 1./6), (2, 1, 1./3), (2, 2, 1./2),] 55 | exact_stationary = {0: 6./25, 1: 10./25, 2:9./25} 56 | 57 | for logspace in [True, False]: 58 | s = stationary_distribution(edges, logspace=logspace) 59 | # Check that the stationary distribution satisfies balance conditions 60 | check_global_balance(edges, s) 61 | check_eigenvalue(edges, s) 62 | # Check that the approximation converged to the exact distribution 63 | for key in s.keys(): 64 | assert_almost_equal(exact_stationary[key], s[key]) 65 | 66 | 67 | def test_stationary_3(): 68 | """ 69 | Test the stationary distribution computations a simple Markov process. 70 | """ 71 | 72 | edges = [(0, 0, 0), (0, 1, 1), (0, 2, 0), (0, 3, 0), 73 | (1, 0, 1./3), (1, 1, 0), (1, 2, 2./3), (1, 3, 0), 74 | (2, 0, 0), (2, 1, 2./3), (2, 2, 0), (2, 3, 1./3), 75 | (3, 0, 0), (3, 1, 0), (3, 2, 1), (3, 3, 0)] 76 | exact_stationary = {0: 1./8, 1: 3./8, 2: 3./8, 3: 1./8} 77 | 78 | for logspace in [True, False]: 79 | s = stationary_distribution(edges, logspace=logspace) 80 | # Check that the stationary distribution satisfies balance conditions 81 | check_detailed_balance(edges, s) 82 | check_global_balance(edges, s) 83 | check_eigenvalue(edges, s) 84 | # Check that the approximation converged to the exact distribution 85 | for key in s.keys(): 86 | assert_almost_equal(exact_stationary[key], s[key]) 87 | 88 | 89 | def test_stationary_4(): 90 | """ 91 | Test the stationary distribution computations a simple Markov process. 92 | """ 93 | 94 | edges = [(0, 0, 1./2), (0, 1, 1./2), (0, 2, 0), (0, 3, 0), 95 | (1, 0, 1./6), (1, 1, 1./2), (1, 2, 1./3), (1, 3, 0), 96 | (2, 0, 0), (2, 1, 1./3), (2, 2, 1./2), (2, 3, 1./6), 97 | (3, 0, 0), (3, 1, 0), (3, 2, 1./2), (3, 3, 1./2)] 98 | exact_stationary = {0: 1./8, 1: 3./8, 2: 3./8, 3: 1./8} 99 | 100 | for logspace in [True, False]: 101 | s = stationary_distribution(edges, logspace=logspace) 102 | # Check that the stationary distribution satisfies balance conditions 103 | check_detailed_balance(edges, s) 104 | check_global_balance(edges, s) 105 | check_eigenvalue(edges, s) 106 | # Check that the approximation converged to the exact distribution 107 | for key in s.keys(): 108 | assert_almost_equal(exact_stationary[key], s[key]) 109 | 110 | ## Test Moran / Incentive Processes 111 | 112 | 113 | def test_incentive_process(lim=1e-14): 114 | """ 115 | Compare stationary distribution computations to known analytic form for 116 | neutral landscape for the Moran process. 117 | """ 118 | 119 | for n, N in [(2, 10), (2, 40), (3, 10), (3, 20), (4, 10)]: 120 | mu = (n - 1.) / n * 1./ (N + 1) 121 | alpha = N * mu / (n - 1. - n * mu) 122 | 123 | # Neutral landscape is the default 124 | edges = incentive_process.compute_edges(N, num_types=n, 125 | incentive_func=replicator, mu=mu) 126 | for logspace in [False, True]: 127 | stationary_1 = incentive_process.neutral_stationary( 128 | N, alpha, n, logspace=logspace) 129 | for exact in [False, True]: 130 | stationary_2 = stationary_distribution( 131 | edges, lim=lim, logspace=logspace, exact=exact) 132 | for key in stationary_1.keys(): 133 | assert_almost_equal( 134 | stationary_1[key], stationary_2[key], places=4) 135 | 136 | # Check that the stationary distribution satisfies balance conditions 137 | check_detailed_balance(edges, stationary_1) 138 | check_global_balance(edges, stationary_1) 139 | check_eigenvalue(edges, stationary_1) 140 | 141 | # Test Entropy Rate bounds 142 | er = entropy_rate(edges, stationary_1) 143 | h = (2. * n - 1) / n * numpy.log(n) 144 | assert_less_equal(er, h) 145 | assert_greater_equal(er, 0) 146 | 147 | 148 | def test_incentive_process_k(lim=1e-14): 149 | """ 150 | Compare stationary distribution computations to known analytic form for 151 | neutral landscape for the Moran process. 152 | """ 153 | for k in [1, 2, 10,]: 154 | for n, N in [(2, 20), (2, 50), (3, 10), (3, 20)]: 155 | mu = (n-1.)/n * 1./(N+1) 156 | m = numpy.ones((n, n)) # neutral landscape 157 | fitness_landscape = linear_fitness_landscape(m) 158 | incentive = replicator(fitness_landscape) 159 | 160 | # Neutral landscape is the default 161 | edges = incentive_process.k_fold_incentive_transitions( 162 | N, incentive, num_types=n, mu=mu, k=k) 163 | stationary_1 = stationary_distribution(edges, lim=lim) 164 | 165 | # Check that the stationary distribution satisfies balance 166 | # conditions 167 | check_detailed_balance(edges, stationary_1) 168 | check_global_balance(edges, stationary_1) 169 | check_eigenvalue(edges, stationary_1) 170 | 171 | # Also check edge_func calculation 172 | edges = incentive_process.multivariate_transitions( 173 | N, incentive, num_types=n, mu=mu) 174 | states = states_from_edges(edges) 175 | edge_func = power_transitions(edges, k) 176 | stationary_2 = stationary_distribution( 177 | edge_func, states=states, lim=lim) 178 | 179 | for key in stationary_1.keys(): 180 | assert_almost_equal( 181 | stationary_1[key], stationary_2[key], places=5) 182 | 183 | 184 | def test_extrema_moran(lim=1e-16): 185 | """ 186 | Test for extrema of the stationary distribution. 187 | """ 188 | n = 2 189 | for N, maxes, mins in [(60, [(30, 30)], [(60, 0), (0, 60)]), 190 | (100, [(50, 50)], [(100, 0), (0, 100)])]: 191 | mu = 1. / N 192 | edges = incentive_process.compute_edges(N, num_types=n, 193 | incentive_func=replicator, mu=mu) 194 | 195 | s = stationary_distribution(edges, lim=lim) 196 | assert_equal(find_local_maxima(s), set(maxes)) 197 | assert_equal(find_local_minima(s), set(mins)) 198 | 199 | 200 | def test_extrema_moran_2(lim=1e-16): 201 | """ 202 | Test for extrema of the stationary distribution. 203 | """ 204 | n = 2 205 | N = 100 206 | mu = 1. / 1000 207 | m = [[1, 2], [3, 1]] 208 | maxes = set([(33, 67), (100,0), (0, 100)]) 209 | fitness_landscape = linear_fitness_landscape(m) 210 | incentive = replicator(fitness_landscape) 211 | edges = incentive_process.multivariate_transitions(N, incentive, num_types=n, mu=mu) 212 | s = stationary_distribution(edges, lim=lim) 213 | s2 = expected_divergence(edges, q_d=0) 214 | 215 | assert_equal(find_local_maxima(s), set(maxes)) 216 | assert_equal(find_local_minima(s2), set(maxes)) 217 | 218 | 219 | def test_extrema_moran_3(lim=1e-12): 220 | """ 221 | Test for extrema of the stationary distribution. 222 | """ 223 | n = 2 224 | N = 100 225 | mu = 6./ 25 226 | m = [[1, 0], [0, 1]] 227 | maxes = set([(38, 62), (62, 38)]) 228 | mins = set([(50, 50), (100, 0), (0, 100)]) 229 | fitness_landscape = linear_fitness_landscape(m) 230 | incentive = replicator(fitness_landscape) 231 | edges = incentive_process.multivariate_transitions(N, incentive, num_types=n, mu=mu) 232 | s = stationary_distribution(edges, lim=lim) 233 | flow = inflow_outflow(edges) 234 | 235 | for q_d in [0, 1]: 236 | s2 = expected_divergence(edges, q_d=1) 237 | assert_equal(find_local_maxima(s), set(maxes)) 238 | assert_equal(find_local_minima(s), set(mins)) 239 | assert_equal(find_local_minima(s2), set([(50,50), (40, 60), (60, 40)])) 240 | assert_equal(find_local_maxima(flow), set(mins)) 241 | 242 | 243 | def test_extrema_moran_4(lim=1e-16): 244 | """ 245 | Test for extrema of the stationary distribution. 246 | """ 247 | n = 3 248 | N = 60 249 | mu = 3./ (2 * N) 250 | m = [[0, 1, 1], [1, 0, 1], [1, 1, 0]] 251 | maxes = set([(20,20,20)]) 252 | mins = set([(0, 0, 60), (0, 60, 0), (60, 0, 0)]) 253 | fitness_landscape = linear_fitness_landscape(m) 254 | incentive = logit(fitness_landscape, beta=0.1) 255 | edges = incentive_process.multivariate_transitions(N, incentive, num_types=n, mu=mu) 256 | s = stationary_distribution(edges, lim=lim) 257 | s2 = expected_divergence(edges, q_d=0) 258 | 259 | assert_equal(find_local_maxima(s), set(maxes)) 260 | assert_equal(find_local_minima(s), set(mins)) 261 | assert_equal(find_local_minima(s2), set(maxes)) 262 | assert_equal(find_local_maxima(s2), set(mins)) 263 | 264 | 265 | def test_extrema_moran_5(lim=1e-16): 266 | """ 267 | Test for extrema of the stationary distribution. 268 | """ 269 | n = 3 270 | N = 60 271 | mu = (3./2) * 1./N 272 | m = [[0, 1, 1], [1, 0, 1], [1, 1, 0]] 273 | maxes = set([(20, 20, 20), (0, 0, 60), (0, 60, 0), (60, 0, 0), 274 | (30, 0, 30), (0, 30, 30), (30, 30, 0)]) 275 | fitness_landscape = linear_fitness_landscape(m) 276 | incentive = fermi(fitness_landscape, beta=0.1) 277 | edges = incentive_process.multivariate_transitions( 278 | N, incentive, num_types=n, mu=mu) 279 | 280 | s = stationary_distribution(edges, lim=lim) 281 | s2 = expected_divergence(edges, q_d=0) 282 | flow = inflow_outflow(edges) 283 | 284 | # These sets should all correspond 285 | assert_equal(find_local_maxima(s), set(maxes)) 286 | assert_equal(find_local_minima(s2), set(maxes)) 287 | assert_equal(find_local_minima(flow), set(maxes)) 288 | 289 | # The minima are pathological 290 | assert_equal(find_local_minima(s), 291 | set([(3, 3, 54), (3, 54, 3), (54, 3, 3)])) 292 | assert_equal(find_local_maxima(s2), 293 | set([(4, 52, 4), (4, 4, 52), (52, 4, 4)])) 294 | assert_equal(find_local_maxima(flow), 295 | set([(1, 58, 1), (1, 1, 58), (58, 1, 1)])) 296 | 297 | 298 | def test_wright_fisher(N=20, lim=1e-10, n=2): 299 | """Test 2 dimensional Wright-Fisher process.""" 300 | for n in [2, 3]: 301 | mu = (n - 1.) / n * 1. / (N + 1) 302 | m = numpy.ones((n, n)) # neutral landscape 303 | fitness_landscape = linear_fitness_landscape(m) 304 | incentive = replicator(fitness_landscape) 305 | 306 | # Wright-Fisher 307 | for low_memory in [True, False]: 308 | edge_func = wright_fisher.multivariate_transitions( 309 | N, incentive, mu=mu, num_types=n, low_memory=low_memory) 310 | states = list(simplex_generator(N, d=n-1)) 311 | for logspace in [False, True]: 312 | s = stationary_distribution( 313 | edge_func, states=states, iterations=200, lim=lim, 314 | logspace=logspace) 315 | wf_edges = edge_func_to_edges(edge_func, states) 316 | 317 | er = entropy_rate(wf_edges, s) 318 | assert_greater_equal(er, 0) 319 | 320 | # Check that the stationary distribution satistifies balance 321 | # conditions 322 | check_detailed_balance(wf_edges, s, places=2) 323 | check_global_balance(wf_edges, s, places=4) 324 | check_eigenvalue(wf_edges, s, places=2) 325 | 326 | 327 | def test_extrema_wf(lim=1e-10): 328 | """ 329 | For small mu, the Wright-Fisher process is minimal in the center. 330 | Test that this happens. 331 | """ 332 | 333 | for n, N, mins in [(2, 40, [(20, 20)]), (3, 30, [(10, 10, 10)])]: 334 | mu = 1. / N ** 3 335 | m = numpy.ones((n, n)) # neutral landscape 336 | fitness_landscape = linear_fitness_landscape(m) 337 | incentive = replicator(fitness_landscape) 338 | 339 | edge_func = wright_fisher.multivariate_transitions( 340 | N, incentive, mu=mu, num_types=n) 341 | states = list(simplex_generator(N, d=n-1)) 342 | s = stationary_distribution( 343 | edge_func, states=states, iterations=4*N, lim=lim) 344 | assert_equal(find_local_minima(s), set(mins)) 345 | er = entropy_rate(edge_func, s, states=states) 346 | assert_greater_equal(er, 0) 347 | 348 | --------------------------------------------------------------------------------