New Fig. 1 (experimental-programme schematic); Table 2 to SI; figures in citation order; Fig. 2B legible labels

Replaces the results table with a pipeline figure: five questions x three
architecture tiers (exact Wright-Fisher simulator, trained networks, language
models), filled cells naming the experiments, dashed cells the honest gaps.
Table 1 (the dictionary) stays; Table 2 moves to SI Appendix Table S2. The
renumber surfaced a pre-existing citation-order violation (the LLM figure was
cited in the recombination section before Figs. 3-6), so figures are renumbered
to strict first-citation order (LLM tier is now Fig. 3). Fig. 2B: the montage's
baked-in raster labels are cropped away and replaced with vector row numbers
under a rotated "generation" header.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BkRLcc18rwT2Lysu6PbG7v
This commit is contained in:
Giorgio Gilestro 2026-09-07 11:12:33 +01:00
parent 073fc33509
commit 0159e2839a
13 changed files with 208 additions and 109 deletions

View file

@ -1,7 +1,7 @@
"""Publication figures for the PNAS draft — unified, lettered, codename-free.
Re-plots every panel directly from the committed results artifacts into six single-file figures
(figs/fig1.pdf .. fig6.pdf): no experiment codenames, no suptitles, no per-panel headline titles
Renders fig1 (the experimental-programme schematic) and re-plots every data panel directly from the
committed results artifacts (figs/fig2.pdf .. fig7.pdf): no experiment codenames, no suptitles, no per-panel headline titles
(interpretation lives in the captions), bold panel letters, one consistent style. The per-experiment
figures under results/ remain the exploratory versions; these are the manuscript's.
@ -42,8 +42,93 @@ def save(fig, name):
print("wrote", OUT / f"{name}.pdf")
# ---------------------------------------------------------------- fig 1: grounding + MNIST
# ---------------------------------------------------------------- fig 1: experimental programme
def fig1():
from matplotlib.patches import FancyBboxPatch
TIERS = [
("Exact model", "Wright\u2013Fisher simulator (NumPy)", "closed forms \u00b7 bitwise-reproducible",
"#4292c6", "#eaf2fa"),
("Trained networks", "RNN \u00b7 MLP \u00b7 VAE on a synthetic oracle;\nconvolutional VAE on MNIST",
"sign-level tests \u00b7 exact oracles", "#41ab5d", "#edf8ea"),
("Language models", "LoRA specialists on Qwen 0.5B & 7B;\nexact-match verifier",
"seed-replicated signs", "#e6550d", "#fdf0e6"),
]
ROWS = [
("Grounding", "how much real data?",
["immigration\u2013drift equilibrium:\n$g \\approx 0.05$ retains $\\geq$95% diversity;\nobservation floor $1-e^{-mp}$",
"collapse & rescue in every\narchitecture; MNIST: dry 30$\\to$1 modes,\n10% grounding holds 30/30;\nestimator-bias learning kernel",
None]),
("Recombination", "blend or merge?",
["blending conservation law\n(first-order cancellation);\nunion-operator gain; Fisher\u2013Muller",
"merge rescues two forgetting\nspecialists ($\\approx$0.50 $\\to$ 0.955)",
"merged specialists beat every parent\n(5 seeds at 0.5B; 7B); routing vs\naveraging: the headroom rule"]),
("Entangled skills", "who merges with whom?",
["NK landscapes: outbreeding\ndepression; directed sex restores\nthe gain; mate-pool breadth optimum",
None,
"bred-and-screened offspring beat\nthe blind blend in every seed\n(hard, unsaturated tasks)"]),
("The composed society", "can the loop sustain itself?",
["four-arm ablation: grounding, sex,\ndiversity each removed\n$\\to$ three distinct failures",
None,
"OPEN"]),
("Speciation & prediction", "when does merging fail?",
["BDM incompatibility model:\nisolation cliff; quadratic snowball",
"barrier decomposition under\npermutation+rescaling; conflict\nsweep 0.97$\\to$0.03; emergent null",
"convention conflict $\\to$ hybrid\nbreakdown; duration null; pre-merge\npredictive test (13 cond. $\\times$ 3 seeds)"]),
]
fig, ax = plt.subplots(figsize=(11.4, 5.4))
ax.set_axis_off()
ax.set_xlim(0, 1)
ax.set_ylim(0, 1)
x0, gap = 0.16, 0.008
cw = (1.0 - x0) / 3
row_h, row_top = 0.152, 0.79
ax.annotate("", xy=(0.995, 0.975), xytext=(x0 + 0.02, 0.975),
arrowprops=dict(arrowstyle="->", color="#555", lw=1.1))
ax.text(x0 + (1 - x0) / 2, 0.988, "the same population-genetic abstractions (Table 1), increasing realism",
ha="center", va="bottom", fontsize=8, style="italic", color="#333")
for j, (name, arch, guarantee, edge, face) in enumerate(TIERS):
x = x0 + j * cw
ax.add_patch(FancyBboxPatch((x + gap, 0.795), cw - 2 * gap, 0.16,
boxstyle="round,pad=0.004", fc=face, ec=edge, lw=1.4))
ax.text(x + cw / 2, 0.944, name, ha="center", va="top", fontsize=9, fontweight="bold", color=edge)
ax.text(x + cw / 2, 0.902, arch, ha="center", va="top", fontsize=6.8, linespacing=1.3)
ax.text(x + cw / 2, 0.803, guarantee, ha="center", va="bottom", fontsize=6.4,
style="italic", color="#555")
for i, (label, question, cells) in enumerate(ROWS):
y1 = row_top - i * row_h
y0 = y1 - row_h + 2 * gap
yc = (y0 + y1) / 2
ax.text(0.0, yc + 0.012, label, ha="left", va="center", fontsize=8, fontweight="bold")
ax.text(0.0, yc - 0.022, question, ha="left", va="center", fontsize=6.8, style="italic", color="#555")
for j, cell in enumerate(cells):
x = x0 + j * cw
edge, face = TIERS[j][3], TIERS[j][4]
if cell is None:
ax.add_patch(FancyBboxPatch((x + gap, y0), cw - 2 * gap, y1 - y0,
boxstyle="round,pad=0.004", fc="white", ec="#bbbbbb",
lw=0.8, ls=(0, (3, 2))))
ax.text(x + cw / 2, yc, "not tested at this tier", ha="center", va="center",
fontsize=6.4, style="italic", color="#999")
elif cell == "OPEN":
ax.add_patch(FancyBboxPatch((x + gap, y0), cw - 2 * gap, y1 - y0,
boxstyle="round,pad=0.004", fc="white", ec=edge,
lw=0.8, ls=(0, (3, 2))))
ax.text(x + cw / 2, yc, "open \u2014 the stated gap", ha="center", va="center",
fontsize=6.6, style="italic", color=edge)
else:
ax.add_patch(FancyBboxPatch((x + gap, y0), cw - 2 * gap, y1 - y0,
boxstyle="round,pad=0.004", fc=face, ec=edge, lw=0.9))
ax.text(x + cw / 2, yc, cell, ha="center", va="center", fontsize=6.4, linespacing=1.35)
save(fig, "fig1")
# ---------------------------------------------------------------- fig 2: grounding + MNIST
def fig2():
from knowledge.analysis import critical_grounding, reduce_to_stationary
from knowledge.metrics import heterozygosity
from knowledge.truth import make_true_distribution
@ -84,15 +169,21 @@ def fig1():
ax = axes[1]
from PIL import Image
im = np.asarray(Image.open("results/mnist_collapse/mnist_montage.png"))
crop = int(im.shape[0] * 0.085) # remove the baked-in title band
ax.imshow(im[crop:], interpolation="bilinear")
# Strip the baked-in title band and left label margin (raster text is unreadable at panel
# size); measured on the committed montage: boxes span y >= 69, x >= 75, row centres below.
top, left = 60, 68
ax.imshow(im[top:, left:], interpolation="bilinear")
for yc, g in zip((101.5, 191.5, 282.0, 372.5, 462.5), (0, 4, 8, 12, 15)):
ax.text(-10, yc - top, str(g), ha="right", va="center", fontsize=8.5)
ax.text(-0.055, 0.5, "generation", transform=ax.transAxes, rotation=90,
ha="center", va="center", fontsize=8.5)
ax.set_axis_off()
letter(ax, "B", x=-0.02)
save(fig, "fig1")
save(fig, "fig2")
# ---------------------------------------------------------------- fig 2: blending vs union + FisherMuller
def fig2():
# ---------------------------------------------------------------- fig 4: blending vs union + FisherMuller
def fig4():
fig, axes = plt.subplots(1, 2, figsize=(10.6, 3.5))
df, _ = load_bundle("results/E4")
@ -122,11 +213,11 @@ def fig2():
ax.set(xlabel="number of parents", ylabel="offspring capability")
ax.legend()
letter(ax, "B")
save(fig, "fig2")
save(fig, "fig4")
# ---------------------------------------------------------------- fig 3: rugged landscapes
def fig3():
# ---------------------------------------------------------------- fig 5: rugged landscapes
def fig5():
fig, axes = plt.subplots(2, 2, figsize=(10.6, 6.8))
df9, _ = load_bundle("results/E9")
@ -173,11 +264,11 @@ def fig3():
ax.set(xlabel="mate-pool breadth (monogamous → panmictic)", ylabel=ylab)
ax.legend(title="ruggedness")
letter(ax, L)
save(fig, "fig3")
save(fig, "fig5")
# ---------------------------------------------------------------- fig 4: the society
def fig4():
# ---------------------------------------------------------------- fig 6: the society
def fig6():
df, _ = load_bundle("results/E11")
arms = [("full", "#2ca02c", "full system"),
("no_sex", "#ff7f0e", "no recombination"),
@ -201,11 +292,11 @@ def fig4():
ax.legend()
ax.set(xlabel="generation", ylabel=ylab)
letter(ax, L)
save(fig, "fig4")
save(fig, "fig6")
# ---------------------------------------------------------------- fig 5: speciation, three tiers
def fig5():
# ---------------------------------------------------------------- fig 7: speciation, three tiers
def fig7():
fig, axes = plt.subplots(2, 3, figsize=(11.4, 6.6))
bdm, _ = load_bundle("results/E12")
@ -294,11 +385,11 @@ def fig5():
ax.set(xlabel="specialist training (epochs)", ylabel="accuracy", ylim=(0, 1.02))
ax.legend()
letter(ax, "F")
save(fig, "fig5")
save(fig, "fig7")
# ---------------------------------------------------------------- fig 6: the language-model tier
def fig6():
# ---------------------------------------------------------------- fig 3: the language-model tier
def fig3():
import pandas as pd
from scipy.stats import spearmanr
@ -373,9 +464,9 @@ def fig6():
ax.set_xticklabels([l for _, l in preds], fontsize=6.5)
ax.set(ylabel="|Spearman ρ| vs merge penalty", ylim=(0, 0.8))
letter(ax, "D")
save(fig, "fig6")
save(fig, "fig3")
if __name__ == "__main__":
for f in (fig1, fig2, fig3, fig4, fig5, fig6):
for f in (fig1, fig2, fig3, fig4, fig5, fig6, fig7):
f()