Files
gausse/tests/test_sensors.py
jze9 57a83cd4f8 Fix float64 overflow in inductive sensor's sech^2 bump function
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>
2026-07-06 20:55:59 +05:00

67 lines
2.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 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 - порог