society: the dynamic Lamarckian society — the vertical claim (E11 / C3)

The culmination. A finite population of agents (genotypes, L loci) evolves
on a rugged NK landscape that IS reality (knowledge/dynamic_society.py),
composing the four operators the whole study built toward: grounding,
directed recombination (sex), quality-diversity selection, and mutation.
Grounding is made load-bearing via the consensus-conformity (self-
consumption) mechanism (GG decision): selection acts on
g*true_fitness + (1-g)*conformity, where conformity = agreement with the
population's own consensus, so at g=0 the society optimises fitting-the-
crowd rather than reality.

4-arm ablation (12 reps), each breaking distinctly, only the full society
climbing (global_opt ~ 0.79):
- full         0.78  climbs to the optimum, diversity maintained longest
- no_sex       0.77  can't recombine to escape local optima
- no_diversity 0.74  greedy: collapses diversity fastest, worse local optimum
- no_grounding 0.48  self-consumption collapse to an unfit consensus
                     (trains on the crowd -> confident-but-wrong mean;
                      conformity-true gap ~ 0.5)

This integrates E1-E6 + the learning kernel + E7-E10 into one system and
shows the Lamarckian society needs ALL of grounding + directed sex +
diversity: on a rugged landscape you need diversity to explore basins, sex
to recombine them, and grounding to select on reality -- remove any one and
you fail differently. Closes the C3 vertical claim analytically; the LLM
rung remains the eventual empirical instantiation.

New: knowledge/dynamic_society.py, configs/layer1/E11.yaml, figures/
plot_E11.py, README, tests/test_dynamic_society.py (+5). kind:
dynamic_society dispatch; make layer1 wired. 122 tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Giorgio Gilestro 2026-07-05 12:34:01 +01:00
parent 48181a1c84
commit 0f7b775ae5
13 changed files with 460 additions and 5 deletions

View file

