Fix two physics bugs the user caught: unbounded field and ignored current limits

1. Iron saturation now in the dynamics (was the "13 Tesla" bug). Flux
   linkage lambda(x,I) = L_air*I + L_iron*overlap(x)*g(I) with
   g(I)=I_sat*tanh(I/I_sat) saturating at the current where iron reaches
   B_sat. Both the circuit back-EMF (dlambda/dx) and the force (coenergy,
   dW'/dx) derive from the SAME lambda, so energy stays conserved (0.025%
   error) AND the field caps at B_sat instead of running to 13 T. Reduces
   to the old 0.5*I^2*dL/dx in the low-current limit. On the config that
   reported 60% efficiency / 11.6 T, it now gives 40% / 1.90 T.

2. Switch surge-current limit is now a hard feasibility constraint: a
   config whose peak discharge current exceeds the switch's pulse rating
   (continuous * surge factor per device kind) is infeasible -- otherwise
   the optimizer "wins" with configs that vaporize their own thyristor
   (e.g. 119 A through a 12 A BT151). Capacitor current stays a warning
   (electrolytics tolerate pulses; DB has continuous not pulse ratings).

Regression tests added for both (field cap near saturation, energy
conservation under strong discharge). 87 tests pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
jze9
2026-07-07 01:45:26 +05:00
parent b3151e94a5
commit 7f040c3cf5
7 changed files with 153 additions and 47 deletions

View File

@@ -28,15 +28,12 @@ def _make_frozen_model() -> CoilInductanceModel:
def _integrate(r_ohm: float, t_span: tuple[float, float], t_eval: np.ndarray):
# mu_eff=1.0 -> вклад железа нулевой, λ = L_air·I, чистый RLC-контур
model = _make_frozen_model()
params = StageCircuitParams(
capacitance_f=C_F,
r_total_ohm=r_ohm,
mass_kg=1.0,
mu_eff=1.0,
total_turns=100,
coil_length_m=0.05,
b_sat_tesla=1e9, # эффективно отключает клэмп насыщения для этого теста
)
state0 = [Q0, 0.0, 0.0, 0.0]
return solve_ivp(

View File

@@ -17,14 +17,15 @@ DB = ComponentDatabase.load()
def _feasible_config() -> CoilgunConfig:
wire = next(w for w in DB.wires if w.part_id == "cu-petv2-1.0mm")
capacitor = next(c for c in DB.capacitors if c.part_number == "cap-470uf-400v")
switch = next(s for s in DB.switches if s.part_number == "IRFP250PBF")
# мощный ключ (180А, surge ~720А) — конфигурация с реальным пиковым током
switch = next(s for s in DB.switches if s.part_number == "IRFP4468PBF")
sensor = next(s for s in DB.sensors if s.part_number == "TCST2103")
steel = next(m for m in DB.projectile_materials if m.name == "Ст3 (конструкционная сталь)")
projectile = ProjectileConfig(material=steel, diameter_m=0.008, length_m=0.02)
stage = 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=0.02, charge_voltage_v=350.0,
turns_per_layer=20, layers=4, sensor_to_coil_distance_m=0.02, charge_voltage_v=200.0,
)
return CoilgunConfig(
stages=[stage, stage], inter_stage_gaps_m=[0.05], projectile=projectile,

View File

@@ -30,8 +30,9 @@ def _wire(resistivity_ohm_m: float = 1.68e-8) -> WireSpec:
def _switch(on_resistance_ohm: float = 0.02) -> SwitchSpec:
# высокий рейтинг тока: эти тесты про энергобаланс/отдачу, а не про токовый предел
return SwitchSpec(
part_number="sw1", kind="MOSFET", max_current_a=200.0, max_voltage_v=500.0,
part_number="sw1", kind="MOSFET", max_current_a=500.0, max_voltage_v=500.0,
on_resistance_ohm=on_resistance_ohm, on_voltage_drop_v=None, turn_on_time_ns=50.0,
price=50.0, source="test",
)
@@ -87,29 +88,42 @@ def test_kinetic_energy_increases_for_approaching_slug():
result = _run(_wire(), _switch())
assert result.feasible, result.reason
assert result.kinetic_energy_delta_j > 0
# рост кинетической энергии тут должен означать разгон ВПЕРЁД, а не
# отброс назад (см. test_overpowered_discharge_can_fling_slug_backward)
# рост кинетической энергии тут должен означать разгон ВПЕРЁД
assert result.exit_v_mps > 0
def test_overpowered_discharge_can_fling_slug_backward():
"""Честная фиксация реального режима отказа, а не только "успешных" случаев.
Слишком большая ёмкость (медленный разряд относительно скорости снаряда)
держит ток высоким уже после того, как снаряд проходит центр катушки —
зона x>0 тянет назад (dL/dx<0), и снаряд может выйти с отрицательной
скоростью. Энергобаланс при этом всё ещё точен (КЭ ~ v^2 не зависит от
знака) — это подтверждает, что находка отражает реальную физику плохо
настроенного расстояния/ёмкости, а не ошибку в уравнениях.
def test_energy_conserved_under_strong_discharge():
"""Энергобаланс должен оставаться точным даже при сильном разряде большой
батареей (нелинейное насыщение железа не должно рвать сохранение энергии —
и сила, и ЭДС выведены из одного λ(x, I), см. inductance.py).
"""
overpowered_capacitor = CapacitorSpec(
big_capacitor = CapacitorSpec(
part_number="c-big", capacitance_uf=1000.0, voltage_v=400.0, esr_ohm=0.05,
max_current_a=300.0, price=300.0, source="test",
)
result = _run(_wire(), _switch(), capacitor=overpowered_capacitor)
result = _run(_wire(), _switch(), capacitor=big_capacitor)
assert result.feasible, result.reason
assert result.exit_v_mps < 0
balance = (
result.energy_remaining_cap_j + result.energy_dissipated_j + result.kinetic_energy_delta_j
)
assert balance == pytest.approx(result.energy_in_j, rel=1e-2)
def test_field_is_capped_near_saturation():
"""Регрессия на баг '13 Тесла': поле не должно улетать за B_sat железа."""
strong_capacitor = CapacitorSpec(
part_number="c-strong", capacitance_uf=200.0, voltage_v=600.0, esr_ohm=0.05,
max_current_a=500.0, price=400.0, source="test",
)
high_v_switch = SwitchSpec(
part_number="sw-hi", kind="MOSFET", max_current_a=1000.0, max_voltage_v=800.0,
on_resistance_ohm=0.01, on_voltage_drop_v=None, turn_on_time_ns=50.0, price=50.0, source="test",
)
stage = StageConfig(
wire=_wire(), capacitor=strong_capacitor, switch=high_v_switch, sensor=OPTICAL_SENSOR,
tube_od_m=0.01, turns_per_layer=40, layers=5, sensor_to_coil_distance_m=0.02, charge_voltage_v=550.0,
)
result = run_stage(entry_x_m=-0.05, entry_v_mps=5.0, stage=stage, projectile=PROJECTILE)
assert result.feasible, result.reason
# поле упирается в B_sat снаряда (1.8 Тл в фикстуре), а не улетает в десятки Тл
assert result.peak_field_estimate_tesla <= STEEL.b_sat_tesla * 1.05