Skip to content

Population engine

PopulationAggregator keeps an archive of committed candidates and asks a SelectionPolicy which of them the next batch of workers should start from. A declared selection policy (anything but SingleHead) installs it through Policies(selection=...). population_islands widens that into a persistent population search: the archive plus a set of head-islands that survive across steps, so a tree-shaped policy scores clades.

Why islands

The archive already records parent for every candidate. But with the old per-step behaviour, the forks (head/<slot>) were created each step from dev and discarded at finalize — a candidate's lineage could not grow, so a tree-shaped policy had at most one generation to score. A persistent island keeps its fork across steps and records its lineage (_branch_parent), so every new candidate admitted from island i is a child of that island's last tip. That is the structure HGM's metaproductivity scores: a clade is a lineage that accumulates.

The dials

Knob What it does
Policies(selection=CladeThompson()) The population layer and its expansion rule. CladeThompson draws theta_a ~ Beta(tau·(1+S_a), tau·(1+F_a)) over clade evidence and expands the argmax — HGM's answer to "which parent has the best descendants".
population_islands=N N persistent head-islands on top of dev. 0 (the default) keeps the old per-step-fork behaviour exactly. A value above 0 without a declared selection policy is refused (the "honour or refuse" rule).
checkpointing The archive, the islands (forks), the per-island lineage (branch_parent) and the selection counter all checkpoint/restore, so a resumed run keeps the population tree it had built.

Reading the lineages

A persistent-island run returns a read-only EvolutionResult.lineages: one row per live lineage (dev plus each island) with its candidates, the archive's best_version / best_score on that branch, the selected count and its lineage tip. It is read from the archive the gate already scored, so it costs no extra evaluation, and it outlives finalize (islands are reaped from the ledger at the end, but the report still names them). None on a single-head run. The official state / rendered remain the primary head; this is a report, not a second way to commit.

result = evolve(..., population_islands=4)
for line in result.lineages or []:
    print(line["branch"], line["best_score"], line["candidates"])

Honest measurement

bench/population_engine.py A/Bs three arms (single-head / CladeThompson / islands+CladeThompson) on a deterministic capability domain at equal budget. On a smooth monotone landscape, single-head's greedy ascent is competitive or better — population exploration spends budget it does not need there. The population engine is for rugged/exploratory landscapes with many local optima, where a lineage that keeps exploring past a dead end is what finds the better basin. The bench is the instrument, not a claim; run it on your landscape before choosing the arm.

Verification

The mechanism is pinned at the aggregator level: islands are created and persist across steps, a worker mutation on an island becomes a child of that island's recorded tip, checkpoint/restore carries the islands and lineages, and island_count=0 keeps the old behaviour with no forks left behind.