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