Layer 1 core: Wright-Fisher knowledge-transmission model with E1-E2

Scaffold plus the Layer 1 analytical core and the first two experiments.

- knowledge/: truth, metrics, teachers (2.7.1 shared-switch construction),
  step, lineage, experiment, config, seeding (imported as `knowledge`).
- Validation spine green: neutral decay (Pred 1), fixation (Pred 2), exact
  mutation-drift equilibrium (Pred 3), union coverage (Pred 5). 68 tests pass.
- E1 reproduces tail-first collapse. E2 delivers the headline: a grounding
  phase boundary g* << 1, with stationary H tracking the exact H_eq closed
  form (g=0.005 -> 68% of truth diversity; g=0.05 -> 96%).
- Reproducibility: uv venv from a hash-pinned uv.lock is the source of truth;
  every run writes results.parquet + resolved_config.yaml + manifest.json
  (lib versions, git commit, sha256). Figures and manifests tracked; the
  large regenerable parquet is gitignored.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Giorgio Gilestro 2026-07-04 18:10:18 +02:00
commit a6eb9b7512
33 changed files with 4356 additions and 0 deletions

106
src/knowledge/lineage.py Normal file
View file

@ -0,0 +1,106 @@
"""Run a single lineage for T generations (blueprint 2.7 interface).
``run_lineage`` is the core driver every experiment builds on. It accepts a plain nested
mapping (the scientific-validation conformance tests pass one directly) or a
:class:`~knowledge.config.LineageCfg`, and returns a tidy DataFrame with one row per
generation (0..T inclusive) carrying every blueprint-2.3 metric, global and per-region.
"""
from __future__ import annotations
from typing import Any, Mapping
import numpy as np
import pandas as pd
from .config import LineageCfg
from .metrics import forward_kl, heterozygosity, per_region, support_size, tail_mass
from .step import StepCtx, allocate_m, generation_step
from .truth import make_true_distribution, uniform_init
def run_lineage(cfg: Mapping[str, Any] | LineageCfg, seed: int) -> pd.DataFrame:
"""Run one lineage and return per-generation metrics.
Args:
cfg (Mapping | LineageCfg): Resolved lineage configuration (blueprint 2.7 schema).
seed (int): Seed for this replicate; the run is a pure function of (cfg, seed).
Returns:
pd.DataFrame: One row per generation 0..T with columns ``generation``,
``heterozygosity``, ``forward_kl``, ``tail_mass``, ``support_size``, and
per-region ``H_region_{r}`` / ``tail_region_{r}`` columns.
"""
cfg = LineageCfg.from_dict(cfg)
td = make_true_distribution(
cfg.truth.K, cfg.truth.R, cfg.truth.tail, cfg.truth.tail_frac,
cfg.truth.zipf_s, seed, tail_threshold=cfg.truth.tail_threshold,
)
p_star_orig = td.p_star # the ORIGINAL truth; forward_kl is always vs this
regions = td.regions
tail_mask = td.tail_mask # tail defined on the original truth
kl_floor = cfg.metrics.kl_floor
eps = cfg.metrics.support_eps
rng = np.random.default_rng(seed)
if cfg.truth.init == "uniform":
p = uniform_init(cfg.truth.K)
elif cfg.truth.init == "truth":
p = p_star_orig.copy()
else:
raise ValueError(f"unknown init {cfg.truth.init!r} (expected uniform|truth)")
p_star_eff = p_star_orig.copy() # grounding reference; may be re-minted (E6)
m_vector = allocate_m(cfg.dynamics.grounding.m, cfg.truth.R,
cfg.dynamics.grounding.policy)
step_ctx = StepCtx(
n=cfg.dynamics.n,
m_vector=m_vector,
policy=cfg.dynamics.grounding.policy,
regions=regions,
selection_mode=cfg.dynamics.selection.mode,
novelty_alpha=cfg.dynamics.selection.novelty_alpha,
)
remint = cfg.dynamics.remint
rows: list[dict] = []
head_mask = ~tail_mask
n_tail = int(tail_mask.sum())
n_head = int(head_mask.sum())
def record(t: int, p: np.ndarray) -> None:
# Tail-vs-head SURVIVAL is the honest collapse signature: under neutral drift the
# mean tail *mass* is a martingale (conserved), but tail *items* go extinct first.
row = {
"generation": t,
"heterozygosity": heterozygosity(p),
"forward_kl": forward_kl(p_star_orig, p, kl_floor),
"tail_mass": tail_mass(p, tail_mask),
"support_size": support_size(p, eps),
"tail_support": int(np.sum(p[tail_mask] > eps)),
"head_support": int(np.sum(p[head_mask] > eps)),
"tail_frac_alive": (float(np.mean(p[tail_mask] > eps)) if n_tail else 0.0),
"head_frac_alive": (float(np.mean(p[head_mask] > eps)) if n_head else 0.0),
}
if cfg.truth.R > 1: # per-region columns only when there is >1 region
for r, v in per_region(heterozygosity, p, regions).items():
row[f"H_region_{r}"] = v
for r, v in per_region(tail_mass, p, regions, tail_mask).items():
row[f"tail_region_{r}"] = v
rows.append(row)
record(0, p)
for t in range(1, cfg.generations + 1):
p = generation_step([p], p_star_eff, step_ctx, rng)
if (remint.enabled and remint.period and t % remint.period == 0):
# Re-mint (founder event): the current distribution becomes the new grounding
# reference and the original truth is discarded for grounding purposes. Gated
# on diversity: only re-mint if H is high enough (E6). forward_kl stays vs the
# original truth, so a collapsed re-mint locks KL high forever.
if remint.H_gate is None or heterozygosity(p) >= remint.H_gate:
p_star_eff = p.copy()
record(t, p)
return pd.DataFrame(rows)