Files
gausse/tests/test_sensors.py
jze9 f9b77b3756 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>
2026-07-06 19:55:26 +05:00

51 lines
1.9 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 numpy as np
from scipy.integrate import solve_ivp
from gausse.physics.sensors import make_inductive_sensor_event, make_optical_sensor_event
def _ballistic(t, state):
return [state[1], 0.0]
def test_optical_sensor_trigger_is_velocity_independent():
x_sensor = 0.02
trigger_xs = []
for v in (1.0, 5.0, 20.0):
event = make_optical_sensor_event(x_sensor)
sol = solve_ivp(_ballistic, (0, 1.0), [-0.05, v], events=event)
assert len(sol.t_events[0]) == 1
trigger_xs.append(sol.y_events[0][0][0])
assert np.allclose(trigger_xs, x_sensor, atol=1e-9)
def test_inductive_sensor_fires_earlier_at_higher_velocity():
x_sensor = 0.02
sensitivity = 0.05
threshold = 0.3
width = 0.005
trigger_xs = []
for v in (10.0, 20.0, 40.0):
event = make_inductive_sensor_event(x_sensor, sensitivity, threshold, width)
sol = solve_ivp(_ballistic, (0, 1.0), [-0.05, v], events=event)
assert len(sol.t_events[0]) == 1
trigger_xs.append(sol.y_events[0][0][0])
# чем выше скорость, тем раньше (дальше от датчика, т.е. при меньшем x)
# срабатывает индукционный датчик, т.к. sensitivity*v*bump(x) достигает
# порога при меньшем bump(x), а значит при большем |x - x_sensor|
assert trigger_xs[0] > trigger_xs[1] > trigger_xs[2]
assert all(x < x_sensor for x in trigger_xs)
def test_inductive_sensor_never_fires_below_threshold_speed():
x_sensor = 0.02
sensitivity = 0.05
threshold = 0.3
width = 0.005
# пиковый сигнал = sensitivity * v = 0.05 * 1.0 = 0.05 << порог 0.3
event = make_inductive_sensor_event(x_sensor, sensitivity, threshold, width)
sol = solve_ivp(_ballistic, (0, 1.0), [-0.05, 1.0], events=event)
assert len(sol.t_events[0]) == 0