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