- 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
72 lines
3.5 KiB
Python
72 lines
3.5 KiB
Python
"""Learning-kernel tests (pure NumPy).
|
|
|
|
The kernel is an additive Layer-1 extension: identity by default (so the neutral Wright-Fisher
|
|
core and every scientific-validation test are unchanged), a sharpening knob that ADDS collapse
|
|
where neutral drift is inert, and a smoothing knob that supplies a diversity FLOOR where neutral
|
|
drift would collapse to zero. These assert exactly those three behaviours.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import numpy as np
|
|
|
|
from inheritance.kernel import LearningKernelCfg, apply_kernel
|
|
from inheritance.lineage import run_lineage
|
|
from inheritance.metrics import heterozygosity
|
|
|
|
|
|
def test_apply_kernel_identity_is_noop():
|
|
p = np.array([0.5, 0.3, 0.15, 0.05])
|
|
assert np.array_equal(apply_kernel(p, LearningKernelCfg()), p)
|
|
|
|
|
|
def test_apply_kernel_reset_mixes_toward_uniform():
|
|
p = np.array([1.0, 0.0, 0.0, 0.0])
|
|
out = apply_kernel(p, LearningKernelCfg(reset=0.2))
|
|
assert np.isclose(out.sum(), 1.0)
|
|
assert np.allclose(out, [0.8 + 0.2 / 4, 0.05, 0.05, 0.05]) # keeps dead modes alive
|
|
assert (out > 0).all()
|
|
|
|
|
|
def test_apply_kernel_sharpen_and_floor_prune():
|
|
p = np.array([0.6, 0.3, 0.09, 0.01])
|
|
sharp = apply_kernel(p, LearningKernelCfg(temperature=0.5)) # p^2, renormalised
|
|
assert sharp[0] > p[0] and sharp[-1] < p[-1] # mass concentrates
|
|
floored = apply_kernel(p, LearningKernelCfg(floor=0.05))
|
|
assert floored[-1] == 0.0 and np.isclose(floored.sum(), 1.0) # weak mode dropped
|
|
|
|
|
|
def test_kernel_identity_leaves_lineage_unchanged():
|
|
base = {"truth": {"K": 64, "init": "truth"}, "dynamics": {"n": 200},
|
|
"generations": 20, "metrics": {"kl_floor": 1e-9, "support_eps": 1e-9}}
|
|
withk = {**base, "dynamics": {"n": 200, "kernel": {"reset": 0.0, "temperature": 1.0}}}
|
|
a = run_lineage(base, seed=3)["heterozygosity"].to_numpy()
|
|
b = run_lineage(withk, seed=3)["heterozygosity"].to_numpy()
|
|
assert np.array_equal(a, b) # identity kernel == neutral Wright-Fisher, bitwise
|
|
|
|
|
|
def test_sharpening_adds_collapse_where_neutral_is_inert():
|
|
# Large n vs small K: neutral drift barely collapses; sharpening drives it to ~1 mode.
|
|
cfg = {"truth": {"K": 30, "tail": "zipf", "zipf_s": 1.5, "tail_threshold": 1e-2,
|
|
"init": "truth"},
|
|
"dynamics": {"n": 6000, "grounding": {"m": 0}},
|
|
"generations": 15, "metrics": {"kl_floor": 1e-9, "support_eps": 1e-9}}
|
|
neutral = run_lineage(cfg, seed=0)
|
|
sharp = run_lineage({**cfg, "dynamics": {**cfg["dynamics"],
|
|
"kernel": {"temperature": 0.8}}}, seed=0)
|
|
assert neutral["support_size"].iloc[-1] > 20 # neutral: ~all modes survive
|
|
assert sharp["support_size"].iloc[-1] <= 3 # sharpening: collapse to a point
|
|
assert sharp["heterozygosity"].iloc[-1] < 0.1
|
|
|
|
|
|
def test_smoothing_floors_diversity_where_neutral_collapses():
|
|
# Small n vs large K: neutral drift drives H toward 0; smoothing holds a positive floor.
|
|
cfg = {"truth": {"K": 256, "tail": "zipf", "zipf_s": 1.3, "tail_threshold": 1e-3,
|
|
"init": "truth"},
|
|
"dynamics": {"n": 200, "grounding": {"m": 0}},
|
|
"generations": 120, "metrics": {"kl_floor": 1e-9, "support_eps": 1e-9}}
|
|
neutral = run_lineage(cfg, seed=0)
|
|
smooth = run_lineage({**cfg, "dynamics": {**cfg["dynamics"],
|
|
"kernel": {"reset": 0.006}}}, seed=0)
|
|
assert neutral["heterozygosity"].iloc[-1] < 0.4 # neutral collapses
|
|
assert smooth["heterozygosity"].iloc[-1] > 0.55 # smoothing floors H well above neutral
|