diff --git a/src/multi_swarm/genome/crossover.py b/src/multi_swarm/genome/crossover.py new file mode 100644 index 0000000..33d2ca5 --- /dev/null +++ b/src/multi_swarm/genome/crossover.py @@ -0,0 +1,33 @@ +from __future__ import annotations + +import random +from typing import Any + +from .hypothesis import HypothesisAgentGenome, ModelTier + + +def uniform_crossover( + p1: HypothesisAgentGenome, + p2: HypothesisAgentGenome, + rng: random.Random, +) -> HypothesisAgentGenome: + """Per ogni campo, eredita da p1 (prob 0.5) o p2.""" + + def pick(field: str) -> Any: + return getattr(p1 if rng.random() < 0.5 else p2, field) + + tier_pick: ModelTier = pick("model_tier") + feature_pick: list[str] = pick("feature_access") + + payload: dict[str, Any] = { + "system_prompt": pick("system_prompt"), + "feature_access": list(feature_pick), + "temperature": pick("temperature"), + "top_p": pick("top_p"), + "model_tier": tier_pick.value, + "lookback_window": pick("lookback_window"), + "cognitive_style": pick("cognitive_style"), + "parent_ids": [p1.id, p2.id], + "generation": max(p1.generation, p2.generation) + 1, + } + return HypothesisAgentGenome.from_dict(payload) diff --git a/tests/unit/test_genome_crossover.py b/tests/unit/test_genome_crossover.py new file mode 100644 index 0000000..7477a14 --- /dev/null +++ b/tests/unit/test_genome_crossover.py @@ -0,0 +1,44 @@ +import random + +from multi_swarm.genome.crossover import uniform_crossover +from multi_swarm.genome.hypothesis import HypothesisAgentGenome, ModelTier + + +def make(name: str) -> HypothesisAgentGenome: + return HypothesisAgentGenome( + system_prompt=f"prompt-{name}", + feature_access=["close"] if name == "A" else ["close", "volume"], + temperature=0.7 if name == "A" else 1.1, + top_p=0.9, + model_tier=ModelTier.C, + lookback_window=100 if name == "A" else 300, + cognitive_style="physicist" if name == "A" else "biologist", + ) + + +def test_crossover_lineage() -> None: + p1 = make("A") + p2 = make("B") + rng = random.Random(0) + child = uniform_crossover(p1, p2, rng) + assert sorted(child.parent_ids[-2:]) == sorted([p1.id, p2.id]) + assert child.generation == max(p1.generation, p2.generation) + 1 + + +def test_crossover_inherits_each_field_from_one_parent() -> None: + p1 = make("A") + p2 = make("B") + rng = random.Random(0) + child = uniform_crossover(p1, p2, rng) + assert child.system_prompt in (p1.system_prompt, p2.system_prompt) + assert child.temperature in (p1.temperature, p2.temperature) + assert child.lookback_window in (p1.lookback_window, p2.lookback_window) + assert child.cognitive_style in (p1.cognitive_style, p2.cognitive_style) + + +def test_crossover_deterministic_with_same_seed() -> None: + p1 = make("A") + p2 = make("B") + c1 = uniform_crossover(p1, p2, random.Random(42)) + c2 = uniform_crossover(p1, p2, random.Random(42)) + assert c1.to_dict() == c2.to_dict()