- 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
100 lines
4.2 KiB
Python
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}"
|