1 /*
2 * Copyright 2023 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 #include <input/MotionPredictor.h>
18
19 #include <cmath>
20 #include <cstddef>
21 #include <cstdint>
22 #include <numeric>
23 #include <vector>
24
25 #include <gmock/gmock.h>
26 #include <gtest/gtest.h>
27 #include <input/InputEventBuilders.h>
28 #include <utils/Timers.h> // for nsecs_t
29
30 #include "Eigen/Core"
31 #include "Eigen/Geometry"
32
33 namespace android {
34 namespace {
35
36 using ::testing::FloatNear;
37 using ::testing::Matches;
38
39 using GroundTruthPoint = MotionPredictorMetricsManager::GroundTruthPoint;
40 using PredictionPoint = MotionPredictorMetricsManager::PredictionPoint;
41 using AtomFields = MotionPredictorMetricsManager::AtomFields;
42 using ReportAtomFunction = MotionPredictorMetricsManager::ReportAtomFunction;
43
44 inline constexpr int NANOS_PER_MILLIS = 1'000'000;
45
46 inline constexpr nsecs_t TEST_INITIAL_TIMESTAMP = 1'000'000'000;
47 inline constexpr size_t TEST_MAX_NUM_PREDICTIONS = 5;
48 inline constexpr nsecs_t TEST_PREDICTION_INTERVAL_NANOS = 12'500'000 / 3; // 1 / (240 hz)
49 inline constexpr int NO_DATA_SENTINEL = MotionPredictorMetricsManager::NO_DATA_SENTINEL;
50
51 // Parameters:
52 // • arg: Eigen::Vector2f
53 // • target: Eigen::Vector2f
54 // • epsilon: float
55 MATCHER_P2(Vector2fNear, target, epsilon, "") {
56 return Matches(FloatNear(target[0], epsilon))(arg[0]) &&
57 Matches(FloatNear(target[1], epsilon))(arg[1]);
58 }
59
60 // Parameters:
61 // • arg: PredictionPoint
62 // • target: PredictionPoint
63 // • epsilon: float
64 MATCHER_P2(PredictionPointNear, target, epsilon, "") {
65 if (!Matches(Vector2fNear(target.position, epsilon))(arg.position)) {
66 *result_listener << "Position mismatch. Actual: (" << arg.position[0] << ", "
67 << arg.position[1] << "), expected: (" << target.position[0] << ", "
68 << target.position[1] << ")";
69 return false;
70 }
71 if (!Matches(FloatNear(target.pressure, epsilon))(arg.pressure)) {
72 *result_listener << "Pressure mismatch. Actual: " << arg.pressure
73 << ", expected: " << target.pressure;
74 return false;
75 }
76 if (arg.originTimestamp != target.originTimestamp) {
77 *result_listener << "Origin timestamp mismatch. Actual: " << arg.originTimestamp
78 << ", expected: " << target.originTimestamp;
79 return false;
80 }
81 if (arg.targetTimestamp != target.targetTimestamp) {
82 *result_listener << "Target timestamp mismatch. Actual: " << arg.targetTimestamp
83 << ", expected: " << target.targetTimestamp;
84 return false;
85 }
86 return true;
87 }
88
89 // --- Mathematical helper functions. ---
90
91 template <typename T>
average(std::vector<T> values)92 T average(std::vector<T> values) {
93 return std::accumulate(values.begin(), values.end(), T{}) / static_cast<T>(values.size());
94 }
95
96 template <typename T>
standardDeviation(std::vector<T> values)97 T standardDeviation(std::vector<T> values) {
98 T mean = average(values);
99 T accumulator = {};
100 for (const T value : values) {
101 accumulator += value * value - mean * mean;
102 }
103 // Take the max with 0 to avoid negative values caused by numerical instability.
104 return std::sqrt(std::max(T{}, accumulator) / static_cast<T>(values.size()));
105 }
106
107 template <typename T>
rmse(std::vector<T> errors)108 T rmse(std::vector<T> errors) {
109 T sse = {};
110 for (const T error : errors) {
111 sse += error * error;
112 }
113 return std::sqrt(sse / static_cast<T>(errors.size()));
114 }
115
TEST(MathematicalHelperFunctionTest,Average)116 TEST(MathematicalHelperFunctionTest, Average) {
117 std::vector<float> values{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
118 EXPECT_EQ(5.5f, average(values));
119 }
120
TEST(MathematicalHelperFunctionTest,StandardDeviation)121 TEST(MathematicalHelperFunctionTest, StandardDeviation) {
122 // https://www.calculator.net/standard-deviation-calculator.html?numberinputs=10%2C+12%2C+23%2C+23%2C+16%2C+23%2C+21%2C+16
123 std::vector<float> values{10, 12, 23, 23, 16, 23, 21, 16};
124 EXPECT_FLOAT_EQ(4.8989794855664f, standardDeviation(values));
125 }
126
TEST(MathematicalHelperFunctionTest,Rmse)127 TEST(MathematicalHelperFunctionTest, Rmse) {
128 std::vector<float> errors{1, 5, 7, 7, 8, 20};
129 EXPECT_FLOAT_EQ(9.899494937f, rmse(errors));
130 }
131
132 // --- MotionEvent-related helper functions. ---
133
134 // Creates a MotionEvent corresponding to the given GroundTruthPoint.
makeMotionEvent(const GroundTruthPoint & groundTruthPoint)135 MotionEvent makeMotionEvent(const GroundTruthPoint& groundTruthPoint) {
136 // Build single pointer of type STYLUS, with coordinates from groundTruthPoint.
137 PointerBuilder pointerBuilder =
138 PointerBuilder(/*id=*/0, ToolType::STYLUS)
139 .x(groundTruthPoint.position[1])
140 .y(groundTruthPoint.position[0])
141 .axis(AMOTION_EVENT_AXIS_PRESSURE, groundTruthPoint.pressure);
142 return MotionEventBuilder(/*action=*/AMOTION_EVENT_ACTION_MOVE,
143 /*source=*/AINPUT_SOURCE_CLASS_POINTER)
144 .eventTime(groundTruthPoint.timestamp)
145 .pointer(pointerBuilder)
146 .build();
147 }
148
149 // Creates a MotionEvent corresponding to the given sequence of PredictionPoints.
makeMotionEvent(const std::vector<PredictionPoint> & predictionPoints)150 MotionEvent makeMotionEvent(const std::vector<PredictionPoint>& predictionPoints) {
151 // Build single pointer of type STYLUS, with coordinates from first prediction point.
152 PointerBuilder pointerBuilder =
153 PointerBuilder(/*id=*/0, ToolType::STYLUS)
154 .x(predictionPoints[0].position[1])
155 .y(predictionPoints[0].position[0])
156 .axis(AMOTION_EVENT_AXIS_PRESSURE, predictionPoints[0].pressure);
157 MotionEvent predictionEvent =
158 MotionEventBuilder(
159 /*action=*/AMOTION_EVENT_ACTION_MOVE, /*source=*/AINPUT_SOURCE_CLASS_POINTER)
160 .eventTime(predictionPoints[0].targetTimestamp)
161 .pointer(pointerBuilder)
162 .build();
163 for (size_t i = 1; i < predictionPoints.size(); ++i) {
164 PointerCoords coords =
165 PointerBuilder(/*id=*/0, ToolType::STYLUS)
166 .x(predictionPoints[i].position[1])
167 .y(predictionPoints[i].position[0])
168 .axis(AMOTION_EVENT_AXIS_PRESSURE, predictionPoints[i].pressure)
169 .buildCoords();
170 predictionEvent.addSample(predictionPoints[i].targetTimestamp, &coords);
171 }
172 return predictionEvent;
173 }
174
175 // Creates a MotionEvent corresponding to a stylus lift (UP) ground truth event.
makeLiftMotionEvent()176 MotionEvent makeLiftMotionEvent() {
177 return MotionEventBuilder(/*action=*/AMOTION_EVENT_ACTION_UP,
178 /*source=*/AINPUT_SOURCE_CLASS_POINTER)
179 .pointer(PointerBuilder(/*id=*/0, ToolType::STYLUS))
180 .build();
181 }
182
TEST(MakeMotionEventTest,MakeGroundTruthMotionEvent)183 TEST(MakeMotionEventTest, MakeGroundTruthMotionEvent) {
184 const GroundTruthPoint groundTruthPoint{{.position = Eigen::Vector2f(10.0f, 20.0f),
185 .pressure = 0.6f},
186 .timestamp = TEST_INITIAL_TIMESTAMP};
187 const MotionEvent groundTruthMotionEvent = makeMotionEvent(groundTruthPoint);
188
189 ASSERT_EQ(1u, groundTruthMotionEvent.getPointerCount());
190 // Note: a MotionEvent's "history size" is one less than its number of samples.
191 ASSERT_EQ(0u, groundTruthMotionEvent.getHistorySize());
192 EXPECT_EQ(groundTruthPoint.position[0], groundTruthMotionEvent.getRawPointerCoords(0)->getY());
193 EXPECT_EQ(groundTruthPoint.position[1], groundTruthMotionEvent.getRawPointerCoords(0)->getX());
194 EXPECT_EQ(groundTruthPoint.pressure,
195 groundTruthMotionEvent.getRawPointerCoords(0)->getAxisValue(
196 AMOTION_EVENT_AXIS_PRESSURE));
197 EXPECT_EQ(AMOTION_EVENT_ACTION_MOVE, groundTruthMotionEvent.getAction());
198 }
199
TEST(MakeMotionEventTest,MakePredictionMotionEvent)200 TEST(MakeMotionEventTest, MakePredictionMotionEvent) {
201 const nsecs_t originTimestamp = TEST_INITIAL_TIMESTAMP;
202 const std::vector<PredictionPoint>
203 predictionPoints{{{.position = Eigen::Vector2f(10.0f, 20.0f), .pressure = 0.6f},
204 .originTimestamp = originTimestamp,
205 .targetTimestamp = originTimestamp + 5 * NANOS_PER_MILLIS},
206 {{.position = Eigen::Vector2f(11.0f, 22.0f), .pressure = 0.5f},
207 .originTimestamp = originTimestamp,
208 .targetTimestamp = originTimestamp + 10 * NANOS_PER_MILLIS},
209 {{.position = Eigen::Vector2f(12.0f, 24.0f), .pressure = 0.4f},
210 .originTimestamp = originTimestamp,
211 .targetTimestamp = originTimestamp + 15 * NANOS_PER_MILLIS}};
212 const MotionEvent predictionMotionEvent = makeMotionEvent(predictionPoints);
213
214 ASSERT_EQ(1u, predictionMotionEvent.getPointerCount());
215 // Note: a MotionEvent's "history size" is one less than its number of samples.
216 ASSERT_EQ(predictionPoints.size(), predictionMotionEvent.getHistorySize() + 1);
217 for (size_t i = 0; i < predictionPoints.size(); ++i) {
218 SCOPED_TRACE(testing::Message() << "i = " << i);
219 const PointerCoords coords = *predictionMotionEvent.getHistoricalRawPointerCoords(
220 /*pointerIndex=*/0, /*historicalIndex=*/i);
221 EXPECT_EQ(predictionPoints[i].position[0], coords.getY());
222 EXPECT_EQ(predictionPoints[i].position[1], coords.getX());
223 EXPECT_EQ(predictionPoints[i].pressure, coords.getAxisValue(AMOTION_EVENT_AXIS_PRESSURE));
224 // Note: originTimestamp is discarded when converting PredictionPoint to MotionEvent.
225 EXPECT_EQ(predictionPoints[i].targetTimestamp,
226 predictionMotionEvent.getHistoricalEventTime(i));
227 EXPECT_EQ(AMOTION_EVENT_ACTION_MOVE, predictionMotionEvent.getAction());
228 }
229 }
230
TEST(MakeMotionEventTest,MakeLiftMotionEvent)231 TEST(MakeMotionEventTest, MakeLiftMotionEvent) {
232 const MotionEvent liftMotionEvent = makeLiftMotionEvent();
233 ASSERT_EQ(1u, liftMotionEvent.getPointerCount());
234 // Note: a MotionEvent's "history size" is one less than its number of samples.
235 ASSERT_EQ(0u, liftMotionEvent.getHistorySize());
236 EXPECT_EQ(AMOTION_EVENT_ACTION_UP, liftMotionEvent.getAction());
237 }
238
239 // --- Ground-truth-generation helper functions. ---
240
241 // Generates numPoints ground truth points with values equal to those of the given
242 // GroundTruthPoint, and with consecutive timestamps separated by the given inputInterval.
generateConstantGroundTruthPoints(const GroundTruthPoint & groundTruthPoint,size_t numPoints,nsecs_t inputInterval=TEST_PREDICTION_INTERVAL_NANOS)243 std::vector<GroundTruthPoint> generateConstantGroundTruthPoints(
244 const GroundTruthPoint& groundTruthPoint, size_t numPoints,
245 nsecs_t inputInterval = TEST_PREDICTION_INTERVAL_NANOS) {
246 std::vector<GroundTruthPoint> groundTruthPoints;
247 nsecs_t timestamp = groundTruthPoint.timestamp;
248 for (size_t i = 0; i < numPoints; ++i) {
249 groundTruthPoints.emplace_back(groundTruthPoint);
250 groundTruthPoints.back().timestamp = timestamp;
251 timestamp += inputInterval;
252 }
253 return groundTruthPoints;
254 }
255
256 // This function uses the coordinate system (y, x), with +y pointing downwards and +x pointing
257 // rightwards. Angles are measured counterclockwise from down (+y).
generateCircularArcGroundTruthPoints(Eigen::Vector2f initialPosition,float initialAngle,float velocity,float turningAngle,size_t numPoints)258 std::vector<GroundTruthPoint> generateCircularArcGroundTruthPoints(Eigen::Vector2f initialPosition,
259 float initialAngle,
260 float velocity,
261 float turningAngle,
262 size_t numPoints) {
263 std::vector<GroundTruthPoint> groundTruthPoints;
264 // Create first point.
265 if (numPoints > 0) {
266 groundTruthPoints.push_back({{.position = initialPosition, .pressure = 0.0f},
267 .timestamp = TEST_INITIAL_TIMESTAMP});
268 }
269 float trajectoryAngle = initialAngle; // measured counterclockwise from +y axis.
270 for (size_t i = 1; i < numPoints; ++i) {
271 const Eigen::Vector2f trajectory =
272 Eigen::Rotation2D(trajectoryAngle) * Eigen::Vector2f(1, 0);
273 groundTruthPoints.push_back(
274 {{.position = groundTruthPoints.back().position + velocity * trajectory,
275 .pressure = 0.0f},
276 .timestamp = groundTruthPoints.back().timestamp + TEST_PREDICTION_INTERVAL_NANOS});
277 trajectoryAngle += turningAngle;
278 }
279 return groundTruthPoints;
280 }
281
TEST(GenerateConstantGroundTruthPointsTest,BasicTest)282 TEST(GenerateConstantGroundTruthPointsTest, BasicTest) {
283 const GroundTruthPoint groundTruthPoint{{.position = Eigen::Vector2f(10, 20), .pressure = 0.3f},
284 .timestamp = TEST_INITIAL_TIMESTAMP};
285 const std::vector<GroundTruthPoint> groundTruthPoints =
286 generateConstantGroundTruthPoints(groundTruthPoint, /*numPoints=*/3,
287 /*inputInterval=*/10);
288
289 ASSERT_EQ(3u, groundTruthPoints.size());
290 // First point.
291 EXPECT_EQ(groundTruthPoints[0].position, groundTruthPoint.position);
292 EXPECT_EQ(groundTruthPoints[0].pressure, groundTruthPoint.pressure);
293 EXPECT_EQ(groundTruthPoints[0].timestamp, groundTruthPoint.timestamp);
294 // Second point.
295 EXPECT_EQ(groundTruthPoints[1].position, groundTruthPoint.position);
296 EXPECT_EQ(groundTruthPoints[1].pressure, groundTruthPoint.pressure);
297 EXPECT_EQ(groundTruthPoints[1].timestamp, groundTruthPoint.timestamp + 10);
298 // Third point.
299 EXPECT_EQ(groundTruthPoints[2].position, groundTruthPoint.position);
300 EXPECT_EQ(groundTruthPoints[2].pressure, groundTruthPoint.pressure);
301 EXPECT_EQ(groundTruthPoints[2].timestamp, groundTruthPoint.timestamp + 20);
302 }
303
TEST(GenerateCircularArcGroundTruthTest,StraightLineUpwards)304 TEST(GenerateCircularArcGroundTruthTest, StraightLineUpwards) {
305 const std::vector<GroundTruthPoint> groundTruthPoints = generateCircularArcGroundTruthPoints(
306 /*initialPosition=*/Eigen::Vector2f(0, 0),
307 /*initialAngle=*/M_PI,
308 /*velocity=*/1.0f,
309 /*turningAngle=*/0.0f,
310 /*numPoints=*/3);
311
312 ASSERT_EQ(3u, groundTruthPoints.size());
313 EXPECT_THAT(groundTruthPoints[0].position, Vector2fNear(Eigen::Vector2f(0, 0), 1e-6));
314 EXPECT_THAT(groundTruthPoints[1].position, Vector2fNear(Eigen::Vector2f(-1, 0), 1e-6));
315 EXPECT_THAT(groundTruthPoints[2].position, Vector2fNear(Eigen::Vector2f(-2, 0), 1e-6));
316 // Check that timestamps are increasing between consecutive ground truth points.
317 EXPECT_GT(groundTruthPoints[1].timestamp, groundTruthPoints[0].timestamp);
318 EXPECT_GT(groundTruthPoints[2].timestamp, groundTruthPoints[1].timestamp);
319 }
320
TEST(GenerateCircularArcGroundTruthTest,CounterclockwiseSquare)321 TEST(GenerateCircularArcGroundTruthTest, CounterclockwiseSquare) {
322 // Generate points in a counterclockwise unit square starting pointing right.
323 const std::vector<GroundTruthPoint> groundTruthPoints = generateCircularArcGroundTruthPoints(
324 /*initialPosition=*/Eigen::Vector2f(10, 100),
325 /*initialAngle=*/M_PI_2,
326 /*velocity=*/1.0f,
327 /*turningAngle=*/M_PI_2,
328 /*numPoints=*/5);
329
330 ASSERT_EQ(5u, groundTruthPoints.size());
331 EXPECT_THAT(groundTruthPoints[0].position, Vector2fNear(Eigen::Vector2f(10, 100), 1e-6));
332 EXPECT_THAT(groundTruthPoints[1].position, Vector2fNear(Eigen::Vector2f(10, 101), 1e-6));
333 EXPECT_THAT(groundTruthPoints[2].position, Vector2fNear(Eigen::Vector2f(9, 101), 1e-6));
334 EXPECT_THAT(groundTruthPoints[3].position, Vector2fNear(Eigen::Vector2f(9, 100), 1e-6));
335 EXPECT_THAT(groundTruthPoints[4].position, Vector2fNear(Eigen::Vector2f(10, 100), 1e-6));
336 }
337
338 // --- Prediction-generation helper functions. ---
339
340 // Generates TEST_MAX_NUM_PREDICTIONS predictions with values equal to those of the given
341 // GroundTruthPoint, and with consecutive timestamps separated by the given predictionInterval.
generateConstantPredictions(const GroundTruthPoint & groundTruthPoint,nsecs_t predictionInterval=TEST_PREDICTION_INTERVAL_NANOS)342 std::vector<PredictionPoint> generateConstantPredictions(
343 const GroundTruthPoint& groundTruthPoint,
344 nsecs_t predictionInterval = TEST_PREDICTION_INTERVAL_NANOS) {
345 std::vector<PredictionPoint> predictions;
346 nsecs_t predictionTimestamp = groundTruthPoint.timestamp + predictionInterval;
347 for (size_t j = 0; j < TEST_MAX_NUM_PREDICTIONS; ++j) {
348 predictions.push_back(PredictionPoint{{.position = groundTruthPoint.position,
349 .pressure = groundTruthPoint.pressure},
350 .originTimestamp = groundTruthPoint.timestamp,
351 .targetTimestamp = predictionTimestamp});
352 predictionTimestamp += predictionInterval;
353 }
354 return predictions;
355 }
356
357 // Generates TEST_MAX_NUM_PREDICTIONS predictions from the given most recent two ground truth points
358 // by linear extrapolation of position and pressure. The interval between consecutive predictions'
359 // timestamps is TEST_PREDICTION_INTERVAL_NANOS.
generatePredictionsByLinearExtrapolation(const GroundTruthPoint & firstGroundTruth,const GroundTruthPoint & secondGroundTruth)360 std::vector<PredictionPoint> generatePredictionsByLinearExtrapolation(
361 const GroundTruthPoint& firstGroundTruth, const GroundTruthPoint& secondGroundTruth) {
362 // Precompute deltas.
363 const Eigen::Vector2f trajectory = secondGroundTruth.position - firstGroundTruth.position;
364 const float deltaPressure = secondGroundTruth.pressure - firstGroundTruth.pressure;
365 // Compute predictions.
366 std::vector<PredictionPoint> predictions;
367 Eigen::Vector2f predictionPosition = secondGroundTruth.position;
368 float predictionPressure = secondGroundTruth.pressure;
369 nsecs_t predictionTargetTimestamp = secondGroundTruth.timestamp;
370 for (size_t i = 0; i < TEST_MAX_NUM_PREDICTIONS; ++i) {
371 predictionPosition += trajectory;
372 predictionPressure += deltaPressure;
373 predictionTargetTimestamp += TEST_PREDICTION_INTERVAL_NANOS;
374 predictions.push_back(
375 PredictionPoint{{.position = predictionPosition, .pressure = predictionPressure},
376 .originTimestamp = secondGroundTruth.timestamp,
377 .targetTimestamp = predictionTargetTimestamp});
378 }
379 return predictions;
380 }
381
TEST(GeneratePredictionsTest,GenerateConstantPredictions)382 TEST(GeneratePredictionsTest, GenerateConstantPredictions) {
383 const GroundTruthPoint groundTruthPoint{{.position = Eigen::Vector2f(10, 20), .pressure = 0.3f},
384 .timestamp = TEST_INITIAL_TIMESTAMP};
385 const nsecs_t predictionInterval = 10;
386 const std::vector<PredictionPoint> predictionPoints =
387 generateConstantPredictions(groundTruthPoint, predictionInterval);
388
389 ASSERT_EQ(TEST_MAX_NUM_PREDICTIONS, predictionPoints.size());
390 for (size_t i = 0; i < predictionPoints.size(); ++i) {
391 SCOPED_TRACE(testing::Message() << "i = " << i);
392 EXPECT_THAT(predictionPoints[i].position, Vector2fNear(groundTruthPoint.position, 1e-6));
393 EXPECT_THAT(predictionPoints[i].pressure, FloatNear(groundTruthPoint.pressure, 1e-6));
394 EXPECT_EQ(predictionPoints[i].originTimestamp, groundTruthPoint.timestamp);
395 EXPECT_EQ(predictionPoints[i].targetTimestamp,
396 TEST_INITIAL_TIMESTAMP + static_cast<nsecs_t>(i + 1) * predictionInterval);
397 }
398 }
399
TEST(GeneratePredictionsTest,LinearExtrapolationFromTwoPoints)400 TEST(GeneratePredictionsTest, LinearExtrapolationFromTwoPoints) {
401 const nsecs_t initialTimestamp = TEST_INITIAL_TIMESTAMP;
402 const std::vector<PredictionPoint> predictionPoints = generatePredictionsByLinearExtrapolation(
403 GroundTruthPoint{{.position = Eigen::Vector2f(100, 200), .pressure = 0.9f},
404 .timestamp = initialTimestamp},
405 GroundTruthPoint{{.position = Eigen::Vector2f(105, 190), .pressure = 0.8f},
406 .timestamp = initialTimestamp + TEST_PREDICTION_INTERVAL_NANOS});
407
408 ASSERT_EQ(TEST_MAX_NUM_PREDICTIONS, predictionPoints.size());
409 const nsecs_t originTimestamp = initialTimestamp + TEST_PREDICTION_INTERVAL_NANOS;
410 EXPECT_THAT(predictionPoints[0],
411 PredictionPointNear(PredictionPoint{{.position = Eigen::Vector2f(110, 180),
412 .pressure = 0.7f},
413 .originTimestamp = originTimestamp,
414 .targetTimestamp = originTimestamp +
415 TEST_PREDICTION_INTERVAL_NANOS},
416 0.001));
417 EXPECT_THAT(predictionPoints[1],
418 PredictionPointNear(PredictionPoint{{.position = Eigen::Vector2f(115, 170),
419 .pressure = 0.6f},
420 .originTimestamp = originTimestamp,
421 .targetTimestamp = originTimestamp +
422 2 * TEST_PREDICTION_INTERVAL_NANOS},
423 0.001));
424 EXPECT_THAT(predictionPoints[2],
425 PredictionPointNear(PredictionPoint{{.position = Eigen::Vector2f(120, 160),
426 .pressure = 0.5f},
427 .originTimestamp = originTimestamp,
428 .targetTimestamp = originTimestamp +
429 3 * TEST_PREDICTION_INTERVAL_NANOS},
430 0.001));
431 EXPECT_THAT(predictionPoints[3],
432 PredictionPointNear(PredictionPoint{{.position = Eigen::Vector2f(125, 150),
433 .pressure = 0.4f},
434 .originTimestamp = originTimestamp,
435 .targetTimestamp = originTimestamp +
436 4 * TEST_PREDICTION_INTERVAL_NANOS},
437 0.001));
438 EXPECT_THAT(predictionPoints[4],
439 PredictionPointNear(PredictionPoint{{.position = Eigen::Vector2f(130, 140),
440 .pressure = 0.3f},
441 .originTimestamp = originTimestamp,
442 .targetTimestamp = originTimestamp +
443 5 * TEST_PREDICTION_INTERVAL_NANOS},
444 0.001));
445 }
446
447 // Generates predictions by linear extrapolation for each consecutive pair of ground truth points
448 // (see the comment for the above function for further explanation). Returns a vector of vectors of
449 // prediction points, where the first index is the source ground truth index, and the second is the
450 // prediction target index.
451 //
452 // The returned vector has size equal to the input vector, and the first element of the returned
453 // vector is always empty.
generateAllPredictionsByLinearExtrapolation(const std::vector<GroundTruthPoint> & groundTruthPoints)454 std::vector<std::vector<PredictionPoint>> generateAllPredictionsByLinearExtrapolation(
455 const std::vector<GroundTruthPoint>& groundTruthPoints) {
456 std::vector<std::vector<PredictionPoint>> allPredictions;
457 allPredictions.emplace_back();
458 for (size_t i = 1; i < groundTruthPoints.size(); ++i) {
459 allPredictions.push_back(generatePredictionsByLinearExtrapolation(groundTruthPoints[i - 1],
460 groundTruthPoints[i]));
461 }
462 return allPredictions;
463 }
464
TEST(GeneratePredictionsTest,GenerateAllPredictions)465 TEST(GeneratePredictionsTest, GenerateAllPredictions) {
466 const nsecs_t initialTimestamp = TEST_INITIAL_TIMESTAMP;
467 std::vector<GroundTruthPoint>
468 groundTruthPoints{GroundTruthPoint{{.position = Eigen::Vector2f(0, 0),
469 .pressure = 0.5f},
470 .timestamp = initialTimestamp},
471 GroundTruthPoint{{.position = Eigen::Vector2f(1, -1),
472 .pressure = 0.51f},
473 .timestamp = initialTimestamp +
474 2 * TEST_PREDICTION_INTERVAL_NANOS},
475 GroundTruthPoint{{.position = Eigen::Vector2f(2, -2),
476 .pressure = 0.52f},
477 .timestamp = initialTimestamp +
478 3 * TEST_PREDICTION_INTERVAL_NANOS}};
479
480 const std::vector<std::vector<PredictionPoint>> allPredictions =
481 generateAllPredictionsByLinearExtrapolation(groundTruthPoints);
482
483 // Check format of allPredictions data.
484 ASSERT_EQ(groundTruthPoints.size(), allPredictions.size());
485 EXPECT_TRUE(allPredictions[0].empty());
486 EXPECT_EQ(TEST_MAX_NUM_PREDICTIONS, allPredictions[1].size());
487 EXPECT_EQ(TEST_MAX_NUM_PREDICTIONS, allPredictions[2].size());
488
489 // Check positions of predictions generated from first pair of ground truth points.
490 EXPECT_THAT(allPredictions[1][0].position, Vector2fNear(Eigen::Vector2f(2, -2), 1e-9));
491 EXPECT_THAT(allPredictions[1][1].position, Vector2fNear(Eigen::Vector2f(3, -3), 1e-9));
492 EXPECT_THAT(allPredictions[1][2].position, Vector2fNear(Eigen::Vector2f(4, -4), 1e-9));
493 EXPECT_THAT(allPredictions[1][3].position, Vector2fNear(Eigen::Vector2f(5, -5), 1e-9));
494 EXPECT_THAT(allPredictions[1][4].position, Vector2fNear(Eigen::Vector2f(6, -6), 1e-9));
495
496 // Check pressures of predictions generated from first pair of ground truth points.
497 EXPECT_FLOAT_EQ(0.52f, allPredictions[1][0].pressure);
498 EXPECT_FLOAT_EQ(0.53f, allPredictions[1][1].pressure);
499 EXPECT_FLOAT_EQ(0.54f, allPredictions[1][2].pressure);
500 EXPECT_FLOAT_EQ(0.55f, allPredictions[1][3].pressure);
501 EXPECT_FLOAT_EQ(0.56f, allPredictions[1][4].pressure);
502 }
503
504 // --- Prediction error helper functions. ---
505
506 struct GeneralPositionErrors {
507 float alongTrajectoryErrorMean;
508 float alongTrajectoryErrorStd;
509 float offTrajectoryRmse;
510 };
511
512 // Inputs:
513 // • Vector of ground truth points
514 // • Vector of vectors of prediction points, where the first index is the source ground truth
515 // index, and the second is the prediction target index.
516 //
517 // Returns a vector of GeneralPositionErrors, indexed by prediction time delta bucket.
computeGeneralPositionErrors(const std::vector<GroundTruthPoint> & groundTruthPoints,const std::vector<std::vector<PredictionPoint>> & predictionPoints)518 std::vector<GeneralPositionErrors> computeGeneralPositionErrors(
519 const std::vector<GroundTruthPoint>& groundTruthPoints,
520 const std::vector<std::vector<PredictionPoint>>& predictionPoints) {
521 // Aggregate errors by time bucket (prediction target index).
522 std::vector<GeneralPositionErrors> generalPostitionErrors;
523 for (size_t predictionTargetIndex = 0; predictionTargetIndex < TEST_MAX_NUM_PREDICTIONS;
524 ++predictionTargetIndex) {
525 std::vector<float> alongTrajectoryErrors;
526 std::vector<float> alongTrajectorySquaredErrors;
527 std::vector<float> offTrajectoryErrors;
528 for (size_t sourceGroundTruthIndex = 1; sourceGroundTruthIndex < groundTruthPoints.size();
529 ++sourceGroundTruthIndex) {
530 const size_t targetGroundTruthIndex =
531 sourceGroundTruthIndex + predictionTargetIndex + 1;
532 // Only include errors for points with a ground truth value.
533 if (targetGroundTruthIndex < groundTruthPoints.size()) {
534 const Eigen::Vector2f trajectory =
535 (groundTruthPoints[targetGroundTruthIndex].position -
536 groundTruthPoints[targetGroundTruthIndex - 1].position)
537 .normalized();
538 const Eigen::Vector2f orthogonalTrajectory =
539 Eigen::Rotation2Df(M_PI_2) * trajectory;
540 const Eigen::Vector2f positionError =
541 predictionPoints[sourceGroundTruthIndex][predictionTargetIndex].position -
542 groundTruthPoints[targetGroundTruthIndex].position;
543 alongTrajectoryErrors.push_back(positionError.dot(trajectory));
544 alongTrajectorySquaredErrors.push_back(alongTrajectoryErrors.back() *
545 alongTrajectoryErrors.back());
546 offTrajectoryErrors.push_back(positionError.dot(orthogonalTrajectory));
547 }
548 }
549 generalPostitionErrors.push_back(
550 {.alongTrajectoryErrorMean = average(alongTrajectoryErrors),
551 .alongTrajectoryErrorStd = standardDeviation(alongTrajectoryErrors),
552 .offTrajectoryRmse = rmse(offTrajectoryErrors)});
553 }
554 return generalPostitionErrors;
555 }
556
557 // Inputs:
558 // • Vector of ground truth points
559 // • Vector of vectors of prediction points, where the first index is the source ground truth
560 // index, and the second is the prediction target index.
561 //
562 // Returns a vector of pressure RMSEs, indexed by prediction time delta bucket.
computePressureRmses(const std::vector<GroundTruthPoint> & groundTruthPoints,const std::vector<std::vector<PredictionPoint>> & predictionPoints)563 std::vector<float> computePressureRmses(
564 const std::vector<GroundTruthPoint>& groundTruthPoints,
565 const std::vector<std::vector<PredictionPoint>>& predictionPoints) {
566 // Aggregate errors by time bucket (prediction target index).
567 std::vector<float> pressureRmses;
568 for (size_t predictionTargetIndex = 0; predictionTargetIndex < TEST_MAX_NUM_PREDICTIONS;
569 ++predictionTargetIndex) {
570 std::vector<float> pressureErrors;
571 for (size_t sourceGroundTruthIndex = 1; sourceGroundTruthIndex < groundTruthPoints.size();
572 ++sourceGroundTruthIndex) {
573 const size_t targetGroundTruthIndex =
574 sourceGroundTruthIndex + predictionTargetIndex + 1;
575 // Only include errors for points with a ground truth value.
576 if (targetGroundTruthIndex < groundTruthPoints.size()) {
577 pressureErrors.push_back(
578 predictionPoints[sourceGroundTruthIndex][predictionTargetIndex].pressure -
579 groundTruthPoints[targetGroundTruthIndex].pressure);
580 }
581 }
582 pressureRmses.push_back(rmse(pressureErrors));
583 }
584 return pressureRmses;
585 }
586
TEST(ErrorComputationHelperTest,ComputeGeneralPositionErrorsSimpleTest)587 TEST(ErrorComputationHelperTest, ComputeGeneralPositionErrorsSimpleTest) {
588 std::vector<GroundTruthPoint> groundTruthPoints =
589 generateConstantGroundTruthPoints(GroundTruthPoint{{.position = Eigen::Vector2f(0, 0),
590 .pressure = 0.0f},
591 .timestamp = TEST_INITIAL_TIMESTAMP},
592 /*numPoints=*/TEST_MAX_NUM_PREDICTIONS + 2);
593 groundTruthPoints[3].position = Eigen::Vector2f(1, 0);
594 groundTruthPoints[4].position = Eigen::Vector2f(1, 1);
595 groundTruthPoints[5].position = Eigen::Vector2f(1, 3);
596 groundTruthPoints[6].position = Eigen::Vector2f(2, 3);
597
598 std::vector<std::vector<PredictionPoint>> predictionPoints =
599 generateAllPredictionsByLinearExtrapolation(groundTruthPoints);
600
601 // The generated predictions look like:
602 //
603 // | Source | Target Ground Truth Index |
604 // | Index | 2 | 3 | 4 | 5 | 6 |
605 // |------------|--------|--------|--------|--------|--------|
606 // | 1 | (0, 0) | (0, 0) | (0, 0) | (0, 0) | (0, 0) |
607 // | 2 | | (0, 0) | (0, 0) | (0, 0) | (0, 0) |
608 // | 3 | | | (2, 0) | (3, 0) | (4, 0) |
609 // | 4 | | | | (1, 2) | (1, 3) |
610 // | 5 | | | | | (1, 5) |
611 // |---------------------------------------------------------|
612 // | Actual Ground Truth Values |
613 // | Position | (0, 0) | (1, 0) | (1, 1) | (1, 3) | (2, 3) |
614 // | Previous | (0, 0) | (0, 0) | (1, 0) | (1, 1) | (1, 3) |
615 //
616 // Note: this table organizes prediction targets by target ground truth index. Metrics are
617 // aggregated across points with the same prediction time bucket index, which is different.
618 // Each down-right diagonal from this table gives us points from a unique time bucket.
619
620 // Initialize expected prediction errors from the table above. The first time bucket corresponds
621 // to the long diagonal of the table, and subsequent time buckets step up-right from there.
622 const std::vector<std::vector<float>> expectedAlongTrajectoryErrors{{0, -1, -1, -1, -1},
623 {-1, -1, -3, -1},
624 {-1, -3, 2},
625 {-3, -2},
626 {-2}};
627 const std::vector<std::vector<float>> expectedOffTrajectoryErrors{{0, 0, 1, 0, 2},
628 {0, 1, 2, 0},
629 {1, 1, 3},
630 {1, 3},
631 {3}};
632
633 std::vector<GeneralPositionErrors> generalPositionErrors =
634 computeGeneralPositionErrors(groundTruthPoints, predictionPoints);
635
636 ASSERT_EQ(TEST_MAX_NUM_PREDICTIONS, generalPositionErrors.size());
637 for (size_t i = 0; i < generalPositionErrors.size(); ++i) {
638 SCOPED_TRACE(testing::Message() << "i = " << i);
639 EXPECT_FLOAT_EQ(average(expectedAlongTrajectoryErrors[i]),
640 generalPositionErrors[i].alongTrajectoryErrorMean);
641 EXPECT_FLOAT_EQ(standardDeviation(expectedAlongTrajectoryErrors[i]),
642 generalPositionErrors[i].alongTrajectoryErrorStd);
643 EXPECT_FLOAT_EQ(rmse(expectedOffTrajectoryErrors[i]),
644 generalPositionErrors[i].offTrajectoryRmse);
645 }
646 }
647
TEST(ErrorComputationHelperTest,ComputePressureRmsesSimpleTest)648 TEST(ErrorComputationHelperTest, ComputePressureRmsesSimpleTest) {
649 // Generate ground truth points with pressures {0.0, 0.0, 0.0, 0.0, 0.5, 0.5, 0.5}.
650 // (We need TEST_MAX_NUM_PREDICTIONS + 2 to test all prediction time buckets.)
651 std::vector<GroundTruthPoint> groundTruthPoints =
652 generateConstantGroundTruthPoints(GroundTruthPoint{{.position = Eigen::Vector2f(0, 0),
653 .pressure = 0.0f},
654 .timestamp = TEST_INITIAL_TIMESTAMP},
655 /*numPoints=*/TEST_MAX_NUM_PREDICTIONS + 2);
656 for (size_t i = 4; i < groundTruthPoints.size(); ++i) {
657 groundTruthPoints[i].pressure = 0.5f;
658 }
659
660 std::vector<std::vector<PredictionPoint>> predictionPoints =
661 generateAllPredictionsByLinearExtrapolation(groundTruthPoints);
662
663 std::vector<float> pressureRmses = computePressureRmses(groundTruthPoints, predictionPoints);
664
665 ASSERT_EQ(TEST_MAX_NUM_PREDICTIONS, pressureRmses.size());
666 EXPECT_FLOAT_EQ(rmse(std::vector<float>{0.0f, 0.0f, -0.5f, 0.5f, 0.0f}), pressureRmses[0]);
667 EXPECT_FLOAT_EQ(rmse(std::vector<float>{0.0f, -0.5f, -0.5f, 1.0f}), pressureRmses[1]);
668 EXPECT_FLOAT_EQ(rmse(std::vector<float>{-0.5f, -0.5f, -0.5f}), pressureRmses[2]);
669 EXPECT_FLOAT_EQ(rmse(std::vector<float>{-0.5f, -0.5f}), pressureRmses[3]);
670 EXPECT_FLOAT_EQ(rmse(std::vector<float>{-0.5f}), pressureRmses[4]);
671 }
672
673 // --- MotionPredictorMetricsManager tests. ---
674
675 // Creates a mock atom reporting function that appends the reported atom to the given vector.
createMockReportAtomFunction(std::vector<AtomFields> & reportedAtomFields)676 ReportAtomFunction createMockReportAtomFunction(std::vector<AtomFields>& reportedAtomFields) {
677 return [&reportedAtomFields](const AtomFields& atomFields) -> void {
678 reportedAtomFields.push_back(atomFields);
679 };
680 }
681
682 // Helper function that instantiates a MetricsManager that reports metrics to outReportedAtomFields.
683 // Takes vectors of ground truth and prediction points of the same length, and passes these points
684 // to the MetricsManager. The format of these vectors is expected to be:
685 // • groundTruthPoints: chronologically-ordered ground truth points, with at least 2 elements.
686 // • predictionPoints: the first index points to a vector of predictions corresponding to the
687 // source ground truth point with the same index.
688 // - For empty prediction vectors, MetricsManager::onPredict will not be called.
689 // - To test all prediction buckets, there should be at least TEST_MAX_NUM_PREDICTIONS non-empty
690 // prediction vectors (that is, excluding the first and last). Thus, groundTruthPoints and
691 // predictionPoints should have size at least TEST_MAX_NUM_PREDICTIONS + 2.
692 //
693 // When the function returns, outReportedAtomFields will contain the reported AtomFields.
694 //
695 // This function returns void so that it can use test assertions.
runMetricsManager(const std::vector<GroundTruthPoint> & groundTruthPoints,const std::vector<std::vector<PredictionPoint>> & predictionPoints,std::vector<AtomFields> & outReportedAtomFields)696 void runMetricsManager(const std::vector<GroundTruthPoint>& groundTruthPoints,
697 const std::vector<std::vector<PredictionPoint>>& predictionPoints,
698 std::vector<AtomFields>& outReportedAtomFields) {
699 MotionPredictorMetricsManager metricsManager(TEST_PREDICTION_INTERVAL_NANOS,
700 TEST_MAX_NUM_PREDICTIONS,
701 createMockReportAtomFunction(
702 outReportedAtomFields));
703
704 ASSERT_GE(groundTruthPoints.size(), 2u);
705 ASSERT_EQ(predictionPoints.size(), groundTruthPoints.size());
706
707 for (size_t i = 0; i < groundTruthPoints.size(); ++i) {
708 metricsManager.onRecord(makeMotionEvent(groundTruthPoints[i]));
709 if (!predictionPoints[i].empty()) {
710 metricsManager.onPredict(makeMotionEvent(predictionPoints[i]));
711 }
712 }
713 // Send a stroke-end event to trigger the logging call.
714 metricsManager.onRecord(makeLiftMotionEvent());
715 }
716
717 // Vacuous test:
718 // • Input: no prediction data.
719 // • Expectation: no metrics should be logged.
TEST(MotionPredictorMetricsManagerTest,NoPredictions)720 TEST(MotionPredictorMetricsManagerTest, NoPredictions) {
721 std::vector<AtomFields> reportedAtomFields;
722 MotionPredictorMetricsManager metricsManager(TEST_PREDICTION_INTERVAL_NANOS,
723 TEST_MAX_NUM_PREDICTIONS,
724 createMockReportAtomFunction(reportedAtomFields));
725
726 metricsManager.onRecord(makeMotionEvent(
727 GroundTruthPoint{{.position = Eigen::Vector2f(0, 0), .pressure = 0}, .timestamp = 0}));
728 metricsManager.onRecord(makeLiftMotionEvent());
729
730 // Check that reportedAtomFields is still empty (as it was initialized empty), ensuring that
731 // no metrics were logged.
732 EXPECT_EQ(0u, reportedAtomFields.size());
733 }
734
735 // Perfect predictions test:
736 // • Input: constant input events, perfect predictions matching the input events.
737 // • Expectation: all error metrics should be zero, or NO_DATA_SENTINEL for "unreported" metrics.
738 // (For example, scale-invariant errors are only reported for the last time bucket.)
TEST(MotionPredictorMetricsManagerTest,ConstantGroundTruthPerfectPredictions)739 TEST(MotionPredictorMetricsManagerTest, ConstantGroundTruthPerfectPredictions) {
740 GroundTruthPoint groundTruthPoint{{.position = Eigen::Vector2f(10.0f, 20.0f), .pressure = 0.6f},
741 .timestamp = TEST_INITIAL_TIMESTAMP};
742
743 // Generate ground truth and prediction points as described by the runMetricsManager comment.
744 std::vector<GroundTruthPoint> groundTruthPoints;
745 std::vector<std::vector<PredictionPoint>> predictionPoints;
746 for (size_t i = 0; i < TEST_MAX_NUM_PREDICTIONS + 2; ++i) {
747 groundTruthPoints.push_back(groundTruthPoint);
748 predictionPoints.push_back(i > 0 ? generateConstantPredictions(groundTruthPoint)
749 : std::vector<PredictionPoint>{});
750 groundTruthPoint.timestamp += TEST_PREDICTION_INTERVAL_NANOS;
751 }
752
753 std::vector<AtomFields> reportedAtomFields;
754 runMetricsManager(groundTruthPoints, predictionPoints, reportedAtomFields);
755
756 ASSERT_EQ(TEST_MAX_NUM_PREDICTIONS, reportedAtomFields.size());
757 // Check that errors are all zero, or NO_DATA_SENTINEL for unreported metrics.
758 for (size_t i = 0; i < reportedAtomFields.size(); ++i) {
759 SCOPED_TRACE(testing::Message() << "i = " << i);
760 const AtomFields& atom = reportedAtomFields[i];
761 const nsecs_t deltaTimeBucketNanos = TEST_PREDICTION_INTERVAL_NANOS * (i + 1);
762 EXPECT_EQ(deltaTimeBucketNanos / NANOS_PER_MILLIS, atom.deltaTimeBucketMilliseconds);
763 // General errors: reported for every time bucket.
764 EXPECT_EQ(0, atom.alongTrajectoryErrorMeanMillipixels);
765 EXPECT_EQ(0, atom.alongTrajectoryErrorStdMillipixels);
766 EXPECT_EQ(0, atom.offTrajectoryRmseMillipixels);
767 EXPECT_EQ(0, atom.pressureRmseMilliunits);
768 // High-velocity errors: reported only for the last two time buckets.
769 // However, this data has zero velocity, so these metrics should all be NO_DATA_SENTINEL.
770 EXPECT_EQ(NO_DATA_SENTINEL, atom.highVelocityAlongTrajectoryRmse);
771 EXPECT_EQ(NO_DATA_SENTINEL, atom.highVelocityOffTrajectoryRmse);
772 // Scale-invariant errors: reported only for the last time bucket.
773 if (i + 1 == reportedAtomFields.size()) {
774 EXPECT_EQ(0, atom.scaleInvariantAlongTrajectoryRmse);
775 EXPECT_EQ(0, atom.scaleInvariantOffTrajectoryRmse);
776 } else {
777 EXPECT_EQ(NO_DATA_SENTINEL, atom.scaleInvariantAlongTrajectoryRmse);
778 EXPECT_EQ(NO_DATA_SENTINEL, atom.scaleInvariantOffTrajectoryRmse);
779 }
780 }
781 }
782
TEST(MotionPredictorMetricsManagerTest,QuadraticPressureLinearPredictions)783 TEST(MotionPredictorMetricsManagerTest, QuadraticPressureLinearPredictions) {
784 // Generate ground truth points.
785 //
786 // Ground truth pressures are a quadratically increasing function from some initial value.
787 const float initialPressure = 0.5f;
788 const float quadraticCoefficient = 0.01f;
789 std::vector<GroundTruthPoint> groundTruthPoints;
790 nsecs_t timestamp = TEST_INITIAL_TIMESTAMP;
791 // As described in the runMetricsManager comment, we should have TEST_MAX_NUM_PREDICTIONS + 2
792 // ground truth points.
793 for (size_t i = 0; i < TEST_MAX_NUM_PREDICTIONS + 2; ++i) {
794 const float pressure = initialPressure + quadraticCoefficient * static_cast<float>(i * i);
795 groundTruthPoints.push_back(
796 GroundTruthPoint{{.position = Eigen::Vector2f(0, 0), .pressure = pressure},
797 .timestamp = timestamp});
798 timestamp += TEST_PREDICTION_INTERVAL_NANOS;
799 }
800
801 // Note: the first index is the source ground truth index, and the second is the prediction
802 // target index.
803 std::vector<std::vector<PredictionPoint>> predictionPoints =
804 generateAllPredictionsByLinearExtrapolation(groundTruthPoints);
805
806 const std::vector<float> pressureErrors =
807 computePressureRmses(groundTruthPoints, predictionPoints);
808
809 // Run test.
810 std::vector<AtomFields> reportedAtomFields;
811 runMetricsManager(groundTruthPoints, predictionPoints, reportedAtomFields);
812
813 // Check logged metrics match expectations.
814 ASSERT_EQ(TEST_MAX_NUM_PREDICTIONS, reportedAtomFields.size());
815 for (size_t i = 0; i < reportedAtomFields.size(); ++i) {
816 SCOPED_TRACE(testing::Message() << "i = " << i);
817 const AtomFields& atom = reportedAtomFields[i];
818 // Check time bucket delta matches expectation based on index and prediction interval.
819 const nsecs_t deltaTimeBucketNanos = TEST_PREDICTION_INTERVAL_NANOS * (i + 1);
820 EXPECT_EQ(deltaTimeBucketNanos / NANOS_PER_MILLIS, atom.deltaTimeBucketMilliseconds);
821 // Check pressure error matches expectation.
822 EXPECT_NEAR(static_cast<int>(1000 * pressureErrors[i]), atom.pressureRmseMilliunits, 1);
823 }
824 }
825
TEST(MotionPredictorMetricsManagerTest,QuadraticPositionLinearPredictionsGeneralErrors)826 TEST(MotionPredictorMetricsManagerTest, QuadraticPositionLinearPredictionsGeneralErrors) {
827 // Generate ground truth points.
828 //
829 // Each component of the ground truth positions are an independent quadratically increasing
830 // function from some initial value.
831 const Eigen::Vector2f initialPosition(200, 300);
832 const Eigen::Vector2f quadraticCoefficients(-2, 3);
833 std::vector<GroundTruthPoint> groundTruthPoints;
834 nsecs_t timestamp = TEST_INITIAL_TIMESTAMP;
835 // As described in the runMetricsManager comment, we should have TEST_MAX_NUM_PREDICTIONS + 2
836 // ground truth points.
837 for (size_t i = 0; i < TEST_MAX_NUM_PREDICTIONS + 2; ++i) {
838 const Eigen::Vector2f position =
839 initialPosition + quadraticCoefficients * static_cast<float>(i * i);
840 groundTruthPoints.push_back(
841 GroundTruthPoint{{.position = position, .pressure = 0.5}, .timestamp = timestamp});
842 timestamp += TEST_PREDICTION_INTERVAL_NANOS;
843 }
844
845 // Note: the first index is the source ground truth index, and the second is the prediction
846 // target index.
847 std::vector<std::vector<PredictionPoint>> predictionPoints =
848 generateAllPredictionsByLinearExtrapolation(groundTruthPoints);
849
850 std::vector<GeneralPositionErrors> generalPositionErrors =
851 computeGeneralPositionErrors(groundTruthPoints, predictionPoints);
852
853 // Run test.
854 std::vector<AtomFields> reportedAtomFields;
855 runMetricsManager(groundTruthPoints, predictionPoints, reportedAtomFields);
856
857 // Check logged metrics match expectations.
858 ASSERT_EQ(TEST_MAX_NUM_PREDICTIONS, reportedAtomFields.size());
859 for (size_t i = 0; i < reportedAtomFields.size(); ++i) {
860 SCOPED_TRACE(testing::Message() << "i = " << i);
861 const AtomFields& atom = reportedAtomFields[i];
862 // Check time bucket delta matches expectation based on index and prediction interval.
863 const nsecs_t deltaTimeBucketNanos = TEST_PREDICTION_INTERVAL_NANOS * (i + 1);
864 EXPECT_EQ(deltaTimeBucketNanos / NANOS_PER_MILLIS, atom.deltaTimeBucketMilliseconds);
865 // Check general position errors match expectation.
866 EXPECT_NEAR(static_cast<int>(1000 * generalPositionErrors[i].alongTrajectoryErrorMean),
867 atom.alongTrajectoryErrorMeanMillipixels, 1);
868 EXPECT_NEAR(static_cast<int>(1000 * generalPositionErrors[i].alongTrajectoryErrorStd),
869 atom.alongTrajectoryErrorStdMillipixels, 1);
870 EXPECT_NEAR(static_cast<int>(1000 * generalPositionErrors[i].offTrajectoryRmse),
871 atom.offTrajectoryRmseMillipixels, 1);
872 }
873 }
874
875 // Counterclockwise regular octagonal section test:
876 // • Input – ground truth: constantly-spaced input events starting at a trajectory pointing exactly
877 // rightwards, and rotating by 45° counterclockwise after each input.
878 // • Input – predictions: simple linear extrapolations of previous two ground truth points.
879 //
880 // The code below uses the following terminology to distinguish references to ground truth events:
881 // • Source ground truth: the most recent ground truth point received at the time the prediction
882 // was made.
883 // • Target ground truth: the ground truth event that the prediction was attempting to match.
TEST(MotionPredictorMetricsManagerTest,CounterclockwiseOctagonGroundTruthLinearPredictions)884 TEST(MotionPredictorMetricsManagerTest, CounterclockwiseOctagonGroundTruthLinearPredictions) {
885 // Select a stroke velocity that exceeds the high-velocity threshold of 1100 px/sec.
886 // For an input rate of 240 hz, 1100 px/sec * (1/240) sec/input ≈ 4.58 pixels per input.
887 const float strokeVelocity = 10; // pixels per input
888
889 // As described in the runMetricsManager comment, we should have TEST_MAX_NUM_PREDICTIONS + 2
890 // ground truth points.
891 std::vector<GroundTruthPoint> groundTruthPoints = generateCircularArcGroundTruthPoints(
892 /*initialPosition=*/Eigen::Vector2f(100, 100),
893 /*initialAngle=*/M_PI_2,
894 /*velocity=*/strokeVelocity,
895 /*turningAngle=*/-M_PI_4,
896 /*numPoints=*/TEST_MAX_NUM_PREDICTIONS + 2);
897
898 std::vector<std::vector<PredictionPoint>> predictionPoints =
899 generateAllPredictionsByLinearExtrapolation(groundTruthPoints);
900
901 std::vector<GeneralPositionErrors> generalPositionErrors =
902 computeGeneralPositionErrors(groundTruthPoints, predictionPoints);
903
904 // Run test.
905 std::vector<AtomFields> reportedAtomFields;
906 runMetricsManager(groundTruthPoints, predictionPoints, reportedAtomFields);
907
908 // Check logged metrics match expectations.
909 ASSERT_EQ(TEST_MAX_NUM_PREDICTIONS, reportedAtomFields.size());
910 for (size_t i = 0; i < reportedAtomFields.size(); ++i) {
911 SCOPED_TRACE(testing::Message() << "i = " << i);
912 const AtomFields& atom = reportedAtomFields[i];
913 const nsecs_t deltaTimeBucketNanos = TEST_PREDICTION_INTERVAL_NANOS * (i + 1);
914 EXPECT_EQ(deltaTimeBucketNanos / NANOS_PER_MILLIS, atom.deltaTimeBucketMilliseconds);
915
916 // General errors: reported for every time bucket.
917 EXPECT_NEAR(static_cast<int>(1000 * generalPositionErrors[i].alongTrajectoryErrorMean),
918 atom.alongTrajectoryErrorMeanMillipixels, 1);
919 // We allow for some floating point error in standard deviation (0.02 pixels).
920 EXPECT_NEAR(1000 * generalPositionErrors[i].alongTrajectoryErrorStd,
921 atom.alongTrajectoryErrorStdMillipixels, 20);
922 // All position errors are equal, so the standard deviation should be approximately zero.
923 EXPECT_NEAR(0, atom.alongTrajectoryErrorStdMillipixels, 20);
924 // Absolute value for RMSE, since it must be non-negative.
925 EXPECT_NEAR(static_cast<int>(1000 * generalPositionErrors[i].offTrajectoryRmse),
926 atom.offTrajectoryRmseMillipixels, 1);
927
928 // High-velocity errors: reported only for the last two time buckets.
929 //
930 // Since our input stroke velocity is chosen to be above the high-velocity threshold, all
931 // data contributes to high-velocity errors, and thus high-velocity errors should be equal
932 // to general errors (where reported).
933 //
934 // As above, use absolute value for RMSE, since it must be non-negative.
935 if (i + 2 >= reportedAtomFields.size()) {
936 EXPECT_NEAR(static_cast<int>(
937 1000 * std::abs(generalPositionErrors[i].alongTrajectoryErrorMean)),
938 atom.highVelocityAlongTrajectoryRmse, 1);
939 EXPECT_NEAR(static_cast<int>(1000 *
940 std::abs(generalPositionErrors[i].offTrajectoryRmse)),
941 atom.highVelocityOffTrajectoryRmse, 1);
942 } else {
943 EXPECT_EQ(NO_DATA_SENTINEL, atom.highVelocityAlongTrajectoryRmse);
944 EXPECT_EQ(NO_DATA_SENTINEL, atom.highVelocityOffTrajectoryRmse);
945 }
946
947 // Scale-invariant errors: reported only for the last time bucket, where the reported value
948 // is the aggregation across all time buckets.
949 //
950 // The MetricsManager stores mMaxNumPredictions recent ground truth segments. Our ground
951 // truth segments here all have a length of strokeVelocity, so we can convert general errors
952 // to scale-invariant errors by dividing by `strokeVelocty * TEST_MAX_NUM_PREDICTIONS`.
953 //
954 // As above, use absolute value for RMSE, since it must be non-negative.
955 if (i + 1 == reportedAtomFields.size()) {
956 const float pathLength = strokeVelocity * TEST_MAX_NUM_PREDICTIONS;
957 std::vector<float> alongTrajectoryAbsoluteErrors;
958 std::vector<float> offTrajectoryAbsoluteErrors;
959 for (size_t j = 0; j < TEST_MAX_NUM_PREDICTIONS; ++j) {
960 alongTrajectoryAbsoluteErrors.push_back(
961 std::abs(generalPositionErrors[j].alongTrajectoryErrorMean));
962 offTrajectoryAbsoluteErrors.push_back(
963 std::abs(generalPositionErrors[j].offTrajectoryRmse));
964 }
965 EXPECT_NEAR(static_cast<int>(1000 * average(alongTrajectoryAbsoluteErrors) /
966 pathLength),
967 atom.scaleInvariantAlongTrajectoryRmse, 1);
968 EXPECT_NEAR(static_cast<int>(1000 * average(offTrajectoryAbsoluteErrors) / pathLength),
969 atom.scaleInvariantOffTrajectoryRmse, 1);
970 } else {
971 EXPECT_EQ(NO_DATA_SENTINEL, atom.scaleInvariantAlongTrajectoryRmse);
972 EXPECT_EQ(NO_DATA_SENTINEL, atom.scaleInvariantOffTrajectoryRmse);
973 }
974 }
975 }
976
977 // Robustness test:
978 // • Input: input events separated by a significantly greater time interval than the interval
979 // between predictions.
980 // • Expectation: the MetricsManager should not crash in this case. (No assertions are made about
981 // the resulting metrics.)
982 //
983 // In practice, this scenario could arise either if the input and prediction intervals are
984 // mismatched, or if input events are missing (dropped or skipped for some reason).
TEST(MotionPredictorMetricsManagerTest,MismatchedInputAndPredictionInterval)985 TEST(MotionPredictorMetricsManagerTest, MismatchedInputAndPredictionInterval) {
986 // Create two ground truth points separated by MAX_NUM_PREDICTIONS * PREDICTION_INTERVAL,
987 // so that the second ground truth point corresponds to the last prediction bucket. This
988 // ensures that the scale-invariant error codepath will be run, giving full code coverage.
989 GroundTruthPoint groundTruthPoint{{.position = Eigen::Vector2f(0.0f, 0.0f), .pressure = 0.5f},
990 .timestamp = TEST_INITIAL_TIMESTAMP};
991 const nsecs_t inputInterval = TEST_MAX_NUM_PREDICTIONS * TEST_PREDICTION_INTERVAL_NANOS;
992 const std::vector<GroundTruthPoint> groundTruthPoints =
993 generateConstantGroundTruthPoints(groundTruthPoint, /*numPoints=*/2, inputInterval);
994
995 // Create predictions separated by the prediction interval.
996 std::vector<std::vector<PredictionPoint>> predictionPoints;
997 for (size_t i = 0; i < groundTruthPoints.size(); ++i) {
998 predictionPoints.push_back(
999 generateConstantPredictions(groundTruthPoints[i], TEST_PREDICTION_INTERVAL_NANOS));
1000 }
1001
1002 // Test that we can run the MetricsManager without crashing.
1003 std::vector<AtomFields> reportedAtomFields;
1004 runMetricsManager(groundTruthPoints, predictionPoints, reportedAtomFields);
1005 }
1006
1007 } // namespace
1008 } // namespace android
1009