Files
gausse/tests/test_search_space.py
jze9 d73341d3a2 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>
2026-07-06 20:36:10 +05:00

107 lines
4.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import random
import pytest
from gausse.components.database import ComponentDatabase
from gausse.optim.search_space import (
TUBE_WALL_CLEARANCE_M,
SearchBounds,
crossover,
decode,
genome_from_dict,
genome_to_dict,
mutate,
repair,
sample_genome,
)
DB = ComponentDatabase.load()
BOUNDS = SearchBounds()
def test_sample_genome_respects_bounds():
rng = random.Random(42)
for _ in range(50):
genome = sample_genome(DB, BOUNDS, rng)
assert BOUNDS.min_stages <= len(genome.stages) <= BOUNDS.max_stages
assert len(genome.inter_stage_gaps_m) == len(genome.stages) - 1
assert BOUNDS.tube_od_m_min <= genome.tube_od_m <= BOUNDS.tube_od_m_max
assert genome.projectile.diameter_m <= genome.tube_od_m - TUBE_WALL_CLEARANCE_M
for stage in genome.stages:
assert 0 <= stage.wire_idx < len(DB.wires)
assert BOUNDS.turns_per_layer_min <= stage.turns_per_layer <= BOUNDS.turns_per_layer_max
assert BOUNDS.layers_min <= stage.layers <= BOUNDS.layers_max
def test_decode_produces_valid_config():
rng = random.Random(1)
genome = sample_genome(DB, BOUNDS, rng)
config, initial_x_m, initial_v_mps = decode(genome, DB, BOUNDS)
assert len(config.stages) == len(genome.stages)
assert len(config.inter_stage_gaps_m) == len(genome.stages) - 1
assert initial_x_m < -config.stages[0].sensor_to_coil_distance_m
assert initial_v_mps == BOUNDS.initial_launch_velocity_mps
for stage_config, gene in zip(config.stages, genome.stages):
max_voltage = min(stage_config.capacitor.voltage_v, stage_config.switch.max_voltage_v)
assert stage_config.charge_voltage_v <= max_voltage + 1e-9
def test_mutate_keeps_genome_within_bounds():
rng = random.Random(7)
genome = sample_genome(DB, BOUNDS, rng)
for _ in range(200):
genome = mutate(genome, DB, BOUNDS, rng, rate=0.5)
assert BOUNDS.min_stages <= len(genome.stages) <= BOUNDS.max_stages
assert len(genome.inter_stage_gaps_m) == len(genome.stages) - 1
assert BOUNDS.tube_od_m_min <= genome.tube_od_m <= BOUNDS.tube_od_m_max
assert genome.projectile.diameter_m <= genome.tube_od_m - TUBE_WALL_CLEARANCE_M + 1e-9
# decode должен всегда успевать без исключений после repair
decode(genome, DB, BOUNDS)
def test_mutate_can_change_stage_count():
rng = random.Random(3)
small_bounds = SearchBounds(min_stages=1, max_stages=3)
genome = sample_genome(DB, small_bounds, rng)
genome.stages = genome.stages[:1]
genome.inter_stage_gaps_m = []
counts = set()
for _ in range(100):
genome = mutate(genome, DB, small_bounds, rng, rate=0.6)
counts.add(len(genome.stages))
assert len(counts) > 1 # число ступеней реально меняется, не застряло
def test_crossover_produces_decodable_child():
rng = random.Random(11)
a = sample_genome(DB, BOUNDS, rng)
b = sample_genome(DB, BOUNDS, rng)
for _ in range(20):
child = crossover(a, b, rng)
child = repair(child, DB, BOUNDS)
config, _, _ = decode(child, DB, BOUNDS)
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)
restored = genome_from_dict(genome_to_dict(genome))
assert restored == genome