MachineSex/tests/test_kernel.py
Giorgio Gilestro 871bc39ec6 knowledge: learning kernel — model the estimator bias, not just sampling
Revisiting Layer 1 against Layer 1.5 (and Riis 2026, arXiv:2604.08554):
neutral Wright-Fisher is a null that BOTH neural architectures deviate
from, in opposite directions. Add a learning kernel to the refit step,
p_{t+1} = T_theta(counts/n), with two population-genetics knobs -- reset u
(mutation toward a prior = smoothing) and temperature tau (sharpening =
mode-competition) -- both identity by default, so the histogram bridge and
all 68 scientific-validation/correctness tests are unchanged.

Result: neutral drift fails both neural models, oppositely.
- VAE regime (n=6000, K=30): neutral drift is inert (no collapse), yet the
  real VAE collapsed to one mode. Sharpening tau=0.8 reproduces it -- the
  estimator ADDS collapse pressure.
- RNN regime (n=200, K=256): neutral drives H->0, but the real RNN only
  partially collapses. Mutation u=0.006 reproduces the H-floor -- the
  estimator REMOVES collapse pressure. Honest caveat: uniform-mutation
  overshoots the RNN's forward-KL, evidence its smoothing prior is
  truth-like, not uniform (future refinement).

This mechanistically explains the architecture-generality result and the
softened neural g*, and develops the estimator axis Riis names as future
work. New: knowledge/kernel.py, configs/layer1/kernel_{sharpen,smooth}.yaml,
figures/plot_kernel.py (overlays analytic arms vs committed neural
endpoints), READMEs, tests/test_kernel.py (+6, 105 total green). Strategic
Riis positioning recorded in CLAUDE.md: concede "collapse=drift" as prior
art; lead with recombination, the kernel axis, and the Lamarckian society.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 10:23:33 +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 knowledge.kernel import LearningKernelCfg, apply_kernel
from knowledge.lineage import run_lineage
from knowledge.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