Add dual sensor models and single-stage simulator; fix energy-conservation bug

- physics/sensors.py: optical/Hall (velocity-independent) and inductive
  (velocity-scaled, sech^2 spatial sensitivity) trigger events for solve_ivp
- sim/stage.py: flight-to-trigger -> fire delay -> discharge -> energy
  accounting, returning StageResult(feasible=False, reason=...) instead of
  raising when a sensor never fires or discharge never commutates
- Found and fixed a real bug caught by the energy-conservation test: the
  saturation clamp was applied to the mechanical force but not the
  electrical back-EMF term, silently breaking energy balance by ~15%.
  Removed the dynamic clamp (documented as a deferred nonlinear-L(x,I)
  limitation) and kept saturation as a diagnostic-only warning
  (StageResult.saturation_warning) so numbers stay honest rather than
  quietly wrong. Balance error is now ~0.02%.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
jze9
2026-07-06 19:55:26 +05:00
parent 2046dcba10
commit f9b77b3756
8 changed files with 484 additions and 33 deletions

View File

@@ -22,14 +22,13 @@ def test_saturation_scale_clamps_above_bsat():
assert scale == pytest.approx(0.5)
def test_force_is_reduced_once_saturated():
common = dict(dl_dx_unsaturated=1e-3, mu_eff=200, total_turns=300, coil_length_m=0.05, b_sat_tesla=1.8)
force_unsaturated = force_on_slug_newtons(current_a=5, **common)
force_saturated = force_on_slug_newtons(current_a=500, **common)
# без клэмпа сила росла бы как I^2 (в 10000 раз); с насыщением рост должен быть намного меньше
assert force_saturated / force_unsaturated < 5000
def test_force_scales_as_current_squared():
# F = 0.5*I^2*dL/dx: без клэмпа (см. модуль-докстринг force.py про энергобаланс)
# сила должна расти строго как I^2, иначе нарушится точный энергобаланс контура.
f_low = force_on_slug_newtons(current_a=5, dl_dx=1e-3)
f_high = force_on_slug_newtons(current_a=50, dl_dx=1e-3)
assert f_high / f_low == pytest.approx(100.0)
def test_force_zero_at_zero_current():
common = dict(dl_dx_unsaturated=1e-3, mu_eff=200, total_turns=300, coil_length_m=0.05, b_sat_tesla=1.8)
assert force_on_slug_newtons(current_a=0, **common) == pytest.approx(0.0)
assert force_on_slug_newtons(current_a=0, dl_dx=1e-3) == pytest.approx(0.0)