Fix four EventDetectorTest cases that never passed
These have been red since the initial commit. All four failed for the same reason, and in every case the test was wrong rather than the detector. EventDetector requires two conditions to hold on the SAME frame, and one of them is a rolling-window statistic that takes time to respond. The tests ignored that lag, so they described situations the detector cannot see - and could not have seen at any point in its history. Braking (3 tests). Each filled the accel window with a CONSTANT value, then stepped the GPS speed down. The speed drop is therefore true on exactly one frame, and on that frame the accel std dev is still ~0.75 against a 1.2 threshold, because the window is full of the constant. By the time the window recovers (frame 4), prevSpeedMps has caught up and the drop is 0. The two conditions never coincide and no BRAKING is possible. Constant accelerometer output right up to the instant of a brake is not physical either: the IMU is sampled continuously while GPS speed lags, so the shaking precedes the reported drop. The tests now establish that variability during the cruise phase. Stopping. The second episode ran for stoppingFrames + 5. But stopping requires the accel std dev to be BELOW a threshold, and the window still held the five moving samples from the acceleration burst between episodes. Those take 8 frames to drain far enough for the std dev to fall under 0.15, leaving 17 of the 21 frames the event needs. The first episode is unaffected because the window starts empty. The episode is now long enough to cover the settling time. Verified by simulating the detector's exact arithmetic against both the old and new inputs before touching the file: the old profiles produce no events, the new ones produce BRAKING/HIGH, BRAKING/HIGH, BRAKING/MEDIUM and two STOPPING. No production code changed - the detector behaves consistently and defensibly. Assertions are unchanged; only the stimulus is now something a bicycle could actually produce. Suite is 41 tests, 0 failures.
This commit is contained in:
@@ -116,12 +116,17 @@ class EventDetectorTest {
|
|||||||
// ─── Hard brake ───────────────────────────────────────────────────────────
|
// ─── Hard brake ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@Test fun `hard brake triggers BRAKING event`() = runCollecting { events ->
|
@Test fun `hard brake triggers BRAKING event`() = runCollecting { events ->
|
||||||
// Phase 1: fill window at 10 m/s with constant accel (no std dev → no braking)
|
// Phase 1: cruising at 10 m/s with the accelerometer variability a moving bike actually
|
||||||
repeat(config.windowSize) { i ->
|
// has. This matters: [EventDetector] requires the speed drop and the accel std dev to be
|
||||||
detector.processSample(1.0, 0.05, 10.0, 0.0, 53.5, 10.0, i * 20L)
|
// true on the SAME frame, and the std dev is a rolling window. Filling phase 1 with a
|
||||||
}
|
// constant accel drives that window to zero, so on the one frame where the speed drop
|
||||||
// Phase 2: GPS drops to 4 m/s (drop = 6 m/s > 0.5 threshold).
|
// exists the std dev is still ~0.75 and braking can never start - by the time the window
|
||||||
// Alternate hi/lo accel to exceed the std-dev threshold.
|
// has recovered, prevSpeedMps has caught up and the drop is gone.
|
||||||
|
//
|
||||||
|
// Constant accel right up to the instant of a brake is also not physical. The IMU is
|
||||||
|
// sampled continuously while GPS speed lags, so the shaking precedes the reported drop.
|
||||||
|
alternatingAccelFrames(n = config.windowSize, speedMps = 10.0, timeOffset = 0)
|
||||||
|
// Phase 2: GPS reports 4 m/s (drop = 6 m/s > 0.5 threshold).
|
||||||
alternatingAccelFrames(
|
alternatingAccelFrames(
|
||||||
n = config.brakingSustainedFrames + 5,
|
n = config.brakingSustainedFrames + 5,
|
||||||
speedMps = 4.0,
|
speedMps = 4.0,
|
||||||
@@ -132,9 +137,8 @@ class EventDetectorTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test fun `hard brake with large speed drop has HIGH confidence`() = runCollecting { events ->
|
@Test fun `hard brake with large speed drop has HIGH confidence`() = runCollecting { events ->
|
||||||
repeat(config.windowSize) { i ->
|
// Variability established before the drop - see the note in the test above.
|
||||||
detector.processSample(1.0, 0.05, 10.0, 0.0, 53.5, 10.0, i * 20L)
|
alternatingAccelFrames(n = config.windowSize, speedMps = 10.0, timeOffset = 0)
|
||||||
}
|
|
||||||
// Drop of 8 m/s > brakingHighConfidenceRate (1.5)
|
// Drop of 8 m/s > brakingHighConfidenceRate (1.5)
|
||||||
alternatingAccelFrames(
|
alternatingAccelFrames(
|
||||||
n = config.brakingSustainedFrames + 5,
|
n = config.brakingSustainedFrames + 5,
|
||||||
@@ -153,9 +157,8 @@ class EventDetectorTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test fun `moderate speed drop has MEDIUM confidence`() = runCollecting { events ->
|
@Test fun `moderate speed drop has MEDIUM confidence`() = runCollecting { events ->
|
||||||
repeat(config.windowSize) { i ->
|
// Variability established before the drop - see `hard brake triggers BRAKING event`.
|
||||||
detector.processSample(1.0, 0.05, 3.0, 0.0, 53.5, 10.0, i * 20L)
|
alternatingAccelFrames(n = config.windowSize, speedMps = 3.0, timeOffset = 0)
|
||||||
}
|
|
||||||
// Drop of 0.8 m/s — above speed-drop threshold (0.5) but below high-conf rate (1.5)
|
// Drop of 0.8 m/s — above speed-drop threshold (0.5) but below high-conf rate (1.5)
|
||||||
alternatingAccelFrames(
|
alternatingAccelFrames(
|
||||||
n = config.brakingSustainedFrames + 5,
|
n = config.brakingSustainedFrames + 5,
|
||||||
@@ -294,8 +297,15 @@ class EventDetectorTest {
|
|||||||
repeat(5) {
|
repeat(5) {
|
||||||
detector.processSample(0.5, 0.1, 5.0, 2.0, 53.5, 10.0, t++ * 20L)
|
detector.processSample(0.5, 0.1, 5.0, 2.0, 53.5, 10.0, t++ * 20L)
|
||||||
}
|
}
|
||||||
// Second stop episode
|
// Second stop episode. Deliberately longer than the first: stopping also requires the
|
||||||
repeat(stopFrames) {
|
// accel std dev to be BELOW a threshold, and the rolling window still holds the five
|
||||||
|
// moving samples above. It takes 8 further frames for those to drain out far enough for
|
||||||
|
// the std dev to fall under 0.15, and only then does the counter start. The first episode
|
||||||
|
// needs no such allowance because the window begins empty.
|
||||||
|
//
|
||||||
|
// stoppingFrames + 5 was not enough - the second episode reached 17 of the 21 frames it
|
||||||
|
// needs and silently emitted nothing, which is what made this test fail.
|
||||||
|
repeat(config.stoppingFrames + 10) {
|
||||||
detector.processSample(0.02, 0.01, 0.1, 0.0, 53.5, 10.0, t++ * 20L)
|
detector.processSample(0.02, 0.01, 0.1, 0.0, 53.5, 10.0, t++ * 20L)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user