MachineSex/tests/test_dynamic_society.py
Giorgio Gilestro 0f7b775ae5 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>
2026-07-05 12:34:01 +01:00

58 lines
2.7 KiB
Python

"""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