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>
This commit is contained in:
jze9
2026-07-06 20:55:59 +05:00
parent 65d8b633ad
commit 57a83cd4f8
2 changed files with 26 additions and 1 deletions

View File

@@ -18,6 +18,15 @@ import numpy as np
SensorEvent = Callable[[float, np.ndarray], float]
# cosh(z)**2 переполняет float64 при |z| > ~355; клэмпим до этого аргумент
# sech^2, а не сам bump -- при |z|>=20 бамп уже ~1e-17, дальнейший рост z
# физически ничего не меняет (снаряд всё равно "не виден" датчику).
_SECH2_ARG_CLIP = 20.0
def _sech2(z: np.ndarray) -> np.ndarray:
return 1.0 / np.cosh(np.clip(z, -_SECH2_ARG_CLIP, _SECH2_ARG_CLIP)) ** 2
def make_optical_sensor_event(x_sensor_m: float) -> SensorEvent:
def event(t: float, state: np.ndarray) -> float:
@@ -36,7 +45,7 @@ def make_inductive_sensor_event(
) -> SensorEvent:
def event(t: float, state: np.ndarray) -> float:
x, v = state
bump = 1.0 / np.cosh((x - x_sensor_m) / width_m) ** 2
bump = _sech2((x - x_sensor_m) / width_m)
return sensitivity_v_per_mps * v * bump - threshold_v
event.terminal = True

View File

@@ -1,3 +1,5 @@
import warnings
import numpy as np
from scipy.integrate import solve_ivp
@@ -48,3 +50,17 @@ def test_inductive_sensor_never_fires_below_threshold_speed():
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 - порог