- 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>
51 lines
1.9 KiB
Python
51 lines
1.9 KiB
Python
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
|