feat(ga): next_generation step (elitism + tournament + mutate/crossover)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-09 20:15:21 +02:00
parent 3f36ad65dd
commit 65dda09aff
2 changed files with 86 additions and 0 deletions
+41
View File
@@ -0,0 +1,41 @@
from __future__ import annotations
import random
from dataclasses import dataclass
from ..genome.crossover import uniform_crossover
from ..genome.hypothesis import HypothesisAgentGenome
from ..genome.mutation import random_mutate
from .selection import elite_select, tournament_select
@dataclass(frozen=True)
class GAConfig:
population_size: int
elite_k: int
tournament_k: int
p_crossover: float
def next_generation(
population: list[HypothesisAgentGenome],
fitnesses: dict[str, float],
cfg: GAConfig,
rng: random.Random,
) -> list[HypothesisAgentGenome]:
"""Costruisce la prossima generazione: elitismo + tournament + crossover/mutate."""
new_pop: list[HypothesisAgentGenome] = list(
elite_select(population, fitnesses, cfg.elite_k)
)
while len(new_pop) < cfg.population_size:
if rng.random() < cfg.p_crossover and len(population) >= 2:
p1 = tournament_select(population, fitnesses, cfg.tournament_k, rng)
p2 = tournament_select(population, fitnesses, cfg.tournament_k, rng)
child = uniform_crossover(p1, p2, rng)
else:
parent = tournament_select(population, fitnesses, cfg.tournament_k, rng)
child = random_mutate(parent, rng)
new_pop.append(child)
return new_pop[: cfg.population_size]
+45
View File
@@ -0,0 +1,45 @@
import random
from multi_swarm.ga.loop import GAConfig, next_generation
from multi_swarm.genome.hypothesis import HypothesisAgentGenome, ModelTier
def make(idx: int) -> HypothesisAgentGenome:
return HypothesisAgentGenome(
system_prompt=f"p-{idx}",
feature_access=["close"],
temperature=0.9,
top_p=0.95,
model_tier=ModelTier.C,
lookback_window=100,
cognitive_style="x",
)
def test_next_generation_size_preserved() -> None:
population = [make(i) for i in range(20)]
fitnesses = {g.id: float(i) for i, g in enumerate(population)}
cfg = GAConfig(population_size=20, elite_k=2, tournament_k=3, p_crossover=0.5)
new_pop = next_generation(population, fitnesses, cfg, rng=random.Random(0))
assert len(new_pop) == 20
def test_next_generation_includes_elites() -> None:
population = [make(i) for i in range(20)]
fitnesses = {g.id: float(i) for i, g in enumerate(population)}
cfg = GAConfig(population_size=20, elite_k=2, tournament_k=3, p_crossover=0.5)
new_pop = next_generation(population, fitnesses, cfg, rng=random.Random(0))
elite_ids = {
g.id for g in sorted(population, key=lambda g: fitnesses[g.id], reverse=True)[:2]
}
new_ids = {g.id for g in new_pop}
assert elite_ids.issubset(new_ids)
def test_next_generation_increments_generation_for_offspring() -> None:
population = [make(i) for i in range(20)]
fitnesses = {g.id: float(i) for i, g in enumerate(population)}
cfg = GAConfig(population_size=20, elite_k=2, tournament_k=3, p_crossover=0.5)
new_pop = next_generation(population, fitnesses, cfg, rng=random.Random(0))
new_offspring = [g for g in new_pop if g.id not in {p.id for p in population}]
assert all(g.generation > 0 for g in new_offspring)