Add (mu+lambda) evolutionary optimizer with Nelder-Mead polish (Stage 6 complete)
- optim/worker_context.py: shared per-process DB/bounds init, extracted from sweep.py so evolutionary.py doesn't reach into another module's private state - optim/objective.py: build_run_record() shared between sweep and evolutionary so both write identically-shaped rows - optim/evolutionary.py: tournament selection, whole-stage-swap crossover, elitism, structural mutation (stage count itself evolves), then a serial Nelder-Mead polish of the best genome's continuous parameters with discrete component choices frozen - Found and fixed a real crash: crossover() indexed into an empty inter_stage_gaps_m list when both parents had only 1 stage (0 gaps), raising IndexError. Fixed the fallback to only choose from gaps that actually exist, defaulting to a neutral value (clipped later by repair()) when neither parent has one. Added a regression test. - Verified 59/59 tests pass both locally and inside the Docker image Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
65
tests/test_evolutionary.py
Normal file
65
tests/test_evolutionary.py
Normal file
@@ -0,0 +1,65 @@
|
||||
import random
|
||||
|
||||
from gausse.components.database import ComponentDatabase
|
||||
from gausse.optim.evolutionary import polish_best, run_evolution
|
||||
from gausse.optim.search_space import SearchBounds, sample_genome
|
||||
from gausse.storage.database import count_runs, fetch_runs, open_connection
|
||||
|
||||
DB = ComponentDatabase.load()
|
||||
|
||||
|
||||
def test_run_evolution_writes_all_evaluations_and_returns_summary(tmp_path):
|
||||
db_path = tmp_path / "runs.sqlite3"
|
||||
bounds = SearchBounds(max_stages=2)
|
||||
|
||||
summary = run_evolution(
|
||||
db_path,
|
||||
n_generations=2,
|
||||
population_size=6,
|
||||
bounds=bounds,
|
||||
n_workers=2,
|
||||
seed=7,
|
||||
polish=False,
|
||||
)
|
||||
|
||||
assert summary["n_evaluated"] == 12 # 2 поколения x 6 особей
|
||||
assert "best_fitness" in summary
|
||||
|
||||
conn = open_connection(db_path)
|
||||
assert count_runs(conn) == 12
|
||||
rows = fetch_runs(conn)
|
||||
assert all(r.search_mode == "evolve" for r in rows)
|
||||
conn.close()
|
||||
|
||||
|
||||
def test_evolution_is_reproducible_given_same_seed(tmp_path):
|
||||
bounds = SearchBounds(max_stages=2)
|
||||
summary_a = run_evolution(
|
||||
tmp_path / "a.sqlite3", n_generations=2, population_size=6, bounds=bounds,
|
||||
n_workers=1, seed=99, polish=False,
|
||||
)
|
||||
summary_b = run_evolution(
|
||||
tmp_path / "b.sqlite3", n_generations=2, population_size=6, bounds=bounds,
|
||||
n_workers=1, seed=99, polish=False,
|
||||
)
|
||||
assert summary_a["best_fitness"] == summary_b["best_fitness"]
|
||||
assert summary_a["best_efficiency"] == summary_b["best_efficiency"]
|
||||
|
||||
|
||||
def test_polish_does_not_make_the_best_genome_worse():
|
||||
rng = random.Random(15)
|
||||
bounds = SearchBounds(max_stages=2)
|
||||
# ищем реализуемый геном как стартовую точку доводки
|
||||
genome = None
|
||||
for _ in range(30):
|
||||
candidate = sample_genome(DB, bounds, rng)
|
||||
from gausse.optim.objective import evaluate
|
||||
result = evaluate(candidate, DB, bounds)
|
||||
if result.feasible:
|
||||
genome = candidate
|
||||
baseline_fitness = result.fitness
|
||||
break
|
||||
assert genome is not None, "не нашли реализуемый геном за 30 попыток"
|
||||
|
||||
_, polished_result = polish_best(genome, DB, bounds)
|
||||
assert polished_result.fitness >= baseline_fitness - 1e-9
|
||||
@@ -83,6 +83,22 @@ def test_crossover_produces_decodable_child():
|
||||
assert len(config.stages) == len(child.stages)
|
||||
|
||||
|
||||
def test_crossover_of_two_single_stage_genomes_does_not_crash():
|
||||
"""Регрессия: оба родителя с 1 ступенью (0 зазоров) роняли crossover
|
||||
с IndexError при попытке взять запасной зазор из пустого списка."""
|
||||
rng = random.Random(13)
|
||||
a = sample_genome(DB, BOUNDS, rng)
|
||||
b = sample_genome(DB, BOUNDS, rng)
|
||||
a.stages = a.stages[:1]
|
||||
a.inter_stage_gaps_m = []
|
||||
b.stages = b.stages[:1]
|
||||
b.inter_stage_gaps_m = []
|
||||
for _ in range(20):
|
||||
child = crossover(a, b, rng)
|
||||
child = repair(child, DB, BOUNDS)
|
||||
decode(child, DB, BOUNDS)
|
||||
|
||||
|
||||
def test_genome_dict_roundtrip():
|
||||
rng = random.Random(5)
|
||||
genome = sample_genome(DB, BOUNDS, rng)
|
||||
|
||||
Reference in New Issue
Block a user