MachineSex/figures/plot_fig5_speciation_bdm.py
Giorgio Gilestro 6f8cef1ac5 main: keep only what reproduces the manuscript; everything else lives on dev
Removed from main (all preserved on the dev branch): the arXiv build and
its sources, design documents (blueprint, results summary, review responses,
essay drafts), tasks/ and CLAUDE.md, the cover letter and reference tooling,
two unused manuscript figures, and every experiment that feeds no figure or
number in the paper: the collapse null, the sexual-vs-asexual lineage, the
NK speciation variant, the 0.5B single-seed LLM prototypes, the compose and
society experiments with their calibration and pilot runs, and their
configs, runners, tests, figure scripts and PBS jobs. Their result bundles
are moved to results/_archive/ (ignored) so the parquets stay on disk.

Also: plot_llm_speciation reads the s{seed}/ layout; the mating-breadth
plot writes under its bundle name; Makefile targets reduced to the kept
experiments; REPRODUCING.md and README point to dev for the rest.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y64o8FKP7rCuXzC48pxpMm
2026-09-13 17:07:23 +01:00

70 lines
3.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""E12 figure — model speciation: the merge-compatibility limit of the sexual society.
Three panels, reading only the committed bundles. (A) BDM: mean recombinant (hybrid) fitness vs
parental divergence, one line per epistasis density rho, against the rising parent fitness — the
compatible -> outbreeding-depression -> hybrid-inviability trajectory, peaking then crashing sooner the
denser the epistasis. (B) BDM: the reproductive-isolation rate (fraction of hybrids below the ancestor)
vs divergence — the isolation cliff, moving to lower divergence as epistasis density rises. (C) NK: the
epistasis wedge — as landscape ruggedness K grows, recombining two adapted local-optimum parents flips
from a gain to outbreeding depression.
Usage: python figures/plot_fig5_speciation_bdm.py
"""
from __future__ import annotations
import sys
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
sys.path.insert(0, str(Path(__file__).parent))
from _figlib import load_bundle, savefig # noqa: E402
def _agg(df, keys, value):
g = df.groupby(keys)[value].agg(["mean", "std", "count"]).reset_index()
g["se"] = g["std"] / np.sqrt(g["count"].clip(lower=1))
return g
def main() -> None:
bdm, _ = load_bundle("results/fig5_speciation_bdm")
rhos = sorted(bdm["rho"].unique())
colors = plt.cm.viridis(np.linspace(0.15, 0.85, len(rhos)))
fig, axes = plt.subplots(1, 2, figsize=(11, 5))
# Panel A: hybrid fitness vs divergence, per epistasis density, + parent fitness.
ax = axes[0]
par = _agg(bdm, "divergence", "parent_fitness")
ax.plot(par["divergence"], par["mean"], "k--", lw=1.6, label="parent fitness")
for rho, c in zip(rhos, colors):
g = _agg(bdm[bdm["rho"] == rho], "divergence", "offspring_fitness")
ax.plot(g["divergence"], g["mean"], "-o", color=c, lw=2, label=f"hybrid, ρ={rho}")
ax.fill_between(g["divergence"], g["mean"] - g["se"], g["mean"] + g["se"], color=c, alpha=0.15)
ax.axhline(0, color="#999", lw=0.8, ls=":")
ax.set(xlabel="parental divergence (substitutions $d$)", ylabel="fitness",
title="Hybrid fitness collapses as lineages diverge\n(compatible → outbreeding depression → inviability)")
ax.legend(frameon=False, fontsize=8)
# Panel B: reproductive-isolation rate vs divergence, per epistasis density.
ax = axes[1]
for rho, c in zip(rhos, colors):
g = _agg(bdm[bdm["rho"] == rho], "divergence", "isolation")
ax.plot(g["divergence"], g["mean"], "-o", color=c, lw=2, label=f"ρ={rho}")
ax.set(xlabel="parental divergence (substitutions $d$)", ylabel="reproductive isolation\n(P hybrid inviable)",
ylim=(-0.02, 1.02),
title="The isolation cliff moves to lower divergence\nas epistasis density rises")
ax.legend(frameon=False, fontsize=9, title="epistasis density")
fig.suptitle("Model speciation: when two diverged models are too incompatible to merge",
y=1.02, fontsize=13)
fig.tight_layout()
savefig(fig, "results/fig5_speciation_bdm", "fig5_speciation_bdm")
if __name__ == "__main__":
main()