Files
gausse/tests/test_coilgun_chain.py
jze9 1029f40318 Fix efficiency metric that could exceed 100% (real bug found by Stage 8 sweep)
A real 5000-run sweep through Docker found the evolutionary optimizer's
best genome reporting efficiency=387%. Root cause: efficiency was
computed as exit_kinetic_energy / capacitor_energy_in, but exit kinetic
energy includes the fixed initial_v_mps launch push (a modeling
assumption, not something paid for by the capacitors) -- for a light
enough projectile, that free energy dominates and the ratio blows past 1.

Fixed by using kinetic_energy_delta (the energy the coils actually added)
instead of absolute exit KE. The per-stage energy identity (energy_in =
remaining_cap + dissipated + kinetic_delta) guarantees kinetic_delta <=
energy_in, so this metric can never exceed 1.0 -- provably, not by luck.
Added a regression test with a light projectile + large initial velocity
reproducing the exact failure mode.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-06 21:03:30 +05:00

129 lines
5.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 pytest
from gausse.components.schema import (
CapacitorSpec,
ProjectileMaterialSpec,
SensorSpec,
SwitchSpec,
WireSpec,
)
from gausse.sim.coilgun import CoilgunConfig, run_coilgun
from gausse.sim.stage import ProjectileConfig, StageConfig
STEEL = ProjectileMaterialSpec(
name="steel", density_kg_m3=7850.0, mu_r=200.0, b_sat_tesla=1.8, price_per_kg=100.0, source="test"
)
PROJECTILE = ProjectileConfig(material=STEEL, diameter_m=0.008, length_m=0.02)
CAPACITOR = CapacitorSpec(
part_number="c1", capacitance_uf=100.0, voltage_v=400.0, esr_ohm=0.05, max_current_a=300.0,
price=300.0, source="test",
)
WIRE = WireSpec(
part_id="w1", material="copper", gauge_mm=0.8, insulation_od_mm=0.85,
resistivity_ohm_m=1.68e-8, max_current_a=10.0, price_per_m=3.0, source="test",
)
SWITCH = SwitchSpec(
part_number="sw1", kind="MOSFET", max_current_a=200.0, max_voltage_v=500.0,
on_resistance_ohm=0.02, on_voltage_drop_v=None, turn_on_time_ns=50.0, price=50.0, source="test",
)
OPTICAL_SENSOR = SensorSpec(
part_number="s1", kind="optical", propagation_delay_ns=500.0, price=20.0, source="test"
)
WEAK_INDUCTIVE_SENSOR = SensorSpec(
part_number="s2", kind="inductive", propagation_delay_ns=0.0, price=5.0, source="test",
sensitivity_v_per_mps=0.001, threshold_v=1.0,
)
def _stage(sensor=OPTICAL_SENSOR, sensor_to_coil_distance_m=0.02):
return StageConfig(
wire=WIRE,
capacitor=CAPACITOR,
switch=SWITCH,
sensor=sensor,
tube_od_m=0.01,
turns_per_layer=20,
layers=4,
sensor_to_coil_distance_m=sensor_to_coil_distance_m,
charge_voltage_v=350.0,
)
def test_three_stage_chain_is_feasible_and_accelerates():
config = CoilgunConfig(
stages=[_stage(), _stage(), _stage()],
inter_stage_gaps_m=[0.05, 0.05],
projectile=PROJECTILE,
initial_x_m=-0.05,
initial_v_mps=5.0,
)
result = run_coilgun(config)
assert result.feasible, result.reason
assert len(result.stage_outcomes) == 3
assert result.exit_v_mps > 5.0
assert 0.0 < result.efficiency < 1.0
def test_global_coil_centers_accumulate_gaps():
config = CoilgunConfig(
stages=[_stage(), _stage(), _stage()],
inter_stage_gaps_m=[0.05, 0.07],
projectile=PROJECTILE,
initial_x_m=-0.05,
initial_v_mps=5.0,
)
result = run_coilgun(config)
assert result.feasible, result.reason
centers = [o.global_coil_center_m for o in result.stage_outcomes]
assert centers == pytest.approx([0.0, 0.05, 0.12])
def test_infeasible_stage_stops_chain_with_honest_reason():
# у слабого индукционного датчика порог 1В при чувствительности 0.001В/(м/с)
# требует скорости 1000 м/с, которой у снаряда не будет -> ступень 2 обязана провалиться
config = CoilgunConfig(
stages=[_stage(), _stage(sensor=WEAK_INDUCTIVE_SENSOR), _stage()],
inter_stage_gaps_m=[0.05, 0.05],
projectile=PROJECTILE,
initial_x_m=-0.05,
initial_v_mps=5.0,
)
result = run_coilgun(config)
assert not result.feasible
assert result.failed_stage_index == 1
assert "индукционный датчик" in result.reason
# ступень 2 (индекс 2) не должна была даже запуститься
assert len(result.stage_outcomes) == 2
def test_efficiency_never_exceeds_one_even_with_large_initial_velocity():
"""Регрессия: КПД считался как exit_KE/energy_in, а не как ДОБАВЛЕННАЯ
катушками энергия/energy_in. Для лёгкого снаряда с большой заданной
initial_v_mps (внешний "толчок", не учтённый в energy_in) это давало
КПД > 100% -- физически бессмысленно, найдено реальным sweep на Этапе 8."""
light_projectile = ProjectileConfig(material=STEEL, diameter_m=0.003, length_m=0.005)
config = CoilgunConfig(
stages=[_stage()],
inter_stage_gaps_m=[],
projectile=light_projectile,
initial_x_m=-0.05,
initial_v_mps=50.0, # заведомо большой "бесплатный" толчок относительно массы снаряда
)
result = run_coilgun(config)
assert result.feasible, result.reason
assert result.efficiency <= 1.0 + 1e-9
# и КПД действительно равен добавленной энергии, а не абсолютной exit-КЭ
expected = result.total_kinetic_energy_delta_j / result.total_energy_in_j
assert result.efficiency == pytest.approx(expected)
def test_mismatched_gap_count_raises():
with pytest.raises(ValueError):
CoilgunConfig(
stages=[_stage(), _stage()],
inter_stage_gaps_m=[0.05, 0.05],
projectile=PROJECTILE,
initial_x_m=-0.05,
initial_v_mps=5.0,
)