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, )