Skip to content

Commit 8a59506

Browse files
rocketmarkclaude
andcommitted
Add input-level lightcap angular rate gate (lc-angular-rate-max)
Adds a pre-update gate that drops lightcap batches implying an angular rate above lc_angular_rate_max rad/s, computed against the last accepted batch. Unlike the existing output-level kalman-max-pose-angular-rate gate, this prevents reflection data from ever touching the Kalman state. Also fixes the innovation gate death spiral: change > 0 to > 1e-4 for light_residuals_all so the outlier threshold can't lock out all data during cold-start when the EWMA mean is near zero. Adds SV_INFO logging for both gates so firings appear in journalctl without --survive-verbose. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 8914463 commit 8a59506

2 files changed

Lines changed: 48 additions & 2 deletions

File tree

src/survive_kalman_tracker.c

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ STRUCT_CONFIG_SECTION(SurviveKalmanTracker)
3232
STRUCT_CONFIG_ITEM("kalman-max-pose-angular-rate",
3333
"Maximum angular rate (rad/s) before suppressing pose output; -1 to disable",
3434
-1., t->max_pose_angular_rate)
35+
STRUCT_CONFIG_ITEM("lc-angular-rate-max",
36+
"Maximum angular rate (rad/s) before rejecting lightcap batch input; -1 to disable",
37+
-1., t->lc_angular_rate_max)
3538
STRUCT_CONFIG_ITEM("min-report-time",
3639
"Minimum kalman report time in s (-1 defaults to 1. / imu_hz)", -1., t->min_report_time)
3740
STRUCT_CONFIG_ITEM("report-covariance", "Report covariance matrix every n poses", -1, t->report_covariance_cnt);
@@ -412,6 +415,28 @@ void survive_kalman_tracker_integrate_saved_light(SurviveKalmanTracker *tracker,
412415
qsort(tracker->savedLight, tracker->savedLight_idx, sizeof(tracker->savedLight[0]), sort_by_lh_axis_sensor);
413416
}
414417

418+
// Input-level angular rate gate: reject lightcap batches implying physically
419+
// impossible rotation speed before they can corrupt the Kalman state.
420+
// Unlike the output-level kalman-max-pose-angular-rate gate, this prevents
421+
// the filter from ever integrating reflection data — the state stays clean
422+
// and coasts on IMU-only until a valid batch arrives.
423+
if (tracker->lc_angular_rate_max > 0 && tracker->last_accepted_lc_time > 0 &&
424+
quatmagnitude(tracker->last_accepted_lc_rot) > 0.5) {
425+
SurvivePose predicted = {0};
426+
survive_kalman_tracker_predict(tracker, time, &predicted);
427+
FLT dt = time - tracker->last_accepted_lc_time;
428+
if (dt > 0) {
429+
FLT ang_rate = quatdist(tracker->last_accepted_lc_rot, predicted.Rot) / dt;
430+
if (ang_rate > tracker->lc_angular_rate_max) {
431+
SV_INFO("lc-gate: dropping batch %.2f rad/s > lc-angular-rate-max %.2f for %s",
432+
ang_rate, tracker->lc_angular_rate_max,
433+
survive_colorize_codename(tracker->so));
434+
tracker->stats.lightcap_model_dropped++;
435+
return;
436+
}
437+
}
438+
}
439+
415440
FLT rtn = 0;
416441
while(tracker->savedLight_idx > 0) {
417442
int lh = tracker->savedLight[tracker->savedLight_idx-1].lh;
@@ -457,7 +482,11 @@ void survive_kalman_tracker_integrate_saved_light(SurviveKalmanTracker *tracker,
457482
// measurements too far from the current state estimate to be trusted
458483
// (typically a reflection or severe miscalibration) and is skipped for
459484
// this sync cycle.
460-
if (tracker->light_outlier_threshold > 0 && tracker->light_residuals_all > 0) {
485+
// Require the EWMA to exceed a floor before gating: on cold start / after
486+
// a reset the mean is tiny and 5*tiny ≈ 0, which rejects everything and
487+
// prevents the EWMA from ever warming up (death spiral). 1e-4 is safely
488+
// below steady-state residuals (~0.0002) but above the warm-up bootstrap.
489+
if (tracker->light_outlier_threshold > 0 && tracker->light_residuals_all > 1e-4) {
461490
CN_CREATE_STACK_VEC(y_dry, cnt);
462491
bool dry_ok = map_light_data(&cbctx, &Z, &tracker->model.state, &y_dry, NULL);
463492
if (dry_ok) {
@@ -466,6 +495,10 @@ void survive_kalman_tracker_integrate_saved_light(SurviveKalmanTracker *tracker,
466495
for (int i = 0; i < cnt; i++) sq += yv[i] * yv[i];
467496
FLT rms = FLT_SQRT(sq / cnt);
468497
if (rms > tracker->light_outlier_threshold * tracker->light_residuals_all) {
498+
SV_INFO("lc-gate: dropping LH%d batch rms=%.4f > %.1f*mean=%.4f for %s",
499+
lh, rms, tracker->light_outlier_threshold,
500+
tracker->light_residuals_all,
501+
survive_colorize_codename(tracker->so));
469502
tracker->stats.lightcap_model_dropped++;
470503
continue;
471504
}
@@ -533,6 +566,14 @@ void survive_kalman_tracker_integrate_saved_light(SurviveKalmanTracker *tracker,
533566
SV_DATA_LOG("res_error_light_avg", &tracker->light_residuals_all, 1);
534567
tracker->stats.lightcap_count++;
535568

569+
// Update the lightcap input gate reference now that this batch was accepted.
570+
if (tracker->lc_angular_rate_max > 0) {
571+
SurvivePose post = {0};
572+
survive_kalman_tracker_predict(tracker, time, &post);
573+
quatcopy(tracker->last_accepted_lc_rot, post.Rot);
574+
tracker->last_accepted_lc_time = time;
575+
}
576+
536577
survive_kalman_tracker_report_state(pd, tracker);
537578
}
538579
}
@@ -1614,6 +1655,7 @@ void survive_kalman_tracker_stats(SurviveKalmanTracker *tracker) {
16141655

16151656
memset(&tracker->stats, 0, sizeof(tracker->stats));
16161657
tracker->first_report_time = tracker->last_report_time = 0;
1658+
tracker->last_accepted_lc_time = 0;
16171659

16181660
SV_VERBOSE(5, " ");
16191661
}

src/survive_kalman_tracker.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,13 @@ typedef struct SurviveKalmanTracker {
6565

6666
int light_batchsize;
6767

68-
FLT max_pose_angular_rate; // rad/s; -1 disables the gate
68+
FLT max_pose_angular_rate; // rad/s; -1 disables the output-level pose gate
6969
LinmathQuat last_reported_pose_rot; // rotation of last emitted pose (for angular rate gate)
7070

71+
FLT lc_angular_rate_max; // rad/s; -1 disables the lightcap input gate
72+
LinmathQuat last_accepted_lc_rot; // rotation when last lightcap batch was accepted
73+
FLT last_accepted_lc_time; // time of last accepted lightcap batch
74+
7175
FLT last_light_time, last_report_time, first_report_time;
7276
FLT first_imu_time, last_imu_time;
7377
FLT min_report_time;

0 commit comments

Comments
 (0)