society: multi-locus recombination frame — the vertical claim (E7/E8)

Enter the Lamarckian society with a robust theoretical frame. The single-
locus, fixed-p* model can only express recovery toward a ceiling; the
society's load-bearing claim is vertical -- capability that EXCEEDS any
component. Generalize knowledge to a distribution over genotypes (L
biallelic loci, K=2^L, additive fitness = # correct loci), reusing all the
K-mode machinery. The one new operator is recombination: free recombination
sends p -> product of per-locus marginals (linkage equilibrium).

E8 (star, kind: society) -- the vertical claim / Fisher-Muller: decorrelated
PARENTS (specialists, expert on their loci, agnostic elsewhere) are
recombined; sexual merge assembles a genotype fitter than any parent,
climbing to the optimum (12/12, a genotype no parent had) as parent count
grows and rho->0, while the best single parent (~8.7) and the mean-mixture
"model soup" (~11.6) plateau below. Reuses make_retention_matrix (locus
mastery replaces tail-item retention).

E7 (kind: genotype_lineage) -- the advantage of sex: a single population
adapts toward the optimum; the sexual lineage adapts faster than asexual
(clonal interference) by keeping loci in linkage equilibrium (LD->0 vs LD
spike). Honest scope: a speed advantage, not a permanent Muller's-ratchet
gap (subtle to force); E8 carries the headline.

Metaphor shift (per GG): the society is sexual reproduction with UNBOUNDED
parents, not teacher->pupil. Teacher->pupil caps at the ceiling; n-parent
recombination is combinatorial and generative, and unlike biology there is
no two-parent limit. Collapse = asexual degradation; the cure = sex. This
unifies E4 (merge != average) + E6 (irreversibility) under evolution-of-sex
theory and reaches ground Riis's single-locus n-grams cannot.

New: knowledge/{genotype,genotype_lineage,society}.py, configs/layer1/{E7,
E8}.yaml, figures/plot_{E7,E8}.py, READMEs, tests/test_genotype.py (+7).
experiment.py dispatch (kind in {genotype_lineage, society}); make layer1
wired. 112 tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Giorgio Gilestro 2026-07-05 10:51:41 +01:00
parent 871bc39ec6
commit 62c68d6c8c
22 changed files with 879 additions and 3 deletions

90
tests/test_genotype.py Normal file
View file

@ -0,0 +1,90 @@
"""Multi-locus / recombination tests (pure NumPy) — the spine of the society frame.
Cover the genotype algebra (bits, additive fitness, linkage equilibrium), the recombination
operators (identity at rate 0, product-of-marginals at rate 1), and the two headline behaviours:
E8 sexual recombination of decorrelated parents *exceeds* the best parent; E7 a sexual lineage
adapts faster than an asexual one.
"""
from __future__ import annotations
import numpy as np
from knowledge.genotype import (
additive_fitness, genotype_bits, linkage_equilibrium, locus_marginals, mutate,
recombine, recombine_teachers,
)
from knowledge.genotype_lineage import run_genotype_lineage
from knowledge.society import make_specialist, run_society
from knowledge.teachers import make_retention_matrix
def test_genotype_bits_and_additive_fitness():
bits = genotype_bits(3)
assert bits.shape == (8, 3)
assert np.array_equal(bits[0], [0, 0, 0]) and np.array_equal(bits[7], [1, 1, 1])
f = additive_fitness(3)
assert f[0] == 0 and f[7] == 3 and f[1] == 1 # MSB-first: g=1 -> [0,0,1]
def test_linkage_equilibrium_is_product_and_normalised():
q = np.array([0.9, 0.5, 0.2])
p = linkage_equilibrium(q)
assert np.isclose(p.sum(), 1.0)
# P(all-correct) = ∏ q; P(all-wrong) = ∏ (1-q)
assert np.isclose(p[-1], np.prod(q)) and np.isclose(p[0], np.prod(1 - q))
assert np.allclose(locus_marginals(p, 3), q) # marginals round-trip
def test_recombine_identity_and_free():
rng = np.random.default_rng(0)
p = rng.dirichlet(np.ones(16)) # L=4
assert np.array_equal(recombine(p, 4, 0.0), p) # rate 0 = asexual (unchanged)
free = recombine(p, 4, 1.0) # rate 1 = product of marginals
assert np.allclose(free, linkage_equilibrium(locus_marginals(p, 4)))
assert np.isclose(free.sum(), 1.0)
def test_mutate_normalises_and_spreads():
L = 4
p = np.zeros(1 << L); p[0] = 1.0 # point mass on all-wrong
out = mutate(p, L, 0.1)
assert np.isclose(out.sum(), 1.0)
assert out[0] < 1.0 and (out > 0).sum() > 1 # mass leaks to neighbours
def test_recombine_teachers_assembles_optimum_only_sexually():
# Two complementary parents: one masters the low loci, the other the high loci. Sexual merge
# assembles the all-correct optimum (a genotype NEITHER parent has as its mode); clonal cannot.
L = 8
lo = np.array([1, 1, 1, 1, 0, 0, 0, 0], dtype=bool)
parents = [make_specialist(lo, 0.9, 0.45), make_specialist(~lo, 0.9, 0.45)]
f = additive_fitness(L)
best_parent = max(f[int(np.argmax(p))] for p in parents)
sexual = f[int(np.argmax(recombine_teachers(parents, L, 1.0)))]
clonal = f[int(np.argmax(recombine_teachers(parents, L, 0.0)))]
assert sexual == L # sexual assembles the optimum
assert sexual > best_parent # ... which exceeds either parent
assert clonal <= best_parent + 0 # clonal (soup) does not assemble it
def test_e8_sexual_exceeds_best_parent_and_soup():
cfg = {"experiment": "e8t", "seed": 1, "n_replicates": 8,
"society": {"L": 10, "q": 0.5, "hi": 0.9, "lo": 0.45},
"sweep": [{"param": "K_T", "values": [8]}, {"param": "rho", "values": [0.0]}]}
df = run_society(cfg)
m = df.mean(numeric_only=True)
assert m["sexual"] > m["best_parent"] + 1.5 # clearly exceeds the best parent
assert m["sexual"] >= m["average"] # and is at least as good as the soup
def test_e7_sexual_adapts_at_least_as_fast():
base = {"genotype": {"L": 10, "n": 150, "mu": 0.02, "base": 1.3, "init": "wrong"},
"generations": 25}
asex = run_genotype_lineage({**base, "genotype": {**base["genotype"], "recomb_rate": 0.0}}, 0)
sex = run_genotype_lineage({**base, "genotype": {**base["genotype"], "recomb_rate": 1.0}}, 0)
mid = 12
a = asex[asex["generation"] == mid]["mean_fitness"].iloc[0]
s = sex[sex["generation"] == mid]["mean_fitness"].iloc[0]
assert s >= a - 1e-9 # sexual adapts at least as fast mid-run
assert sex["ld"].max() < asex["ld"].max() # ... by keeping loci in linkage equilibrium