A new analytic experiment on an orthogonal evolution-of-sex axis: not the recombination RATE (E9) but the population's mating STRUCTURE. Agents on a ring recombine with a second parent drawn from a window of breadth b (b->0 monogamous/isolation-by-distance, b=1 promiscuous/panmictic), under local selection, swept against NK ruggedness K. Finding: the optimal mate-pool breadth SHRINKS as skills get more entangled. Wide/promiscuous merging wins the champion on additive landscapes (K<=3, b=0.6), but on rugged ones (K>=6) it prematurely converges to a worse champion and an intermediate breadth (b~0.35) wins; pure monogamy over-fragments. Throughout, promiscuity monotonically lifts the population MEAN but destroys diversity and parallel exploration. The design rule extends E9: merge widely for additive skills, keep island-structured sub-populations for entangled ones — a merging-native axis the panmixia-assuming literature lacks. - src/knowledge/mating_system.py + experiment.py dispatch (kind: mating_system) - configs/layer1/E14.yaml (breadth x K sweep, 20 reps, bitwise-reproducible) - figures/plot_E14.py; results/E14/ (figure, README, manifest, resolved config) - tests/test_mating_system.py (+5, 147 green); make layer1 wired - folded into both papers (full + accessible) as the third §5 result Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
57 lines
2.5 KiB
Python
57 lines
2.5 KiB
Python
"""Mating-system tests (pure NumPy) — monogamy vs promiscuity as mate-pool breadth (E14).
|
|
|
|
Cover the diversity helpers and the two load-bearing behaviours: promiscuity (wide mate-pool breadth)
|
|
monotonically destroys standing diversity, and the run is deterministic and well-formed. The full
|
|
ruggedness crossover (intermediate breadth wins the champion on rugged landscapes) is a swept,
|
|
multi-replicate result asserted only in aggregate here to keep the test fast.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import numpy as np
|
|
|
|
from knowledge.mating_system import _distinct_peaks, _diversity, run_mating_system
|
|
from knowledge.genotype import nk_fitness
|
|
|
|
|
|
def _run(breadth: float, K: int = 6, seed: int = 0, gens: int = 40, N: int = 32, L: int = 10):
|
|
cfg = {"mating": {"L": L, "N": N, "breadth": breadth, "K": K, "recomb_rate": 0.5, "mu": 0.005},
|
|
"generations": gens}
|
|
return run_mating_system(cfg, seed=seed)
|
|
|
|
|
|
def test_diversity_zero_for_clones_and_positive_for_spread():
|
|
clones = np.ones((5, 8), dtype=np.int8)
|
|
assert _diversity(clones) == 0.0 # identical -> no diversity
|
|
spread = np.array([[0] * 8, [1] * 8], dtype=np.int8)
|
|
assert np.isclose(_diversity(spread), 1.0) # opposite -> maximal diversity
|
|
|
|
|
|
def test_distinct_peaks_counts_basins():
|
|
fitness = nk_fitness(6, 2, seed=0)
|
|
pop = np.zeros((4, 6), dtype=np.int8) # all identical -> one basin
|
|
assert _distinct_peaks(pop, fitness, 6) == 1
|
|
|
|
|
|
def test_schema_and_bounds():
|
|
df = _run(0.5)
|
|
for col in ["generation", "best_fitness", "mean_fitness", "diversity", "distinct_peaks", "global_opt"]:
|
|
assert col in df.columns
|
|
assert (df["best_fitness"] <= df["global_opt"] + 1e-9).all() # nothing beats reality's optimum
|
|
assert (df["diversity"] >= 0).all() and (df["diversity"] <= 1).all()
|
|
assert df["distinct_peaks"].iloc[-1] >= 1
|
|
|
|
|
|
def test_deterministic_given_seed():
|
|
a = _run(0.3, seed=7)
|
|
b = _run(0.3, seed=7)
|
|
assert np.allclose(a["best_fitness"], b["best_fitness"]) # pure function of the seed
|
|
|
|
|
|
def test_promiscuity_destroys_diversity():
|
|
# Averaged over replicates, wide mate-pool breadth (promiscuity) leaves LESS standing diversity than
|
|
# narrow breadth (monogamy) — the homogenisation effect, robust on a rugged landscape.
|
|
def final_div(b):
|
|
return np.mean([_run(b, K=8, seed=s, gens=40, N=32, L=10)["diversity"].iloc[-1]
|
|
for s in range(6)])
|
|
assert final_div(1.0) < final_div(0.05) # panmixia < isolation-by-distance
|