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 #define LOG_TAG "MotionPredictorMetricsManager"
18 
19 #include <input/MotionPredictorMetricsManager.h>
20 
21 #include <algorithm>
22 
23 #include <android-base/logging.h>
24 #ifdef __ANDROID__
25 #include <statslog_libinput.h>
26 #endif // __ANDROID__
27 
28 #include "Eigen/Core"
29 #include "Eigen/Geometry"
30 
31 namespace android {
32 namespace {
33 
34 inline constexpr int NANOS_PER_SECOND = 1'000'000'000; // nanoseconds per second
35 inline constexpr int NANOS_PER_MILLIS = 1'000'000;     // nanoseconds per millisecond
36 
37 // Velocity threshold at which we report "high-velocity" metrics, in pixels per second.
38 // This value was selected from manual experimentation, as a threshold that separates "fast"
39 // (semi-sloppy) handwriting from more careful medium to slow handwriting.
40 inline constexpr float HIGH_VELOCITY_THRESHOLD = 1100.0;
41 
42 // Small value to add to the path length when computing scale-invariant error to avoid division by
43 // zero.
44 inline constexpr float PATH_LENGTH_EPSILON = 0.001;
45 
46 } // namespace
47 
defaultReportAtomFunction(const MotionPredictorMetricsManager::AtomFields & atomFields)48 void MotionPredictorMetricsManager::defaultReportAtomFunction(
49         const MotionPredictorMetricsManager::AtomFields& atomFields) {
50 #ifdef __ANDROID__
51     android::libinput::stats_write(android::libinput::STYLUS_PREDICTION_METRICS_REPORTED,
52                                    /*stylus_vendor_id=*/0,
53                                    /*stylus_product_id=*/0,
54                                    atomFields.deltaTimeBucketMilliseconds,
55                                    atomFields.alongTrajectoryErrorMeanMillipixels,
56                                    atomFields.alongTrajectoryErrorStdMillipixels,
57                                    atomFields.offTrajectoryRmseMillipixels,
58                                    atomFields.pressureRmseMilliunits,
59                                    atomFields.highVelocityAlongTrajectoryRmse,
60                                    atomFields.highVelocityOffTrajectoryRmse,
61                                    atomFields.scaleInvariantAlongTrajectoryRmse,
62                                    atomFields.scaleInvariantOffTrajectoryRmse);
63 #endif // __ANDROID__
64 }
65 
MotionPredictorMetricsManager(nsecs_t predictionInterval,size_t maxNumPredictions,ReportAtomFunction reportAtomFunction)66 MotionPredictorMetricsManager::MotionPredictorMetricsManager(
67         nsecs_t predictionInterval,
68         size_t maxNumPredictions,
69         ReportAtomFunction reportAtomFunction)
70       : mPredictionInterval(predictionInterval),
71         mMaxNumPredictions(maxNumPredictions),
72         mRecentGroundTruthPoints(maxNumPredictions + 1),
73         mAggregatedMetrics(maxNumPredictions),
74         mAtomFields(maxNumPredictions),
75         mReportAtomFunction(reportAtomFunction ? reportAtomFunction : defaultReportAtomFunction) {}
76 
onRecord(const MotionEvent & inputEvent)77 void MotionPredictorMetricsManager::onRecord(const MotionEvent& inputEvent) {
78     // Convert MotionEvent to GroundTruthPoint.
79     const PointerCoords* coords = inputEvent.getRawPointerCoords(/*pointerIndex=*/0);
80     LOG_ALWAYS_FATAL_IF(coords == nullptr);
81     const GroundTruthPoint groundTruthPoint{{.position = Eigen::Vector2f{coords->getY(),
82                                                                          coords->getX()},
83                                              .pressure =
84                                                      inputEvent.getPressure(/*pointerIndex=*/0)},
85                                             .timestamp = inputEvent.getEventTime()};
86 
87     // Handle event based on action type.
88     switch (inputEvent.getActionMasked()) {
89         case AMOTION_EVENT_ACTION_DOWN: {
90             clearStrokeData();
91             incorporateNewGroundTruth(groundTruthPoint);
92             break;
93         }
94         case AMOTION_EVENT_ACTION_MOVE: {
95             incorporateNewGroundTruth(groundTruthPoint);
96             break;
97         }
98         case AMOTION_EVENT_ACTION_UP:
99         case AMOTION_EVENT_ACTION_CANCEL: {
100             // Only expect meaningful predictions when given at least two input points.
101             if (mRecentGroundTruthPoints.size() >= 2) {
102                 computeAtomFields();
103                 reportMetrics();
104             }
105             break;
106         }
107     }
108 }
109 
110 // Adds new predictions to mRecentPredictions and maintains the invariant that elements are
111 // sorted in ascending order of targetTimestamp.
onPredict(const MotionEvent & predictionEvent)112 void MotionPredictorMetricsManager::onPredict(const MotionEvent& predictionEvent) {
113     const size_t numPredictions = predictionEvent.getHistorySize() + 1;
114     if (numPredictions > mMaxNumPredictions) {
115         LOG(WARNING) << "numPredictions (" << numPredictions << ") > mMaxNumPredictions ("
116                      << mMaxNumPredictions << "). Ignoring extra predictions in metrics.";
117     }
118     for (size_t i = 0; (i < numPredictions) && (i < mMaxNumPredictions); ++i) {
119         // Convert MotionEvent to PredictionPoint.
120         const PointerCoords* coords =
121                 predictionEvent.getHistoricalRawPointerCoords(/*pointerIndex=*/0, i);
122         LOG_ALWAYS_FATAL_IF(coords == nullptr);
123         const nsecs_t targetTimestamp = predictionEvent.getHistoricalEventTime(i);
124         mRecentPredictions.push_back(
125                 PredictionPoint{{.position = Eigen::Vector2f{coords->getY(), coords->getX()},
126                                  .pressure =
127                                          predictionEvent.getHistoricalPressure(/*pointerIndex=*/0,
128                                                                                i)},
129                                 .originTimestamp = mRecentGroundTruthPoints.back().timestamp,
130                                 .targetTimestamp = targetTimestamp});
131     }
132 
133     std::sort(mRecentPredictions.begin(), mRecentPredictions.end());
134 }
135 
clearStrokeData()136 void MotionPredictorMetricsManager::clearStrokeData() {
137     mRecentGroundTruthPoints.clear();
138     mRecentPredictions.clear();
139     std::fill(mAggregatedMetrics.begin(), mAggregatedMetrics.end(), AggregatedStrokeMetrics{});
140     std::fill(mAtomFields.begin(), mAtomFields.end(), AtomFields{});
141 }
142 
incorporateNewGroundTruth(const GroundTruthPoint & groundTruthPoint)143 void MotionPredictorMetricsManager::incorporateNewGroundTruth(
144         const GroundTruthPoint& groundTruthPoint) {
145     // Note: this removes the oldest point if `mRecentGroundTruthPoints` is already at capacity.
146     mRecentGroundTruthPoints.pushBack(groundTruthPoint);
147 
148     // Remove outdated predictions – those that can never be matched with the current or any future
149     // ground truth points. We use fuzzy association for the timestamps here, because ground truth
150     // and prediction timestamps may not be perfectly synchronized.
151     const nsecs_t fuzzy_association_time_delta = mPredictionInterval / 4;
152     const auto firstCurrentIt =
153             std::find_if(mRecentPredictions.begin(), mRecentPredictions.end(),
154                          [&groundTruthPoint,
155                           fuzzy_association_time_delta](const PredictionPoint& prediction) {
156                              return prediction.targetTimestamp >
157                                      groundTruthPoint.timestamp - fuzzy_association_time_delta;
158                          });
159     mRecentPredictions.erase(mRecentPredictions.begin(), firstCurrentIt);
160 
161     // Fuzzily match the new ground truth's timestamp to recent predictions' targetTimestamp and
162     // update the corresponding metrics.
163     for (const PredictionPoint& prediction : mRecentPredictions) {
164         if ((prediction.targetTimestamp >
165              groundTruthPoint.timestamp - fuzzy_association_time_delta) &&
166             (prediction.targetTimestamp <
167              groundTruthPoint.timestamp + fuzzy_association_time_delta)) {
168             updateAggregatedMetrics(prediction);
169         }
170     }
171 }
172 
updateAggregatedMetrics(const PredictionPoint & predictionPoint)173 void MotionPredictorMetricsManager::updateAggregatedMetrics(
174         const PredictionPoint& predictionPoint) {
175     if (mRecentGroundTruthPoints.size() < 2) {
176         return;
177     }
178 
179     const GroundTruthPoint& latestGroundTruthPoint = mRecentGroundTruthPoints.back();
180     const GroundTruthPoint& previousGroundTruthPoint =
181             mRecentGroundTruthPoints[mRecentGroundTruthPoints.size() - 2];
182     // Calculate prediction error vector.
183     const Eigen::Vector2f groundTruthTrajectory =
184             latestGroundTruthPoint.position - previousGroundTruthPoint.position;
185     const Eigen::Vector2f predictionTrajectory =
186             predictionPoint.position - previousGroundTruthPoint.position;
187     const Eigen::Vector2f predictionError = predictionTrajectory - groundTruthTrajectory;
188 
189     // By default, prediction error counts fully as both off-trajectory and along-trajectory error.
190     // This serves as the fallback when the two most recent ground truth points are equal.
191     const float predictionErrorNorm = predictionError.norm();
192     float alongTrajectoryError = predictionErrorNorm;
193     float offTrajectoryError = predictionErrorNorm;
194     if (groundTruthTrajectory.squaredNorm() > 0) {
195         // Rotate the prediction error vector by the angle of the ground truth trajectory vector.
196         // This yields a vector whose first component is the along-trajectory error and whose
197         // second component is the off-trajectory error.
198         const float theta = std::atan2(groundTruthTrajectory[1], groundTruthTrajectory[0]);
199         const Eigen::Vector2f rotatedPredictionError = Eigen::Rotation2Df(-theta) * predictionError;
200         alongTrajectoryError = rotatedPredictionError[0];
201         offTrajectoryError = rotatedPredictionError[1];
202     }
203 
204     // Compute the multiple of mPredictionInterval nearest to the amount of time into the
205     // future being predicted. This serves as the time bucket index into mAggregatedMetrics.
206     const float timestampDeltaFloat =
207             static_cast<float>(predictionPoint.targetTimestamp - predictionPoint.originTimestamp);
208     const size_t tIndex =
209             static_cast<size_t>(std::round(timestampDeltaFloat / mPredictionInterval - 1));
210 
211     // Aggregate values into "general errors".
212     mAggregatedMetrics[tIndex].alongTrajectoryErrorSum += alongTrajectoryError;
213     mAggregatedMetrics[tIndex].alongTrajectorySumSquaredErrors +=
214             alongTrajectoryError * alongTrajectoryError;
215     mAggregatedMetrics[tIndex].offTrajectorySumSquaredErrors +=
216             offTrajectoryError * offTrajectoryError;
217     const float pressureError = predictionPoint.pressure - latestGroundTruthPoint.pressure;
218     mAggregatedMetrics[tIndex].pressureSumSquaredErrors += pressureError * pressureError;
219     ++mAggregatedMetrics[tIndex].generalErrorsCount;
220 
221     // Aggregate values into high-velocity metrics, if we are in one of the last two time buckets
222     // and the velocity is above the threshold. Velocity here is measured in pixels per second.
223     const float velocity = groundTruthTrajectory.norm() /
224             (static_cast<float>(latestGroundTruthPoint.timestamp -
225                                 previousGroundTruthPoint.timestamp) /
226              NANOS_PER_SECOND);
227     if ((tIndex + 2 >= mMaxNumPredictions) && (velocity > HIGH_VELOCITY_THRESHOLD)) {
228         mAggregatedMetrics[tIndex].highVelocityAlongTrajectorySse +=
229                 alongTrajectoryError * alongTrajectoryError;
230         mAggregatedMetrics[tIndex].highVelocityOffTrajectorySse +=
231                 offTrajectoryError * offTrajectoryError;
232         ++mAggregatedMetrics[tIndex].highVelocityErrorsCount;
233     }
234 
235     // Compute path length for scale-invariant errors.
236     float pathLength = 0;
237     for (size_t i = 1; i < mRecentGroundTruthPoints.size(); ++i) {
238         pathLength +=
239                 (mRecentGroundTruthPoints[i].position - mRecentGroundTruthPoints[i - 1].position)
240                         .norm();
241     }
242     // Avoid overweighting errors at the beginning of a stroke: compute the path length as if there
243     // were a full ground truth history by filling in missing segments with the average length.
244     // Note: the "- 1" is needed to translate from number of endpoints to number of segments.
245     pathLength *= static_cast<float>(mRecentGroundTruthPoints.capacity() - 1) /
246             (mRecentGroundTruthPoints.size() - 1);
247     pathLength += PATH_LENGTH_EPSILON; // Ensure path length is nonzero (>= PATH_LENGTH_EPSILON).
248 
249     // Compute and aggregate scale-invariant errors.
250     const float scaleInvariantAlongTrajectoryError = alongTrajectoryError / pathLength;
251     const float scaleInvariantOffTrajectoryError = offTrajectoryError / pathLength;
252     mAggregatedMetrics[tIndex].scaleInvariantAlongTrajectorySse +=
253             scaleInvariantAlongTrajectoryError * scaleInvariantAlongTrajectoryError;
254     mAggregatedMetrics[tIndex].scaleInvariantOffTrajectorySse +=
255             scaleInvariantOffTrajectoryError * scaleInvariantOffTrajectoryError;
256     ++mAggregatedMetrics[tIndex].scaleInvariantErrorsCount;
257 }
258 
computeAtomFields()259 void MotionPredictorMetricsManager::computeAtomFields() {
260     for (size_t i = 0; i < mAggregatedMetrics.size(); ++i) {
261         if (mAggregatedMetrics[i].generalErrorsCount == 0) {
262             // We have not received data corresponding to metrics for this time bucket.
263             continue;
264         }
265 
266         mAtomFields[i].deltaTimeBucketMilliseconds =
267                 static_cast<int>(mPredictionInterval / NANOS_PER_MILLIS * (i + 1));
268 
269         // Note: we need the "* 1000"s below because we report values in integral milli-units.
270 
271         { // General errors: reported for every time bucket.
272             const float alongTrajectoryErrorMean = mAggregatedMetrics[i].alongTrajectoryErrorSum /
273                     mAggregatedMetrics[i].generalErrorsCount;
274             mAtomFields[i].alongTrajectoryErrorMeanMillipixels =
275                     static_cast<int>(alongTrajectoryErrorMean * 1000);
276 
277             const float alongTrajectoryMse = mAggregatedMetrics[i].alongTrajectorySumSquaredErrors /
278                     mAggregatedMetrics[i].generalErrorsCount;
279             // Take the max with 0 to avoid negative values caused by numerical instability.
280             const float alongTrajectoryErrorVariance =
281                     std::max(0.0f,
282                              alongTrajectoryMse -
283                                      alongTrajectoryErrorMean * alongTrajectoryErrorMean);
284             const float alongTrajectoryErrorStd = std::sqrt(alongTrajectoryErrorVariance);
285             mAtomFields[i].alongTrajectoryErrorStdMillipixels =
286                     static_cast<int>(alongTrajectoryErrorStd * 1000);
287 
288             LOG_ALWAYS_FATAL_IF(mAggregatedMetrics[i].offTrajectorySumSquaredErrors < 0,
289                                 "mAggregatedMetrics[%zu].offTrajectorySumSquaredErrors = %f should "
290                                 "not be negative",
291                                 i, mAggregatedMetrics[i].offTrajectorySumSquaredErrors);
292             const float offTrajectoryRmse =
293                     std::sqrt(mAggregatedMetrics[i].offTrajectorySumSquaredErrors /
294                               mAggregatedMetrics[i].generalErrorsCount);
295             mAtomFields[i].offTrajectoryRmseMillipixels =
296                     static_cast<int>(offTrajectoryRmse * 1000);
297 
298             LOG_ALWAYS_FATAL_IF(mAggregatedMetrics[i].pressureSumSquaredErrors < 0,
299                                 "mAggregatedMetrics[%zu].pressureSumSquaredErrors = %f should not "
300                                 "be negative",
301                                 i, mAggregatedMetrics[i].pressureSumSquaredErrors);
302             const float pressureRmse = std::sqrt(mAggregatedMetrics[i].pressureSumSquaredErrors /
303                                                  mAggregatedMetrics[i].generalErrorsCount);
304             mAtomFields[i].pressureRmseMilliunits = static_cast<int>(pressureRmse * 1000);
305         }
306 
307         // High-velocity errors: reported only for last two time buckets.
308         // Check if we are in one of the last two time buckets, and there is high-velocity data.
309         if ((i + 2 >= mMaxNumPredictions) && (mAggregatedMetrics[i].highVelocityErrorsCount > 0)) {
310             LOG_ALWAYS_FATAL_IF(mAggregatedMetrics[i].highVelocityAlongTrajectorySse < 0,
311                                 "mAggregatedMetrics[%zu].highVelocityAlongTrajectorySse = %f "
312                                 "should not be negative",
313                                 i, mAggregatedMetrics[i].highVelocityAlongTrajectorySse);
314             const float alongTrajectoryRmse =
315                     std::sqrt(mAggregatedMetrics[i].highVelocityAlongTrajectorySse /
316                               mAggregatedMetrics[i].highVelocityErrorsCount);
317             mAtomFields[i].highVelocityAlongTrajectoryRmse =
318                     static_cast<int>(alongTrajectoryRmse * 1000);
319 
320             LOG_ALWAYS_FATAL_IF(mAggregatedMetrics[i].highVelocityOffTrajectorySse < 0,
321                                 "mAggregatedMetrics[%zu].highVelocityOffTrajectorySse = %f should "
322                                 "not be negative",
323                                 i, mAggregatedMetrics[i].highVelocityOffTrajectorySse);
324             const float offTrajectoryRmse =
325                     std::sqrt(mAggregatedMetrics[i].highVelocityOffTrajectorySse /
326                               mAggregatedMetrics[i].highVelocityErrorsCount);
327             mAtomFields[i].highVelocityOffTrajectoryRmse =
328                     static_cast<int>(offTrajectoryRmse * 1000);
329         }
330     }
331 
332     // Scale-invariant errors: the average scale-invariant error across all time buckets
333     // is reported in the last time bucket.
334     {
335         // Compute error averages.
336         float alongTrajectoryRmseSum = 0;
337         float offTrajectoryRmseSum = 0;
338         int bucket_count = 0;
339         for (size_t j = 0; j < mAggregatedMetrics.size(); ++j) {
340             if (mAggregatedMetrics[j].scaleInvariantErrorsCount == 0) {
341                 continue;
342             }
343 
344             LOG_ALWAYS_FATAL_IF(mAggregatedMetrics[j].scaleInvariantAlongTrajectorySse < 0,
345                                 "mAggregatedMetrics[%zu].scaleInvariantAlongTrajectorySse = %f "
346                                 "should not be negative",
347                                 j, mAggregatedMetrics[j].scaleInvariantAlongTrajectorySse);
348             alongTrajectoryRmseSum +=
349                     std::sqrt(mAggregatedMetrics[j].scaleInvariantAlongTrajectorySse /
350                               mAggregatedMetrics[j].scaleInvariantErrorsCount);
351 
352             LOG_ALWAYS_FATAL_IF(mAggregatedMetrics[j].scaleInvariantOffTrajectorySse < 0,
353                                 "mAggregatedMetrics[%zu].scaleInvariantOffTrajectorySse = %f "
354                                 "should not be negative",
355                                 j, mAggregatedMetrics[j].scaleInvariantOffTrajectorySse);
356             offTrajectoryRmseSum += std::sqrt(mAggregatedMetrics[j].scaleInvariantOffTrajectorySse /
357                                               mAggregatedMetrics[j].scaleInvariantErrorsCount);
358 
359             ++bucket_count;
360         }
361 
362         if (bucket_count > 0) {
363             const float averageAlongTrajectoryRmse = alongTrajectoryRmseSum / bucket_count;
364             mAtomFields.back().scaleInvariantAlongTrajectoryRmse =
365                     static_cast<int>(averageAlongTrajectoryRmse * 1000);
366 
367             const float averageOffTrajectoryRmse = offTrajectoryRmseSum / bucket_count;
368             mAtomFields.back().scaleInvariantOffTrajectoryRmse =
369                     static_cast<int>(averageOffTrajectoryRmse * 1000);
370         }
371     }
372 }
373 
reportMetrics()374 void MotionPredictorMetricsManager::reportMetrics() {
375     LOG_ALWAYS_FATAL_IF(!mReportAtomFunction);
376     // Report one atom for each prediction time bucket.
377     for (size_t i = 0; i < mAtomFields.size(); ++i) {
378         mReportAtomFunction(mAtomFields[i]);
379     }
380 }
381 
382 } // namespace android
383