cosh(z)**2 overflows past |z|~355, which happens routinely once solve_ivp evaluates the sensor event far from x_sensor over a wide time budget -- correct result (bump->0) but with a RuntimeWarning on every call, which would flood stderr across a million-run sweep. Clamp the argument to ±20 (already ~1e-17, physically indistinguishable from further growth) before computing cosh. Caught by running a real 500-run sweep through Docker as part of Stage 8 verification, not by unit tests alone -- added a regression test asserting no warning and a finite result far from the sensor. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
67 lines
2.9 KiB
Python
67 lines
2.9 KiB
Python
import warnings
|
||
|
||
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
|
||
|
||
|
||
def test_inductive_event_far_from_sensor_is_finite_and_does_not_warn():
|
||
"""Регрессия: cosh(z)**2 переполнял float64 для больших |x - x_sensor| /
|
||
width_m (типично при интегрировании на широком временном бюджете) —
|
||
результат физически верный (bump -> 0), но с RuntimeWarning на каждый
|
||
вызов, что при миллионах прогонов sweep превращается в лавину спама."""
|
||
event = make_inductive_sensor_event(x_sensor_m=0.02, sensitivity_v_per_mps=0.05, threshold_v=0.3, width_m=0.005)
|
||
far_state = np.array([-5.0, 20.0]) # 5 м от датчика при ширине чувствительности 5мм
|
||
with warnings.catch_warnings():
|
||
warnings.simplefilter("error")
|
||
value = event(0.0, far_state)
|
||
assert np.isfinite(value)
|
||
assert value == -0.3 # bump практически 0 -> событие = 0 - порог
|