Skip to content

Commit fa3ec1a

Browse files
committed
added new property test for this mpfit regression.
1 parent 0d42afb commit fa3ec1a

2 files changed

Lines changed: 62 additions & 3 deletions

File tree

docs/property-tests.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,9 +106,12 @@ Fuzz math functions with adversarial and degenerate inputs to verify no crashes
106106
| `KabschCollinear` | Kabsch with collinear points doesn't crash, produces finite translation (rotation underdetermined) |
107107
| `KabschCoplanar` | Kabsch with coplanar points doesn't crash, produces finite pose (rotation underdetermined around normal axis) |
108108
| `LargeQuatNoOverflow` | `quatrotatevector` with very large quaternion components doesn't overflow |
109+
| `AggregateErrorOverCountGate` | `sqrtf(sum/cnt)` compared against a status gate: `cnt==0` must never take the success branch; finite `sum`/`cnt>0` always yields finite `sensor_error`; the gate's success/failure decision is self-consistent with `sensor_error <= max_cal_error` |
109110

110111
`quatnormalize(zero)` and `normalize3d(zero)` produce NaN (documented, not a crash). Callers that may receive zero-magnitude input must guard against NaN propagation at their own boundary.
111112

113+
`AggregateErrorOverCountGate` is a regression test for the `mpfit_nan_guard` gap (Stagehand project, `poser_mpfit.c::solve_global_scene`): two existing guards filtered non-finite *inputs* into the GSS error accumulator, but nothing checked the *output* of `sqrtf(stats.sensor_error / stats.sensor_error_cnt)` before the success/failure branch. When every measurement was filtered upstream, `sensor_error_cnt` reached 0, producing `NaN`, which compares false against any threshold — so a 0-measurement solve silently took the success branch and poisoned the Kalman filter. The test isolates the divide-then-gate arithmetic so any optimizer code with the same shape (`status_failure || !isfinite(x) || x > threshold`) is exercised without needing a full `SurviveContext`/GSS scene fixture.
114+
112115
### 6. Reprojection Residual / BSVD Pose Solver (`reproject_residual_props.c`)
113116

114117
6 properties, 1,000 random trials each (6,000 total).
@@ -166,11 +169,11 @@ At the default threshold 0.0, sensors within 90° of the lighthouse are accepted
166169
| Reprojection | `reproject_props.c` | 13 | 65,000 |
167170
| Kabsch | `kabsch_props.c` | 5 | 25,000 |
168171
| Kalman Predict | `kalman_props.c` | 10 | ~74M steps |
169-
| Numeric Robustness | `numeric_props.c` | 10 | ~80,000 |
172+
| Numeric Robustness | `numeric_props.c` | 11 | ~90,000 |
170173
| Reprojection Residual | `reproject_residual_props.c` | 6 | 6,000 |
171174
| Event Queue | `event_queue_props.c` | 7 | ~3,500 |
172175
| Normal Filter | `normal_filter_props.c` | 3 | 30,000 |
173-
| **Total** | | **70** | |
176+
| **Total** | | **71** | |
174177

175178
## Running the Tests
176179

src/test_cases/numeric_props.c

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,63 @@ TEST(NumericProps, KabschCoplanar) {
314314
return 0;
315315
}
316316

317-
// 10. Large quaternion components don't cause overflow in quatrotatevector
317+
// 10. Aggregate-error-over-count gate: mirrors solve_global_scene()'s
318+
// `sensor_error = sqrtf(sum / cnt); if (status_failure || !isfinite(sensor_error) || ...)`
319+
// pattern. Any optimizer success path that divides an accumulated error
320+
// by a measurement count must reject non-finite results — including the
321+
// cnt==0 case, where 0/0 produces NaN that compares false against every
322+
// threshold and would otherwise fall through to "success".
323+
// Regression test for the mpfit_nan_guard gap: two upstream guards
324+
// filtered non-finite *inputs* to the accumulator, but nothing checked
325+
// the *output* of the division before the success/failure decision.
326+
TEST(NumericProps, AggregateErrorOverCountGate) {
327+
unsigned seed = (unsigned)time(NULL);
328+
srand(seed);
329+
330+
// cnt == 0: every measurement was filtered upstream (e.g. all NaN angles).
331+
{
332+
FLT sum = 0, cnt = 0;
333+
FLT sensor_error = sqrtf(sum / cnt);
334+
int status_failure = 0; // upstream solve "succeeded" on zero measurements
335+
FLT max_cal_error = 1.0;
336+
int took_success_branch = !(status_failure || !isfinite(sensor_error) || sensor_error > max_cal_error);
337+
if (took_success_branch) {
338+
fprintf(stderr, "AggregateErrorOverCountGate FAILED: cnt=0 took success branch "
339+
"(sensor_error=%f)\n", sensor_error);
340+
return -1;
341+
}
342+
}
343+
344+
// Random finite sum/cnt with cnt>0 must never spuriously fail the
345+
// isfinite check, and the gate must be self-consistent regardless of
346+
// where the threshold falls.
347+
for (int trial = 0; trial < N_TRIALS; trial++) {
348+
FLT sum = rand_range(0.0, 1e6);
349+
FLT cnt = rand_range(1.0, 1e4);
350+
FLT sensor_error = sqrtf(sum / cnt);
351+
FLT max_cal_error = rand_range(0.0, 100.0);
352+
int status_failure = 0;
353+
354+
if (!isfinite(sensor_error)) {
355+
fprintf(stderr, "AggregateErrorOverCountGate FAILED (seed=%u, trial=%d): "
356+
"finite sum=%f cnt=%f produced non-finite sensor_error\n",
357+
seed, trial, sum, cnt);
358+
return -1;
359+
}
360+
361+
int took_success_branch = !(status_failure || !isfinite(sensor_error) || sensor_error > max_cal_error);
362+
int should_succeed = sensor_error <= max_cal_error;
363+
if (took_success_branch != should_succeed) {
364+
fprintf(stderr, "AggregateErrorOverCountGate FAILED (seed=%u, trial=%d): "
365+
"gate disagreement sensor_error=%f max_cal_error=%f\n",
366+
seed, trial, sensor_error, max_cal_error);
367+
return -1;
368+
}
369+
}
370+
return 0;
371+
}
372+
373+
// 11. Large quaternion components don't cause overflow in quatrotatevector
318374
TEST(NumericProps, LargeQuatNoOverflow) {
319375
unsigned seed = (unsigned)time(NULL);
320376
srand(seed);

0 commit comments

Comments
 (0)