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>
This commit is contained in:
parent
79bbc45f41
commit
871bc39ec6
21 changed files with 660 additions and 3 deletions
|
|
@ -76,6 +76,10 @@ E4's whole purpose is to isolate the effect of teacher **decorrelation ρ**, so
|
|||
|
||||
**Finding (2026-07-05, real-MNIST `mnist_collapse`) — collapse and grounding-rescue reproduce on real images.** External-validity tier: a small **convolutional VAE** (the canonical generative-collapse model) is retrained each generation on its own generated digits. Modes = digit class × stroke-thickness bin (K=30, Zipf, ~18 tail modes); the oracle is a **frozen CNN + deterministic thickness** at **98.5% mode accuracy** (its 30×30 confusion matrix is recorded in the manifest as the measurement floor). Result (4 reps): the **dry (g=0) lineage collapses to a single mode** — forward-KL 0.5→18, support 30→1, tail truth-mass 1.0→0.06, H→0 — while **10% grounding holds all 30 modes** (KL≈0.6, full tail, H≈0.9). The VAE needs ~10% grounding here vs the synthetic histogram's ~5%, consistent with the `grounding` finding that trained neural models need more grounding than the exact operator. **Confirmation-only (signs, not magnitudes; blueprint §3.5)** — the exact synthetic oracle stays the quantitative anchor. `figures/mnist_montage.py` is an eyeball diagnostic (re-runs a short dry lineage; NOT a parquet figure). Build gates passed: CNN mode accuracy 98.5%; VAE gen-0 recovers full 30/30 support (over-smooths frequencies, KL≈0.5, no prior hole — unlike the *synthetic*-codeword VAE, which is why the MNIST VAE works where that one didn't). The MNIST tier is heavy (torchvision `--extra mnist`, downloads MNIST, ~5 min): `make mnist`, kept out of the `make neural` loop.
|
||||
|
||||
**Finding (2026-07-05, learning kernel) — neutral drift is a null both real models fail, oppositely; the estimator bias is a signed operator.** Layer-1 extension (`knowledge/kernel.py`, `LearningKernelCfg`): the refit becomes `p_{t+1} = T_θ(counts/n)` with two pop-gen knobs — **reset `u`** (mutation toward a prior = smoothing) and **temperature `τ`** (sharpening = mode-competition) — both identity at their defaults, so the histogram bridge and every scientific-validation test are unchanged (68 core tests still green). Result: **neutral Wright–Fisher fails both neural architectures, in opposite directions.** VAE regime (`n=6000, K=30`): neutral drift is *inert* (no collapse), yet the real VAE collapsed to one mode — **sharpening `τ=0.8` reproduces it** (the estimator ADDS collapse). 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). Honest caveat: uniform-mutation matches the RNN `H`-floor but overshoots its forward-KL (~5 vs ~2), evidence the RNN's smoothing prior is *truth-like, not uniform* (future refinement). Configs `configs/layer1/kernel_{sharpen,smooth}.yaml`, figure `plot_kernel.py`. This mechanistically explains the architecture-generality result and the softened neural `g*`.
|
||||
|
||||
**Strategic positioning vs Riis 2026 (arXiv:2604.08554, "Drift and selection in LLM text ecosystems").** Riis independently formalizes **collapse = Wright–Fisher drift** (his Thm 1) with n-gram agents: minority-mass martingale, rare-first extinction, single-token dropout ≈ αe^{−α}, de Bruijn-polytope fixed points, plus descriptive-vs-normative *selection* (Thm 2). **Concede as prior art:** "collapse is literally Wright–Fisher", the martingale, rare-first loss, the WF/effective-population formalism — cite him; do **not** frame these as our contribution. **Crucial distinction that protects us:** his "mixed environment" *retains the lineage's own old synthetic tokens* — there is **no injection of fresh real data from a fixed `p*`**, so his headline is *pessimistic* (Thm 1c: extinction is independent of α — retention only changes speed). Our **grounding is immigration from a non-drifting external truth**, giving a stationary `H_eq>0` and a critical `g*≪1` that *prevents* collapse — the mechanism his closed loop lacks. **Our defensible novelty, ranked:** (1) **recombination + "merge, don't average" conservation law** (E4) — he has no model-merging operator; flagship; (2) **the learning-kernel / estimator-bias axis** — he *explicitly names it as future work*; we now build+measure it; (3) grounding threshold (solid anchor, but immigration–drift balance is classic — not a flagship); (4) architecture-generality in real weights + MNIST; (5) **the Lamarckian society + the vertical/cumulative C3 claim — wholly ours, not yet run.** Reposition the paper from *"collapse is drift"* (now contested) to **a population-genetic *control theory* for sustaining open-ended knowledge**: drift is the diagnosed disease (cite Riis), our contribution is the engineered remedies and their integration.
|
||||
|
||||
## Build order (blueprint §7) — respect the gate
|
||||
|
||||
1. Scaffold: repo layout (§5), container, pytest skeleton, config system, seeding utils. `make test` green.
|
||||
|
|
|
|||
4
Makefile
4
Makefile
|
|
@ -15,8 +15,8 @@ env-mnist: ## add torchvision for the real-MNIST confirmation tier
|
|||
test: ## correctness tests + scientific-validation tests (the spine of trust)
|
||||
uv run pytest
|
||||
|
||||
layer1: ## run experiments E1-E6
|
||||
for e in E1 E2 E3 E4 E5 E6; do uv run python -m knowledge.experiment configs/layer1/$$e.yaml; done
|
||||
layer1: ## run experiments E1-E6 + the learning-kernel bridge (analytic)
|
||||
for e in E1 E2 E3 E4 E5 E6 kernel_sharpen kernel_smooth; do uv run python -m knowledge.experiment configs/layer1/$$e.yaml; done
|
||||
|
||||
neural: ## run Layer 1.5 synthetic neural experiments (excludes the heavy MNIST tier)
|
||||
for c in configs/neural/*.yaml; do case "$$c" in *mnist*) ;; \
|
||||
|
|
|
|||
29
configs/layer1/kernel_sharpen.yaml
Normal file
29
configs/layer1/kernel_sharpen.yaml
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
experiment: kernel_sharpen
|
||||
kind: lineage
|
||||
seed: 20260705
|
||||
n_replicates: 24
|
||||
|
||||
# (Learning-kernel bridge, pro-collapse arm): does neutral Wright-Fisher explain the VAE's
|
||||
# collapse on MNIST? NO -- and that is the point. This matches the MNIST regime (K=30, n=6000,
|
||||
# Zipf) where drift is nearly inert: neutral (temperature=1.0) barely moves (H stays ~H*, ~all
|
||||
# modes alive), yet the real VAE collapsed to a SINGLE mode (results/mnist_collapse). Adding the
|
||||
# estimator's sharpening / mode-competition (temperature<1: p ~ p^(1/tau)) reproduces the
|
||||
# catastrophic collapse. tau=0.8 is calibrated to reproduce collapse-to-one-mode. This is the
|
||||
# axis Riis (2026) names as future work: the estimator, not the sampling, drives VAE collapse.
|
||||
|
||||
truth: {K: 30, R: 1, tail: zipf, zipf_s: 1.5, tail_threshold: 0.01, init: truth}
|
||||
|
||||
dynamics:
|
||||
n: 6000 # huge vs K=30 -> neutral drift is essentially inert
|
||||
grounding: {m: 0, policy: proportional}
|
||||
kernel: {reset: 0.0, temperature: 1.0, floor: 0.0}
|
||||
|
||||
generations: 15
|
||||
|
||||
metrics: {kl_floor: 1.0e-9, support_eps: 1.0e-9}
|
||||
|
||||
sweep:
|
||||
- param: dynamics.kernel.temperature
|
||||
values: [1.0, 0.8] # neutral (no collapse) vs sharpened (catastrophic collapse)
|
||||
|
||||
output: {dir: results/kernel_sharpen}
|
||||
32
configs/layer1/kernel_smooth.yaml
Normal file
32
configs/layer1/kernel_smooth.yaml
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
experiment: kernel_smooth
|
||||
kind: lineage
|
||||
seed: 20260705
|
||||
n_replicates: 24
|
||||
|
||||
# (Learning-kernel bridge, anti-collapse arm): neutral Wright-Fisher OVER-predicts the RNN's
|
||||
# collapse. This matches the RNN grounding regime (K=256, n=200, Zipf): neutral (reset=0) drives
|
||||
# H all the way to 0, but the real RNN only PARTIALLY collapses -- H plateaus ~0.68 of a possible
|
||||
# 0.88, forward-KL plateaus ~2 (does not diverge), ~half the tail stays alive (results/grounding).
|
||||
# The estimator's smoothing / regularisation supplies a diversity FLOOR. A mutation-toward-prior
|
||||
# knob (reset=u: p <- (1-u)p + u*uniform) reproduces the H-floor. reset=0.006 is calibrated to the
|
||||
# RNN's stationary dry H. Honest caveat carried in the write-up: uniform-mutation matches the
|
||||
# H-floor but overshoots forward-KL (analytic ~6 vs RNN ~2), evidence the RNN's smoothing target
|
||||
# is TRUTH-LIKE, not uniform -- a refinement for future work. The sign, though, is unambiguous:
|
||||
# the estimator here REMOVES collapse pressure (opposite to the VAE's sharpening).
|
||||
|
||||
truth: {K: 256, R: 1, tail: zipf, zipf_s: 1.3, tail_threshold: 0.001, init: truth}
|
||||
|
||||
dynamics:
|
||||
n: 200
|
||||
grounding: {m: 0, policy: proportional}
|
||||
kernel: {reset: 0.0, temperature: 1.0, floor: 0.0}
|
||||
|
||||
generations: 100 # long enough to show neutral -> 0 vs smoothed -> floor clearly
|
||||
|
||||
metrics: {kl_floor: 1.0e-9, support_eps: 1.0e-9}
|
||||
|
||||
sweep:
|
||||
- param: dynamics.kernel.reset
|
||||
values: [0.0, 0.006] # neutral (H -> 0) vs smoothed (H floors, like the RNN)
|
||||
|
||||
output: {dir: results/kernel_smooth}
|
||||
121
figures/plot_kernel.py
Normal file
121
figures/plot_kernel.py
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
"""`kernel` figure — the learning kernel: neutral drift fails both neural models, oppositely.
|
||||
|
||||
The Layer-1.5 architecture-generality result gets a mechanistic explanation. Neutral Wright-Fisher
|
||||
(the histogram / Riis baseline) is the null; a real learner adds an estimator bias that can point
|
||||
either way. Two regimes, each falsifying neutral drift in the OPPOSITE direction, each repaired by
|
||||
one knob of the learning kernel:
|
||||
|
||||
* **VAE regime (n=6000, K=30):** drift is nearly inert — neutral holds ~all modes — yet the real
|
||||
VAE collapsed to a single mode. Sharpening (temperature<1) reproduces it. The estimator ADDS
|
||||
collapse pressure.
|
||||
* **RNN regime (n=200, K=256):** neutral drives H to 0, but the real RNN only partially collapses
|
||||
(H floors, forward-KL plateaus). Mutation-toward-prior (reset>0) reproduces the floor. The
|
||||
estimator REMOVES collapse pressure.
|
||||
|
||||
Analytic arms are read from results/kernel_{sharpen,smooth}; the neural reference endpoints
|
||||
(dashed) are read from the committed results/mnist_collapse and results/grounding parquets — so the
|
||||
figure is a pure function of committed artifacts.
|
||||
|
||||
Usage: python figures/plot_kernel.py
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
from _figlib import load_bundle, savefig # noqa: E402
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parents[1] / "src"))
|
||||
from knowledge.metrics import heterozygosity # noqa: E402
|
||||
from knowledge.truth import make_true_distribution # noqa: E402
|
||||
|
||||
|
||||
def _mean_traj(df, knob, val, col):
|
||||
sub = df[df[knob] == val]
|
||||
s = sub.groupby("generation")[col].mean()
|
||||
return s.index.to_numpy(), s.to_numpy()
|
||||
|
||||
|
||||
def _neural_dry(results_dir, col, stationary_frac=0.0):
|
||||
"""Mean of ``col`` on the dry (g=0) neural arm — endpoint, or stationary tail if frac>0."""
|
||||
df, _ = load_bundle(results_dir)
|
||||
dry = df[df["g"] == 0.0]
|
||||
if stationary_frac > 0:
|
||||
dry = dry[dry["generation"] >= int(dry["generation"].max() * (1 - stationary_frac))]
|
||||
return float(dry[col].mean())
|
||||
return float(dry[dry["generation"] == dry["generation"].max()][col].mean())
|
||||
|
||||
|
||||
def main() -> None:
|
||||
sh, sh_cfg = load_bundle("results/kernel_sharpen")
|
||||
sm, sm_cfg = load_bundle("results/kernel_smooth")
|
||||
Hstar_sh = heterozygosity(make_true_distribution(
|
||||
sh_cfg["truth"]["K"], 1, "zipf", 0.5, sh_cfg["truth"]["zipf_s"], 0,
|
||||
tail_threshold=sh_cfg["truth"]["tail_threshold"]).p_star)
|
||||
Hstar_sm = heterozygosity(make_true_distribution(
|
||||
sm_cfg["truth"]["K"], 1, "zipf", 0.5, sm_cfg["truth"]["zipf_s"], 0,
|
||||
tail_threshold=sm_cfg["truth"]["tail_threshold"]).p_star)
|
||||
|
||||
# Neural reference endpoints (dashed) from the committed neural runs.
|
||||
vae_H = _neural_dry("results/mnist_collapse", "heterozygosity")
|
||||
vae_sup = _neural_dry("results/mnist_collapse", "support_size")
|
||||
rnn_H = _neural_dry("results/grounding", "heterozygosity", stationary_frac=0.4)
|
||||
rnn_KL = _neural_dry("results/grounding", "forward_kl", stationary_frac=0.4)
|
||||
|
||||
fig, axes = plt.subplots(2, 2, figsize=(13, 9))
|
||||
NEU, KER = "#1f77b4", "#d62728"
|
||||
|
||||
# --- VAE regime: sharpening ---
|
||||
ax = axes[0, 0]
|
||||
for val, c, lab in [(1.0, NEU, "neutral (τ=1)"), (0.8, KER, "sharpened (τ=0.8)")]:
|
||||
g, y = _mean_traj(sh, "temperature", val, "heterozygosity")
|
||||
ax.plot(g, y, "-o", color=c, ms=3, label=lab)
|
||||
ax.axhline(Hstar_sh, ls=":", color="gray", lw=1, label="$H^*$")
|
||||
ax.axhline(vae_H, ls="--", color="#2ca02c", lw=1.3, label=f"real VAE (dry): {vae_H:.2f}")
|
||||
ax.set(xlabel="generation", ylabel="heterozygosity $H$",
|
||||
title="VAE regime ($n$=6000, $K$=30): neutral drift is inert;\nsharpening collapses (like the VAE)")
|
||||
ax.legend(frameon=False, fontsize=8)
|
||||
|
||||
ax = axes[0, 1]
|
||||
for val, c, lab in [(1.0, NEU, "neutral (τ=1)"), (0.8, KER, "sharpened (τ=0.8)")]:
|
||||
g, y = _mean_traj(sh, "temperature", val, "support_size")
|
||||
ax.plot(g, y, "-o", color=c, ms=3, label=lab)
|
||||
ax.axhline(vae_sup, ls="--", color="#2ca02c", lw=1.3, label=f"real VAE (dry): {vae_sup:.0f}")
|
||||
ax.set(xlabel="generation", ylabel="distinct modes alive",
|
||||
title="Support: neutral holds ~all; sharpening → 1 mode")
|
||||
ax.legend(frameon=False, fontsize=8)
|
||||
|
||||
# --- RNN regime: smoothing ---
|
||||
ax = axes[1, 0]
|
||||
for val, c, lab in [(0.0, NEU, "neutral (u=0)"), (0.006, KER, "smoothed (u=0.006)")]:
|
||||
g, y = _mean_traj(sm, "reset", val, "heterozygosity")
|
||||
ax.plot(g, y, "-", color=c, lw=1.8, label=lab)
|
||||
ax.axhline(Hstar_sm, ls=":", color="gray", lw=1, label="$H^*$")
|
||||
ax.axhline(rnn_H, ls="--", color="#2ca02c", lw=1.3, label=f"real RNN (dry): {rnn_H:.2f}")
|
||||
ax.set(xlabel="generation", ylabel="heterozygosity $H$",
|
||||
title="RNN regime ($n$=200, $K$=256): neutral → 0;\nsmoothing floors $H$ (like the RNN)")
|
||||
ax.legend(frameon=False, fontsize=8)
|
||||
|
||||
ax = axes[1, 1]
|
||||
for val, c, lab in [(0.0, NEU, "neutral (u=0)"), (0.006, KER, "smoothed (u=0.006)")]:
|
||||
g, y = _mean_traj(sm, "reset", val, "forward_kl")
|
||||
ax.plot(g, y, "-", color=c, lw=1.8, label=lab)
|
||||
ax.axhline(rnn_KL, ls="--", color="#2ca02c", lw=1.3, label=f"real RNN (dry): {rnn_KL:.1f}")
|
||||
ax.set(xlabel="generation", ylabel=r"forward-KL $D(p^*\Vert p)$",
|
||||
title="Forward-KL: neutral diverges; smoothing plateaus\n(overshoots RNN → prior is truth-like, not uniform)")
|
||||
ax.legend(frameon=False, fontsize=8)
|
||||
|
||||
fig.suptitle("learning kernel — neutral Wright–Fisher fails both neural models, oppositely: "
|
||||
"the estimator sharpens (VAE) or smooths (RNN)", y=1.0, fontsize=12)
|
||||
fig.tight_layout()
|
||||
for d in ("results/kernel_sharpen", "results/kernel_smooth"):
|
||||
savefig(fig, d, "kernel")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
42
results/kernel_sharpen/README.md
Normal file
42
results/kernel_sharpen/README.md
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
# kernel — the learning kernel: why real learners deviate from neutral drift
|
||||
|
||||
*(This legend covers both `results/kernel_sharpen/` and `results/kernel_smooth/`; the figure
|
||||
`kernel.png` is written into both.)*
|
||||
|
||||
**Claim tested.** Neutral Wright–Fisher drift (the histogram bridge, and the baseline of Riis
|
||||
2026) is the *null* model of collapse. But Layer 1.5 showed real trained models deviate from it —
|
||||
and in **opposite directions**. Can a single extension of Layer 1 — a parameterized *learning
|
||||
kernel* on the refit step, `p_{t+1} = T_θ(counts/n)` — reproduce both deviations, and does neutral
|
||||
drift genuinely fail without it?
|
||||
|
||||
**Setup.** The kernel (`knowledge/kernel.py`) has two population-genetics knobs, both reducing to
|
||||
neutral drift at their defaults (so the histogram and every scientific-validation test are
|
||||
unchanged): **reset `u`** — mutation toward a prior (`p ← (1−u)p + u·π`), i.e. smoothing; and
|
||||
**temperature `τ`** — sharpening (`p ∝ p^{1/τ}`, `τ<1` concentrates), i.e. mode-competition. Two
|
||||
matched-to-neural regimes, 24 replicates each.
|
||||
|
||||
### The four panels (`kernel.png`; blue = neutral, red = kernel-on, green dashed = the real neural model)
|
||||
**Top row — VAE regime (`n=6000`, `K=30`), pro-collapse:**
|
||||
1. **Heterozygosity.** Neutral drift is **inert** — at `n=6000` it barely moves (`H` stays at
|
||||
`H*`). Yet the real VAE (green) collapsed to `H≈0`. **Sharpening (`τ=0.8`) reproduces the
|
||||
collapse.** Neutral drift is *falsified*; the estimator's mode-competition is required.
|
||||
2. **Support.** Neutral holds ~all 30 modes; sharpening → 1 mode, matching the VAE.
|
||||
|
||||
**Bottom row — RNN regime (`n=200`, `K=256`), anti-collapse:**
|
||||
3. **Heterozygosity.** Neutral drift drives `H → 0`, but the real RNN (green) only *partially*
|
||||
collapses (`H` floors at ~0.68). **Mutation `u=0.006` reproduces the floor.** The estimator here
|
||||
*removes* collapse pressure.
|
||||
4. **Forward-KL.** Neutral diverges; smoothing plateaus. Honest caveat: uniform-mutation plateaus
|
||||
*above* the RNN's KL (~5 vs ~2) — evidence the RNN's smoothing target is **truth-like, not
|
||||
uniform** (a refinement for future work). The *sign* is unambiguous.
|
||||
|
||||
### Takeaway
|
||||
Model collapse in real learners = **neutral drift ⊕ an architecture-specific estimator-bias
|
||||
operator that can point either way.** The histogram sits at the neutral null (`u=0, τ=1`); the VAE
|
||||
**sharpens** (adds collapse); the RNN/MLP **smooth** (add a diversity floor). This mechanistically
|
||||
explains the Layer-1.5 architecture-generality result and the softened neural `g*`, and develops
|
||||
the exact axis Riis (2026) names as future work ("different smoothing schemes… each induce their
|
||||
own fixed-point geometry… a natural direction for further work"). `u`/`τ` are calibrated from a
|
||||
single neural diagnostic and pinned in the configs. **Falsifier (not triggered):** if neutral drift
|
||||
had already reproduced the neural curves, the estimator axis would be superfluous — instead it fails
|
||||
in *both* regimes, oppositely.
|
||||
BIN
results/kernel_sharpen/kernel.pdf
Normal file
BIN
results/kernel_sharpen/kernel.pdf
Normal file
Binary file not shown.
BIN
results/kernel_sharpen/kernel.png
Normal file
BIN
results/kernel_sharpen/kernel.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 270 KiB |
14
results/kernel_sharpen/manifest.json
Normal file
14
results/kernel_sharpen/manifest.json
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"experiment": "kernel_sharpen",
|
||||
"master_seed": 20260705,
|
||||
"git_commit": "79bbc45f41822757e60d7f1994a82c5171a9e554",
|
||||
"python": "3.14.5",
|
||||
"libraries": {
|
||||
"numpy": "2.5.0",
|
||||
"scipy": "1.18.0",
|
||||
"pandas": "3.0.3",
|
||||
"pyarrow": "24.0.0"
|
||||
},
|
||||
"rows": 768,
|
||||
"results_sha256": "d5651a841d58f12abed1339476b1839fb03fe02329e38d675d0602ccb5691e52"
|
||||
}
|
||||
82
results/kernel_sharpen/resolved_config.yaml
Normal file
82
results/kernel_sharpen/resolved_config.yaml
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
experiment: kernel_sharpen
|
||||
seed: 20260705
|
||||
n_replicates: 24
|
||||
source_config:
|
||||
experiment: kernel_sharpen
|
||||
kind: lineage
|
||||
seed: 20260705
|
||||
n_replicates: 24
|
||||
truth:
|
||||
K: 30
|
||||
R: 1
|
||||
tail: zipf
|
||||
zipf_s: 1.5
|
||||
tail_threshold: 0.01
|
||||
init: truth
|
||||
dynamics:
|
||||
n: 6000
|
||||
grounding:
|
||||
m: 0
|
||||
policy: proportional
|
||||
kernel:
|
||||
reset: 0.0
|
||||
temperature: 1.0
|
||||
floor: 0.0
|
||||
generations: 15
|
||||
metrics:
|
||||
kl_floor: 1.0e-09
|
||||
support_eps: 1.0e-09
|
||||
sweep:
|
||||
- param: dynamics.kernel.temperature
|
||||
values:
|
||||
- 1.0
|
||||
- 0.8
|
||||
output:
|
||||
dir: results/kernel_sharpen
|
||||
grid:
|
||||
- label:
|
||||
temperature: 1.0
|
||||
lineage_cfg:
|
||||
truth:
|
||||
K: 30
|
||||
R: 1
|
||||
tail: zipf
|
||||
zipf_s: 1.5
|
||||
tail_threshold: 0.01
|
||||
init: truth
|
||||
dynamics:
|
||||
n: 6000
|
||||
grounding:
|
||||
m: 0
|
||||
policy: proportional
|
||||
kernel:
|
||||
reset: 0.0
|
||||
temperature: 1.0
|
||||
floor: 0.0
|
||||
generations: 15
|
||||
metrics:
|
||||
kl_floor: 1.0e-09
|
||||
support_eps: 1.0e-09
|
||||
- label:
|
||||
temperature: 0.8
|
||||
lineage_cfg:
|
||||
truth:
|
||||
K: 30
|
||||
R: 1
|
||||
tail: zipf
|
||||
zipf_s: 1.5
|
||||
tail_threshold: 0.01
|
||||
init: truth
|
||||
dynamics:
|
||||
n: 6000
|
||||
grounding:
|
||||
m: 0
|
||||
policy: proportional
|
||||
kernel:
|
||||
reset: 0.0
|
||||
temperature: 0.8
|
||||
floor: 0.0
|
||||
generations: 15
|
||||
metrics:
|
||||
kl_floor: 1.0e-09
|
||||
support_eps: 1.0e-09
|
||||
42
results/kernel_smooth/README.md
Normal file
42
results/kernel_smooth/README.md
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
# kernel — the learning kernel: why real learners deviate from neutral drift
|
||||
|
||||
*(This legend covers both `results/kernel_sharpen/` and `results/kernel_smooth/`; the figure
|
||||
`kernel.png` is written into both.)*
|
||||
|
||||
**Claim tested.** Neutral Wright–Fisher drift (the histogram bridge, and the baseline of Riis
|
||||
2026) is the *null* model of collapse. But Layer 1.5 showed real trained models deviate from it —
|
||||
and in **opposite directions**. Can a single extension of Layer 1 — a parameterized *learning
|
||||
kernel* on the refit step, `p_{t+1} = T_θ(counts/n)` — reproduce both deviations, and does neutral
|
||||
drift genuinely fail without it?
|
||||
|
||||
**Setup.** The kernel (`knowledge/kernel.py`) has two population-genetics knobs, both reducing to
|
||||
neutral drift at their defaults (so the histogram and every scientific-validation test are
|
||||
unchanged): **reset `u`** — mutation toward a prior (`p ← (1−u)p + u·π`), i.e. smoothing; and
|
||||
**temperature `τ`** — sharpening (`p ∝ p^{1/τ}`, `τ<1` concentrates), i.e. mode-competition. Two
|
||||
matched-to-neural regimes, 24 replicates each.
|
||||
|
||||
### The four panels (`kernel.png`; blue = neutral, red = kernel-on, green dashed = the real neural model)
|
||||
**Top row — VAE regime (`n=6000`, `K=30`), pro-collapse:**
|
||||
1. **Heterozygosity.** Neutral drift is **inert** — at `n=6000` it barely moves (`H` stays at
|
||||
`H*`). Yet the real VAE (green) collapsed to `H≈0`. **Sharpening (`τ=0.8`) reproduces the
|
||||
collapse.** Neutral drift is *falsified*; the estimator's mode-competition is required.
|
||||
2. **Support.** Neutral holds ~all 30 modes; sharpening → 1 mode, matching the VAE.
|
||||
|
||||
**Bottom row — RNN regime (`n=200`, `K=256`), anti-collapse:**
|
||||
3. **Heterozygosity.** Neutral drift drives `H → 0`, but the real RNN (green) only *partially*
|
||||
collapses (`H` floors at ~0.68). **Mutation `u=0.006` reproduces the floor.** The estimator here
|
||||
*removes* collapse pressure.
|
||||
4. **Forward-KL.** Neutral diverges; smoothing plateaus. Honest caveat: uniform-mutation plateaus
|
||||
*above* the RNN's KL (~5 vs ~2) — evidence the RNN's smoothing target is **truth-like, not
|
||||
uniform** (a refinement for future work). The *sign* is unambiguous.
|
||||
|
||||
### Takeaway
|
||||
Model collapse in real learners = **neutral drift ⊕ an architecture-specific estimator-bias
|
||||
operator that can point either way.** The histogram sits at the neutral null (`u=0, τ=1`); the VAE
|
||||
**sharpens** (adds collapse); the RNN/MLP **smooth** (add a diversity floor). This mechanistically
|
||||
explains the Layer-1.5 architecture-generality result and the softened neural `g*`, and develops
|
||||
the exact axis Riis (2026) names as future work ("different smoothing schemes… each induce their
|
||||
own fixed-point geometry… a natural direction for further work"). `u`/`τ` are calibrated from a
|
||||
single neural diagnostic and pinned in the configs. **Falsifier (not triggered):** if neutral drift
|
||||
had already reproduced the neural curves, the estimator axis would be superfluous — instead it fails
|
||||
in *both* regimes, oppositely.
|
||||
BIN
results/kernel_smooth/kernel.pdf
Normal file
BIN
results/kernel_smooth/kernel.pdf
Normal file
Binary file not shown.
BIN
results/kernel_smooth/kernel.png
Normal file
BIN
results/kernel_smooth/kernel.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 270 KiB |
14
results/kernel_smooth/manifest.json
Normal file
14
results/kernel_smooth/manifest.json
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"experiment": "kernel_smooth",
|
||||
"master_seed": 20260705,
|
||||
"git_commit": "79bbc45f41822757e60d7f1994a82c5171a9e554",
|
||||
"python": "3.14.5",
|
||||
"libraries": {
|
||||
"numpy": "2.5.0",
|
||||
"scipy": "1.18.0",
|
||||
"pandas": "3.0.3",
|
||||
"pyarrow": "24.0.0"
|
||||
},
|
||||
"rows": 4848,
|
||||
"results_sha256": "8e50c084f8b3ca707375e677a977c00ac36aa96b3363ebecefad2d96a9912f3f"
|
||||
}
|
||||
82
results/kernel_smooth/resolved_config.yaml
Normal file
82
results/kernel_smooth/resolved_config.yaml
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
experiment: kernel_smooth
|
||||
seed: 20260705
|
||||
n_replicates: 24
|
||||
source_config:
|
||||
experiment: kernel_smooth
|
||||
kind: lineage
|
||||
seed: 20260705
|
||||
n_replicates: 24
|
||||
truth:
|
||||
K: 256
|
||||
R: 1
|
||||
tail: zipf
|
||||
zipf_s: 1.3
|
||||
tail_threshold: 0.001
|
||||
init: truth
|
||||
dynamics:
|
||||
n: 200
|
||||
grounding:
|
||||
m: 0
|
||||
policy: proportional
|
||||
kernel:
|
||||
reset: 0.0
|
||||
temperature: 1.0
|
||||
floor: 0.0
|
||||
generations: 100
|
||||
metrics:
|
||||
kl_floor: 1.0e-09
|
||||
support_eps: 1.0e-09
|
||||
sweep:
|
||||
- param: dynamics.kernel.reset
|
||||
values:
|
||||
- 0.0
|
||||
- 0.006
|
||||
output:
|
||||
dir: results/kernel_smooth
|
||||
grid:
|
||||
- label:
|
||||
reset: 0.0
|
||||
lineage_cfg:
|
||||
truth:
|
||||
K: 256
|
||||
R: 1
|
||||
tail: zipf
|
||||
zipf_s: 1.3
|
||||
tail_threshold: 0.001
|
||||
init: truth
|
||||
dynamics:
|
||||
n: 200
|
||||
grounding:
|
||||
m: 0
|
||||
policy: proportional
|
||||
kernel:
|
||||
reset: 0.0
|
||||
temperature: 1.0
|
||||
floor: 0.0
|
||||
generations: 100
|
||||
metrics:
|
||||
kl_floor: 1.0e-09
|
||||
support_eps: 1.0e-09
|
||||
- label:
|
||||
reset: 0.006
|
||||
lineage_cfg:
|
||||
truth:
|
||||
K: 256
|
||||
R: 1
|
||||
tail: zipf
|
||||
zipf_s: 1.3
|
||||
tail_threshold: 0.001
|
||||
init: truth
|
||||
dynamics:
|
||||
n: 200
|
||||
grounding:
|
||||
m: 0
|
||||
policy: proportional
|
||||
kernel:
|
||||
reset: 0.006
|
||||
temperature: 1.0
|
||||
floor: 0.0
|
||||
generations: 100
|
||||
metrics:
|
||||
kl_floor: 1.0e-09
|
||||
support_eps: 1.0e-09
|
||||
|
|
@ -11,6 +11,8 @@ from __future__ import annotations
|
|||
from dataclasses import dataclass, field, replace
|
||||
from typing import Any, Mapping, Optional
|
||||
|
||||
from .kernel import LearningKernelCfg
|
||||
|
||||
# Runtime-tunable knobs live here, defaults chosen to match the blueprint's illustrative
|
||||
# schema. Nothing here is a magic number buried in algorithm code.
|
||||
|
||||
|
|
@ -61,6 +63,7 @@ class DynamicsCfg:
|
|||
grounding: GroundingCfg = field(default_factory=GroundingCfg)
|
||||
selection: SelectionCfg = field(default_factory=SelectionCfg)
|
||||
remint: RemintCfg = field(default_factory=RemintCfg)
|
||||
kernel: LearningKernelCfg = field(default_factory=LearningKernelCfg)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
|
|
@ -89,6 +92,7 @@ class LineageCfg:
|
|||
grounding=_sub(dyn_raw.get("grounding", {}), GroundingCfg),
|
||||
selection=_sub(dyn_raw.get("selection", {}), SelectionCfg),
|
||||
remint=_sub(dyn_raw.get("remint", {}), RemintCfg),
|
||||
kernel=_sub(dyn_raw.get("kernel", {}), LearningKernelCfg),
|
||||
)
|
||||
metrics = _sub(cfg.get("metrics", {}), MetricsCfg)
|
||||
return LineageCfg(
|
||||
|
|
|
|||
87
src/knowledge/kernel.py
Normal file
87
src/knowledge/kernel.py
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
"""The learning kernel — the estimator/inductive-bias operator (Layer-1 extension).
|
||||
|
||||
Neutral Wright-Fisher models the generational step as *resample and refit the raw empirical
|
||||
distribution* (``p_{t+1} = counts/n``). A real learner does not refit the raw histogram: it
|
||||
applies a **biased estimator** — it smooths (regularises toward a simpler distribution) and it
|
||||
can sharpen (concentrate mass, drop weakly-supported modes). Layer 1.5 measured exactly these
|
||||
biases: the RNN/MLP *over-smooth* (H resists collapse, spurious tail support stays alive), the
|
||||
VAE *self-reinforces* (accelerating collapse to a single mode). This module turns the refit into
|
||||
a parameterised kernel ``p_{t+1} = T_θ(counts/n)`` so the analytic core can reproduce those
|
||||
deviations — the axis Riis (2026) names as future work.
|
||||
|
||||
Two population-genetics knobs, both reducing to the neutral null at their defaults:
|
||||
|
||||
* **reset ``u``** — mutation toward a prior: ``p <- (1-u)·p + u·π``. Models smoothing /
|
||||
regularisation. Gives a diversity floor (H stops collapsing to 0), keeps rare modes alive, and
|
||||
softens the grounding threshold — the RNN/MLP signature.
|
||||
* **temperature ``τ`` + floor ``ε``** — sharpening / support pruning: ``p ∝ p^{1/τ}`` then drop
|
||||
mass below ``ε``. Models the winner-take-all mode competition of a mode-dropping generator —
|
||||
the VAE signature. ``τ<1`` sharpens (pro-collapse).
|
||||
|
||||
Identity (``u=0, τ=1, ε=0``) recovers Layer 1 exactly, so the histogram bridge and every
|
||||
scientific-validation test are unchanged.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LearningKernelCfg:
|
||||
"""Estimator-bias knobs for the generational refit (all defaults = neutral Wright-Fisher).
|
||||
|
||||
Attributes:
|
||||
reset (float): Mutation rate ``u`` toward the prior (smoothing). 0 = off.
|
||||
temperature (float): Sharpening temperature ``τ`` (``p ∝ p^{1/τ}``); <1 sharpens
|
||||
(mode competition), >1 flattens, 1 = off.
|
||||
floor (float): Hard support threshold ``ε``: modes below it are dropped. 0 = off.
|
||||
prior (str): Smoothing target for ``reset``: currently ``uniform``.
|
||||
"""
|
||||
|
||||
reset: float = 0.0
|
||||
temperature: float = 1.0
|
||||
floor: float = 0.0
|
||||
prior: str = "uniform"
|
||||
|
||||
@property
|
||||
def is_identity(self) -> bool:
|
||||
"""True when the kernel is the neutral refit (recovers Layer 1 exactly)."""
|
||||
return self.reset == 0.0 and self.temperature == 1.0 and self.floor == 0.0
|
||||
|
||||
|
||||
def _prior_vector(kind: str, K: int) -> np.ndarray:
|
||||
"""Return the smoothing-target distribution of length ``K``."""
|
||||
if kind == "uniform":
|
||||
return np.full(K, 1.0 / K)
|
||||
raise ValueError(f"unknown kernel prior {kind!r} (expected uniform)")
|
||||
|
||||
|
||||
def apply_kernel(p: np.ndarray, cfg: LearningKernelCfg) -> np.ndarray:
|
||||
"""Apply the estimator-bias kernel to a refit distribution.
|
||||
|
||||
Composition order: mutation toward the prior (keeps modes alive) -> sharpening (concentrates
|
||||
mass) -> support floor (drops weak modes). Each step is a no-op at its default.
|
||||
|
||||
Args:
|
||||
p (np.ndarray): The raw refit distribution ``counts/n`` (length ``K``, sums to 1).
|
||||
cfg (LearningKernelCfg): The kernel knobs.
|
||||
|
||||
Returns:
|
||||
np.ndarray: The estimator's distribution ``p_{t+1}`` (length ``K``, sums to 1).
|
||||
"""
|
||||
p = np.asarray(p, dtype=float)
|
||||
if cfg.is_identity:
|
||||
return p
|
||||
K = p.size
|
||||
if cfg.reset > 0.0: # mutation toward the prior (smoothing)
|
||||
p = (1.0 - cfg.reset) * p + cfg.reset * _prior_vector(cfg.prior, K)
|
||||
if cfg.temperature != 1.0: # sharpening / flattening
|
||||
with np.errstate(divide="ignore"):
|
||||
p = np.power(p, 1.0 / cfg.temperature)
|
||||
if cfg.floor > 0.0: # hard support pruning
|
||||
p = np.where(p < cfg.floor, 0.0, p)
|
||||
total = p.sum()
|
||||
return p / total if total > 0 else np.full(K, 1.0 / K)
|
||||
|
|
@ -66,6 +66,7 @@ def run_lineage(cfg: Mapping[str, Any] | LineageCfg, seed: int) -> pd.DataFrame:
|
|||
regions=regions,
|
||||
selection_mode=cfg.dynamics.selection.mode,
|
||||
novelty_alpha=cfg.dynamics.selection.novelty_alpha,
|
||||
kernel=cfg.dynamics.kernel,
|
||||
)
|
||||
remint = cfg.dynamics.remint
|
||||
|
||||
|
|
|
|||
|
|
@ -7,10 +7,12 @@ in the perspective paper is one operator here; they compose in the order below.
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
import numpy as np
|
||||
|
||||
from .kernel import LearningKernelCfg, apply_kernel
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class StepCtx:
|
||||
|
|
@ -24,6 +26,8 @@ class StepCtx:
|
|||
regions (np.ndarray): Length-K region index per item.
|
||||
selection_mode (str): ``none`` | ``greedy`` | ``qd``.
|
||||
novelty_alpha (float): QD novelty exponent (0 recovers greedy).
|
||||
kernel (LearningKernelCfg): Estimator-bias kernel applied to the refit (default
|
||||
identity = neutral Wright-Fisher).
|
||||
"""
|
||||
|
||||
n: int
|
||||
|
|
@ -32,6 +36,7 @@ class StepCtx:
|
|||
regions: np.ndarray
|
||||
selection_mode: str = "none"
|
||||
novelty_alpha: float = 0.0
|
||||
kernel: LearningKernelCfg = field(default_factory=LearningKernelCfg)
|
||||
|
||||
|
||||
def _normed(p: np.ndarray) -> np.ndarray:
|
||||
|
|
@ -166,6 +171,7 @@ def generation_step(teachers: list[np.ndarray], p_star_eff: np.ndarray,
|
|||
cfg.regions, cfg.policy, rng)
|
||||
counts = c_syn + c_real
|
||||
p_next = counts / counts.sum()
|
||||
p_next = apply_kernel(p_next, cfg.kernel) # (estimator bias)
|
||||
p_next = apply_selection(p_next, p_star_eff, # (selection)
|
||||
cfg.selection_mode, cfg.novelty_alpha)
|
||||
return p_next
|
||||
|
|
|
|||
|
|
@ -245,8 +245,33 @@ C3 vertical claim deferred.*
|
|||
confirmed on real images. VAE needs ~10% grounding vs synthetic ~5% (cf. the `grounding` finding).
|
||||
**99 tests green** (+5 torchvision-gated). `make mnist` / `make env-mnist` (kept out of `make neural`).
|
||||
|
||||
**2026-07-05 — learning kernel (Layer-1 extension) + Riis positioning.**
|
||||
|
||||
- Prompted by revisiting Layer 1 vs 1.5 and the Riis 2026 paper (arXiv:2604.08554). Added
|
||||
`knowledge/kernel.py` (`LearningKernelCfg`: reset `u` = smoothing, temperature `τ` = sharpening,
|
||||
floor `ε`), wired into `step.generation_step` (post-refit) / `StepCtx` / `DynamicsCfg` — **identity
|
||||
by default, so the 68 Layer-1 scientific-validation + correctness tests are unchanged.**
|
||||
- **Result:** neutral Wright–Fisher fails BOTH neural models, oppositely. VAE regime (n=6000,K=30):
|
||||
neutral is inert, sharpening `τ=0.8` reproduces the collapse-to-one-mode. RNN regime (n=200,K=256):
|
||||
neutral → H=0, mutation `u=0.006` reproduces the H-floor (~0.68). Uniform-mutation overshoots the
|
||||
RNN's KL → its prior is truth-like, not uniform (honest caveat, future refinement).
|
||||
- `configs/layer1/kernel_{sharpen,smooth}.yaml`, `figures/plot_kernel.py` (overlays analytic arms vs
|
||||
the committed neural endpoints), READMEs, `tests/test_kernel.py` (+6). Wired into `make layer1`.
|
||||
- **Strategic (see CLAUDE.md finding):** concede "collapse=drift" to Riis (prior art; cite); his
|
||||
mixed environment retains OLD SYNTHETIC (no real-data injection) → pessimistic, no g* that prevents
|
||||
collapse. Our defensible novelty: recombination "merge-don't-average" (flagship), the learning-kernel
|
||||
axis (he flags as future work), grounding-as-immigration, real-weights+MNIST breadth, and the
|
||||
Lamarckian society + vertical/cumulative C3 claim (not yet run). Reposition: from "collapse is drift"
|
||||
to a population-genetic CONTROL THEORY for sustaining open-ended knowledge.
|
||||
|
||||
## Remaining (all optional)
|
||||
|
||||
- [ ] **Learning-kernel refinement:** truth-like smoothing prior (`prior="truth"`) + measurement floor
|
||||
for a quantitative RNN match; **multi-locus / linkage** modes (class×style) as the rigorous home for
|
||||
recombination. Both enrich predictive power and separate us further from Riis's single-locus n-grams.
|
||||
- [ ] **The Lamarckian society experiments** (multi-agent grounding + decorrelated specialists +
|
||||
recombination + QD-selection + re-mint) and the **vertical/cumulative C3 claim** — the highest-ceiling,
|
||||
wholly-novel frame; not yet entered.
|
||||
- [ ] **`region_matched`** grounding (R>1), **`remint`** re-mint gate (optional).
|
||||
- [ ] **VAE fidelity:** fix the prior-hole mismatch (KL-annealing / free-bits / larger latent) so it
|
||||
clears the gen-0 gate, then add to `architectures`. Or document as a known limitation.
|
||||
|
|
|
|||
72
tests/test_kernel.py
Normal file
72
tests/test_kernel.py
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
"""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
|
||||
Loading…
Add table
Add a link
Reference in a new issue