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

@ -167,6 +167,46 @@ def run_genotype_experiment(cfg: dict) -> pd.DataFrame:
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:
"""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":
from .society import run_directed_sex # E10: directed sex beats biology
df = run_directed_sex(cfg)
elif kind == "dynamic_society":
df = run_dynamic_experiment(cfg) # E11: the dynamic society (C3)
else:
df = run_experiment(cfg)
save_artifacts(cfg, df, out_dir)