@ -84,6 +84,8 @@ E4's whole purpose is to isolate the effect of teacher **decorrelation ρ**, so
**Finding (2026-07-05, E9/E10 — the sexual-transmission model made rigorous: when sex helps, and directed sex).** Deepening the sexual metaphor (GG excited; wanted it robust before the full society). Added a **Kauffman NK landscape** (`genotype.nk_fitness`, tunable ruggedness `K`), finite **crossover** (`genotype.crossover`, n-parent, per-gap recombination rate), and **hill-climb** (parents = local optima = "trained models"). **E9 (`kind: recomb_landscape`) — landscape robustness / "why sex?":** E8's dramatic transgression used an *additive* landscape; on rugged (epistatic) landscapes, blindly recombining local optima causes **outbreeding depression** — mean offspring fall *below* the parents, worse with ruggedness AND recombination rate (`K=8`, free recomb: ≈ 0.23), and the **optimal recombination rate shrinks as ruggedness grows**. Design rule: *merge freely when skills are complementary/additive; sparingly + with selection when entangled.* **E10 (`kind: directed_sex`) — directed sex beats biological sex (the AI superpower):** biology is stuck with 2 random-mating parents and no offspring preview; an AI can **choose complementary mates + evaluate many recombinant offspring + keep the fittest + use unbounded parents** (iterated recombine-then-select). Result: random ("biological") sex craters with ruggedness (0.66→0.51), while **directed sex tracks/exceeds the best parent at every ruggedness** — converting the outbreeding-depression catastrophe into a win. This is the practical, distinctly-AI payoff and has no biological analog. `configs/layer1/{E9,E10}.yaml`, `plot_{E9,E10}.py`, READMEs, +5 tests (117 green). Complete sexual-transmission picture: **dramatic super-parent offspring when skills are complementary (E8); outbreeding-depression risk when entangled (E9); directed sex resolves the risk (E10).** **Finding (2026-07-05, E9/E10 — the sexual-transmission model made rigorous: when sex helps, and directed sex).** Deepening the sexual metaphor (GG excited; wanted it robust before the full society). Added a **Kauffman NK landscape** (`genotype.nk_fitness`, tunable ruggedness `K`), finite **crossover** (`genotype.crossover`, n-parent, per-gap recombination rate), and **hill-climb** (parents = local optima = "trained models"). **E9 (`kind: recomb_landscape`) — landscape robustness / "why sex?":** E8's dramatic transgression used an *additive* landscape; on rugged (epistatic) landscapes, blindly recombining local optima causes **outbreeding depression** — mean offspring fall *below* the parents, worse with ruggedness AND recombination rate (`K=8`, free recomb: ≈ 0.23), and the **optimal recombination rate shrinks as ruggedness grows**. Design rule: *merge freely when skills are complementary/additive; sparingly + with selection when entangled.* **E10 (`kind: directed_sex`) — directed sex beats biological sex (the AI superpower):** biology is stuck with 2 random-mating parents and no offspring preview; an AI can **choose complementary mates + evaluate many recombinant offspring + keep the fittest + use unbounded parents** (iterated recombine-then-select). Result: random ("biological") sex craters with ruggedness (0.66→0.51), while **directed sex tracks/exceeds the best parent at every ruggedness** — converting the outbreeding-depression catastrophe into a win. This is the practical, distinctly-AI payoff and has no biological analog. `configs/layer1/{E9,E10}.yaml`, `plot_{E9,E10}.py`, READMEs, +5 tests (117 green). Complete sexual-transmission picture: **dramatic super-parent offspring when skills are complementary (E8); outbreeding-depression risk when entangled (E9); directed sex resolves the risk (E10).**
**Finding (2026-07-05, E11 — the dynamic Lamarckian society: the vertical claim / C3, realized).** The culmination: a finite population of `N` agents (genotypes, `L` loci) evolves on a rugged NK landscape that *is* reality (`knowledge/dynamic_society.py`), composing the four operators the whole study built toward — grounding, directed recombination (sex), quality-diversity selection, mutation. Grounding is made load-bearing via the **consensus-conformity (self-consumption)** mechanism (GG decision): selection acts on `g·true_fitness + (1g)·conformity` (conformity = agreement with the population's own consensus), so `g=0` optimises fitting-the-crowd rather than reality. **4-arm ablation (12 reps), each breaking distinctly, only the full society climbing (global_opt≈0.79):** `full` 0.78 (climbs to the optimum, diversity maintained longest) · `no_sex` 0.77 (can't recombine to escape local optima) · `no_diversity`/greedy 0.74 (collapses diversity fastest, stuck at a worse local optimum) · **`no_grounding` 0.48 (self-consumption collapse to an unfit consensus** — trains on the crowd, regresses to a confident-but-wrong mean; conformitytrue gap ≈0.5). This integrates E1E6 + the kernel + E7E10 into one system and shows the society needs **all** of grounding + directed sex + diversity: on a rugged landscape you need diversity to explore basins, sex to recombine them, grounding to select on reality — remove any and you fail differently. `configs/layer1/E11.yaml`, `plot_E11.py`, README, +5 tests (122 green). **This closes the C3 vertical claim analytically** (the LLM rung remains the eventual empirical instantiation).
## Build order (blueprint §7) — respect the gate ## Build order (blueprint §7) — respect the gate
1. Scaffold: repo layout (§5), container, pytest skeleton, config system, seeding utils. `make test` green. 1. Scaffold: repo layout (§5), container, pytest skeleton, config system, seeding utils. `make test` green.

View file

