- sim/coilgun.py: chains StageConfig results through a global coordinate, stops honestly on the first infeasible stage (with which stage and why), and computes overall efficiency = exit KE / total capacitor energy in - Discovered while wiring up the chain: a high-capacitance single-stage test configuration let current stay high past the coil center, decelerating and reversing the slug (exit velocity negative). This is a real coilgun tuning failure mode (not a bug) since KE ~ v^2 doesn't care about direction and the energy balance still holds to ~1%. Kept it as an explicit regression test (test_overpowered_discharge_can_fling_slug_backward) instead of quietly picking parameters that hide it, and tightened the "kinetic energy increases" test to also require forward (exit_v > 0) motion. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
108 lines
3.7 KiB
Python
108 lines
3.7 KiB
Python
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_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,
|
||
)
|