From 3f36ad65dd2724626bccd4bbeaa7bc8abbf65322 Mon Sep 17 00:00:00 2001 From: AdrianoDev Date: Sat, 9 May 2026 20:12:51 +0200 Subject: [PATCH] feat(ga): tournament selection + elitism Co-Authored-By: Claude Opus 4.7 (1M context) --- src/multi_swarm/ga/selection.py | 30 +++++++++++++++++++++++ tests/unit/test_selection.py | 42 +++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) create mode 100644 src/multi_swarm/ga/selection.py create mode 100644 tests/unit/test_selection.py diff --git a/src/multi_swarm/ga/selection.py b/src/multi_swarm/ga/selection.py new file mode 100644 index 0000000..df4b19c --- /dev/null +++ b/src/multi_swarm/ga/selection.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +import random + +from ..genome.hypothesis import HypothesisAgentGenome + + +def tournament_select( + population: list[HypothesisAgentGenome], + fitnesses: dict[str, float], + k: int, + rng: random.Random, +) -> HypothesisAgentGenome: + """Estrae k individui random e restituisce il migliore.""" + if k < 1: + raise ValueError("k must be >= 1") + if not population: + raise ValueError("empty population") + candidates = rng.sample(population, k=min(k, len(population))) + return max(candidates, key=lambda g: fitnesses.get(g.id, 0.0)) + + +def elite_select( + population: list[HypothesisAgentGenome], + fitnesses: dict[str, float], + k: int, +) -> list[HypothesisAgentGenome]: + """Restituisce i k genomi con fitness più alta.""" + sorted_pop = sorted(population, key=lambda g: fitnesses.get(g.id, 0.0), reverse=True) + return sorted_pop[:k] diff --git a/tests/unit/test_selection.py b/tests/unit/test_selection.py new file mode 100644 index 0000000..dca01d5 --- /dev/null +++ b/tests/unit/test_selection.py @@ -0,0 +1,42 @@ +import random + +from multi_swarm.ga.selection import elite_select, tournament_select +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_tournament_picks_best_in_sample() -> None: + population = [make(i) for i in range(10)] + fitnesses = {g.id: float(i) for i, g in enumerate(population)} + rng = random.Random(0) + winner = tournament_select(population, fitnesses, k=5, rng=rng) + assert isinstance(winner, HypothesisAgentGenome) + assert fitnesses[winner.id] >= 0.0 + + +def test_tournament_size_one_is_random() -> None: + population = [make(i) for i in range(10)] + fitnesses = {g.id: float(i) for i, g in enumerate(population)} + rng = random.Random(0) + picks = [tournament_select(population, fitnesses, k=1, rng=rng) for _ in range(50)] + distinct = {p.id for p in picks} + assert len(distinct) > 1 + + +def test_elite_select_returns_top_k() -> None: + population = [make(i) for i in range(10)] + fitnesses = {g.id: float(i) for i, g in enumerate(population)} + elites = elite_select(population, fitnesses, k=3) + elite_fitnesses = sorted([fitnesses[g.id] for g in elites], reverse=True) + assert elite_fitnesses == [9.0, 8.0, 7.0]