@ -16,7 +16,7 @@ test: ## correctness tests + scientific-validation tests (the spine
uv run pytest uv run pytest
layer1: ## run experiments E1-E6 + the learning-kernel bridge (analytic) layer1: ## run experiments E1-E6 + the learning-kernel bridge (analytic)
for e in E1 E2 E3 E4 E5 E6 E7 E8 E9 E10 kernel_sharpen kernel_smooth; do uv run python -m knowledge.experiment configs/layer1/$$e.yaml; done for e in E1 E2 E3 E4 E5 E6 E7 E8 E9 E10 E11 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) neural: ## run Layer 1.5 synthetic neural experiments (excludes the heavy MNIST tier)
for c in configs/neural/*.yaml; do case "$$c" in *mnist*) ;; \ for c in configs/neural/*.yaml; do case "$$c" in *mnist*) ;; \

40
configs/layer1/E11.yaml Normal file
View file

@ -0,0 +1,40 @@
experiment: E11
kind: dynamic_society
seed: 20260705
n_replicates: 12
# (The dynamic Lamarckian society — the vertical claim, C3): a finite population of agents (genotypes)
# evolves on a rugged NK fitness landscape that IS reality. The full society composes the four
# operators the whole study built toward — grounding, directed recombination (sex), quality-diversity
# selection, and mutation — and a 4-arm ablation shows each is load-bearing. Grounding is made load-
# bearing via the consensus-conformity (self-consumption) mechanism: selection acts on
# g*true_fitness + (1-g)*conformity, so at g=0 the society optimises agreement with its own majority
# rather than reality and drifts to a fit-looking but actually-poor consensus. Expect: FULL climbs to
# near the global optimum while maintaining diversity longest; NO_GROUNDING collapses to the unfit
# consensus; NO_SEX plateaus (can't recombine to escape local optima); NO_DIVERSITY (greedy) collapses
# diversity fast and stalls at a worse local optimum. Falsifier: an ablation matches the full society,
# or the full society fails to exceed every ablation.
society:
L: 12
K: 8 # landscape ruggedness (epistasis) — rugged enough that diversity + sex matter
N: 60 # population size
g: 0.85 # grounding fraction (overwritten to 0 in the no_grounding arm)
mu: 0.03 # per-locus mutation rate
novelty: 0.5 # quality-diversity weight (0 in the no_diversity/greedy arm)
n_off: 120 # directed-recombination offspring pool per generation
recomb_rate: 0.2 # crossover rate
sex: true # directed recombination on (false in the no_sex arm)
select: qd # quality-diversity survival (greedy in the no_diversity arm)
generations: 80
sweep:
- param: arm
values:
- {name: full, set: {}}
- {name: no_grounding, set: {society.g: 0.0}}
- {name: no_sex, set: {society.sex: false}}
- {name: no_diversity, set: {society.select: greedy, society.novelty: 0.0}}
output: {dir: results/E11}

68
figures/plot_E11.py Normal file
View file

@ -0,0 +1,68 @@
"""E11 figure — the dynamic Lamarckian society: the vertical claim (C3).
A finite population of agents evolves on a rugged NK landscape (reality). The **full** society
grounding + directed recombination (sex) + quality-diversity selection climbs to the global optimum
while maintaining diversity longest. A 4-arm ablation shows every operator is load-bearing, each
breaking distinctly: **no_grounding** collapses to a fit-looking but actually-poor consensus
(self-consumption); **no_sex** plateaus (can't recombine to escape local optima); **no_diversity**
(greedy) collapses diversity fastest and stalls at a worse local optimum.
Three panels over generations: (A) best real capability the vertical climb, full highest, no_grounding
crashing below the rest; (B) population diversity full explores longest, no_grounding collapses
almost immediately; (C) the self-consumption signature conformity minus true fitness (how far the
population's mutual agreement exceeds its real capability), largest for no_grounding. Reads only the
committed bundle.
Usage: python figures/plot_E11.py [results/E11]
"""
from __future__ import annotations
import sys
from pathlib import Path
import matplotlib.pyplot as plt
sys.path.insert(0, str(Path(__file__).parent))
from _figlib import load_bundle, mean_ci, savefig # noqa: E402
_ARMS = [("full", "#2ca02c", "full society"),
("no_sex", "#ff7f0e", "no sex (no recombination)"),
("no_diversity", "#9467bd", "no diversity (greedy)"),
("no_grounding", "#d62728", "no grounding (self-consumption)")]
def main(results_dir: str = "results/E11") -> None:
df, _ = load_bundle(results_dir)
arms = [a for a in _ARMS if a[0] in set(df["arm"].unique())]
g_opt = df["global_opt"].mean()
fig, axes = plt.subplots(1, 3, figsize=(16, 4.8))
def traj(ax, col, title, ylabel, hline=None):
for name, c, lab in arms:
sub = df[df["arm"] == name]
g, m, ci = mean_ci(sub, "generation", col)
ax.plot(g, m, "-", color=c, lw=1.9, label=lab)
ax.fill_between(g, m - ci, m + ci, color=c, alpha=0.15)
if hline is not None:
ax.axhline(hline[0], ls=":", color="gray", lw=1, label=hline[1])
ax.set(xlabel="generation", ylabel=ylabel, title=title)
ax.legend(frameon=False, fontsize=8)
traj(axes[0], "best_fitness", "The vertical climb: general capability\n"
"(full climbs highest; no-grounding collapses)", "best real fitness",
hline=(g_opt, "global optimum"))
traj(axes[1], "diversity", "Specialties maintained: diversity during search\n"
"(full explores longest; ablations collapse fast)", "population diversity")
traj(axes[2], "conformity_true_gap", "Self-consumption signature:\n"
"agreement minus real capability", "conformity true fitness")
fig.suptitle("E11 — the dynamic Lamarckian society: grounding + directed sex + diversity climb to "
"the optimum; remove any one and it breaks (the vertical claim, C3)", y=1.02, fontsize=12)
fig.tight_layout()
savefig(fig, results_dir, "E11")
if __name__ == "__main__":
main(*sys.argv[1:])

BIN
results/E11/E11.pdf Normal file

Binary file not shown.

BIN
results/E11/E11.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 KiB

42
results/E11/README.md Normal file
View file

@ -0,0 +1,42 @@
# E11 — the dynamic Lamarckian society: the vertical claim (C3)
**Claim tested.** The culmination. Everything before was a *single operator*; the society's
load-bearing, un-preempted claim is that composing them makes **general capability climb over
generations while specialties are re-earned**, and that **every operator is necessary** — remove one
and it breaks. This is blueprint C3, the "vertical."
**Setup.** A finite population of `N=60` agents (genotypes of `L=12` biallelic loci) evolves on a
**rugged Kauffman NK landscape** (`K=8`) that *is* reality. Each generation composes: grounding,
**directed recombination (sex)**, **quality-diversity selection**, and mutation. Grounding is made
load-bearing by the **consensus-conformity (self-consumption)** mechanism — selection acts on
`g·true_fitness + (1g)·conformity`, where conformity = agreement with the population's own consensus,
so at `g=0` the society optimises fitting-the-crowd rather than reality. A 4-arm ablation, 12
replicate landscapes, 80 generations.
### Symbols
- **agent** = a model (a genotype); **reality** = the NK fitness landscape; **grounding `g`** = share of the selection signal that is real fitness vs conformity.
- **directed sex** = recombine many parents + keep the fittest offspring (E10). **quality-diversity** = select for capability *and* novelty, so specialties persist.
- **conformity true fitness** = how far the population's mutual agreement exceeds its real capability (the self-consumption "delusion" signature).
### The three panels (over generations)
1. **The vertical climb.** Best real capability: the **full society (green) climbs to the global
optimum** (≈0.78 of 0.79), while **no_grounding (red) collapses** to a fit-looking but poor
consensus (0.48). `no_sex` and `no_diversity` plateau *below* the full society.
2. **Specialties maintained.** Population diversity: the **full society explores longest** (diversity
decays slowly as it searches), while every ablation collapses diversity fast — `no_grounding`
almost immediately.
3. **Self-consumption signature.** Conformity minus true fitness: **largest for `no_grounding`** — the
population strongly *agrees* while being *wrong*, the signature of training on its own consensus.
### Takeaway — every operator is load-bearing
- **full** → climbs to the optimum with diversity maintained. **The vertical claim.**
- **no_grounding** → self-consumption collapse to an unfit consensus (train on the crowd → regress to a confident, wrong mean).
- **no_sex** → can't recombine complementary specialists to escape local optima → plateaus below full.
- **no_diversity (greedy)** → collapses diversity fastest, gets stuck at a *worse* local optimum.
This integrates the whole study — E1E6 (collapse/grounding/selection), the learning kernel, and
E7E10 (sexual transmission) — into one system, and shows the Lamarckian society needs **all** of
grounding + directed sex + diversity to climb without collapsing. On a rugged landscape you need
diversity to explore basins, sex to recombine them, and grounding to select on reality; remove any and
you fail differently. **Falsifier (not triggered):** if any ablation had matched the full society, or
the full society had failed to exceed every ablation, the integration claim would fail.

14
results/E11/manifest.json Normal file
View file

@ -0,0 +1,14 @@
{
"experiment": "E11",
"master_seed": 20260705,
"git_commit": "48181a1c846f41e9254abe4b1d0f843d1df9667f",
"python": "3.14.5",
"libraries": {
"numpy": "2.5.0",
"scipy": "1.18.0",
"pandas": "3.0.3",
"pyarrow": "24.0.0"
},
"rows": 3888,
"results_sha256": "e8d2c3ecb5200578d16a977d147b6ee05ebf83e1a81d9287f6a740114ef3968a"
}

View file

@ -0,0 +1,37 @@
experiment: E11
seed: 20260705
n_replicates: 12
source_config:
experiment: E11
kind: dynamic_society
seed: 20260705
n_replicates: 12
society:
L: 12
K: 8
N: 60
g: 0.85
mu: 0.03
novelty: 0.5
n_off: 120
recomb_rate: 0.2
sex: true
select: qd
generations: 80
sweep:
- param: arm
values:
- name: full
set: {}
- name: no_grounding
set:
society.g: 0.0
- name: no_sex
set:
society.sex: false
- name: no_diversity
set:
society.select: greedy
society.novelty: 0.0
output:
dir: results/E11

View file

@ -0,0 +1,143 @@
"""The dynamic Lamarckian society — the vertical claim (E11 / C3).
A finite population of ``N`` agents (genotypes of ``L`` biallelic loci) evolves on a Kauffman NK
landscape that *is* reality. The society climbs in real capability by composing the four operators the
whole study built toward **grounding**, **directed recombination (sex)**, **quality-diversity
selection**, and mutation and an ablation shows each is load-bearing.
The crux is what happens WITHOUT grounding. A plain genetic algorithm on true fitness would just
improve, so grounding must corrupt the *selection signal* to cause collapse. Here selection acts on a
**grounded score** ``g·true_fitness + (1g)·conformity``, where conformity is agreement with the
population's own consensus (modal genotype). At ``g=0`` selection rewards fitting the crowd rather
than reality self-consumption and the society drifts to a fit-looking but actually-poor consensus,
losing diversity: the direct analogue of training on the majority of AI-generated outputs.
Four ablation arms, each breaking distinctly (only ``full`` avoids all three failures):
``full`` (climbs) · ``no_grounding`` (conformity collapse) · ``no_sex`` (stuck at local optima) ·
``no_diversity`` (collapses to one lineage, recombination starves).
"""
from __future__ import annotations
from typing import Any, Mapping
import numpy as np
import pandas as pd
from .genotype import bits_to_index, crossover, genotype_bits, nk_fitness
def _consensus(pop_bits: np.ndarray) -> np.ndarray:
"""Population consensus genotype: the modal allele at each locus (majority vote)."""
return (pop_bits.mean(axis=0) >= 0.5).astype(pop_bits.dtype)
def _conformity(pop_bits: np.ndarray, consensus: np.ndarray) -> np.ndarray:
"""Per-agent agreement with the consensus (fraction of loci matching the majority)."""
return (pop_bits == consensus[None, :]).mean(axis=1)
def _novelty(pop_bits: np.ndarray) -> np.ndarray:
"""Per-agent novelty: mean Hamming distance to the rest of the population (diversity signal)."""
N, L = pop_bits.shape
if N < 2:
return np.zeros(N)
# pairwise Hamming via allele agreement: distance_ij = L - matches; mean over j != i.
match = (pop_bits[:, None, :] == pop_bits[None, :, :]).sum(axis=2) # (N, N) matches
ham = L - match
return (ham.sum(axis=1) / (N - 1)) / L # normalised to [0,1]
def _directed_offspring(pop_bits, fitness, n_off, rate, rng):
"""Directed sex: make ``n_off`` recombinants from the whole population, return them ranked-ready.
Unbounded-parent crossover (the AI move); offspring selection happens in the survival step, so
here we just generate the candidate offspring bit-matrix.
"""
return np.stack([crossover(pop_bits, rate, rng) for _ in range(n_off)])
def run_dynamic_society(cfg: Mapping[str, Any], seed: int) -> pd.DataFrame:
"""Run one dynamic-society lineage; return per-generation metrics.
Args:
cfg (Mapping): Config with a ``society`` block (``L``, ``K`` landscape ruggedness, ``N``
population, ``g`` grounding, ``mu`` mutation, ``novelty`` QD weight, ``n_off`` offspring
pool, ``recomb_rate``, ``sex`` on/off, ``select`` in {``qd``, ``greedy``}) and
``generations``.
seed (int): Replicate seed; the landscape and the run are a pure function of it.
Returns:
pd.DataFrame: One row per generation with ``best_fitness`` (real), ``mean_fitness`` (real),
``diversity`` (mean normalised pairwise Hamming), ``consensus_fitness``,
``conformity_true_gap`` (mean conformity mean true fitness; exposes the no-grounding
collapse), and ``global_opt``.
"""
soc = cfg["society"]
L, K, N = int(soc["L"]), int(soc["K"]), int(soc["N"])
g = float(soc.get("g", 1.0))
mu = float(soc.get("mu", 0.02))
novelty_w = float(soc.get("novelty", 0.0))
n_off = int(soc.get("n_off", N))
rate = float(soc.get("recomb_rate", 0.2))
sex = bool(soc.get("sex", True))
select = soc.get("select", "qd")
generations = int(cfg.get("generations", 100))
fitness = nk_fitness(L, K, seed) # reality
global_opt = float(fitness.max())
all_bits = genotype_bits(L)
rng = np.random.default_rng(seed)
# Initialise a diverse population of random genotypes.
pop = rng.integers(0, 2, size=(N, L)).astype(all_bits.dtype)
def true_fit(bits):
return np.array([fitness[bits_to_index(b)] for b in bits])
rows: list[dict] = []
def record(t: int) -> None:
tf = true_fit(pop)
cons = _consensus(pop)
conf = _conformity(pop, cons)
rows.append({
"generation": t,
"best_fitness": float(tf.max()),
"mean_fitness": float(tf.mean()),
"diversity": float(_novelty(pop).mean()),
"consensus_fitness": float(fitness[bits_to_index(cons)]),
"conformity_true_gap": float(conf.mean() - tf.mean()),
"global_opt": global_opt,
})
record(0)
for t in range(1, generations + 1):
# (1) candidate pool = current population + directed offspring (sex) or mutated clones.
if sex:
offspring = _directed_offspring(pop, fitness, n_off, rate, rng)
else: # asexual: offspring are mutated copies
idx = rng.integers(0, N, size=n_off)
offspring = pop[idx].copy()
# mutation on the offspring
flip = rng.random(offspring.shape) < mu
offspring = np.where(flip, 1 - offspring, offspring).astype(pop.dtype)
pool = np.concatenate([pop, offspring], axis=0)
# (2) grounded score: g*true_fitness + (1-g)*conformity (conformity vs the *current* consensus).
cons = _consensus(pop)
tf = true_fit(pool)
conf = _conformity(pool, cons)
score = g * tf + (1.0 - g) * conf
# (3) survival: QD (score + novelty) keeps diverse high-scorers; greedy keeps top score only.
if select == "qd" and novelty_w > 0.0:
nov = _novelty(pool)
merit = score + novelty_w * nov
else:
merit = score
keep = np.argsort(merit)[-N:] # elitist truncation survival
pop = pool[keep]
record(t)
return pd.DataFrame(rows)

View file

@ -167,6 +167,46 @@ def run_genotype_experiment(cfg: dict) -> pd.DataFrame:
return out return out
_DYNAMIC_KEYS = ("society", "generations")
def run_dynamic_experiment(cfg: dict) -> pd.DataFrame:
"""Run the dynamic society across an ``arm`` ablation sweep x replicates (E11).
Mirrors ``run_genotype_experiment`` but assembles the base from the ``society``/``generations``
blocks and calls ``run_dynamic_society``. Arms are named override bundles (reuse ``_apply_param``
``arm`` handling), e.g. ``no_grounding`` sets ``society.g=0``.
"""
from .dynamic_society import run_dynamic_society
base = {k: copy.deepcopy(cfg[k]) for k in _DYNAMIC_KEYS if k in cfg}
sweeps = cfg.get("sweep", [])
if isinstance(sweeps, dict):
sweeps = [sweeps]
params = [s["param"] for s in sweeps]
value_lists = [list(s["values"]) for s in sweeps]
combos = [({}, base)] if not sweeps else []
for values in itertools.product(*value_lists):
lin = copy.deepcopy(base)
label: dict = {}
for param, val in zip(params, values):
label.update(_apply_param(lin, param, val))
combos.append((label, lin))
seeds = spawn_seeds(int(cfg["seed"]), int(cfg["n_replicates"]))
frames: list[pd.DataFrame] = []
for label, lin in combos:
for rep, ss in enumerate(seeds):
df = run_dynamic_society(lin, int(ss.generate_state(1)[0]))
for col, val in label.items():
df[col] = val
df["replicate"] = rep
frames.append(df)
out = pd.concat(frames, ignore_index=True)
out.insert(0, "experiment", cfg["experiment"])
return out
def run_coverage(cfg: dict) -> pd.DataFrame: def run_coverage(cfg: dict) -> pd.DataFrame:
"""E4 runner: multi-teacher recombination coverage (blueprint 2.5-E4 / 2.7.1). """E4 runner: multi-teacher recombination coverage (blueprint 2.5-E4 / 2.7.1).
@ -332,6 +372,8 @@ def run_and_save(config_path: str | Path) -> Path:
elif kind == "directed_sex": elif kind == "directed_sex":
from .society import run_directed_sex # E10: directed sex beats biology from .society import run_directed_sex # E10: directed sex beats biology
df = run_directed_sex(cfg) df = run_directed_sex(cfg)
elif kind == "dynamic_society":
df = run_dynamic_experiment(cfg) # E11: the dynamic society (C3)
else: else:
df = run_experiment(cfg) df = run_experiment(cfg)
save_artifacts(cfg, df, out_dir) save_artifacts(cfg, df, out_dir)

View file

@ -296,11 +296,20 @@ C3 vertical claim deferred.*
risk when entangled (E9); directed sex resolves it (E10). `configs/layer1/{E9,E10}.yaml`, risk when entangled (E9); directed sex resolves it (E10). `configs/layer1/{E9,E10}.yaml`,
`plot_{E9,E10}.py`, READMEs, +5 tests (117 green). `plot_{E9,E10}.py`, READMEs, +5 tests (117 green).
## Remaining (all optional / next) **2026-07-05 — the dynamic Lamarckian society (E11): the vertical claim / C3 realized.**
- [ ] **The dynamic society:** an evolving *population of parents* (specialists) that ground + - `knowledge/dynamic_society.py`: finite population of N agents (genotypes) on a rugged NK landscape
recombine + select over generations — capability climbing while specialties are re-earned (the full (reality); composes grounding + directed sex + quality-diversity selection + mutation. Grounding
C3, dynamic). E7/E8 give the static + single-population pieces; the multi-parent *lineage* is next. made load-bearing via consensus-conformity (self-consumption): selection on
`g·true_fitness + (1-g)·conformity` (GG decision). `kind: dynamic_society` dispatch.
- **4-arm ablation (12 reps), each breaks distinctly (global_opt≈0.79):** full 0.78 (climbs to optimum,
diversity maintained longest); no_sex 0.77; no_diversity/greedy 0.74; **no_grounding 0.48
(self-consumption collapse to unfit consensus).** Only the full society climbs. Integrates E1-E6 +
kernel + E7-E10 into one system: needs ALL of grounding + directed sex + diversity.
- `configs/layer1/E11.yaml`, `plot_E11.py`, README, `tests/test_dynamic_society.py` (+5, 122 green).
Closes C3 analytically; the LLM rung remains the eventual empirical instantiation.
## Remaining (all optional / next)
- [ ] **NK/epistasis landscape** (sign epistasis can make recombination harmful — the honest limit of - [ ] **NK/epistasis landscape** (sign epistasis can make recombination harmful — the honest limit of
"sex always helps"); **multi-allelic loci**. Deepens the frame. "sex always helps"); **multi-allelic loci**. Deepens the frame.
- [ ] **Learning-kernel refinement:** truth-like smoothing prior (`prior="truth"`) + measurement floor - [ ] **Learning-kernel refinement:** truth-like smoothing prior (`prior="truth"`) + measurement floor

View file

@ -0,0 +1,58 @@
"""Dynamic-society tests (pure NumPy) — the culminating vertical claim (E11 / C3).
Cover the finite-population operators (consensus, conformity, novelty) and the four ablation
behaviours: the full society climbs to near the optimum; removing grounding collapses it to an unfit
consensus (self-consumption); removing sex or diversity leaves it stuck below the full society.
"""
from __future__ import annotations
import numpy as np
from knowledge.dynamic_society import _conformity, _consensus, _novelty, run_dynamic_society
def _run(arm_overrides: dict, seed: int = 0, gens: int = 50):
base = {"L": 10, "K": 6, "N": 50, "g": 0.85, "mu": 0.03, "novelty": 0.5,
"n_off": 100, "recomb_rate": 0.2, "sex": True, "select": "qd"}
base.update(arm_overrides)
return run_dynamic_society({"society": base, "generations": gens}, seed=seed)
def test_consensus_and_conformity():
pop = np.array([[1, 1, 0, 0], [1, 0, 0, 1], [1, 1, 1, 0]], dtype=np.int8)
cons = _consensus(pop)
assert np.array_equal(cons, [1, 1, 0, 0]) # majority vote per locus
conf = _conformity(pop, cons)
assert np.isclose(conf[0], 1.0) # agent 0 == consensus
assert conf.min() >= 0.0 and conf.max() <= 1.0
def test_novelty_is_zero_for_clones_and_high_for_spread():
clones = np.ones((4, 8), dtype=np.int8)
assert np.allclose(_novelty(clones), 0.0) # identical -> no diversity
spread = np.array([[0] * 8, [1] * 8], dtype=np.int8)
assert np.allclose(_novelty(spread), 1.0) # opposite -> maximal diversity
def test_full_society_climbs_toward_optimum():
df = _run({})
go = df["global_opt"].iloc[0]
assert df["best_fitness"].iloc[-1] > df["best_fitness"].iloc[0] + 0.05 # it climbs
assert df["best_fitness"].iloc[-1] > 0.9 * go # ... to near the optimum
def test_no_grounding_collapses_to_unfit_consensus():
full = _run({})["best_fitness"].iloc[-1]
dry = _run({"g": 0.0})
assert dry["best_fitness"].iloc[-1] < full - 0.1 # far below the grounded society
assert dry["diversity"].iloc[-1] < 0.05 # diversity collapsed
assert dry["conformity_true_gap"].iloc[-1] > 0.3 # agreement >> real capability (delusion)
def test_ablations_stay_below_the_full_society():
full = _run({})["best_fitness"].iloc[-1]
no_sex = _run({"sex": False})["best_fitness"].iloc[-1]
no_div = _run({"select": "greedy", "novelty": 0.0})["best_fitness"].iloc[-1]
assert no_sex <= full + 1e-6 and no_div <= full + 1e-6 # neither beats the full society
assert min(no_sex, no_div) < full # ... and at least one is strictly worse