feat(genome): deterministic mutation operators (numeric + categorical)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,77 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import random
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from .hypothesis import HypothesisAgentGenome
|
||||||
|
|
||||||
|
FEATURE_POOL: tuple[str, ...] = ("open", "high", "low", "close", "volume")
|
||||||
|
|
||||||
|
COGNITIVE_STYLES: tuple[str, ...] = (
|
||||||
|
"physicist",
|
||||||
|
"biologist",
|
||||||
|
"historian",
|
||||||
|
"meteorologist",
|
||||||
|
"ecologist",
|
||||||
|
"engineer",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _clone_with(g: HypothesisAgentGenome, **overrides: Any) -> HypothesisAgentGenome:
|
||||||
|
payload: dict[str, Any] = g.to_dict()
|
||||||
|
payload.update(overrides)
|
||||||
|
payload.pop("id", None)
|
||||||
|
payload["parent_ids"] = [*g.parent_ids, g.id]
|
||||||
|
payload["generation"] = g.generation + 1
|
||||||
|
return HypothesisAgentGenome.from_dict(payload)
|
||||||
|
|
||||||
|
|
||||||
|
def mutate_temperature(g: HypothesisAgentGenome, rng: random.Random) -> HypothesisAgentGenome:
|
||||||
|
delta = rng.choice([-0.1, 0.1])
|
||||||
|
new_t = max(0.6, min(1.3, g.temperature + delta))
|
||||||
|
return _clone_with(g, temperature=round(new_t, 4))
|
||||||
|
|
||||||
|
|
||||||
|
def mutate_lookback(g: HypothesisAgentGenome, rng: random.Random) -> HypothesisAgentGenome:
|
||||||
|
delta = rng.choice([-50, 50])
|
||||||
|
new_lb = max(50, min(500, g.lookback_window + delta))
|
||||||
|
return _clone_with(g, lookback_window=new_lb)
|
||||||
|
|
||||||
|
|
||||||
|
def mutate_feature_access(g: HypothesisAgentGenome, rng: random.Random) -> HypothesisAgentGenome:
|
||||||
|
current = set(g.feature_access)
|
||||||
|
if len(current) == len(FEATURE_POOL):
|
||||||
|
op = "remove"
|
||||||
|
elif len(current) <= 1:
|
||||||
|
op = "add"
|
||||||
|
else:
|
||||||
|
op = rng.choice(["add", "remove"])
|
||||||
|
|
||||||
|
if op == "add":
|
||||||
|
candidates = [f for f in FEATURE_POOL if f not in current]
|
||||||
|
choice = rng.choice(candidates)
|
||||||
|
new_set = current | {choice}
|
||||||
|
else:
|
||||||
|
choice = rng.choice(sorted(current))
|
||||||
|
new_set = current - {choice}
|
||||||
|
|
||||||
|
return _clone_with(g, feature_access=sorted(new_set))
|
||||||
|
|
||||||
|
|
||||||
|
def mutate_cognitive_style(g: HypothesisAgentGenome, rng: random.Random) -> HypothesisAgentGenome:
|
||||||
|
candidates = [s for s in COGNITIVE_STYLES if s != g.cognitive_style]
|
||||||
|
new_style = rng.choice(candidates)
|
||||||
|
return _clone_with(g, cognitive_style=new_style)
|
||||||
|
|
||||||
|
|
||||||
|
MUTATION_OPS = (
|
||||||
|
mutate_temperature,
|
||||||
|
mutate_lookback,
|
||||||
|
mutate_feature_access,
|
||||||
|
mutate_cognitive_style,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def random_mutate(g: HypothesisAgentGenome, rng: random.Random) -> HypothesisAgentGenome:
|
||||||
|
op = rng.choice(MUTATION_OPS)
|
||||||
|
return op(g, rng)
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import random
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from multi_swarm.genome.hypothesis import HypothesisAgentGenome, ModelTier
|
||||||
|
from multi_swarm.genome.mutation import (
|
||||||
|
COGNITIVE_STYLES,
|
||||||
|
FEATURE_POOL,
|
||||||
|
mutate_cognitive_style,
|
||||||
|
mutate_feature_access,
|
||||||
|
mutate_lookback,
|
||||||
|
mutate_temperature,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def base_genome() -> HypothesisAgentGenome:
|
||||||
|
return HypothesisAgentGenome(
|
||||||
|
system_prompt="x",
|
||||||
|
feature_access=["close"],
|
||||||
|
temperature=0.9,
|
||||||
|
top_p=0.95,
|
||||||
|
model_tier=ModelTier.C,
|
||||||
|
lookback_window=200,
|
||||||
|
cognitive_style="physicist",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_mutate_temperature_within_bounds(base_genome: HypothesisAgentGenome) -> None:
|
||||||
|
rng = random.Random(0)
|
||||||
|
for _ in range(50):
|
||||||
|
new = mutate_temperature(base_genome, rng)
|
||||||
|
assert 0.6 <= new.temperature <= 1.3
|
||||||
|
|
||||||
|
|
||||||
|
def test_mutate_lookback_within_bounds(base_genome: HypothesisAgentGenome) -> None:
|
||||||
|
rng = random.Random(0)
|
||||||
|
for _ in range(50):
|
||||||
|
new = mutate_lookback(base_genome, rng)
|
||||||
|
assert 50 <= new.lookback_window <= 500
|
||||||
|
|
||||||
|
|
||||||
|
def test_mutate_feature_access_changes_set(base_genome: HypothesisAgentGenome) -> None:
|
||||||
|
rng = random.Random(0)
|
||||||
|
new = mutate_feature_access(base_genome, rng)
|
||||||
|
assert set(new.feature_access) != set(base_genome.feature_access) or len(FEATURE_POOL) == 1
|
||||||
|
assert all(f in FEATURE_POOL for f in new.feature_access)
|
||||||
|
assert len(new.feature_access) >= 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_mutate_cognitive_style_uses_pool(base_genome: HypothesisAgentGenome) -> None:
|
||||||
|
rng = random.Random(0)
|
||||||
|
new = mutate_cognitive_style(base_genome, rng)
|
||||||
|
assert new.cognitive_style in COGNITIVE_STYLES
|
||||||
|
|
||||||
|
|
||||||
|
def test_mutation_preserves_lineage(base_genome: HypothesisAgentGenome) -> None:
|
||||||
|
rng = random.Random(0)
|
||||||
|
new = mutate_temperature(base_genome, rng)
|
||||||
|
assert base_genome.id in new.parent_ids
|
||||||
|
assert new.id != base_genome.id
|
||||||
Reference in New Issue
Block a user