Files
gausse/tests/test_search_space.py
jze9 56c7ab9c1d Add variable-length genome search space and objective function (Stage 6 pt.1)
- optim/search_space.py: Genome encodes only indices into the real
  component database plus continuous/discrete geometry parameters; number
  of stages is itself mutable via add/remove/duplicate-stage operators.
  repair() clips everything back into bounds after mutation/crossover so
  decode() never sees an invalid genome.
- optim/objective.py: evaluate() decodes a genome, computes real BOM cost
  from wire length/price and component prices, runs the full chain, and
  scores fitness = efficiency for feasible runs or a soft penalty scaled
  by how many stages it got through for infeasible ones (so the GA gets
  gradient instead of a wall).

Tested against the real component database (not synthetic fixtures) —
including an explicit infeasible case using the real inductive sensor's
actual sensitivity/threshold values.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-06 20:26:16 +05:00

91 lines
3.4 KiB
Python

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_genome_dict_roundtrip():
rng = random.Random(5)
genome = sample_genome(DB, BOUNDS, rng)
restored = genome_from_dict(genome_to_dict(genome))
assert restored == genome