codexreplicator-coordination.py
replicator-coordination.py
"""Toy replicator coordination figure; no measured data.
Tested with Python 3.14.4, NumPy 2.3.5, Matplotlib 3.10.7+dfsg1.
Run from _media: MPLCONFIGDIR=/tmp/codex-wiki-matplotlib python ../replicator-coordination.py
The matching PNG is written to the current working directory.
"""
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
def trajectory(x0, times):
"""Fourth-order Runge–Kutta integration of the stated scalar model."""
values = np.empty_like(times)
values[0] = x0
def rhs(x):
return x * (1 - x) * (5 * x - 2)
for k, dt in enumerate(np.diff(times)):
x = values[k]
a = rhs(x)
b = rhs(x + dt * a / 2)
c = rhs(x + dt * b / 2)
d = rhs(x + dt * c)
values[k + 1] = x + dt * (a + 2 * b + 2 * c + d) / 6
return values
def main():
plt.rcParams.update({"font.size": 11, "axes.spines.top": False,
"axes.spines.right": False, "figure.facecolor": "white",
"axes.facecolor": "white", "savefig.facecolor": "white"})
fig, (payoff, paths) = plt.subplots(1, 2, figsize=(9.4, 4.4))
fig.suptitle("Coordination: a threshold separates two outcomes", fontsize=16, y=0.97)
x = np.linspace(0, 1, 501)
payoff.plot(x, 3 * x, color="#2364a0", lw=2.5, label=r"$f_1=3x$")
payoff.plot(x, 2 * (1 - x), color="#be571c", lw=2.5, label=r"$f_2=2(1-x)$")
payoff.axvline(0.4, color="#777777", ls="--", lw=1)
payoff.scatter([0.4], [1.2], color="#333333", zorder=5)
payoff.text(0.43, 1.35, r"$x_*=2/5$", fontsize=12)
payoff.set(xlim=(-0.04, 1.04), ylim=(-0.68, 3.2), xlabel="Frequency x of convention 1",
ylabel="Expected payoff", title=r"Toy payoff table: $A=[(3,0),(0,2)]$")
payoff.axhline(-0.35, color="#444444", lw=1)
payoff.annotate("", xy=(0.07, -0.35), xytext=(0.31, -0.35),
arrowprops={"arrowstyle": "->", "color": "#333333", "lw": 2})
payoff.annotate("", xy=(0.91, -0.35), xytext=(0.54, -0.35),
arrowprops={"arrowstyle": "->", "color": "#333333", "lw": 2})
payoff.scatter([0, 1], [-0.35, -0.35], color="#333333", zorder=5)
payoff.scatter([0.4], [-0.35], facecolors="white", edgecolors="#333333", zorder=5)
payoff.legend(loc="upper center", frameon=False)
times = np.linspace(0, 5, 2001)
for x0 in [0.08, 0.25, 0.38, 0.42, 0.65, 0.92]:
color = "#be571c" if x0 < 0.4 else "#2364a0"
paths.plot(times, trajectory(x0, times), color=color, lw=1.8)
paths.axhline(0.4, color="#777777", ls="--", lw=1.4)
paths.text(1.85, 0.43, "Exactly at 2/5: stationary", color="#555555", fontsize=10)
paths.text(2.35, 0.91, "Above 2/5: to x = 1", color="#2364a0", fontsize=10)
paths.text(2.35, 0.09, "Below 2/5: to x = 0", color="#be571c", fontsize=10)
paths.set(xlim=(0, 5), ylim=(-0.04, 1.04), xlabel="Time (model units)",
ylabel="Frequency x", title=r"$\dot{x}=x(1-x)(5x-2)$")
paths.grid(alpha=0.16)
fig.text(0.5, 0.02, "Toy model, not measured data. Phase line: filled = attracting; open = repelling.",
ha="center", fontsize=10, color="#444444")
fig.subplots_adjust(left=0.08, right=0.98, bottom=0.18, top=0.79, wspace=0.3)
fig.savefig(Path(__file__).with_suffix(".png").name, dpi=120, transparent=False)
plt.close(fig)
if __name__ == "__main__":
main()