MachineSex/figures/plot_E5.py
Giorgio Gilestro 1721d047fa Layer 1 complete: E3-E6 + E2 analysis add-ons
Finishes the Layer 1 analytical core. All six experiments run with honest,
publication-quality figures; 71 tests green.

- E3 region-matched grounding: `grounding.exercised` knob + per-region tail
  survival. Matched holds the exercised region's tail (0.49) where uniform
  spreads thin and lets it collapse (0.07).
- E4 multi-teacher recombination: `run_coverage` runner. Union coverage matches
  U(K_T,rho,q) exactly. Finding: mean-mixture distillation shows NO surviving
  benefit (a conservation law — 1/K_T dilution cancels the union gain); a
  union-preserving max-merge (M2N2-style) does. E4 reports both operators.
- E5 QD vs greedy: greedy drives fixation (H~0.01); QD holds H at 0.48-0.88,
  rising with the novelty exponent.
- E6 re-mint gate: `arm` multi-override sweep. Re-minting a collapsed lineage
  locks in divergence of KL-to-original; gating on diversity prevents it.
- E2 analysis add-ons (from the companion work order, numbers verified): new
  analysis.py (reduce_to_stationary, critical_grounding with bootstrap CI ->
  g*=0.048, 95% CI [0.047,0.050]); tail_band_metrics + per-band logging; the
  E2 figure rebuilt as a 2x2 (defined g*+CI, g=0 flagged as a finite-time
  artifact, tail item-vs-mass, per-rarity-band panel). Uses truth-mass-weighted
  tail coverage rather than the raw (martingale) tail_mass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 18:54:42 +02:00

80 lines
3.2 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.

"""E5 figure: quality-diversity vs greedy selection.
At matched grounding, greedy (directional) selection drives the lineage toward the
fittest items and collapses diversity, while quality-diversity selection (a novelty bonus
w_i ∝ f_i·p_i^{-alpha}) maintains a high stationary heterozygosity that rises with the
novelty exponent alpha. Usage: python figures/plot_E5.py [results/E5]
"""
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 main(results_dir: str = "results/E5") -> None:
df, cfg = load_bundle(results_dir)
last = int(cfg["generations"] * 0.8)
def arm(mode, alpha=1.0):
return df[(df["mode"] == mode) & (df["novelty_alpha"] == alpha)]
fig, axes = plt.subplots(1, 3, figsize=(15, 4.3))
# Panel 1: H trajectories
ax = axes[0]
series = [("greedy", 1.0, "#d62728", "greedy"),
("qd", 1.0, "#ff7f0e", "qd (α=1)"),
("qd", 2.0, "#1f77b4", "qd (α=2)"),
("none", 1.0, "#2ca02c", "none (grounding only)")]
for mode, a, c, lab in series:
s = arm(mode, a).groupby("generation")["heterozygosity"].mean()
ax.plot(s.index, s.values, color=c, label=lab)
ax.set(xlabel="generation", ylabel="heterozygosity $H$",
title="Greedy collapses; QD maintains diversity")
ax.legend(frameon=False, fontsize=8)
# Panel 2: stationary H vs alpha for qd, with greedy/none reference lines
ax = axes[1]
qd = df[(df["mode"] == "qd") & (df["generation"] >= last)]
st = qd.groupby("novelty_alpha")["heterozygosity"].agg(["mean", "sem"])
ax.errorbar(st.index, st["mean"], yerr=1.96 * st["sem"], fmt="-o",
color="#ff7f0e", capsize=3, label="qd")
for mode, c in (("greedy", "#d62728"), ("none", "#2ca02c")):
h = arm(mode, 1.0)
h = h[h["generation"] >= last]["heterozygosity"].mean()
ax.axhline(h, ls="--", color=c, label=f"{mode}")
ax.set(xlabel=r"novelty exponent $\alpha$", ylabel="stationary $H$",
title="QD maintains H above greedy for all α")
ax.legend(frameon=False, fontsize=9)
# Panel 3: stationary support size per arm
ax = axes[2]
arms = [("greedy", 1.0, "greedy"), ("qd", 0.5, "qd α=0.5"),
("qd", 1.0, "qd α=1"), ("qd", 2.0, "qd α=2"), ("none", 1.0, "none")]
labels, vals, errs, colors = [], [], [], []
palette = {"greedy": "#d62728", "qd": "#ff7f0e", "none": "#2ca02c"}
for mode, a, lab in arms:
s = arm(mode, a)
s = s[s["generation"] >= last]["support_size"]
labels.append(lab); vals.append(s.mean()); errs.append(1.96 * s.sem())
colors.append(palette[mode])
ax.bar(range(len(labels)), vals, yerr=errs, color=colors, capsize=3)
ax.set(ylabel="stationary support size", title="Surviving items per arm",
xticks=range(len(labels)))
ax.set_xticklabels(labels, rotation=25, ha="right", fontsize=8)
fig.suptitle("E5 — quality-diversity selection maintains diversity where greedy "
"fixes it", y=1.02)
fig.tight_layout()
savefig(fig, results_dir, "E5")
if __name__ == "__main__":
main(*sys.argv[1:])