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:
jze9
2026-07-06 20:36:10 +05:00
parent 891ac8fee0
commit d73341d3a2
8 changed files with 305 additions and 47 deletions

View File

@@ -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)