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>
This commit is contained in:
parent
9fea375ff8
commit
f5f68f5249
13 changed files with 472 additions and 2 deletions
2
Makefile
2
Makefile
|
|
@ -16,7 +16,7 @@ test: ## correctness tests + scientific-validation tests (the spine
|
||||||
uv run pytest
|
uv run pytest
|
||||||
|
|
||||||
layer1: ## run experiments E1-E6 + the learning-kernel bridge (analytic)
|
layer1: ## run experiments E1-E6 + the learning-kernel bridge (analytic)
|
||||||
for e in E1 E2 E3 E4 E5 E6 E7 E8 E9 E10 E11 E12 E12_nk kernel_sharpen kernel_smooth; do uv run python -m knowledge.experiment configs/layer1/$$e.yaml; done
|
for e in E1 E2 E3 E4 E5 E6 E7 E8 E9 E10 E11 E12 E12_nk E14 kernel_sharpen kernel_smooth; do uv run python -m knowledge.experiment configs/layer1/$$e.yaml; done
|
||||||
|
|
||||||
neural: ## run Layer 1.5 synthetic neural experiments (excludes the MNIST/torchvision tiers)
|
neural: ## run Layer 1.5 synthetic neural experiments (excludes the MNIST/torchvision tiers)
|
||||||
for c in configs/neural/*.yaml; do case "$$c" in *mnist*|*speciation_real*) ;; \
|
for c in configs/neural/*.yaml; do case "$$c" in *mnist*|*speciation_real*) ;; \
|
||||||
|
|
|
||||||
34
configs/layer1/E14.yaml
Normal file
34
configs/layer1/E14.yaml
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
experiment: E14
|
||||||
|
kind: mating_system
|
||||||
|
seed: 20260709
|
||||||
|
n_replicates: 20
|
||||||
|
|
||||||
|
# (Mating systems — monogamy vs promiscuity): a finite population of genotypes evolves on a Kauffman
|
||||||
|
# NK landscape, recombining sexually, but the MATE-POOL BREADTH is swept. Agents sit on a ring; an
|
||||||
|
# offspring's second parent is drawn from a window of half-width ~ breadth*N/2 around the focal parent,
|
||||||
|
# and selection is LOCAL (offspring competes only against the incumbent at its ring position). breadth
|
||||||
|
# -> 0 is monogamous / structured (local mating, isolation by distance); breadth = 1 is promiscuous /
|
||||||
|
# panmictic (mate with anyone). Crossed with ruggedness K, this is the mating-system image of the E9
|
||||||
|
# design rule. Expect: on smooth landscapes (K low) promiscuity maximises the best fitness (spread the
|
||||||
|
# single good direction fastest); as ruggedness rises the OPTIMAL breadth SHRINKS toward an intermediate
|
||||||
|
# value (full promiscuity prematurely converges below it); and diversity + occupied local optima are
|
||||||
|
# monotonically destroyed by breadth at every K, most severely on rugged landscapes. Falsifier: the best
|
||||||
|
# breadth is independent of K (no crossover), or promiscuity is best at every ruggedness.
|
||||||
|
|
||||||
|
mating:
|
||||||
|
L: 12 # loci (genotype space 2^L)
|
||||||
|
N: 48 # population size (ring positions)
|
||||||
|
breadth: 1.0 # mate-pool breadth in [0,1] (overwritten by the sweep)
|
||||||
|
K: 0 # landscape ruggedness / epistasis (overwritten by the sweep)
|
||||||
|
recomb_rate: 0.5 # per-gap crossover rate (near-free reassortment within a mating)
|
||||||
|
mu: 0.003 # per-locus mutation rate
|
||||||
|
|
||||||
|
generations: 60
|
||||||
|
|
||||||
|
sweep:
|
||||||
|
- param: mating.K
|
||||||
|
values: [0, 3, 6, 10]
|
||||||
|
- param: mating.breadth
|
||||||
|
values: [0.03, 0.08, 0.17, 0.35, 0.6, 1.0]
|
||||||
|
|
||||||
|
output: {dir: results/E14}
|
||||||
68
figures/plot_E14.py
Normal file
68
figures/plot_E14.py
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
"""E14 figure — mating systems: monogamy vs promiscuity (mate-pool breadth) across ruggedness.
|
||||||
|
|
||||||
|
Three panels, each vs mate-pool breadth (log x: 0.03 = monogamous/structured -> 1.0 = promiscuous/
|
||||||
|
panmictic), one line per landscape ruggedness K:
|
||||||
|
|
||||||
|
(A) best fitness / global optimum — the *champion*. On smooth landscapes (low K) it is maximised by
|
||||||
|
wide breadth; as ruggedness rises the peak shifts to an INTERMEDIATE breadth (full promiscuity
|
||||||
|
prematurely converges below it) — the mating-system image of E9's "optimal recombination rate
|
||||||
|
shrinks with ruggedness".
|
||||||
|
(B) mean fitness / global optimum — the *typical* individual. Monotonically favoured by breadth at
|
||||||
|
every K: panmixia lifts the whole population toward a good consensus.
|
||||||
|
(C) diversity (mean normalised pairwise Hamming) — monotonically DESTROYED by breadth at every K
|
||||||
|
(promiscuity homogenises), the reservoir largest under monogamy and on rugged landscapes.
|
||||||
|
|
||||||
|
The tension between (A)/(C) is the result: promiscuity maximises the typical model and kills diversity;
|
||||||
|
on rugged landscapes the best model needs preserved diversity, so an intermediate breadth wins.
|
||||||
|
|
||||||
|
Usage: python figures/plot_E14.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent))
|
||||||
|
from _figlib import load_bundle, savefig # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
df, _ = load_bundle("results/E14")
|
||||||
|
last = df[df["generation"] == df["generation"].max()].copy()
|
||||||
|
last["best_n"] = last["best_fitness"] / last["global_opt"]
|
||||||
|
last["mean_n"] = last["mean_fitness"] / last["global_opt"]
|
||||||
|
Ks = sorted(last["K"].unique())
|
||||||
|
cmap = plt.get_cmap("viridis")
|
||||||
|
colors = {K: cmap(i / max(1, len(Ks) - 1)) for i, K in enumerate(Ks)}
|
||||||
|
|
||||||
|
fig, axes = plt.subplots(1, 3, figsize=(16, 5))
|
||||||
|
panels = [
|
||||||
|
("best_n", "best fitness / global optimum", "(A) the champion: best model in the population",
|
||||||
|
"best fitness peaks at INTERMEDIATE breadth\non rugged landscapes (the peak shifts left as K rises)"),
|
||||||
|
("mean_n", "mean fitness / global optimum", "(B) the typical model: population mean",
|
||||||
|
"monotonically favoured by wide breadth\n(panmixia lifts the whole population)"),
|
||||||
|
("diversity", "diversity (mean pairwise Hamming)", "(C) standing diversity",
|
||||||
|
"monotonically destroyed by breadth\n(promiscuity homogenises; monogamy preserves)"),
|
||||||
|
]
|
||||||
|
for ax, (col, ylab, title, subtitle) in zip(axes, panels):
|
||||||
|
for K in Ks:
|
||||||
|
g = (last[last["K"] == K].groupby("breadth")[col]
|
||||||
|
.agg(["mean", "sem"]).reset_index())
|
||||||
|
ax.errorbar(g["breadth"], g["mean"], yerr=1.96 * g["sem"].fillna(0.0),
|
||||||
|
marker="o", lw=1.8, capsize=2, color=colors[K], label=f"K={K}")
|
||||||
|
ax.set_xscale("log")
|
||||||
|
ax.set(xlabel="mate-pool breadth (monogamous ← → promiscuous)", ylabel=ylab)
|
||||||
|
ax.set_title(f"{title}\n{subtitle}", fontsize=9)
|
||||||
|
ax.legend(title="ruggedness", frameon=False, fontsize=8)
|
||||||
|
|
||||||
|
fig.suptitle("E14 — monogamy vs promiscuity: the best mate-pool breadth shrinks as skills get more entangled",
|
||||||
|
y=1.02, fontsize=13)
|
||||||
|
fig.tight_layout()
|
||||||
|
savefig(fig, "results/E14", "E14")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -187,6 +187,18 @@ only worked as a whole. Biologists call this **outbreeding depression**, and we
|
||||||
gets. The design rule: *combine freely when skills are independent; combine sparingly and carefully when
|
gets. The design rule: *combine freely when skills are independent; combine sparingly and carefully when
|
||||||
they're tangled.*
|
they're tangled.*
|
||||||
|
|
||||||
|
**How *widely* you mate matters too.** That last point was about *how much* to mix; a separate knob is
|
||||||
|
*who mixes with whom*. **Monogamy** = each model only ever combines within a small, fixed circle;
|
||||||
|
**promiscuity** = any model can combine with any other. Almost all model-merging today is promiscuous by
|
||||||
|
default — throw everything in one pot. But there's a catch: wide mixing spreads good traits fast, but it
|
||||||
|
also makes the whole population converge to the *same thing*, killing variety. Narrow, local mixing keeps
|
||||||
|
separate sub-groups exploring different solutions. We tested this against tangledness, and the best answer
|
||||||
|
*moves*: on simple (independent-skill) problems, wide promiscuous merging is best; but the more tangled
|
||||||
|
the skills, the more you want to *narrow* it — full promiscuity converges too fast onto one mediocre
|
||||||
|
solution and finds a *worse* champion, while keeping structured sub-groups preserves the variety a hard
|
||||||
|
problem needs. So the rule extends: *merge widely for independent skills; keep separate sub-populations
|
||||||
|
("island" merging) for tangled ones.*
|
||||||
|
|
||||||
**AI can do sex better than biology can.** Biology is stuck with two parents, mating more or less at
|
**AI can do sex better than biology can.** Biology is stuck with two parents, mating more or less at
|
||||||
random, and can't inspect a child before it's born. AI has none of those limits. It can combine **many**
|
random, and can't inspect a child before it's born. AI has none of those limits. It can combine **many**
|
||||||
parents at once; it can **choose** which parents to combine, for complementary skills; and it can
|
parents at once; it can **choose** which parents to combine, for complementary skills; and it can
|
||||||
|
|
@ -449,6 +461,12 @@ real language models. Here's the shape of the evidence (a separate document has
|
||||||
level off below. On *tangled* problems, blind combining instead produces below-parent children
|
level off below. On *tangled* problems, blind combining instead produces below-parent children
|
||||||
(outbreeding depression) — and *directed* combining (choose mates, screen offspring, many parents)
|
(outbreeding depression) — and *directed* combining (choose mates, screen offspring, many parents)
|
||||||
reliably fixes it.
|
reliably fixes it.
|
||||||
|
- **Monogamy vs promiscuity.** Sweeping how *widely* models merge — from local/monogamous to
|
||||||
|
everyone-with-everyone/promiscuous — against how tangled the skills are, the best breadth **shrinks as
|
||||||
|
the skills get more tangled**: wide promiscuous merging wins when skills are independent, but on tangled
|
||||||
|
problems it converges too fast onto one mediocre solution and finds a worse champion, so keeping
|
||||||
|
structured sub-populations wins. (Promiscuity always lifts the *typical* model but always destroys
|
||||||
|
variety.) A merging design knob the field, which throws everything in one pot, doesn't currently have.
|
||||||
- **The combining claims, in real language models — with a sharp condition.** Merging fine-tuned Qwen
|
- **The combining claims, in real language models — with a sharp condition.** Merging fine-tuned Qwen
|
||||||
models (up to 7B on a GPU cluster) produces a generalist that beats every specialist parent; and
|
models (up to 7B on a GPU cluster) produces a generalist that beats every specialist parent; and
|
||||||
keeping parents separate and *routing*, or *breeding and screening* offspring, beats the plain average
|
keeping parents separate and *routing*, or *breeding and screening* offspring, beats the plain average
|
||||||
|
|
|
||||||
|
|
@ -270,7 +270,7 @@ beats the average in exact proportion to how far the average is from the best at
|
||||||
that sharpens rather than weakens the claim, and that a practitioner needs before spending compute on
|
that sharpens rather than weakens the claim, and that a practitioner needs before spending compute on
|
||||||
the fancier operator.
|
the fancier operator.
|
||||||
|
|
||||||
Two caveats keep this honest, and both are results, not hand-waving.
|
Three results keep this honest, and all are results, not hand-waving.
|
||||||
|
|
||||||
*Sex can backfire.* When the parents' skills are not cleanly separable but **entangled** — when the
|
*Sex can backfire.* When the parents' skills are not cleanly separable but **entangled** — when the
|
||||||
value of one capability depends on which others are present (geneticists call this **epistasis**) —
|
value of one capability depends on which others are present (geneticists call this **epistasis**) —
|
||||||
|
|
@ -280,6 +280,22 @@ reproduce it: on "rugged" (highly entangled) problems, naive merging drops offsp
|
||||||
parents, and the more you mix the worse it gets. The design rule that falls out is simple: *merge
|
parents, and the more you mix the worse it gets. The design rule that falls out is simple: *merge
|
||||||
freely when skills are complementary; merge sparingly, and carefully, when they are entangled.*
|
freely when skills are complementary; merge sparingly, and carefully, when they are entangled.*
|
||||||
|
|
||||||
|
*The mating system matters too — not just who mates, but how widely.* The result above is about the
|
||||||
|
recombination *rate*; a separate knob is the population's **mating structure** — whether reproduction is
|
||||||
|
**monogamous** (each model recombines within a narrow, local circle) or **promiscuous** (mates drawn
|
||||||
|
freely from the whole population). Almost all model-merging implicitly assumes promiscuity — fuse
|
||||||
|
everything, or route over one flat pool — but population genetics says the breadth of gene flow is itself
|
||||||
|
consequential, because wide flow spreads good variants fast while **homogenising** the population, and
|
||||||
|
narrow flow preserves the distinct sub-populations needed to explore several solutions at once (Wright's
|
||||||
|
*shifting balance*). We sweep exactly this breadth against landscape ruggedness, and the optimum moves:
|
||||||
|
on smooth (additive) landscapes wide, promiscuous mating is best (spread the one good direction fastest),
|
||||||
|
but as the landscape gets rugged the best breadth **shrinks to an intermediate value** — full promiscuity
|
||||||
|
prematurely converges onto one basin and finds a *worse* champion, while pure monogamy over-fragments.
|
||||||
|
Throughout, wide mating lifts the *typical* model but monotonically **destroys diversity** — so on rugged
|
||||||
|
problems, where the best model needs preserved diversity to be found, structured (partly monogamous)
|
||||||
|
merging wins. The design rule extends the one above: *merge widely when skills are additive; keep
|
||||||
|
structured sub-populations — island-style merging — when skills are rugged.* (Figure: `results/E14/E14.png`.)
|
||||||
|
|
||||||
*AI can do sex better than biology can.* Biology is stuck with two parents, mating roughly at random,
|
*AI can do sex better than biology can.* Biology is stuck with two parents, mating roughly at random,
|
||||||
and cannot inspect an offspring before it is born. An AI has none of those limits. It can recombine
|
and cannot inspect an offspring before it is born. An AI has none of those limits. It can recombine
|
||||||
**many** parents at once; it can **choose** which parents to combine, for complementarity; and it can
|
**many** parents at once; it can **choose** which parents to combine, for complementarity; and it can
|
||||||
|
|
@ -599,6 +615,13 @@ shape.)
|
||||||
produces below-parent offspring (outbreeding depression) — and *directed* recombination (choose mates,
|
produces below-parent offspring (outbreeding depression) — and *directed* recombination (choose mates,
|
||||||
screen offspring, unbounded parents) reliably fixes it. This is the concrete evidence for the paper's
|
screen offspring, unbounded parents) reliably fixes it. This is the concrete evidence for the paper's
|
||||||
central reframing.
|
central reframing.
|
||||||
|
- *The mating system, not just the mating.* Sweeping how *widely* models recombine — from monogamous
|
||||||
|
(local, structured) to promiscuous (panmictic) — against landscape ruggedness, the best breadth
|
||||||
|
**shrinks as skills get more entangled**: wide, promiscuous merging wins on additive landscapes, but on
|
||||||
|
rugged ones it prematurely converges to a worse champion and an intermediate, structured breadth wins,
|
||||||
|
because promiscuity monotonically destroys the diversity a rugged search needs. A merging-native design
|
||||||
|
axis — *merge widely for additive skills, keep island-structured sub-populations for entangled ones* —
|
||||||
|
that the model-merging literature, which assumes panmixia, does not have.
|
||||||
- *The recombination claims, in real language models — with a sharp condition.* Merging LoRA-specialised
|
- *The recombination claims, in real language models — with a sharp condition.* Merging LoRA-specialised
|
||||||
Qwen models (up to 7B on a GPU cluster) produces a generalist that beats every specialist parent
|
Qwen models (up to 7B on a GPU cluster) produces a generalist that beats every specialist parent
|
||||||
(Fisher–Muller, for real); and keeping parents intact and *routing*, or *breeding and screening*
|
(Fisher–Muller, for real); and keeping parents intact and *routing*, or *breeding and screening*
|
||||||
|
|
|
||||||
BIN
results/E14/E14.pdf
Normal file
BIN
results/E14/E14.pdf
Normal file
Binary file not shown.
BIN
results/E14/E14.png
Normal file
BIN
results/E14/E14.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 234 KiB |
68
results/E14/README.md
Normal file
68
results/E14/README.md
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
# E14 — Mating systems: monogamy vs promiscuity (mate-pool breadth)
|
||||||
|
|
||||||
|
**Claim tested.** The society experiments (E8–E11) assumed **panmixia** — every offspring recombined
|
||||||
|
from parents sampled across the whole population. Biology's mating systems instead span a continuum
|
||||||
|
from **monogamy** (mating within a narrow, local circle) to **promiscuity** (mates drawn freely from
|
||||||
|
everyone), and population genetics says the choice matters: wide gene flow spreads a good allele fast
|
||||||
|
but **homogenises** the population, while restricted gene flow (population structure / *isolation by
|
||||||
|
distance*) keeps demes distinct so several fitness peaks can be explored in parallel (Wright's shifting
|
||||||
|
balance). E14 asks how the best mating system depends on how **entangled** the skills are.
|
||||||
|
|
||||||
|
**Setup.** A finite population of `N=48` genotypes (`L=12` biallelic loci) evolves on a Kauffman **NK**
|
||||||
|
landscape (ruggedness `K`). Agents sit on a **ring**; an offspring's second parent is drawn from a
|
||||||
|
window of half-width `≈ breadth·N/2` around the focal parent, so **mate-pool breadth** `b` is a single
|
||||||
|
scalar: `b→0` = monogamous / structured (local mating), `b=1` = promiscuous / panmictic. Selection is
|
||||||
|
**local** — an offspring replaces the incumbent at its own ring position only if strictly fitter — so
|
||||||
|
restricted mating can actually sustain distinct demes instead of being washed out. Sweep `b ∈ {0.03,
|
||||||
|
0.08, 0.17, 0.35, 0.6, 1.0}` × `K ∈ {0, 3, 6, 10}`, 60 generations, 20 replicates, `μ=0.003`,
|
||||||
|
crossover rate 0.5. Bitwise-reproducible from the master seed.
|
||||||
|
|
||||||
|
### Results — the best breadth shrinks as the landscape gets more rugged
|
||||||
|
`best_fitness / global_opt` (the *champion*), mean over 20 reps; **bold = best breadth at that K**:
|
||||||
|
|
||||||
|
| K \ breadth | 0.03 | 0.08 | 0.17 | 0.35 | 0.60 | 1.00 |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| **0** (additive) | 1.000 | 1.000 | 1.000 | 1.000 | 1.000 | 1.000 |
|
||||||
|
| **3** (mild) | 0.995 | 0.993 | 0.997 | 0.993 | **0.9997** | 0.993 |
|
||||||
|
| **6** (rugged) | 0.986 | 0.972 | 0.983 | **0.989** | 0.984 | 0.982 |
|
||||||
|
| **10** (very rugged) | 0.961 | 0.968 | 0.967 | **0.980** | 0.964 | 0.965 |
|
||||||
|
|
||||||
|
- **K=0** saturates: an additive (single-peak) landscape is solved by everyone regardless of mating, so
|
||||||
|
the champion metric can't discriminate (it only shows up in diversity, below).
|
||||||
|
- **K=3**: the optimum is at **wide** breadth (`b=0.6`) — near-promiscuous mating maximises the champion
|
||||||
|
when the landscape is mild.
|
||||||
|
- **K=6, K=10**: the optimum moves to an **intermediate** breadth (`b=0.35`), and *full promiscuity*
|
||||||
|
falls below it. Wide mating **prematurely converges** on rugged landscapes; pure monogamy
|
||||||
|
over-fragments (too little gene flow to combine complementary basins). The best of both is
|
||||||
|
intermediate structure — the mating-system image of E9's "optimal recombination rate shrinks with
|
||||||
|
ruggedness."
|
||||||
|
|
||||||
|
### Results — the diversity/mean tension that drives it
|
||||||
|
Two monotone effects, opposite in sign, at **every** K (mean over reps at K=10):
|
||||||
|
|
||||||
|
| breadth | 0.03 | 0.08 | 0.17 | 0.35 | 0.60 | 1.00 |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| mean fitness / opt | 0.890 | 0.914 | 0.926 | 0.941 | 0.941 | 0.943 |
|
||||||
|
| diversity (pairwise Hamming) | 0.441 | 0.413 | 0.384 | 0.346 | 0.265 | 0.282 |
|
||||||
|
| distinct local optima occupied | 11.0 | 8.6 | 7.4 | 7.3 | 7.2 | 6.9 |
|
||||||
|
|
||||||
|
- **Mean fitness** rises monotonically with breadth: panmixia lifts the *typical* individual toward a
|
||||||
|
good consensus fastest.
|
||||||
|
- **Diversity** and **occupied peaks** fall monotonically with breadth: promiscuity **homogenises**;
|
||||||
|
monogamy preserves the standing variation (and the parallel exploration of distinct basins) — most
|
||||||
|
strongly on rugged landscapes.
|
||||||
|
|
||||||
|
So promiscuity maximises the *typical* model and destroys diversity; on a rugged landscape the *best*
|
||||||
|
model needs that preserved diversity, so an intermediate breadth wins the champion even though the wide
|
||||||
|
breadth still wins the mean. (Panel A = champion, Panel B = mean, Panel C = diversity.)
|
||||||
|
|
||||||
|
### Positioning
|
||||||
|
This is the population-**structure** axis the model-merging literature does not have. Merging/soup work
|
||||||
|
implicitly assumes panmixia (fuse everything, or route among a flat pool); E14 says the *breadth* of who
|
||||||
|
merges with whom is itself a design knob, and its optimum is set by the entanglement of the skills:
|
||||||
|
**merge widely when skills are additive; keep sub-populations (structured / island merging) when skills
|
||||||
|
are rugged and diversity must be preserved to explore and later combine basins.** It complements E9
|
||||||
|
(recombination *rate*) and E11 (diversity is load-bearing) on a new, orthogonal axis. **Falsifier (not
|
||||||
|
triggered):** the best breadth independent of `K` (no crossover), or promiscuity best at every
|
||||||
|
ruggedness — instead the optimal breadth shifts from `0.6` (K=3) to `0.35` (K≥6), and diversity is
|
||||||
|
monotonically lost to breadth throughout.
|
||||||
14
results/E14/manifest.json
Normal file
14
results/E14/manifest.json
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"experiment": "E14",
|
||||||
|
"master_seed": 20260709,
|
||||||
|
"git_commit": "9fea375ff89f2b88a4deb5881894087555558863",
|
||||||
|
"python": "3.14.5",
|
||||||
|
"libraries": {
|
||||||
|
"numpy": "2.5.0",
|
||||||
|
"scipy": "1.18.0",
|
||||||
|
"pandas": "3.0.3",
|
||||||
|
"pyarrow": "24.0.0"
|
||||||
|
},
|
||||||
|
"rows": 29280,
|
||||||
|
"results_sha256": "5766302157f44c0f27228f361b43c06b0651697a7dcc38848eb1d94d0502faa6"
|
||||||
|
}
|
||||||
33
results/E14/resolved_config.yaml
Normal file
33
results/E14/resolved_config.yaml
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
experiment: E14
|
||||||
|
seed: 20260709
|
||||||
|
n_replicates: 20
|
||||||
|
source_config:
|
||||||
|
experiment: E14
|
||||||
|
kind: mating_system
|
||||||
|
seed: 20260709
|
||||||
|
n_replicates: 20
|
||||||
|
mating:
|
||||||
|
L: 12
|
||||||
|
N: 48
|
||||||
|
breadth: 1.0
|
||||||
|
K: 0
|
||||||
|
recomb_rate: 0.5
|
||||||
|
mu: 0.003
|
||||||
|
generations: 60
|
||||||
|
sweep:
|
||||||
|
- param: mating.K
|
||||||
|
values:
|
||||||
|
- 0
|
||||||
|
- 3
|
||||||
|
- 6
|
||||||
|
- 10
|
||||||
|
- param: mating.breadth
|
||||||
|
values:
|
||||||
|
- 0.03
|
||||||
|
- 0.08
|
||||||
|
- 0.17
|
||||||
|
- 0.35
|
||||||
|
- 0.6
|
||||||
|
- 1.0
|
||||||
|
output:
|
||||||
|
dir: results/E14
|
||||||
|
|
@ -207,6 +207,46 @@ def run_dynamic_experiment(cfg: dict) -> pd.DataFrame:
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
_MATING_KEYS = ("mating", "generations")
|
||||||
|
|
||||||
|
|
||||||
|
def run_mating_experiment(cfg: dict) -> pd.DataFrame:
|
||||||
|
"""Run the mating-system experiment across a ``breadth`` x ``K`` sweep x replicates (E14).
|
||||||
|
|
||||||
|
Mirrors ``run_dynamic_experiment``: assembles the base from the ``mating``/``generations`` blocks,
|
||||||
|
takes the Cartesian product of the swept params (typically ``mating.breadth`` and ``mating.K``,
|
||||||
|
plain dotted paths), and calls ``run_mating_system`` per grid point x replicate with paired seeds.
|
||||||
|
"""
|
||||||
|
from .mating_system import run_mating_system
|
||||||
|
|
||||||
|
base = {k: copy.deepcopy(cfg[k]) for k in _MATING_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_mating_system(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:
|
def run_coverage(cfg: dict) -> pd.DataFrame:
|
||||||
"""E4 runner: multi-teacher recombination coverage (blueprint 2.5-E4 / 2.7.1).
|
"""E4 runner: multi-teacher recombination coverage (blueprint 2.5-E4 / 2.7.1).
|
||||||
|
|
||||||
|
|
@ -377,6 +417,8 @@ def run_and_save(config_path: str | Path) -> Path:
|
||||||
elif kind == "speciation":
|
elif kind == "speciation":
|
||||||
from .speciation import run_speciation # E12: reproductive isolation / merge limits
|
from .speciation import run_speciation # E12: reproductive isolation / merge limits
|
||||||
df = run_speciation(cfg, int(cfg["seed"]))
|
df = run_speciation(cfg, int(cfg["seed"]))
|
||||||
|
elif kind == "mating_system":
|
||||||
|
df = run_mating_experiment(cfg) # E14: monogamy vs promiscuity (mate-pool breadth)
|
||||||
else:
|
else:
|
||||||
df = run_experiment(cfg)
|
df = run_experiment(cfg)
|
||||||
save_artifacts(cfg, df, out_dir)
|
save_artifacts(cfg, df, out_dir)
|
||||||
|
|
|
||||||
113
src/knowledge/mating_system.py
Normal file
113
src/knowledge/mating_system.py
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
"""Mating systems — monogamy vs promiscuity as mate-pool breadth (E14).
|
||||||
|
|
||||||
|
The society experiments (E8–E11) assumed **panmixia**: every offspring is recombined from parents
|
||||||
|
sampled across the *whole* population. But biology's mating systems span a continuum from **monogamy**
|
||||||
|
(each individual mates within a narrow, local circle) to **promiscuity** (mates drawn freely from the
|
||||||
|
whole population), and population genetics says the choice is consequential. Wide gene flow spreads a
|
||||||
|
beneficial allele across the population fast but **homogenises** it; restricted gene flow (population
|
||||||
|
structure / *isolation by distance*) keeps demes distinct so several fitness peaks can be explored in
|
||||||
|
parallel — Wright's *shifting balance*.
|
||||||
|
|
||||||
|
Here the mating system is one scalar: mate-pool **breadth** ``b``. Agents sit on a ring; an offspring's
|
||||||
|
second parent is drawn from a window of half-width ``≈ b·N/2`` around the focal parent. ``b→0`` =
|
||||||
|
**monogamous / structured** (local mating, isolation by distance); ``b=1`` = **promiscuous / panmictic**
|
||||||
|
(mate with anyone). Selection is **local** — an offspring competes only against the incumbent at its own
|
||||||
|
ring position — so restricted mating can actually sustain distinct demes rather than being washed out by
|
||||||
|
global truncation.
|
||||||
|
|
||||||
|
Crossed with landscape ruggedness ``K`` (Kauffman NK epistasis), this is the mating-system image of the
|
||||||
|
E9 design rule. Prediction: **promiscuity wins on additive/smooth landscapes** (one peak — spread the
|
||||||
|
single good direction fastest), while **structured/monogamous mating wins on rugged/epistatic
|
||||||
|
landscapes** (many peaks — diversity must be preserved to explore basins that recombination can later
|
||||||
|
combine). Falsifier: the best mating system is independent of ruggedness (no crossover).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any, Mapping
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
from .genotype import bits_to_index, crossover, hill_climb, nk_fitness
|
||||||
|
|
||||||
|
|
||||||
|
def _diversity(pop_bits: np.ndarray) -> float:
|
||||||
|
"""Mean normalised pairwise Hamming distance over the population (0 = clonal, 1 = maximal)."""
|
||||||
|
N, L = pop_bits.shape
|
||||||
|
if N < 2:
|
||||||
|
return 0.0
|
||||||
|
match = (pop_bits[:, None, :] == pop_bits[None, :, :]).sum(axis=2) # (N, N) locus agreements
|
||||||
|
ham = L - match # pairwise Hamming distances
|
||||||
|
return float(ham.sum() / (N * (N - 1)) / L) # mean over ordered pairs, /L
|
||||||
|
|
||||||
|
|
||||||
|
def _distinct_peaks(pop_bits: np.ndarray, fitness: np.ndarray, L: int) -> int:
|
||||||
|
"""Number of distinct local optima the population occupies (hill-climb each agent to its basin)."""
|
||||||
|
return len({hill_climb(fitness, L, bits_to_index(b)) for b in pop_bits})
|
||||||
|
|
||||||
|
|
||||||
|
def run_mating_system(cfg: Mapping[str, Any], seed: int) -> pd.DataFrame:
|
||||||
|
"""Run one mating-system lineage; return per-generation metrics.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cfg (Mapping): Config with a ``mating`` block (``L`` loci, ``K`` landscape ruggedness, ``N``
|
||||||
|
population, ``breadth`` mate-pool breadth ``b∈[0,1]``, ``recomb_rate`` crossover rate,
|
||||||
|
``mu`` per-locus mutation) and ``generations``.
|
||||||
|
seed (int): Replicate seed; the landscape and the run are a pure function of it.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
pd.DataFrame: One row per generation with ``best_fitness`` (real), ``mean_fitness`` (real),
|
||||||
|
``diversity`` (mean normalised pairwise Hamming), ``distinct_peaks`` (local optima occupied),
|
||||||
|
and ``global_opt``.
|
||||||
|
"""
|
||||||
|
ms = cfg["mating"]
|
||||||
|
L, K, N = int(ms["L"]), int(ms["K"]), int(ms["N"])
|
||||||
|
b = float(ms.get("breadth", 1.0))
|
||||||
|
rate = float(ms.get("recomb_rate", 0.5))
|
||||||
|
mu = float(ms.get("mu", 0.01))
|
||||||
|
generations = int(cfg.get("generations", 100))
|
||||||
|
|
||||||
|
fitness = nk_fitness(L, K, seed) # reality
|
||||||
|
global_opt = float(fitness.max())
|
||||||
|
rng = np.random.default_rng(seed)
|
||||||
|
|
||||||
|
# Population on a ring: position i is fixed ring slot i (so structure persists across generations).
|
||||||
|
pop = rng.integers(0, 2, size=(N, L)).astype(np.int8)
|
||||||
|
half = max(1, int(round(b * N / 2))) # mate-window half-width; b=1 -> whole ring
|
||||||
|
|
||||||
|
def fit_of(bits: np.ndarray) -> float:
|
||||||
|
return float(fitness[bits_to_index(bits)])
|
||||||
|
|
||||||
|
rows: list[dict] = []
|
||||||
|
|
||||||
|
def record(t: int) -> None:
|
||||||
|
tf = np.array([fit_of(g) for g in pop])
|
||||||
|
rows.append({
|
||||||
|
"generation": t,
|
||||||
|
"best_fitness": float(tf.max()),
|
||||||
|
"mean_fitness": float(tf.mean()),
|
||||||
|
"diversity": _diversity(pop),
|
||||||
|
"distinct_peaks": _distinct_peaks(pop, fitness, L),
|
||||||
|
"global_opt": global_opt,
|
||||||
|
})
|
||||||
|
|
||||||
|
record(0)
|
||||||
|
for t in range(1, generations + 1):
|
||||||
|
new = pop.copy()
|
||||||
|
for i in range(N):
|
||||||
|
# Second parent from a ring window of half-width `half` around i (isolation by distance).
|
||||||
|
offset = 0
|
||||||
|
while offset == 0:
|
||||||
|
offset = int(rng.integers(-half, half + 1))
|
||||||
|
j = (i + offset) % N
|
||||||
|
child = crossover(np.stack([pop[i], pop[j]]), rate, rng)
|
||||||
|
flip = rng.random(L) < mu
|
||||||
|
child = np.where(flip, 1 - child, child).astype(pop.dtype)
|
||||||
|
# Local selection: the child replaces the incumbent at i only if strictly fitter.
|
||||||
|
if fit_of(child) > fit_of(pop[i]):
|
||||||
|
new[i] = child
|
||||||
|
pop = new
|
||||||
|
record(t)
|
||||||
|
|
||||||
|
return pd.DataFrame(rows)
|
||||||
57
tests/test_mating_system.py
Normal file
57
tests/test_mating_system.py
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
"""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
|
||||||
Loading…
Add table
Add a link
Reference in a new issue