MachineSex/tests/test_neural_validation.py
Giorgio Gilestro ab3dc10587 Restructure: descriptive tier and experiment names, paper/manuscript
- paper/pnas -> paper/manuscript (venue-neutral)
- configs/layer1 -> configs/inheritance, src/knowledge -> src/inheritance
  (imported as `inheritance`), make layer1 -> make inheritance; layer2 alias dropped
- inheritance and trained-network bundles named after the manuscript figure
  they feed (fig2_grounding_sweep, figS3_rebaselining, ...), or descriptively
  where they feed none; configs keep their `experiment:` value so parquet
  hashes are unchanged, only output.dir moves
- figure scripts, SI figure sources, notebooks, REPRODUCING.md, README and the
  SI Methods/tables updated; make clean no longer deletes tracked manifests;
  reproduce.sh hashes the s{seed}/ layouts too

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

100 lines
4.2 KiB
Python

"""Stage B — the HARD GATE: the neural runner reproduces the Layer-1 analytic core.
With ``model.kind == "histogram"`` the neural generational step (train-on-parent's-samples
+ grounding) is *exactly* neutral Wright-Fisher drift with immigration. This module asserts
that the neural runner reproduces the two closed forms Layer 1 is validated against
(blueprint 2.4-1 neutral heterozygosity decay, 2.4-3 exact mutation-drift equilibrium) and
that its mean H-trajectory tracks ``inheritance.lineage.run_lineage`` directly. If any of
these fail the neural plumbing is wrong — no real network should be trained until they pass.
"""
from __future__ import annotations
import numpy as np
import pytest
from inheritance.lineage import run_lineage
from inheritance.seeding import spawn_seeds
from neural.generation_loop import run_generative_lineage
from neural.synthetic import make_mode_truth
from neural.config import SyntheticCfg
def theory_decay(H0: float, n: int, t: np.ndarray) -> np.ndarray:
return H0 * (1.0 - 1.0 / n) ** np.asarray(t, dtype=float)
def theory_H_eq(n: int, m: int, H_star: float) -> float:
return H_star * m * (2 * n + m - 1) / (n + 2 * n * m + m * m)
def _mean_H(cfg: dict, n_rep: int, master: int = 20260704) -> np.ndarray:
"""Mean heterozygosity trajectory over ``n_rep`` histogram-model replicates."""
seeds = spawn_seeds(master, n_rep)
Hs = [run_generative_lineage(cfg, int(s.generate_state(1)[0]))["heterozygosity"].to_numpy()
for s in seeds]
return np.mean(np.stack(Hs), axis=0)
# --- Pred. 1: neutral heterozygosity decay ----------------------------------------------
def test_bridge_neutral_decay_matches_theory():
K, n, gens, reps = 50, 100, 20, 800
cfg = {
"synthetic": {"K": K, "R": 1, "zipf_s": 1.1, "init": "uniform",
"style_len": 2, "style_vocab": 4, "id_base": 2},
"model": {"kind": "histogram"},
"dynamics": {"n": n, "grounding": {"m": 0}},
"generations": gens,
}
H_sim = _mean_H(cfg, reps)
t = np.arange(gens + 1)
H_theory = theory_decay(1.0 - 1.0 / K, n, t)
rel_err = np.abs(H_sim - H_theory) / H_theory
assert rel_err.max() < 0.03, f"max rel err {rel_err.max():.4f} exceeds 0.03"
# --- Pred. 3: exact mutation-drift equilibrium under grounding ---------------------------
def test_bridge_grounded_equilibrium_matches_theory():
K, n, m, gens, reps = 80, 100, 8, 220, 300
cfg = {
"synthetic": {"K": K, "R": 1, "zipf_s": 1.1, "init": "uniform",
"style_len": 2, "style_vocab": 4, "id_base": 2},
"model": {"kind": "histogram"},
"dynamics": {"n": n, "grounding": {"m": m}}, # R=1 -> proportional immigration
"generations": gens,
}
H_sim_traj = _mean_H(cfg, reps)
H_sim = float(H_sim_traj[-60:].mean()) # stationary average
td = make_mode_truth(SyntheticCfg(K=K, R=1, zipf_s=1.1))
H_star = 1.0 - float(np.sum(td.p_star ** 2))
H_eq = theory_H_eq(n, m, H_star)
assert H_sim == pytest.approx(H_eq, rel=0.05), f"sim {H_sim:.4f} vs theory {H_eq:.4f}"
# --- Direct bridge: histogram lineage tracks Layer-1 run_lineage -------------------------
def test_bridge_tracks_layer1_trajectory():
K, n, m, gens, reps = 60, 120, 6, 40, 400
neural_cfg = {
"synthetic": {"K": K, "R": 1, "zipf_s": 1.1, "init": "uniform",
"style_len": 2, "style_vocab": 4, "id_base": 2},
"model": {"kind": "histogram"},
"dynamics": {"n": n, "grounding": {"m": m}},
"generations": gens,
}
layer1_cfg = {
"truth": {"K": K, "R": 1, "zipf_s": 1.1, "init": "uniform"},
"dynamics": {"n": n, "grounding": {"m": m, "policy": "proportional"}},
"generations": gens,
}
seeds = spawn_seeds(20260704, reps)
H_neural = np.mean(np.stack([
run_generative_lineage(neural_cfg, int(s.generate_state(1)[0]))["heterozygosity"].to_numpy()
for s in seeds]), axis=0)
H_layer1 = np.mean(np.stack([
run_lineage(layer1_cfg, int(s.generate_state(1)[0]))["heterozygosity"].to_numpy()
for s in seeds]), axis=0)
rel_err = np.abs(H_neural - H_layer1) / H_layer1
assert rel_err.max() < 0.03, f"neural vs Layer-1 max rel err {rel_err.max():.4f}"