MachineSex/tests/test_mating_system.py
Giorgio Gilestro ab3dc10587 Restructure: descriptive tier and experiment names, paper/manuscript
- paper/pnas -> paper/manuscript (venue-neutral)
- configs/layer1 -> configs/inheritance, src/knowledge -> src/inheritance
  (imported as `inheritance`), make layer1 -> make inheritance; layer2 alias dropped
- inheritance and trained-network bundles named after the manuscript figure
  they feed (fig2_grounding_sweep, figS3_rebaselining, ...), or descriptively
  where they feed none; configs keep their `experiment:` value so parquet
  hashes are unchanged, only output.dir moves
- figure scripts, SI figure sources, notebooks, REPRODUCING.md, README and the
  SI Methods/tables updated; make clean no longer deletes tracked manifests;
  reproduce.sh hashes the s{seed}/ layouts too

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y64o8FKP7rCuXzC48pxpMm
2026-09-13 17:00:40 +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 inheritance.mating_system import _distinct_peaks, _diversity, run_mating_system
from inheritance.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