MachineSex/tests/test_mating_system.py
Giorgio Gilestro f5f68f5249 E14: mating systems — monogamy vs promiscuity (mate-pool breadth)
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>
2026-07-09 12:38:50 +01:00

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