├── 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 | 
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 | 
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 | 
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 | 
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 | 
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