mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-08-21 20:03:46 +08:00
feat(long): proactive closing jerk
This commit is contained in:
@@ -8,7 +8,10 @@ Acceleration Personality (ECO / NORMAL / SPORT). Tunes only MPC INPUTS, never th
|
||||
* positive-accel ceiling + speed-dependent per-cycle open-rate -> tier-scaled take-off from a stop
|
||||
(the open-rate is fast near v=0 so launch is never delayed, tapering to a steady-state rate at speed);
|
||||
* jerk-cost relaxation (scales the core MPC's jerk_factor) -> smooth accel/decel onset: near a stop, on
|
||||
any fresh accel<->decel direction change, and when the tracked lead is itself braking hard;
|
||||
any fresh accel<->decel direction change, when the tracked lead is itself braking hard, or when the gap
|
||||
is closing fast for any other reason (cut-in, ego overtaking a slower lead) -- the last one is the only
|
||||
proactive trigger keyed on an MPC INPUT (vRel) rather than a_ego's own realized sign flip, so it can
|
||||
soften the very first brake jab instead of only the recovery after it;
|
||||
* add-only, speed-dependent follow-gap widen on the MPC t_follow -> earlier/gentler braking, roomier gap;
|
||||
* sticky should_stop hysteresis -> no stop-and-go gas-brake-gas-brake.
|
||||
Add-only gap => desired distance >= stock => braking >= stock. Disabled => stock everywhere (byte-stock).
|
||||
@@ -24,8 +27,8 @@ from openpilot.sunnypilot import get_sanitize_int_param
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.accel_personality.constants import \
|
||||
NORMAL, PERSONALITY_MIN, PERSONALITY_MAX, A_CRUISE_MAX_BP, A_CRUISE_MAX_V, STOCK_A_CRUISE_MAX_V, \
|
||||
RISE_RATE_BP, RISE_RATE_V, STOCK_RISE_RATE, JERK_SCALE_BP, JERK_SCALE_V, ONSET_DEADBAND, ONSET_RAMP_S, \
|
||||
ONSET_FLOOR, LEAD_BRAKE_ALEAD_BP, LEAD_BRAKE_FACTOR_V, TF_WIDEN_V_BP, TF_WIDEN_BASE_V, TF_WIDEN_TIER, \
|
||||
TF_WIDEN_MAX, TF_SLEW_PER_S, TF_DECEL_HOLD_A
|
||||
ONSET_FLOOR, LEAD_BRAKE_ALEAD_BP, LEAD_BRAKE_FACTOR_V, CLOSING_VREL_BP, CLOSING_FACTOR_V, TF_WIDEN_V_BP, \
|
||||
TF_WIDEN_BASE_V, TF_WIDEN_TIER, TF_WIDEN_MAX, TF_SLEW_PER_S, TF_DECEL_HOLD_A
|
||||
|
||||
|
||||
class _OnsetRelax:
|
||||
@@ -69,6 +72,7 @@ class AccelController:
|
||||
self._onset_relax = _OnsetRelax()
|
||||
self._onset_factor = 1.0
|
||||
self._lead_brake_factor = 1.0
|
||||
self._closing_factor = 1.0
|
||||
self._read_params()
|
||||
|
||||
def _read_params(self) -> None:
|
||||
@@ -85,12 +89,15 @@ class AccelController:
|
||||
self._a_ego = float(sm['carState'].aEgo)
|
||||
|
||||
if self._enabled:
|
||||
lead = sm['radarState'].leadOne
|
||||
self._onset_factor = self._onset_relax.update(self._a_ego, ONSET_FLOOR[self._personality])
|
||||
self._lead_brake_factor = self._get_lead_brake_factor(sm['radarState'].leadOne)
|
||||
self._lead_brake_factor = self._get_lead_brake_factor(lead)
|
||||
self._closing_factor = self._get_closing_factor(lead)
|
||||
else:
|
||||
self._onset_relax.reset()
|
||||
self._onset_factor = 1.0
|
||||
self._lead_brake_factor = 1.0
|
||||
self._closing_factor = 1.0
|
||||
|
||||
self._frame += 1
|
||||
|
||||
@@ -99,12 +106,18 @@ class AccelController:
|
||||
return 1.0
|
||||
return float(np.interp(lead.aLeadK, LEAD_BRAKE_ALEAD_BP, LEAD_BRAKE_FACTOR_V[self._personality]))
|
||||
|
||||
def _get_closing_factor(self, lead) -> float:
|
||||
if not lead.status:
|
||||
return 1.0
|
||||
return float(np.interp(lead.vRel, CLOSING_VREL_BP, CLOSING_FACTOR_V[self._personality]))
|
||||
|
||||
def reset(self) -> None:
|
||||
# Drop the accumulated widen (e.g. on disengage / standstill re-init) so it re-ramps cleanly.
|
||||
self._widen = 0.0
|
||||
self._onset_relax.reset()
|
||||
self._onset_factor = 1.0
|
||||
self._lead_brake_factor = 1.0
|
||||
self._closing_factor = 1.0
|
||||
|
||||
def get_max_accel(self, v_ego: float) -> float:
|
||||
# Disabled -> stock ceiling (off == stock, independent of the NORMAL profile so NORMAL is free to differ).
|
||||
@@ -119,13 +132,14 @@ class AccelController:
|
||||
return float(np.interp(v_ego, RISE_RATE_BP, RISE_RATE_V[self._personality]))
|
||||
|
||||
def get_jerk_scale(self, v_ego: float) -> float:
|
||||
# Disabled -> 1.0 -> byte-stock jerk cost. Enabled: takes the most-relaxed of three tier-scaled factors
|
||||
# -- near a stop (v_ego), a fresh accel<->decel onset (any speed), and a hard-braking lead -- each never
|
||||
# exceeding 1.0 (stock), so this only ever relaxes jerk cost, never tightens it beyond stock.
|
||||
# Disabled -> 1.0 -> byte-stock jerk cost. Enabled: takes the most-relaxed of four tier-scaled factors --
|
||||
# near a stop (v_ego), a fresh accel<->decel onset (any speed), a hard-braking lead, and a fast-closing
|
||||
# gap (any cause) -- each never exceeding 1.0 (stock), so this only ever relaxes jerk cost, never tightens
|
||||
# it beyond stock.
|
||||
if not self._enabled:
|
||||
return 1.0
|
||||
near_stop = float(np.interp(v_ego, JERK_SCALE_BP, JERK_SCALE_V[self._personality]))
|
||||
return min(near_stop, self._onset_factor, self._lead_brake_factor)
|
||||
return min(near_stop, self._onset_factor, self._lead_brake_factor, self._closing_factor)
|
||||
|
||||
def get_t_follow(self, t_follow: float, v_ego: float) -> float:
|
||||
# MPC t_follow hook. Adds a slewed, decel-held, speed-dependent comfort widen on top of the stock
|
||||
|
||||
@@ -84,6 +84,20 @@ LEAD_BRAKE_FACTOR_V = {
|
||||
SPORT: [0.45, 1.0],
|
||||
}
|
||||
|
||||
# --- Closing-rate jerk-cost relaxation (MPC INPUT: react faster to a fast-closing gap, any cause) ----------
|
||||
# Complements LEAD_BRAKE_FACTOR_V, which keys off the LEAD's own deceleration: a gap can close quickly for
|
||||
# reasons aLeadK never reflects (a cut-in, or ego simply catching up faster than the lead is slowing). Onset
|
||||
# relax (above) only reacts the cycle AFTER a_ego has already crossed its deadband -- reactive on a realized
|
||||
# signal, so it structurally can't soften the very first jab into a fresh, fast-closing gap. vRel is an MPC
|
||||
# INPUT (causal, known before any brake is commanded), so keying off it directly closes that gap. No lead, or
|
||||
# not closing past the gate -> 1.0. Disabled -> 1.0.
|
||||
CLOSING_VREL_BP = [-6.0, -1.5] # m/s, closing rate (negative = closing), ascending for np.interp
|
||||
CLOSING_FACTOR_V = {
|
||||
ECO: [0.75, 1.0],
|
||||
NORMAL: [0.60, 1.0],
|
||||
SPORT: [0.45, 1.0],
|
||||
}
|
||||
|
||||
# --- Follow-gap widen (add-only, fed to the MPC t_follow) ------------------------------------------------
|
||||
# Add a small speed-dependent widen to the stock t_follow (the driver's gap-button value). Wider gap ->
|
||||
# MPC brakes earlier + gentler onto a slowing lead and settles a roomier cruise gap. Invariants:
|
||||
|
||||
@@ -20,8 +20,8 @@ from openpilot.sunnypilot.selfdrive.controls.lib.accel_personality.accel_control
|
||||
from openpilot.sunnypilot.selfdrive.controls.lib.accel_personality.constants import \
|
||||
ECO, NORMAL, SPORT, PERSONALITY_MIN, PERSONALITY_MAX, A_CRUISE_MAX_BP, RISE_RATE_V, \
|
||||
STOCK_A_CRUISE_MAX_V, STOCK_RISE_RATE, JERK_SCALE_BP, JERK_SCALE_V, ONSET_DEADBAND, ONSET_RAMP_S, \
|
||||
ONSET_FLOOR, LEAD_BRAKE_ALEAD_BP, LEAD_BRAKE_FACTOR_V, TF_WIDEN_V_BP, TF_WIDEN_BASE_V, TF_WIDEN_TIER, \
|
||||
TF_WIDEN_MAX, TF_SLEW_PER_S, TF_DECEL_HOLD_A, AccelerationPersonality
|
||||
ONSET_FLOOR, LEAD_BRAKE_ALEAD_BP, LEAD_BRAKE_FACTOR_V, CLOSING_VREL_BP, CLOSING_FACTOR_V, TF_WIDEN_V_BP, \
|
||||
TF_WIDEN_BASE_V, TF_WIDEN_TIER, TF_WIDEN_MAX, TF_SLEW_PER_S, TF_DECEL_HOLD_A, AccelerationPersonality
|
||||
|
||||
_EPS = 1e-6
|
||||
_TF_STOCK = 1.45 # a representative stock t_follow (standard personality); the widen is add-only on top
|
||||
@@ -42,8 +42,8 @@ class FakeParams:
|
||||
self.store[key] = val
|
||||
|
||||
|
||||
def make_lead(status=False, aLeadK=0.0):
|
||||
return SimpleNamespace(status=status, aLeadK=aLeadK)
|
||||
def make_lead(status=False, aLeadK=0.0, vRel=0.0):
|
||||
return SimpleNamespace(status=status, aLeadK=aLeadK, vRel=vRel)
|
||||
|
||||
|
||||
def make_sm(v_ego=20.0, a_ego=0.0, lead=None):
|
||||
@@ -287,6 +287,56 @@ def test_lead_brake_matches_constants_table():
|
||||
assert ctrl.get_jerk_scale(20.0) == pytest.approx(expected)
|
||||
|
||||
|
||||
# --- closing-rate relax: fast-closing gap relaxes jerk cost proactively, any cause -------------------------
|
||||
|
||||
def test_closing_no_lead_is_stock():
|
||||
ctrl = make_controller(personality=SPORT)
|
||||
ctrl.update(make_sm(v_ego=20.0, lead=make_lead(status=False, vRel=-8.0)))
|
||||
assert ctrl.get_jerk_scale(20.0) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_closing_relaxes_with_fast_closing_lead():
|
||||
for personality, floor in ((ECO, 0.75), (NORMAL, 0.60), (SPORT, 0.45)):
|
||||
ctrl = make_controller(personality=personality)
|
||||
ctrl.update(make_sm(v_ego=20.0, lead=make_lead(status=True, vRel=-6.0)))
|
||||
assert ctrl.get_jerk_scale(20.0) == pytest.approx(floor)
|
||||
|
||||
|
||||
def test_closing_slow_closing_is_stock():
|
||||
ctrl = make_controller(personality=SPORT)
|
||||
ctrl.update(make_sm(v_ego=20.0, lead=make_lead(status=True, vRel=-0.5)))
|
||||
assert ctrl.get_jerk_scale(20.0) == pytest.approx(1.0) # above the -1.5 gate -> no relax
|
||||
|
||||
|
||||
def test_closing_opening_gap_is_stock():
|
||||
ctrl = make_controller(personality=SPORT)
|
||||
ctrl.update(make_sm(v_ego=20.0, lead=make_lead(status=True, vRel=3.0))) # lead pulling away
|
||||
assert ctrl.get_jerk_scale(20.0) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_closing_matches_constants_table():
|
||||
for personality in (ECO, NORMAL, SPORT):
|
||||
ctrl = make_controller(personality=personality)
|
||||
for v_rel in (-1.5, -3.0, -6.0):
|
||||
ctrl.update(make_sm(v_ego=20.0, lead=make_lead(status=True, vRel=v_rel)))
|
||||
expected = np.interp(v_rel, CLOSING_VREL_BP, CLOSING_FACTOR_V[personality])
|
||||
assert ctrl.get_jerk_scale(20.0) == pytest.approx(expected)
|
||||
|
||||
|
||||
def test_closing_disabled_is_stock():
|
||||
ctrl = make_controller(enabled=False, personality=SPORT)
|
||||
ctrl.update(make_sm(v_ego=20.0, lead=make_lead(status=True, vRel=-6.0)))
|
||||
assert ctrl.get_jerk_scale(20.0) == pytest.approx(1.0)
|
||||
|
||||
|
||||
def test_closing_fires_before_a_ego_moves():
|
||||
# The whole point: on the VERY FIRST cycle a fast-closing lead appears, before a_ego has had any chance to
|
||||
# react (still 0.0, so onset-relax is untouched) -- the closing factor alone must already be relaxed.
|
||||
ctrl = make_controller(personality=NORMAL)
|
||||
ctrl.update(make_sm(v_ego=20.0, a_ego=0.0, lead=make_lead(status=True, vRel=-6.0)))
|
||||
assert ctrl.get_jerk_scale(20.0) == pytest.approx(CLOSING_FACTOR_V[NORMAL][0])
|
||||
|
||||
|
||||
# --- combined: get_jerk_scale takes the most-relaxed of all three factors ----------------------------------
|
||||
|
||||
def test_combined_takes_most_relaxed_factor():
|
||||
@@ -300,7 +350,7 @@ def test_combined_takes_most_relaxed_factor():
|
||||
def test_reset_clears_onset_and_lead_brake_state():
|
||||
ctrl = make_controller(personality=SPORT)
|
||||
ctrl.update(make_sm(v_ego=20.0, a_ego=1.0))
|
||||
ctrl.update(make_sm(v_ego=20.0, a_ego=-1.0, lead=make_lead(status=True, aLeadK=-3.0)))
|
||||
ctrl.update(make_sm(v_ego=20.0, a_ego=-1.0, lead=make_lead(status=True, aLeadK=-3.0, vRel=-6.0)))
|
||||
assert ctrl.get_jerk_scale(20.0) < 1.0 - _EPS
|
||||
ctrl.reset()
|
||||
assert ctrl.get_jerk_scale(20.0) == pytest.approx(1.0)
|
||||
|
||||
@@ -12,10 +12,15 @@ reports a farther-or-faster lead than reality, so braking is always >= stock. Fo
|
||||
out. A closer jump of any size always passes immediately -- this only ever delays relief, never a brake;
|
||||
* flicker-hold: keep a just-dropped, recently-sustained lead alive (dead-reckoned) through a brief radar
|
||||
dropout so the MPC does not lose and re-grab it (which reads as a phantom release then a catch-up brake);
|
||||
* churn smoother: a short EMA on a trackId-churning lead's dRel/vLead/vRel so the MPC stops hunting the gap
|
||||
(removes the follow-jitter that reads as rubber-banding). dRel is asymmetric -- closer accepted
|
||||
immediately, only farther is EMA-lagged -- so it can't hold a steadily-closing lead farther-than-true;
|
||||
vLead/vRel stay symmetric (secondary terms, no demonstrated need to bias them);
|
||||
* churn/noise smoother: a short EMA on a lead's dRel/vLead/vRel so the MPC stops hunting the gap (removes
|
||||
the follow-jitter that reads as rubber-banding and, on the sensor side, as a lead-detection "lurch").
|
||||
Covers two DISTINCT same-physical-object noise signatures: trackId churn (id flips frame-to-frame but the
|
||||
kinematics stay coherent -- one real lead getting re-labeled) and same-track noise (id stays constant but
|
||||
vLead itself is bimodal/bouncing -- one real lead with a noisy fusion/Doppler velocity read). Both are
|
||||
safe to EMA because the id evidence pins them to a SINGLE physical object; a bimodal vLead WITH the id
|
||||
also changing is left alone (ambiguous -- could be two really-different real objects) so this can never
|
||||
average two real tracks together. dRel is asymmetric -- closer accepted immediately, only farther is
|
||||
EMA-lagged -- so it can't hold a steadily-closing lead farther-than-true; vLead/vRel stay symmetric;
|
||||
* stop-gap: near a (near-)stopped lead at low speed report dRel a touch closer so the MPC's own smooth stop
|
||||
settles farther back (the Prius TSS2 stock crawl creeps in to ~1.5 m). Monotone (closer => brake >= stock).
|
||||
Overridden off by sustained lead motion (even slow creep) so it can't suppress a real, growing gap during
|
||||
@@ -134,12 +139,13 @@ class _RadarStateProxy:
|
||||
|
||||
|
||||
class _LeadSmoother:
|
||||
# EMA on a churning lead's dRel/vLead/vRel (jitter removal). A hold keeps it active through brief churn
|
||||
# gaps (churn toggles); passthrough + reset only after the hold lapses. dRel is ASYMMETRIC: a closer raw
|
||||
# reading is accepted immediately (never delay awareness of closer -- the file's own invariant), only a
|
||||
# FARTHER raw reading is EMA-lagged (reject noise in that direction). Without this, a lead that's genuinely
|
||||
# closing steadily while churn is (even briefly) active gets held farther-than-true for the full
|
||||
# LEAD_SMOOTH_HOLD window, then snaps -- a false-relief-then-correction that itself becomes a hard brake.
|
||||
# EMA on a noisy same-physical-object lead's dRel/vLead/vRel (jitter removal; see _LeadStability for what
|
||||
# qualifies as "same object"). A hold keeps it active through brief noise gaps (the trigger toggles on/off);
|
||||
# passthrough + reset only after the hold lapses. dRel is ASYMMETRIC: a closer raw reading is accepted
|
||||
# immediately (never delay awareness of closer -- the file's own invariant), only a FARTHER raw reading is
|
||||
# EMA-lagged (reject noise in that direction). Without this, a lead that's genuinely closing steadily while
|
||||
# noisy (even briefly) gets held farther-than-true for the full LEAD_SMOOTH_HOLD window, then snaps -- a
|
||||
# false-relief-then-correction that itself becomes a hard brake.
|
||||
def __init__(self):
|
||||
self._d = None
|
||||
self._vl = None
|
||||
@@ -152,8 +158,8 @@ class _LeadSmoother:
|
||||
self._vr = None
|
||||
self._hold = 0
|
||||
|
||||
def update(self, lead, churn: bool):
|
||||
self._hold = LEAD_SMOOTH_HOLD if churn else self._hold - 1
|
||||
def update(self, lead, noisy: bool):
|
||||
self._hold = LEAD_SMOOTH_HOLD if noisy else self._hold - 1
|
||||
if self._hold <= 0 or not lead.status:
|
||||
self.reset()
|
||||
return lead
|
||||
@@ -234,12 +240,21 @@ class _LeadHold:
|
||||
class _LeadStability:
|
||||
# Read-only monitor: flags an unstable leadOne -- bimodal/bouncing vLead, dRel track-switch jumps, or
|
||||
# radarTrackId churn (a steady lead flipping track ids -> vRel jitter -> follow-hunting). Telemetry only.
|
||||
# Also derives same_track_noise: a bimodal/bouncing vLead while radarTrackId sat CONSTANT the whole window
|
||||
# -- i.e. the id evidence pins the noise to one physical object (a Doppler/fusion-noisy velocity read on one
|
||||
# real lead), so it is safe to feed the smoother (see _LeadSmoother). A bimodal vLead WITH the id also
|
||||
# changing stays outside same_track_noise (could be two really-different real objects at different speeds)
|
||||
# and is left unmitigated, same as before. dRel track-jumps are deliberately excluded here: while status
|
||||
# stays True (this class's own precondition), a repeated FARTHER dRel jump is already absorbed by
|
||||
# _JumpGuard upstream (same SWITCH_DREL threshold), so adding it here would just double up on the same
|
||||
# signal rather than covering a real gap.
|
||||
def __init__(self):
|
||||
self._v = deque(maxlen=STABILITY_WINDOW)
|
||||
self._d = deque(maxlen=STABILITY_WINDOW)
|
||||
self._id = deque(maxlen=ID_CHURN_WINDOW)
|
||||
self.unstable = False
|
||||
self.churn = False
|
||||
self.same_track_noise = False
|
||||
|
||||
def reset(self):
|
||||
self._v.clear()
|
||||
@@ -247,6 +262,7 @@ class _LeadStability:
|
||||
self._id.clear()
|
||||
self.unstable = False
|
||||
self.churn = False
|
||||
self.same_track_noise = False
|
||||
|
||||
def update(self, lead, v_ego: float) -> None:
|
||||
if not lead.status or v_ego < CREEP_PASSTHROUGH_V:
|
||||
@@ -262,7 +278,10 @@ class _LeadStability:
|
||||
d_jumps = sum(abs(b - a) > SWITCH_DREL for a, b in zip(self._d, list(self._d)[1:], strict=False))
|
||||
ids = list(self._id)
|
||||
id_churn = sum(1 for a, b in zip(ids, ids[1:], strict=False) if a != b and a > 0 and b > 0)
|
||||
recent_ids = ids[-STABILITY_WINDOW:]
|
||||
same_track = recent_ids[0] > 0 and len(set(recent_ids)) == 1
|
||||
self.churn = id_churn >= ID_CHURN and v_spread <= VLEAD_SPREAD # steady lead, flipping ids (not bimodal)
|
||||
self.same_track_noise = same_track and v_spread > VLEAD_SPREAD
|
||||
self.unstable = v_spread > VLEAD_SPREAD or d_jumps >= 2 or self.churn
|
||||
|
||||
|
||||
@@ -333,14 +352,15 @@ class RadarDistanceController:
|
||||
if not self._enabled:
|
||||
return radarstate # off: byte-stock passthrough
|
||||
two = radarstate.leadTwo
|
||||
noisy = self._stability.churn or self._stability.same_track_noise
|
||||
if self._v_ego >= LOW_SPEED_PASSTHROUGH_V:
|
||||
one = self._jump_guard.step(radarstate.leadOne) # reject a same-cycle farther-jump transient ...
|
||||
one = self._one.step(one) # ... + flicker-hold ...
|
||||
two = self._two.step(radarstate.leadTwo)
|
||||
one = self._smoother.update(one, self._stability.churn) # ... + churn de-jitter (anti follow-hunt)
|
||||
one = self._smoother.update(one, noisy) # ... + same-object de-jitter (anti follow-hunt)
|
||||
elif self._v_ego >= CREEP_PASSTHROUGH_V:
|
||||
# creep band: churn de-jitter ONLY (symmetric EMA), no flicker-hold (a stale held lead would delay launch)
|
||||
one = self._smoother.update(radarstate.leadOne, self._stability.churn)
|
||||
# creep band: de-jitter ONLY (symmetric EMA), no flicker-hold (a stale held lead would delay launch)
|
||||
one = self._smoother.update(radarstate.leadOne, noisy)
|
||||
else:
|
||||
one = radarstate.leadOne # full standstill: no hold/smoothing
|
||||
one = self._stop_gap_bias(one) # low-speed near-stopped: settle farther back
|
||||
|
||||
@@ -464,3 +464,39 @@ def test_stability_runs_even_when_disabled():
|
||||
for i in range(10):
|
||||
c.smooth_radarstate(rs(lead(dRel=40.0, vLead=18.0 if i % 2 else 10.0)))
|
||||
assert c.lead_unstable() # telemetry not gated by the RadarDistance param
|
||||
|
||||
|
||||
# --- same-track noise smoother (bimodal vLead / repeated dRel jump on a CONSTANT radarTrackId) -------------
|
||||
|
||||
def test_smoother_dejitters_bimodal_vlead_on_same_track():
|
||||
# Same physical object (radarTrackId constant) but a bouncing velocity read (Doppler/fusion noise) -- the
|
||||
# id evidence pins this to ONE real lead, so it's safe to EMA (unlike a bimodal read with a changing id).
|
||||
c = ctrl()
|
||||
out = None
|
||||
for i in range(30):
|
||||
out = c.smooth_radarstate(rs(lead(dRel=40.0, vLead=18.0 if i % 2 else 10.0, vRel=-1.0, radarTrackId=9)))
|
||||
assert c.lead_unstable()
|
||||
assert 10.0 < out.leadOne.vLead < 18.0 # EMA settled between the two bouncing readings
|
||||
assert out.leadOne.vLead not in (10.0, 18.0)
|
||||
|
||||
|
||||
def test_smoother_inactive_on_bimodal_vlead_with_changing_track():
|
||||
# Same bimodal vLead signature, but radarTrackId ALSO changes -- ambiguous (could be two really-different
|
||||
# real objects at different speeds), so this must NOT be smoothed, unlike the same-track case above.
|
||||
c = ctrl()
|
||||
one = lead(dRel=40.0, vLead=18.0, radarTrackId=1)
|
||||
for i in range(10):
|
||||
c.smooth_radarstate(rs(lead(dRel=40.0, vLead=18.0 if i % 2 else 10.0, radarTrackId=1 if i % 2 else 2)))
|
||||
out = c.smooth_radarstate(rs(one))
|
||||
assert out.leadOne is one # exact passthrough -- not averaged across tracks
|
||||
|
||||
|
||||
def test_smoother_same_track_noise_ignores_drel_jump():
|
||||
# dRel track-jumps are excluded from same_track_noise on purpose: while status stays True, a repeated
|
||||
# farther jump this large is already absorbed by _JumpGuard upstream, so the smoother never even sees the
|
||||
# raw alternation here -- confirms the two mechanisms don't double up on the same signal.
|
||||
c = ctrl()
|
||||
out = None
|
||||
for i in range(30):
|
||||
out = c.smooth_radarstate(rs(lead(dRel=40.0 if i % 2 == 0 else 55.0, vLead=18.0, vRel=-1.0, radarTrackId=4)))
|
||||
assert out.leadOne.dRel < 45.0 # held near the trusted value by the jump-guard, not 55
|
||||
|
||||
Reference in New Issue
Block a user