MachineSex/tests/test_kernel.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

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