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>
95 lines
3.7 KiB
Python
95 lines
3.7 KiB
Python
"""The true distribution p* over K knowledge items (blueprint 2.1, 2.7).
|
|
|
|
``p*`` is fixed and deliberately heavy-tailed: most mass on common ("head") items, a
|
|
long thin tail of rare items whose loss *is* model collapse. Items are partitioned into
|
|
R disjoint regions; regions are what grounding and specialisation target.
|
|
|
|
Region design: each region is an identical, independently-normalised block carrying
|
|
mass 1/R, so regions are symmetric and every region has its own head and tail. This
|
|
makes region-matched grounding (E3) well posed and, at R=1, reduces to a single global
|
|
Zipf identical to the reference used by the scientific-validation suite.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
|
|
import numpy as np
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class TrueDist:
|
|
"""The fixed ground truth for a lineage.
|
|
|
|
Attributes:
|
|
p_star (np.ndarray): Length-K probability vector (sums to 1).
|
|
regions (np.ndarray): Length-K int array; region index of each item.
|
|
tail_mask (np.ndarray): Length-K bool array; True for designated tail items.
|
|
"""
|
|
|
|
p_star: np.ndarray
|
|
regions: np.ndarray
|
|
tail_mask: np.ndarray
|
|
|
|
|
|
def make_true_distribution(
|
|
K: int,
|
|
R: int,
|
|
tail: str,
|
|
tail_frac: float,
|
|
zipf_s: float,
|
|
seed: int,
|
|
*,
|
|
tail_threshold: float = 1e-3,
|
|
) -> TrueDist:
|
|
"""Construct the true distribution, region assignment, and tail mask.
|
|
|
|
Args:
|
|
K (int): Number of knowledge items. Must be divisible by R.
|
|
R (int): Number of regions (disjoint contiguous blocks of size K/R).
|
|
tail (str): Tail family, ``"zipf"`` or ``"twocomponent"``.
|
|
tail_frac (float): For ``twocomponent``, the fraction of each region's items
|
|
placed in the low-mass tail component. Unused for ``zipf``.
|
|
zipf_s (float): Zipf exponent (larger -> heavier head, thinner tail).
|
|
seed (int): Present for interface symmetry; the construction is deterministic,
|
|
so the seed only matters if a randomised item ordering is added later.
|
|
tail_threshold (float): Items with ``p*_i < tail_threshold`` are the tail
|
|
(blueprint 2.3). Keyword-only.
|
|
|
|
Returns:
|
|
TrueDist: p_star, regions, tail_mask.
|
|
|
|
Raises:
|
|
ValueError: If ``K`` is not divisible by ``R`` or ``tail`` is unknown.
|
|
"""
|
|
if K % R != 0:
|
|
raise ValueError(f"K={K} must be divisible by R={R} (contiguous equal regions).")
|
|
per = K // R
|
|
regions = np.repeat(np.arange(R), per)
|
|
|
|
if tail == "zipf":
|
|
block = 1.0 / np.arange(1, per + 1, dtype=float) ** zipf_s
|
|
block = block / block.sum() # each region normalised to 1
|
|
elif tail == "twocomponent":
|
|
n_tail = max(1, int(round(tail_frac * per)))
|
|
block = np.ones(per, dtype=float)
|
|
# Tail items sit safely below threshold; heads carry the rest. Per-region mass
|
|
# is 1/R after the global normalisation below.
|
|
block[per - n_tail:] = tail_threshold * 0.5 * R
|
|
block = block / block.sum()
|
|
else:
|
|
raise ValueError(f"unknown tail family {tail!r} (expected zipf|twocomponent)")
|
|
|
|
p_star = np.tile(block, R) / R # R blocks, total mass 1
|
|
p_star = p_star / p_star.sum() # guard float drift
|
|
tail_mask = p_star < tail_threshold
|
|
return TrueDist(p_star=p_star, regions=regions, tail_mask=tail_mask)
|
|
|
|
|
|
def uniform_init(K: int) -> np.ndarray:
|
|
"""The maximum-entropy initial distribution p_0 = 1/K (blueprint: the fresh base).
|
|
|
|
The default lineage start. The neutral-decay law E[H_t]=H_0(1-1/n)^t holds from any
|
|
start, so this choice fixes H_0 = 1 - 1/K without loss of generality.
|
|
"""
|
|
return np.full(K, 1.0 / K)
|