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