|
|
/*
|
|
|
* Copyright 2023 The Android Open Source Project
|
|
|
*
|
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
* you may not use this file except in compliance with the License.
|
|
|
* You may obtain a copy of the License at
|
|
|
*
|
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
|
*
|
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
* See the License for the specific language governing permissions and
|
|
|
* limitations under the License.
|
|
|
*/
|
|
|
|
|
|
#include <input/MotionPredictor.h>
|
|
|
|
|
|
#include <cmath>
|
|
|
#include <cstddef>
|
|
|
#include <cstdint>
|
|
|
#include <numeric>
|
|
|
#include <vector>
|
|
|
|
|
|
#include <gmock/gmock.h>
|
|
|
#include <gtest/gtest.h>
|
|
|
#include <input/InputEventBuilders.h>
|
|
|
#include <utils/Timers.h> // for nsecs_t
|
|
|
|
|
|
#include "Eigen/Core"
|
|
|
#include "Eigen/Geometry"
|
|
|
|
|
|
namespace android {
|
|
|
namespace {
|
|
|
|
|
|
using ::testing::FloatNear;
|
|
|
using ::testing::Matches;
|
|
|
|
|
|
using GroundTruthPoint = MotionPredictorMetricsManager::GroundTruthPoint;
|
|
|
using PredictionPoint = MotionPredictorMetricsManager::PredictionPoint;
|
|
|
using AtomFields = MotionPredictorMetricsManager::AtomFields;
|
|
|
|
|
|
inline constexpr int NANOS_PER_MILLIS = 1'000'000;
|
|
|
|
|
|
inline constexpr nsecs_t TEST_INITIAL_TIMESTAMP = 1'000'000'000;
|
|
|
inline constexpr size_t TEST_MAX_NUM_PREDICTIONS = 5;
|
|
|
inline constexpr nsecs_t TEST_PREDICTION_INTERVAL_NANOS = 12'500'000 / 3; // 1 / (240 hz)
|
|
|
inline constexpr int NO_DATA_SENTINEL = MotionPredictorMetricsManager::NO_DATA_SENTINEL;
|
|
|
|
|
|
// Parameters:
|
|
|
// • arg: Eigen::Vector2f
|
|
|
// • target: Eigen::Vector2f
|
|
|
// • epsilon: float
|
|
|
MATCHER_P2(Vector2fNear, target, epsilon, "") {
|
|
|
return Matches(FloatNear(target[0], epsilon))(arg[0]) &&
|
|
|
Matches(FloatNear(target[1], epsilon))(arg[1]);
|
|
|
}
|
|
|
|
|
|
// Parameters:
|
|
|
// • arg: PredictionPoint
|
|
|
// • target: PredictionPoint
|
|
|
// • epsilon: float
|
|
|
MATCHER_P2(PredictionPointNear, target, epsilon, "") {
|
|
|
if (!Matches(Vector2fNear(target.position, epsilon))(arg.position)) {
|
|
|
*result_listener << "Position mismatch. Actual: (" << arg.position[0] << ", "
|
|
|
<< arg.position[1] << "), expected: (" << target.position[0] << ", "
|
|
|
<< target.position[1] << ")";
|
|
|
return false;
|
|
|
}
|
|
|
if (!Matches(FloatNear(target.pressure, epsilon))(arg.pressure)) {
|
|
|
*result_listener << "Pressure mismatch. Actual: " << arg.pressure
|
|
|
<< ", expected: " << target.pressure;
|
|
|
return false;
|
|
|
}
|
|
|
if (arg.originTimestamp != target.originTimestamp) {
|
|
|
*result_listener << "Origin timestamp mismatch. Actual: " << arg.originTimestamp
|
|
|
<< ", expected: " << target.originTimestamp;
|
|
|
return false;
|
|
|
}
|
|
|
if (arg.targetTimestamp != target.targetTimestamp) {
|
|
|
*result_listener << "Target timestamp mismatch. Actual: " << arg.targetTimestamp
|
|
|
<< ", expected: " << target.targetTimestamp;
|
|
|
return false;
|
|
|
}
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
// --- Mathematical helper functions. ---
|
|
|
|
|
|
template <typename T>
|
|
|
T average(std::vector<T> values) {
|
|
|
return std::accumulate(values.begin(), values.end(), T{}) / static_cast<T>(values.size());
|
|
|
}
|
|
|
|
|
|
template <typename T>
|
|
|
T standardDeviation(std::vector<T> values) {
|
|
|
T mean = average(values);
|
|
|
T accumulator = {};
|
|
|
for (const T value : values) {
|
|
|
accumulator += value * value - mean * mean;
|
|
|
}
|
|
|
// Take the max with 0 to avoid negative values caused by numerical instability.
|
|
|
return std::sqrt(std::max(T{}, accumulator) / static_cast<T>(values.size()));
|
|
|
}
|
|
|
|
|
|
template <typename T>
|
|
|
T rmse(std::vector<T> errors) {
|
|
|
T sse = {};
|
|
|
for (const T error : errors) {
|
|
|
sse += error * error;
|
|
|
}
|
|
|
return std::sqrt(sse / static_cast<T>(errors.size()));
|
|
|
}
|
|
|
|
|
|
TEST(MathematicalHelperFunctionTest, Average) {
|
|
|
std::vector<float> values{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
|
|
|
EXPECT_EQ(5.5f, average(values));
|
|
|
}
|
|
|
|
|
|
TEST(MathematicalHelperFunctionTest, StandardDeviation) {
|
|
|
// https://www.calculator.net/standard-deviation-calculator.html?numberinputs=10%2C+12%2C+23%2C+23%2C+16%2C+23%2C+21%2C+16
|
|
|
std::vector<float> values{10, 12, 23, 23, 16, 23, 21, 16};
|
|
|
EXPECT_FLOAT_EQ(4.8989794855664f, standardDeviation(values));
|
|
|
}
|
|
|
|
|
|
TEST(MathematicalHelperFunctionTest, Rmse) {
|
|
|
std::vector<float> errors{1, 5, 7, 7, 8, 20};
|
|
|
EXPECT_FLOAT_EQ(9.899494937f, rmse(errors));
|
|
|
}
|
|
|
|
|
|
// --- MotionEvent-related helper functions. ---
|
|
|
|
|
|
// Creates a MotionEvent corresponding to the given GroundTruthPoint.
|
|
|
MotionEvent makeMotionEvent(const GroundTruthPoint& groundTruthPoint) {
|
|
|
// Build single pointer of type STYLUS, with coordinates from groundTruthPoint.
|
|
|
PointerBuilder pointerBuilder =
|
|
|
PointerBuilder(/*id=*/0, ToolType::STYLUS)
|
|
|
.x(groundTruthPoint.position[1])
|
|
|
.y(groundTruthPoint.position[0])
|
|
|
.axis(AMOTION_EVENT_AXIS_PRESSURE, groundTruthPoint.pressure);
|
|
|
return MotionEventBuilder(/*action=*/AMOTION_EVENT_ACTION_MOVE,
|
|
|
/*source=*/AINPUT_SOURCE_CLASS_POINTER)
|
|
|
.eventTime(groundTruthPoint.timestamp)
|
|
|
.pointer(pointerBuilder)
|
|
|
.build();
|
|
|
}
|
|
|
|
|
|
// Creates a MotionEvent corresponding to the given sequence of PredictionPoints.
|
|
|
MotionEvent makeMotionEvent(const std::vector<PredictionPoint>& predictionPoints) {
|
|
|
// Build single pointer of type STYLUS, with coordinates from first prediction point.
|
|
|
PointerBuilder pointerBuilder =
|
|
|
PointerBuilder(/*id=*/0, ToolType::STYLUS)
|
|
|
.x(predictionPoints[0].position[1])
|
|
|
.y(predictionPoints[0].position[0])
|
|
|
.axis(AMOTION_EVENT_AXIS_PRESSURE, predictionPoints[0].pressure);
|
|
|
MotionEvent predictionEvent =
|
|
|
MotionEventBuilder(
|
|
|
/*action=*/AMOTION_EVENT_ACTION_MOVE, /*source=*/AINPUT_SOURCE_CLASS_POINTER)
|
|
|
.eventTime(predictionPoints[0].targetTimestamp)
|
|
|
.pointer(pointerBuilder)
|
|
|
.build();
|
|
|
for (size_t i = 1; i < predictionPoints.size(); ++i) {
|
|
|
PointerCoords coords =
|
|
|
PointerBuilder(/*id=*/0, ToolType::STYLUS)
|
|
|
.x(predictionPoints[i].position[1])
|
|
|
.y(predictionPoints[i].position[0])
|
|
|
.axis(AMOTION_EVENT_AXIS_PRESSURE, predictionPoints[i].pressure)
|
|
|
.buildCoords();
|
|
|
predictionEvent.addSample(predictionPoints[i].targetTimestamp, &coords);
|
|
|
}
|
|
|
return predictionEvent;
|
|
|
}
|
|
|
|
|
|
// Creates a MotionEvent corresponding to a stylus lift (UP) ground truth event.
|
|
|
MotionEvent makeLiftMotionEvent() {
|
|
|
return MotionEventBuilder(/*action=*/AMOTION_EVENT_ACTION_UP,
|
|
|
/*source=*/AINPUT_SOURCE_CLASS_POINTER)
|
|
|
.pointer(PointerBuilder(/*id=*/0, ToolType::STYLUS))
|
|
|
.build();
|
|
|
}
|
|
|
|
|
|
TEST(MakeMotionEventTest, MakeGroundTruthMotionEvent) {
|
|
|
const GroundTruthPoint groundTruthPoint{{.position = Eigen::Vector2f(10.0f, 20.0f),
|
|
|
.pressure = 0.6f},
|
|
|
.timestamp = TEST_INITIAL_TIMESTAMP};
|
|
|
const MotionEvent groundTruthMotionEvent = makeMotionEvent(groundTruthPoint);
|
|
|
|
|
|
ASSERT_EQ(1u, groundTruthMotionEvent.getPointerCount());
|
|
|
// Note: a MotionEvent's "history size" is one less than its number of samples.
|
|
|
ASSERT_EQ(0u, groundTruthMotionEvent.getHistorySize());
|
|
|
EXPECT_EQ(groundTruthPoint.position[0], groundTruthMotionEvent.getRawPointerCoords(0)->getY());
|
|
|
EXPECT_EQ(groundTruthPoint.position[1], groundTruthMotionEvent.getRawPointerCoords(0)->getX());
|
|
|
EXPECT_EQ(groundTruthPoint.pressure,
|
|
|
groundTruthMotionEvent.getRawPointerCoords(0)->getAxisValue(
|
|
|
AMOTION_EVENT_AXIS_PRESSURE));
|
|
|
EXPECT_EQ(AMOTION_EVENT_ACTION_MOVE, groundTruthMotionEvent.getAction());
|
|
|
}
|
|
|
|
|
|
TEST(MakeMotionEventTest, MakePredictionMotionEvent) {
|
|
|
const nsecs_t originTimestamp = TEST_INITIAL_TIMESTAMP;
|
|
|
const std::vector<PredictionPoint>
|
|
|
predictionPoints{{{.position = Eigen::Vector2f(10.0f, 20.0f), .pressure = 0.6f},
|
|
|
.originTimestamp = originTimestamp,
|
|
|
.targetTimestamp = originTimestamp + 5 * NANOS_PER_MILLIS},
|
|
|
{{.position = Eigen::Vector2f(11.0f, 22.0f), .pressure = 0.5f},
|
|
|
.originTimestamp = originTimestamp,
|
|
|
.targetTimestamp = originTimestamp + 10 * NANOS_PER_MILLIS},
|
|
|
{{.position = Eigen::Vector2f(12.0f, 24.0f), .pressure = 0.4f},
|
|
|
.originTimestamp = originTimestamp,
|
|
|
.targetTimestamp = originTimestamp + 15 * NANOS_PER_MILLIS}};
|
|
|
const MotionEvent predictionMotionEvent = makeMotionEvent(predictionPoints);
|
|
|
|
|
|
ASSERT_EQ(1u, predictionMotionEvent.getPointerCount());
|
|
|
// Note: a MotionEvent's "history size" is one less than its number of samples.
|
|
|
ASSERT_EQ(predictionPoints.size(), predictionMotionEvent.getHistorySize() + 1);
|
|
|
for (size_t i = 0; i < predictionPoints.size(); ++i) {
|
|
|
SCOPED_TRACE(testing::Message() << "i = " << i);
|
|
|
const PointerCoords coords = *predictionMotionEvent.getHistoricalRawPointerCoords(
|
|
|
/*pointerIndex=*/0, /*historicalIndex=*/i);
|
|
|
EXPECT_EQ(predictionPoints[i].position[0], coords.getY());
|
|
|
EXPECT_EQ(predictionPoints[i].position[1], coords.getX());
|
|
|
EXPECT_EQ(predictionPoints[i].pressure, coords.getAxisValue(AMOTION_EVENT_AXIS_PRESSURE));
|
|
|
// Note: originTimestamp is discarded when converting PredictionPoint to MotionEvent.
|
|
|
EXPECT_EQ(predictionPoints[i].targetTimestamp,
|
|
|
predictionMotionEvent.getHistoricalEventTime(i));
|
|
|
EXPECT_EQ(AMOTION_EVENT_ACTION_MOVE, predictionMotionEvent.getAction());
|
|
|
}
|
|
|
}
|
|
|
|
|
|
TEST(MakeMotionEventTest, MakeLiftMotionEvent) {
|
|
|
const MotionEvent liftMotionEvent = makeLiftMotionEvent();
|
|
|
ASSERT_EQ(1u, liftMotionEvent.getPointerCount());
|
|
|
// Note: a MotionEvent's "history size" is one less than its number of samples.
|
|
|
ASSERT_EQ(0u, liftMotionEvent.getHistorySize());
|
|
|
EXPECT_EQ(AMOTION_EVENT_ACTION_UP, liftMotionEvent.getAction());
|
|
|
}
|
|
|
|
|
|
// --- Ground-truth-generation helper functions. ---
|
|
|
|
|
|
std::vector<GroundTruthPoint> generateConstantGroundTruthPoints(
|
|
|
const GroundTruthPoint& groundTruthPoint, size_t numPoints) {
|
|
|
std::vector<GroundTruthPoint> groundTruthPoints;
|
|
|
nsecs_t timestamp = groundTruthPoint.timestamp;
|
|
|
for (size_t i = 0; i < numPoints; ++i) {
|
|
|
groundTruthPoints.emplace_back(groundTruthPoint);
|
|
|
groundTruthPoints.back().timestamp = timestamp;
|
|
|
timestamp += TEST_PREDICTION_INTERVAL_NANOS;
|
|
|
}
|
|
|
return groundTruthPoints;
|
|
|
}
|
|
|
|
|
|
// This function uses the coordinate system (y, x), with +y pointing downwards and +x pointing
|
|
|
// rightwards. Angles are measured counterclockwise from down (+y).
|
|
|
std::vector<GroundTruthPoint> generateCircularArcGroundTruthPoints(Eigen::Vector2f initialPosition,
|
|
|
float initialAngle,
|
|
|
float velocity,
|
|
|
float turningAngle,
|
|
|
size_t numPoints) {
|
|
|
std::vector<GroundTruthPoint> groundTruthPoints;
|
|
|
// Create first point.
|
|
|
if (numPoints > 0) {
|
|
|
groundTruthPoints.push_back({{.position = initialPosition, .pressure = 0.0f},
|
|
|
.timestamp = TEST_INITIAL_TIMESTAMP});
|
|
|
}
|
|
|
float trajectoryAngle = initialAngle; // measured counterclockwise from +y axis.
|
|
|
for (size_t i = 1; i < numPoints; ++i) {
|
|
|
const Eigen::Vector2f trajectory =
|
|
|
Eigen::Rotation2D(trajectoryAngle) * Eigen::Vector2f(1, 0);
|
|
|
groundTruthPoints.push_back(
|
|
|
{{.position = groundTruthPoints.back().position + velocity * trajectory,
|
|
|
.pressure = 0.0f},
|
|
|
.timestamp = groundTruthPoints.back().timestamp + TEST_PREDICTION_INTERVAL_NANOS});
|
|
|
trajectoryAngle += turningAngle;
|
|
|
}
|
|
|
return groundTruthPoints;
|
|
|
}
|
|
|
|
|
|
TEST(GenerateConstantGroundTruthPointsTest, BasicTest) {
|
|
|
const GroundTruthPoint groundTruthPoint{{.position = Eigen::Vector2f(10, 20), .pressure = 0.3f},
|
|
|
.timestamp = TEST_INITIAL_TIMESTAMP};
|
|
|
const std::vector<GroundTruthPoint> groundTruthPoints =
|
|
|
generateConstantGroundTruthPoints(groundTruthPoint, /*numPoints=*/3);
|
|
|
|
|
|
ASSERT_EQ(3u, groundTruthPoints.size());
|
|
|
// First point.
|
|
|
EXPECT_EQ(groundTruthPoints[0].position, groundTruthPoint.position);
|
|
|
EXPECT_EQ(groundTruthPoints[0].pressure, groundTruthPoint.pressure);
|
|
|
EXPECT_EQ(groundTruthPoints[0].timestamp, groundTruthPoint.timestamp);
|
|
|
// Second point.
|
|
|
EXPECT_EQ(groundTruthPoints[1].position, groundTruthPoint.position);
|
|
|
EXPECT_EQ(groundTruthPoints[1].pressure, groundTruthPoint.pressure);
|
|
|
EXPECT_GT(groundTruthPoints[1].timestamp, groundTruthPoints[0].timestamp);
|
|
|
// Third point.
|
|
|
EXPECT_EQ(groundTruthPoints[2].position, groundTruthPoint.position);
|
|
|
EXPECT_EQ(groundTruthPoints[2].pressure, groundTruthPoint.pressure);
|
|
|
EXPECT_GT(groundTruthPoints[2].timestamp, groundTruthPoints[1].timestamp);
|
|
|
}
|
|
|
|
|
|
TEST(GenerateCircularArcGroundTruthTest, StraightLineUpwards) {
|
|
|
const std::vector<GroundTruthPoint> groundTruthPoints = generateCircularArcGroundTruthPoints(
|
|
|
/*initialPosition=*/Eigen::Vector2f(0, 0),
|
|
|
/*initialAngle=*/M_PI,
|
|
|
/*velocity=*/1.0f,
|
|
|
/*turningAngle=*/0.0f,
|
|
|
/*numPoints=*/3);
|
|
|
|
|
|
ASSERT_EQ(3u, groundTruthPoints.size());
|
|
|
EXPECT_THAT(groundTruthPoints[0].position, Vector2fNear(Eigen::Vector2f(0, 0), 1e-6));
|
|
|
EXPECT_THAT(groundTruthPoints[1].position, Vector2fNear(Eigen::Vector2f(-1, 0), 1e-6));
|
|
|
EXPECT_THAT(groundTruthPoints[2].position, Vector2fNear(Eigen::Vector2f(-2, 0), 1e-6));
|
|
|
// Check that timestamps are increasing between consecutive ground truth points.
|
|
|
EXPECT_GT(groundTruthPoints[1].timestamp, groundTruthPoints[0].timestamp);
|
|
|
EXPECT_GT(groundTruthPoints[2].timestamp, groundTruthPoints[1].timestamp);
|
|
|
}
|
|
|
|
|
|
TEST(GenerateCircularArcGroundTruthTest, CounterclockwiseSquare) {
|
|
|
// Generate points in a counterclockwise unit square starting pointing right.
|
|
|
const std::vector<GroundTruthPoint> groundTruthPoints = generateCircularArcGroundTruthPoints(
|
|
|
/*initialPosition=*/Eigen::Vector2f(10, 100),
|
|
|
/*initialAngle=*/M_PI_2,
|
|
|
/*velocity=*/1.0f,
|
|
|
/*turningAngle=*/M_PI_2,
|
|
|
/*numPoints=*/5);
|
|
|
|
|
|
ASSERT_EQ(5u, groundTruthPoints.size());
|
|
|
EXPECT_THAT(groundTruthPoints[0].position, Vector2fNear(Eigen::Vector2f(10, 100), 1e-6));
|
|
|
EXPECT_THAT(groundTruthPoints[1].position, Vector2fNear(Eigen::Vector2f(10, 101), 1e-6));
|
|
|
EXPECT_THAT(groundTruthPoints[2].position, Vector2fNear(Eigen::Vector2f(9, 101), 1e-6));
|
|
|
EXPECT_THAT(groundTruthPoints[3].position, Vector2fNear(Eigen::Vector2f(9, 100), 1e-6));
|
|
|
EXPECT_THAT(groundTruthPoints[4].position, Vector2fNear(Eigen::Vector2f(10, 100), 1e-6));
|
|
|
}
|
|
|
|
|
|
// --- Prediction-generation helper functions. ---
|
|
|
|
|
|
// Creates a sequence of predictions with values equal to those of the given GroundTruthPoint.
|
|
|
std::vector<PredictionPoint> generateConstantPredictions(const GroundTruthPoint& groundTruthPoint) {
|
|
|
std::vector<PredictionPoint> predictions;
|
|
|
nsecs_t predictionTimestamp = groundTruthPoint.timestamp + TEST_PREDICTION_INTERVAL_NANOS;
|
|
|
for (size_t j = 0; j < TEST_MAX_NUM_PREDICTIONS; ++j) {
|
|
|
predictions.push_back(PredictionPoint{{.position = groundTruthPoint.position,
|
|
|
.pressure = groundTruthPoint.pressure},
|
|
|
.originTimestamp = groundTruthPoint.timestamp,
|
|
|
.targetTimestamp = predictionTimestamp});
|
|
|
predictionTimestamp += TEST_PREDICTION_INTERVAL_NANOS;
|
|
|
}
|
|
|
return predictions;
|
|
|
}
|
|
|
|
|
|
// Generates TEST_MAX_NUM_PREDICTIONS predictions from the given most recent two ground truth points
|
|
|
// by linear extrapolation of position and pressure. The interval between consecutive predictions'
|
|
|
// timestamps is TEST_PREDICTION_INTERVAL_NANOS.
|
|
|
std::vector<PredictionPoint> generatePredictionsByLinearExtrapolation(
|
|
|
const GroundTruthPoint& firstGroundTruth, const GroundTruthPoint& secondGroundTruth) {
|
|
|
// Precompute deltas.
|
|
|
const Eigen::Vector2f trajectory = secondGroundTruth.position - firstGroundTruth.position;
|
|
|
const float deltaPressure = secondGroundTruth.pressure - firstGroundTruth.pressure;
|
|
|
// Compute predictions.
|
|
|
std::vector<PredictionPoint> predictions;
|
|
|
Eigen::Vector2f predictionPosition = secondGroundTruth.position;
|
|
|
float predictionPressure = secondGroundTruth.pressure;
|
|
|
nsecs_t predictionTargetTimestamp = secondGroundTruth.timestamp;
|
|
|
for (size_t i = 0; i < TEST_MAX_NUM_PREDICTIONS; ++i) {
|
|
|
predictionPosition += trajectory;
|
|
|
predictionPressure += deltaPressure;
|
|
|
predictionTargetTimestamp += TEST_PREDICTION_INTERVAL_NANOS;
|
|
|
predictions.push_back(
|
|
|
PredictionPoint{{.position = predictionPosition, .pressure = predictionPressure},
|
|
|
.originTimestamp = secondGroundTruth.timestamp,
|
|
|
.targetTimestamp = predictionTargetTimestamp});
|
|
|
}
|
|
|
return predictions;
|
|
|
}
|
|
|
|
|
|
TEST(GeneratePredictionsTest, GenerateConstantPredictions) {
|
|
|
const GroundTruthPoint groundTruthPoint{{.position = Eigen::Vector2f(10, 20), .pressure = 0.3f},
|
|
|
.timestamp = TEST_INITIAL_TIMESTAMP};
|
|
|
const std::vector<PredictionPoint> predictionPoints =
|
|
|
generateConstantPredictions(groundTruthPoint);
|
|
|
|
|
|
ASSERT_EQ(TEST_MAX_NUM_PREDICTIONS, predictionPoints.size());
|
|
|
for (size_t i = 0; i < predictionPoints.size(); ++i) {
|
|
|
SCOPED_TRACE(testing::Message() << "i = " << i);
|
|
|
EXPECT_THAT(predictionPoints[i].position, Vector2fNear(groundTruthPoint.position, 1e-6));
|
|
|
EXPECT_THAT(predictionPoints[i].pressure, FloatNear(groundTruthPoint.pressure, 1e-6));
|
|
|
EXPECT_EQ(predictionPoints[i].originTimestamp, groundTruthPoint.timestamp);
|
|
|
EXPECT_EQ(predictionPoints[i].targetTimestamp,
|
|
|
groundTruthPoint.timestamp +
|
|
|
static_cast<nsecs_t>(i + 1) * TEST_PREDICTION_INTERVAL_NANOS);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
TEST(GeneratePredictionsTest, LinearExtrapolationFromTwoPoints) {
|
|
|
const nsecs_t initialTimestamp = TEST_INITIAL_TIMESTAMP;
|
|
|
const std::vector<PredictionPoint> predictionPoints = generatePredictionsByLinearExtrapolation(
|
|
|
GroundTruthPoint{{.position = Eigen::Vector2f(100, 200), .pressure = 0.9f},
|
|
|
.timestamp = initialTimestamp},
|
|
|
GroundTruthPoint{{.position = Eigen::Vector2f(105, 190), .pressure = 0.8f},
|
|
|
.timestamp = initialTimestamp + TEST_PREDICTION_INTERVAL_NANOS});
|
|
|
|
|
|
ASSERT_EQ(TEST_MAX_NUM_PREDICTIONS, predictionPoints.size());
|
|
|
const nsecs_t originTimestamp = initialTimestamp + TEST_PREDICTION_INTERVAL_NANOS;
|
|
|
EXPECT_THAT(predictionPoints[0],
|
|
|
PredictionPointNear(PredictionPoint{{.position = Eigen::Vector2f(110, 180),
|
|
|
.pressure = 0.7f},
|
|
|
.originTimestamp = originTimestamp,
|
|
|
.targetTimestamp = originTimestamp +
|
|
|
TEST_PREDICTION_INTERVAL_NANOS},
|
|
|
0.001));
|
|
|
EXPECT_THAT(predictionPoints[1],
|
|
|
PredictionPointNear(PredictionPoint{{.position = Eigen::Vector2f(115, 170),
|
|
|
.pressure = 0.6f},
|
|
|
.originTimestamp = originTimestamp,
|
|
|
.targetTimestamp = originTimestamp +
|
|
|
2 * TEST_PREDICTION_INTERVAL_NANOS},
|
|
|
0.001));
|
|
|
EXPECT_THAT(predictionPoints[2],
|
|
|
PredictionPointNear(PredictionPoint{{.position = Eigen::Vector2f(120, 160),
|
|
|
.pressure = 0.5f},
|
|
|
.originTimestamp = originTimestamp,
|
|
|
.targetTimestamp = originTimestamp +
|
|
|
3 * TEST_PREDICTION_INTERVAL_NANOS},
|
|
|
0.001));
|
|
|
EXPECT_THAT(predictionPoints[3],
|
|
|
PredictionPointNear(PredictionPoint{{.position = Eigen::Vector2f(125, 150),
|
|
|
.pressure = 0.4f},
|
|
|
.originTimestamp = originTimestamp,
|
|
|
.targetTimestamp = originTimestamp +
|
|
|
4 * TEST_PREDICTION_INTERVAL_NANOS},
|
|
|
0.001));
|
|
|
EXPECT_THAT(predictionPoints[4],
|
|
|
PredictionPointNear(PredictionPoint{{.position = Eigen::Vector2f(130, 140),
|
|
|
.pressure = 0.3f},
|
|
|
.originTimestamp = originTimestamp,
|
|
|
.targetTimestamp = originTimestamp +
|
|
|
5 * TEST_PREDICTION_INTERVAL_NANOS},
|
|
|
0.001));
|
|
|
}
|
|
|
|
|
|
// Generates predictions by linear extrapolation for each consecutive pair of ground truth points
|
|
|
// (see the comment for the above function for further explanation). Returns a vector of vectors of
|
|
|
// prediction points, where the first index is the source ground truth index, and the second is the
|
|
|
// prediction target index.
|
|
|
//
|
|
|
// The returned vector has size equal to the input vector, and the first element of the returned
|
|
|
// vector is always empty.
|
|
|
std::vector<std::vector<PredictionPoint>> generateAllPredictionsByLinearExtrapolation(
|
|
|
const std::vector<GroundTruthPoint>& groundTruthPoints) {
|
|
|
std::vector<std::vector<PredictionPoint>> allPredictions;
|
|
|
allPredictions.emplace_back();
|
|
|
for (size_t i = 1; i < groundTruthPoints.size(); ++i) {
|
|
|
allPredictions.push_back(generatePredictionsByLinearExtrapolation(groundTruthPoints[i - 1],
|
|
|
groundTruthPoints[i]));
|
|
|
}
|
|
|
return allPredictions;
|
|
|
}
|
|
|
|
|
|
TEST(GeneratePredictionsTest, GenerateAllPredictions) {
|
|
|
const nsecs_t initialTimestamp = TEST_INITIAL_TIMESTAMP;
|
|
|
std::vector<GroundTruthPoint>
|
|
|
groundTruthPoints{GroundTruthPoint{{.position = Eigen::Vector2f(0, 0),
|
|
|
.pressure = 0.5f},
|
|
|
.timestamp = initialTimestamp},
|
|
|
GroundTruthPoint{{.position = Eigen::Vector2f(1, -1),
|
|
|
.pressure = 0.51f},
|
|
|
.timestamp = initialTimestamp +
|
|
|
2 * TEST_PREDICTION_INTERVAL_NANOS},
|
|
|
GroundTruthPoint{{.position = Eigen::Vector2f(2, -2),
|
|
|
.pressure = 0.52f},
|
|
|
.timestamp = initialTimestamp +
|
|
|
3 * TEST_PREDICTION_INTERVAL_NANOS}};
|
|
|
|
|
|
const std::vector<std::vector<PredictionPoint>> allPredictions =
|
|
|
generateAllPredictionsByLinearExtrapolation(groundTruthPoints);
|
|
|
|
|
|
// Check format of allPredictions data.
|
|
|
ASSERT_EQ(groundTruthPoints.size(), allPredictions.size());
|
|
|
EXPECT_TRUE(allPredictions[0].empty());
|
|
|
EXPECT_EQ(TEST_MAX_NUM_PREDICTIONS, allPredictions[1].size());
|
|
|
EXPECT_EQ(TEST_MAX_NUM_PREDICTIONS, allPredictions[2].size());
|
|
|
|
|
|
// Check positions of predictions generated from first pair of ground truth points.
|
|
|
EXPECT_THAT(allPredictions[1][0].position, Vector2fNear(Eigen::Vector2f(2, -2), 1e-9));
|
|
|
EXPECT_THAT(allPredictions[1][1].position, Vector2fNear(Eigen::Vector2f(3, -3), 1e-9));
|
|
|
EXPECT_THAT(allPredictions[1][2].position, Vector2fNear(Eigen::Vector2f(4, -4), 1e-9));
|
|
|
EXPECT_THAT(allPredictions[1][3].position, Vector2fNear(Eigen::Vector2f(5, -5), 1e-9));
|
|
|
EXPECT_THAT(allPredictions[1][4].position, Vector2fNear(Eigen::Vector2f(6, -6), 1e-9));
|
|
|
|
|
|
// Check pressures of predictions generated from first pair of ground truth points.
|
|
|
EXPECT_FLOAT_EQ(0.52f, allPredictions[1][0].pressure);
|
|
|
EXPECT_FLOAT_EQ(0.53f, allPredictions[1][1].pressure);
|
|
|
EXPECT_FLOAT_EQ(0.54f, allPredictions[1][2].pressure);
|
|
|
EXPECT_FLOAT_EQ(0.55f, allPredictions[1][3].pressure);
|
|
|
EXPECT_FLOAT_EQ(0.56f, allPredictions[1][4].pressure);
|
|
|
}
|
|
|
|
|
|
// --- Prediction error helper functions. ---
|
|
|
|
|
|
struct GeneralPositionErrors {
|
|
|
float alongTrajectoryErrorMean;
|
|
|
float alongTrajectoryErrorStd;
|
|
|
float offTrajectoryRmse;
|
|
|
};
|
|
|
|
|
|
// Inputs:
|
|
|
// • Vector of ground truth points
|
|
|
// • Vector of vectors of prediction points, where the first index is the source ground truth
|
|
|
// index, and the second is the prediction target index.
|
|
|
//
|
|
|
// Returns a vector of GeneralPositionErrors, indexed by prediction time delta bucket.
|
|
|
std::vector<GeneralPositionErrors> computeGeneralPositionErrors(
|
|
|
const std::vector<GroundTruthPoint>& groundTruthPoints,
|
|
|
const std::vector<std::vector<PredictionPoint>>& predictionPoints) {
|
|
|
// Aggregate errors by time bucket (prediction target index).
|
|
|
std::vector<GeneralPositionErrors> generalPostitionErrors;
|
|
|
for (size_t predictionTargetIndex = 0; predictionTargetIndex < TEST_MAX_NUM_PREDICTIONS;
|
|
|
++predictionTargetIndex) {
|
|
|
std::vector<float> alongTrajectoryErrors;
|
|
|
std::vector<float> alongTrajectorySquaredErrors;
|
|
|
std::vector<float> offTrajectoryErrors;
|
|
|
for (size_t sourceGroundTruthIndex = 1; sourceGroundTruthIndex < groundTruthPoints.size();
|
|
|
++sourceGroundTruthIndex) {
|
|
|
const size_t targetGroundTruthIndex =
|
|
|
sourceGroundTruthIndex + predictionTargetIndex + 1;
|
|
|
// Only include errors for points with a ground truth value.
|
|
|
if (targetGroundTruthIndex < groundTruthPoints.size()) {
|
|
|
const Eigen::Vector2f trajectory =
|
|
|
(groundTruthPoints[targetGroundTruthIndex].position -
|
|
|
groundTruthPoints[targetGroundTruthIndex - 1].position)
|
|
|
.normalized();
|
|
|
const Eigen::Vector2f orthogonalTrajectory =
|
|
|
Eigen::Rotation2Df(M_PI_2) * trajectory;
|
|
|
const Eigen::Vector2f positionError =
|
|
|
predictionPoints[sourceGroundTruthIndex][predictionTargetIndex].position -
|
|
|
groundTruthPoints[targetGroundTruthIndex].position;
|
|
|
alongTrajectoryErrors.push_back(positionError.dot(trajectory));
|
|
|
alongTrajectorySquaredErrors.push_back(alongTrajectoryErrors.back() *
|
|
|
alongTrajectoryErrors.back());
|
|
|
offTrajectoryErrors.push_back(positionError.dot(orthogonalTrajectory));
|
|
|
}
|
|
|
}
|
|
|
generalPostitionErrors.push_back(
|
|
|
{.alongTrajectoryErrorMean = average(alongTrajectoryErrors),
|
|
|
.alongTrajectoryErrorStd = standardDeviation(alongTrajectoryErrors),
|
|
|
.offTrajectoryRmse = rmse(offTrajectoryErrors)});
|
|
|
}
|
|
|
return generalPostitionErrors;
|
|
|
}
|
|
|
|
|
|
// Inputs:
|
|
|
// • Vector of ground truth points
|
|
|
// • Vector of vectors of prediction points, where the first index is the source ground truth
|
|
|
// index, and the second is the prediction target index.
|
|
|
//
|
|
|
// Returns a vector of pressure RMSEs, indexed by prediction time delta bucket.
|
|
|
std::vector<float> computePressureRmses(
|
|
|
const std::vector<GroundTruthPoint>& groundTruthPoints,
|
|
|
const std::vector<std::vector<PredictionPoint>>& predictionPoints) {
|
|
|
// Aggregate errors by time bucket (prediction target index).
|
|
|
std::vector<float> pressureRmses;
|
|
|
for (size_t predictionTargetIndex = 0; predictionTargetIndex < TEST_MAX_NUM_PREDICTIONS;
|
|
|
++predictionTargetIndex) {
|
|
|
std::vector<float> pressureErrors;
|
|
|
for (size_t sourceGroundTruthIndex = 1; sourceGroundTruthIndex < groundTruthPoints.size();
|
|
|
++sourceGroundTruthIndex) {
|
|
|
const size_t targetGroundTruthIndex =
|
|
|
sourceGroundTruthIndex + predictionTargetIndex + 1;
|
|
|
// Only include errors for points with a ground truth value.
|
|
|
if (targetGroundTruthIndex < groundTruthPoints.size()) {
|
|
|
pressureErrors.push_back(
|
|
|
predictionPoints[sourceGroundTruthIndex][predictionTargetIndex].pressure -
|
|
|
groundTruthPoints[targetGroundTruthIndex].pressure);
|
|
|
}
|
|
|
}
|
|
|
pressureRmses.push_back(rmse(pressureErrors));
|
|
|
}
|
|
|
return pressureRmses;
|
|
|
}
|
|
|
|
|
|
TEST(ErrorComputationHelperTest, ComputeGeneralPositionErrorsSimpleTest) {
|
|
|
std::vector<GroundTruthPoint> groundTruthPoints =
|
|
|
generateConstantGroundTruthPoints(GroundTruthPoint{{.position = Eigen::Vector2f(0, 0),
|
|
|
.pressure = 0.0f},
|
|
|
.timestamp = TEST_INITIAL_TIMESTAMP},
|
|
|
/*numPoints=*/TEST_MAX_NUM_PREDICTIONS + 2);
|
|
|
groundTruthPoints[3].position = Eigen::Vector2f(1, 0);
|
|
|
groundTruthPoints[4].position = Eigen::Vector2f(1, 1);
|
|
|
groundTruthPoints[5].position = Eigen::Vector2f(1, 3);
|
|
|
groundTruthPoints[6].position = Eigen::Vector2f(2, 3);
|
|
|
|
|
|
std::vector<std::vector<PredictionPoint>> predictionPoints =
|
|
|
generateAllPredictionsByLinearExtrapolation(groundTruthPoints);
|
|
|
|
|
|
// The generated predictions look like:
|
|
|
//
|
|
|
// | Source | Target Ground Truth Index |
|
|
|
// | Index | 2 | 3 | 4 | 5 | 6 |
|
|
|
// |------------|--------|--------|--------|--------|--------|
|
|
|
// | 1 | (0, 0) | (0, 0) | (0, 0) | (0, 0) | (0, 0) |
|
|
|
// | 2 | | (0, 0) | (0, 0) | (0, 0) | (0, 0) |
|
|
|
// | 3 | | | (2, 0) | (3, 0) | (4, 0) |
|
|
|
// | 4 | | | | (1, 2) | (1, 3) |
|
|
|
// | 5 | | | | | (1, 5) |
|
|
|
// |---------------------------------------------------------|
|
|
|
// | Actual Ground Truth Values |
|
|
|
// | Position | (0, 0) | (1, 0) | (1, 1) | (1, 3) | (2, 3) |
|
|
|
// | Previous | (0, 0) | (0, 0) | (1, 0) | (1, 1) | (1, 3) |
|
|
|
//
|
|
|
// Note: this table organizes prediction targets by target ground truth index. Metrics are
|
|
|
// aggregated across points with the same prediction time bucket index, which is different.
|
|
|
// Each down-right diagonal from this table gives us points from a unique time bucket.
|
|
|
|
|
|
// Initialize expected prediction errors from the table above. The first time bucket corresponds
|
|
|
// to the long diagonal of the table, and subsequent time buckets step up-right from there.
|
|
|
const std::vector<std::vector<float>> expectedAlongTrajectoryErrors{{0, -1, -1, -1, -1},
|
|
|
{-1, -1, -3, -1},
|
|
|
{-1, -3, 2},
|
|
|
{-3, -2},
|
|
|
{-2}};
|
|
|
const std::vector<std::vector<float>> expectedOffTrajectoryErrors{{0, 0, 1, 0, 2},
|
|
|
{0, 1, 2, 0},
|
|
|
{1, 1, 3},
|
|
|
{1, 3},
|
|
|
{3}};
|
|
|
|
|
|
std::vector<GeneralPositionErrors> generalPositionErrors =
|
|
|
computeGeneralPositionErrors(groundTruthPoints, predictionPoints);
|
|
|
|
|
|
ASSERT_EQ(TEST_MAX_NUM_PREDICTIONS, generalPositionErrors.size());
|
|
|
for (size_t i = 0; i < generalPositionErrors.size(); ++i) {
|
|
|
SCOPED_TRACE(testing::Message() << "i = " << i);
|
|
|
EXPECT_FLOAT_EQ(average(expectedAlongTrajectoryErrors[i]),
|
|
|
generalPositionErrors[i].alongTrajectoryErrorMean);
|
|
|
EXPECT_FLOAT_EQ(standardDeviation(expectedAlongTrajectoryErrors[i]),
|
|
|
generalPositionErrors[i].alongTrajectoryErrorStd);
|
|
|
EXPECT_FLOAT_EQ(rmse(expectedOffTrajectoryErrors[i]),
|
|
|
generalPositionErrors[i].offTrajectoryRmse);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
TEST(ErrorComputationHelperTest, ComputePressureRmsesSimpleTest) {
|
|
|
// Generate ground truth points with pressures {0.0, 0.0, 0.0, 0.0, 0.5, 0.5, 0.5}.
|
|
|
// (We need TEST_MAX_NUM_PREDICTIONS + 2 to test all prediction time buckets.)
|
|
|
std::vector<GroundTruthPoint> groundTruthPoints =
|
|
|
generateConstantGroundTruthPoints(GroundTruthPoint{{.position = Eigen::Vector2f(0, 0),
|
|
|
.pressure = 0.0f},
|
|
|
.timestamp = TEST_INITIAL_TIMESTAMP},
|
|
|
/*numPoints=*/TEST_MAX_NUM_PREDICTIONS + 2);
|
|
|
for (size_t i = 4; i < groundTruthPoints.size(); ++i) {
|
|
|
groundTruthPoints[i].pressure = 0.5f;
|
|
|
}
|
|
|
|
|
|
std::vector<std::vector<PredictionPoint>> predictionPoints =
|
|
|
generateAllPredictionsByLinearExtrapolation(groundTruthPoints);
|
|
|
|
|
|
std::vector<float> pressureRmses = computePressureRmses(groundTruthPoints, predictionPoints);
|
|
|
|
|
|
ASSERT_EQ(TEST_MAX_NUM_PREDICTIONS, pressureRmses.size());
|
|
|
EXPECT_FLOAT_EQ(rmse(std::vector<float>{0.0f, 0.0f, -0.5f, 0.5f, 0.0f}), pressureRmses[0]);
|
|
|
EXPECT_FLOAT_EQ(rmse(std::vector<float>{0.0f, -0.5f, -0.5f, 1.0f}), pressureRmses[1]);
|
|
|
EXPECT_FLOAT_EQ(rmse(std::vector<float>{-0.5f, -0.5f, -0.5f}), pressureRmses[2]);
|
|
|
EXPECT_FLOAT_EQ(rmse(std::vector<float>{-0.5f, -0.5f}), pressureRmses[3]);
|
|
|
EXPECT_FLOAT_EQ(rmse(std::vector<float>{-0.5f}), pressureRmses[4]);
|
|
|
}
|
|
|
|
|
|
// --- MotionPredictorMetricsManager tests. ---
|
|
|
|
|
|
// Helper function that instantiates a MetricsManager with the given mock logged AtomFields. Takes
|
|
|
// vectors of ground truth and prediction points of the same length, and passes these points to the
|
|
|
// MetricsManager. The format of these vectors is expected to be:
|
|
|
// • groundTruthPoints: chronologically-ordered ground truth points, with at least 2 elements.
|
|
|
// • predictionPoints: the first index points to a vector of predictions corresponding to the
|
|
|
// source ground truth point with the same index.
|
|
|
// - The first element should be empty, because there are not expected to be predictions until
|
|
|
// we have received 2 ground truth points.
|
|
|
// - The last element may be empty, because there will be no future ground truth points to
|
|
|
// associate with those predictions (if not empty, it will be ignored).
|
|
|
// - To test all prediction buckets, there should be at least TEST_MAX_NUM_PREDICTIONS non-empty
|
|
|
// prediction sets (that is, excluding the first and last). Thus, groundTruthPoints and
|
|
|
// predictionPoints should have size at least TEST_MAX_NUM_PREDICTIONS + 2.
|
|
|
//
|
|
|
// The passed-in outAtomFields will contain the logged AtomFields when the function returns.
|
|
|
//
|
|
|
// This function returns void so that it can use test assertions.
|
|
|
void runMetricsManager(const std::vector<GroundTruthPoint>& groundTruthPoints,
|
|
|
const std::vector<std::vector<PredictionPoint>>& predictionPoints,
|
|
|
std::vector<AtomFields>& outAtomFields) {
|
|
|
MotionPredictorMetricsManager metricsManager(TEST_PREDICTION_INTERVAL_NANOS,
|
|
|
TEST_MAX_NUM_PREDICTIONS);
|
|
|
metricsManager.setMockLoggedAtomFields(&outAtomFields);
|
|
|
|
|
|
// Validate structure of groundTruthPoints and predictionPoints.
|
|
|
ASSERT_EQ(predictionPoints.size(), groundTruthPoints.size());
|
|
|
ASSERT_GE(groundTruthPoints.size(), 2u);
|
|
|
ASSERT_EQ(predictionPoints[0].size(), 0u);
|
|
|
for (size_t i = 1; i + 1 < predictionPoints.size(); ++i) {
|
|
|
SCOPED_TRACE(testing::Message() << "i = " << i);
|
|
|
ASSERT_EQ(predictionPoints[i].size(), TEST_MAX_NUM_PREDICTIONS);
|
|
|
}
|
|
|
|
|
|
// Pass ground truth points and predictions (for all except first and last ground truth).
|
|
|
for (size_t i = 0; i < groundTruthPoints.size(); ++i) {
|
|
|
metricsManager.onRecord(makeMotionEvent(groundTruthPoints[i]));
|
|
|
if ((i > 0) && (i + 1 < predictionPoints.size())) {
|
|
|
metricsManager.onPredict(makeMotionEvent(predictionPoints[i]));
|
|
|
}
|
|
|
}
|
|
|
// Send a stroke-end event to trigger the logging call.
|
|
|
metricsManager.onRecord(makeLiftMotionEvent());
|
|
|
}
|
|
|
|
|
|
// Vacuous test:
|
|
|
// • Input: no prediction data.
|
|
|
// • Expectation: no metrics should be logged.
|
|
|
TEST(MotionPredictorMetricsManagerTest, NoPredictions) {
|
|
|
std::vector<AtomFields> mockLoggedAtomFields;
|
|
|
MotionPredictorMetricsManager metricsManager(TEST_PREDICTION_INTERVAL_NANOS,
|
|
|
TEST_MAX_NUM_PREDICTIONS);
|
|
|
metricsManager.setMockLoggedAtomFields(&mockLoggedAtomFields);
|
|
|
|
|
|
metricsManager.onRecord(makeMotionEvent(
|
|
|
GroundTruthPoint{{.position = Eigen::Vector2f(0, 0), .pressure = 0}, .timestamp = 0}));
|
|
|
metricsManager.onRecord(makeLiftMotionEvent());
|
|
|
|
|
|
// Check that mockLoggedAtomFields is still empty (as it was initialized empty), ensuring that
|
|
|
// no metrics were logged.
|
|
|
EXPECT_EQ(0u, mockLoggedAtomFields.size());
|
|
|
}
|
|
|
|
|
|
// Perfect predictions test:
|
|
|
// • Input: constant input events, perfect predictions matching the input events.
|
|
|
// • Expectation: all error metrics should be zero, or NO_DATA_SENTINEL for "unreported" metrics.
|
|
|
// (For example, scale-invariant errors are only reported for the final time bucket.)
|
|
|
TEST(MotionPredictorMetricsManagerTest, ConstantGroundTruthPerfectPredictions) {
|
|
|
GroundTruthPoint groundTruthPoint{{.position = Eigen::Vector2f(10.0f, 20.0f), .pressure = 0.6f},
|
|
|
.timestamp = TEST_INITIAL_TIMESTAMP};
|
|
|
|
|
|
// Generate ground truth and prediction points as described by the runMetricsManager comment.
|
|
|
std::vector<GroundTruthPoint> groundTruthPoints;
|
|
|
std::vector<std::vector<PredictionPoint>> predictionPoints;
|
|
|
for (size_t i = 0; i < TEST_MAX_NUM_PREDICTIONS + 2; ++i) {
|
|
|
groundTruthPoints.push_back(groundTruthPoint);
|
|
|
predictionPoints.push_back(i > 0 ? generateConstantPredictions(groundTruthPoint)
|
|
|
: std::vector<PredictionPoint>{});
|
|
|
groundTruthPoint.timestamp += TEST_PREDICTION_INTERVAL_NANOS;
|
|
|
}
|
|
|
|
|
|
std::vector<AtomFields> atomFields;
|
|
|
runMetricsManager(groundTruthPoints, predictionPoints, atomFields);
|
|
|
|
|
|
ASSERT_EQ(TEST_MAX_NUM_PREDICTIONS, atomFields.size());
|
|
|
// Check that errors are all zero, or NO_DATA_SENTINEL for unreported metrics.
|
|
|
for (size_t i = 0; i < atomFields.size(); ++i) {
|
|
|
SCOPED_TRACE(testing::Message() << "i = " << i);
|
|
|
const AtomFields& atom = atomFields[i];
|
|
|
const nsecs_t deltaTimeBucketNanos = TEST_PREDICTION_INTERVAL_NANOS * (i + 1);
|
|
|
EXPECT_EQ(deltaTimeBucketNanos / NANOS_PER_MILLIS, atom.deltaTimeBucketMilliseconds);
|
|
|
// General errors: reported for every time bucket.
|
|
|
EXPECT_EQ(0, atom.alongTrajectoryErrorMeanMillipixels);
|
|
|
EXPECT_EQ(0, atom.alongTrajectoryErrorStdMillipixels);
|
|
|
EXPECT_EQ(0, atom.offTrajectoryRmseMillipixels);
|
|
|
EXPECT_EQ(0, atom.pressureRmseMilliunits);
|
|
|
// High-velocity errors: reported only for the last two time buckets.
|
|
|
// However, this data has zero velocity, so these metrics should all be NO_DATA_SENTINEL.
|
|
|
EXPECT_EQ(NO_DATA_SENTINEL, atom.highVelocityAlongTrajectoryRmse);
|
|
|
EXPECT_EQ(NO_DATA_SENTINEL, atom.highVelocityOffTrajectoryRmse);
|
|
|
// Scale-invariant errors: reported only for the last time bucket.
|
|
|
if (i + 1 == atomFields.size()) {
|
|
|
EXPECT_EQ(0, atom.scaleInvariantAlongTrajectoryRmse);
|
|
|
EXPECT_EQ(0, atom.scaleInvariantOffTrajectoryRmse);
|
|
|
} else {
|
|
|
EXPECT_EQ(NO_DATA_SENTINEL, atom.scaleInvariantAlongTrajectoryRmse);
|
|
|
EXPECT_EQ(NO_DATA_SENTINEL, atom.scaleInvariantOffTrajectoryRmse);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
TEST(MotionPredictorMetricsManagerTest, QuadraticPressureLinearPredictions) {
|
|
|
// Generate ground truth points.
|
|
|
//
|
|
|
// Ground truth pressures are a quadratically increasing function from some initial value.
|
|
|
const float initialPressure = 0.5f;
|
|
|
const float quadraticCoefficient = 0.01f;
|
|
|
std::vector<GroundTruthPoint> groundTruthPoints;
|
|
|
nsecs_t timestamp = TEST_INITIAL_TIMESTAMP;
|
|
|
// As described in the runMetricsManager comment, we should have TEST_MAX_NUM_PREDICTIONS + 2
|
|
|
// ground truth points.
|
|
|
for (size_t i = 0; i < TEST_MAX_NUM_PREDICTIONS + 2; ++i) {
|
|
|
const float pressure = initialPressure + quadraticCoefficient * static_cast<float>(i * i);
|
|
|
groundTruthPoints.push_back(
|
|
|
GroundTruthPoint{{.position = Eigen::Vector2f(0, 0), .pressure = pressure},
|
|
|
.timestamp = timestamp});
|
|
|
timestamp += TEST_PREDICTION_INTERVAL_NANOS;
|
|
|
}
|
|
|
|
|
|
// Note: the first index is the source ground truth index, and the second is the prediction
|
|
|
// target index.
|
|
|
std::vector<std::vector<PredictionPoint>> predictionPoints =
|
|
|
generateAllPredictionsByLinearExtrapolation(groundTruthPoints);
|
|
|
|
|
|
const std::vector<float> pressureErrors =
|
|
|
computePressureRmses(groundTruthPoints, predictionPoints);
|
|
|
|
|
|
// Run test.
|
|
|
std::vector<AtomFields> atomFields;
|
|
|
runMetricsManager(groundTruthPoints, predictionPoints, atomFields);
|
|
|
|
|
|
// Check logged metrics match expectations.
|
|
|
ASSERT_EQ(TEST_MAX_NUM_PREDICTIONS, atomFields.size());
|
|
|
for (size_t i = 0; i < atomFields.size(); ++i) {
|
|
|
SCOPED_TRACE(testing::Message() << "i = " << i);
|
|
|
const AtomFields& atom = atomFields[i];
|
|
|
// Check time bucket delta matches expectation based on index and prediction interval.
|
|
|
const nsecs_t deltaTimeBucketNanos = TEST_PREDICTION_INTERVAL_NANOS * (i + 1);
|
|
|
EXPECT_EQ(deltaTimeBucketNanos / NANOS_PER_MILLIS, atom.deltaTimeBucketMilliseconds);
|
|
|
// Check pressure error matches expectation.
|
|
|
EXPECT_NEAR(static_cast<int>(1000 * pressureErrors[i]), atom.pressureRmseMilliunits, 1);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
TEST(MotionPredictorMetricsManagerTest, QuadraticPositionLinearPredictionsGeneralErrors) {
|
|
|
// Generate ground truth points.
|
|
|
//
|
|
|
// Each component of the ground truth positions are an independent quadratically increasing
|
|
|
// function from some initial value.
|
|
|
const Eigen::Vector2f initialPosition(200, 300);
|
|
|
const Eigen::Vector2f quadraticCoefficients(-2, 3);
|
|
|
std::vector<GroundTruthPoint> groundTruthPoints;
|
|
|
nsecs_t timestamp = TEST_INITIAL_TIMESTAMP;
|
|
|
// As described in the runMetricsManager comment, we should have TEST_MAX_NUM_PREDICTIONS + 2
|
|
|
// ground truth points.
|
|
|
for (size_t i = 0; i < TEST_MAX_NUM_PREDICTIONS + 2; ++i) {
|
|
|
const Eigen::Vector2f position =
|
|
|
initialPosition + quadraticCoefficients * static_cast<float>(i * i);
|
|
|
groundTruthPoints.push_back(
|
|
|
GroundTruthPoint{{.position = position, .pressure = 0.5}, .timestamp = timestamp});
|
|
|
timestamp += TEST_PREDICTION_INTERVAL_NANOS;
|
|
|
}
|
|
|
|
|
|
// Note: the first index is the source ground truth index, and the second is the prediction
|
|
|
// target index.
|
|
|
std::vector<std::vector<PredictionPoint>> predictionPoints =
|
|
|
generateAllPredictionsByLinearExtrapolation(groundTruthPoints);
|
|
|
|
|
|
std::vector<GeneralPositionErrors> generalPositionErrors =
|
|
|
computeGeneralPositionErrors(groundTruthPoints, predictionPoints);
|
|
|
|
|
|
// Run test.
|
|
|
std::vector<AtomFields> atomFields;
|
|
|
runMetricsManager(groundTruthPoints, predictionPoints, atomFields);
|
|
|
|
|
|
// Check logged metrics match expectations.
|
|
|
ASSERT_EQ(TEST_MAX_NUM_PREDICTIONS, atomFields.size());
|
|
|
for (size_t i = 0; i < atomFields.size(); ++i) {
|
|
|
SCOPED_TRACE(testing::Message() << "i = " << i);
|
|
|
const AtomFields& atom = atomFields[i];
|
|
|
// Check time bucket delta matches expectation based on index and prediction interval.
|
|
|
const nsecs_t deltaTimeBucketNanos = TEST_PREDICTION_INTERVAL_NANOS * (i + 1);
|
|
|
EXPECT_EQ(deltaTimeBucketNanos / NANOS_PER_MILLIS, atom.deltaTimeBucketMilliseconds);
|
|
|
// Check general position errors match expectation.
|
|
|
EXPECT_NEAR(static_cast<int>(1000 * generalPositionErrors[i].alongTrajectoryErrorMean),
|
|
|
atom.alongTrajectoryErrorMeanMillipixels, 1);
|
|
|
EXPECT_NEAR(static_cast<int>(1000 * generalPositionErrors[i].alongTrajectoryErrorStd),
|
|
|
atom.alongTrajectoryErrorStdMillipixels, 1);
|
|
|
EXPECT_NEAR(static_cast<int>(1000 * generalPositionErrors[i].offTrajectoryRmse),
|
|
|
atom.offTrajectoryRmseMillipixels, 1);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// Counterclockwise regular octagonal section test:
|
|
|
// • Input – ground truth: constantly-spaced input events starting at a trajectory pointing exactly
|
|
|
// rightwards, and rotating by 45° counterclockwise after each input.
|
|
|
// • Input – predictions: simple linear extrapolations of previous two ground truth points.
|
|
|
//
|
|
|
// The code below uses the following terminology to distinguish references to ground truth events:
|
|
|
// • Source ground truth: the most recent ground truth point received at the time the prediction
|
|
|
// was made.
|
|
|
// • Target ground truth: the ground truth event that the prediction was attempting to match.
|
|
|
TEST(MotionPredictorMetricsManagerTest, CounterclockwiseOctagonGroundTruthLinearPredictions) {
|
|
|
// Select a stroke velocity that exceeds the high-velocity threshold of 1100 px/sec.
|
|
|
// For an input rate of 240 hz, 1100 px/sec * (1/240) sec/input ≈ 4.58 pixels per input.
|
|
|
const float strokeVelocity = 10; // pixels per input
|
|
|
|
|
|
// As described in the runMetricsManager comment, we should have TEST_MAX_NUM_PREDICTIONS + 2
|
|
|
// ground truth points.
|
|
|
std::vector<GroundTruthPoint> groundTruthPoints = generateCircularArcGroundTruthPoints(
|
|
|
/*initialPosition=*/Eigen::Vector2f(100, 100),
|
|
|
/*initialAngle=*/M_PI_2,
|
|
|
/*velocity=*/strokeVelocity,
|
|
|
/*turningAngle=*/-M_PI_4,
|
|
|
/*numPoints=*/TEST_MAX_NUM_PREDICTIONS + 2);
|
|
|
|
|
|
std::vector<std::vector<PredictionPoint>> predictionPoints =
|
|
|
generateAllPredictionsByLinearExtrapolation(groundTruthPoints);
|
|
|
|
|
|
std::vector<GeneralPositionErrors> generalPositionErrors =
|
|
|
computeGeneralPositionErrors(groundTruthPoints, predictionPoints);
|
|
|
|
|
|
// Run test.
|
|
|
std::vector<AtomFields> atomFields;
|
|
|
runMetricsManager(groundTruthPoints, predictionPoints, atomFields);
|
|
|
|
|
|
// Check logged metrics match expectations.
|
|
|
ASSERT_EQ(TEST_MAX_NUM_PREDICTIONS, atomFields.size());
|
|
|
for (size_t i = 0; i < atomFields.size(); ++i) {
|
|
|
SCOPED_TRACE(testing::Message() << "i = " << i);
|
|
|
const AtomFields& atom = atomFields[i];
|
|
|
const nsecs_t deltaTimeBucketNanos = TEST_PREDICTION_INTERVAL_NANOS * (i + 1);
|
|
|
EXPECT_EQ(deltaTimeBucketNanos / NANOS_PER_MILLIS, atom.deltaTimeBucketMilliseconds);
|
|
|
|
|
|
// General errors: reported for every time bucket.
|
|
|
EXPECT_NEAR(static_cast<int>(1000 * generalPositionErrors[i].alongTrajectoryErrorMean),
|
|
|
atom.alongTrajectoryErrorMeanMillipixels, 1);
|
|
|
// We allow for some floating point error in standard deviation (0.02 pixels).
|
|
|
EXPECT_NEAR(1000 * generalPositionErrors[i].alongTrajectoryErrorStd,
|
|
|
atom.alongTrajectoryErrorStdMillipixels, 20);
|
|
|
// All position errors are equal, so the standard deviation should be approximately zero.
|
|
|
EXPECT_NEAR(0, atom.alongTrajectoryErrorStdMillipixels, 20);
|
|
|
// Absolute value for RMSE, since it must be non-negative.
|
|
|
EXPECT_NEAR(static_cast<int>(1000 * generalPositionErrors[i].offTrajectoryRmse),
|
|
|
atom.offTrajectoryRmseMillipixels, 1);
|
|
|
|
|
|
// High-velocity errors: reported only for the last two time buckets.
|
|
|
//
|
|
|
// Since our input stroke velocity is chosen to be above the high-velocity threshold, all
|
|
|
// data contributes to high-velocity errors, and thus high-velocity errors should be equal
|
|
|
// to general errors (where reported).
|
|
|
//
|
|
|
// As above, use absolute value for RMSE, since it must be non-negative.
|
|
|
if (i + 2 >= atomFields.size()) {
|
|
|
EXPECT_NEAR(static_cast<int>(
|
|
|
1000 * std::abs(generalPositionErrors[i].alongTrajectoryErrorMean)),
|
|
|
atom.highVelocityAlongTrajectoryRmse, 1);
|
|
|
EXPECT_NEAR(static_cast<int>(1000 *
|
|
|
std::abs(generalPositionErrors[i].offTrajectoryRmse)),
|
|
|
atom.highVelocityOffTrajectoryRmse, 1);
|
|
|
} else {
|
|
|
EXPECT_EQ(NO_DATA_SENTINEL, atom.highVelocityAlongTrajectoryRmse);
|
|
|
EXPECT_EQ(NO_DATA_SENTINEL, atom.highVelocityOffTrajectoryRmse);
|
|
|
}
|
|
|
|
|
|
// Scale-invariant errors: reported only for the last time bucket, where the reported value
|
|
|
// is the aggregation across all time buckets.
|
|
|
//
|
|
|
// The MetricsManager stores mMaxNumPredictions recent ground truth segments. Our ground
|
|
|
// truth segments here all have a length of strokeVelocity, so we can convert general errors
|
|
|
// to scale-invariant errors by dividing by `strokeVelocty * TEST_MAX_NUM_PREDICTIONS`.
|
|
|
//
|
|
|
// As above, use absolute value for RMSE, since it must be non-negative.
|
|
|
if (i + 1 == atomFields.size()) {
|
|
|
const float pathLength = strokeVelocity * TEST_MAX_NUM_PREDICTIONS;
|
|
|
std::vector<float> alongTrajectoryAbsoluteErrors;
|
|
|
std::vector<float> offTrajectoryAbsoluteErrors;
|
|
|
for (size_t j = 0; j < TEST_MAX_NUM_PREDICTIONS; ++j) {
|
|
|
alongTrajectoryAbsoluteErrors.push_back(
|
|
|
std::abs(generalPositionErrors[j].alongTrajectoryErrorMean));
|
|
|
offTrajectoryAbsoluteErrors.push_back(
|
|
|
std::abs(generalPositionErrors[j].offTrajectoryRmse));
|
|
|
}
|
|
|
EXPECT_NEAR(static_cast<int>(1000 * average(alongTrajectoryAbsoluteErrors) /
|
|
|
pathLength),
|
|
|
atom.scaleInvariantAlongTrajectoryRmse, 1);
|
|
|
EXPECT_NEAR(static_cast<int>(1000 * average(offTrajectoryAbsoluteErrors) / pathLength),
|
|
|
atom.scaleInvariantOffTrajectoryRmse, 1);
|
|
|
} else {
|
|
|
EXPECT_EQ(NO_DATA_SENTINEL, atom.scaleInvariantAlongTrajectoryRmse);
|
|
|
EXPECT_EQ(NO_DATA_SENTINEL, atom.scaleInvariantOffTrajectoryRmse);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
} // namespace
|
|
|
} // namespace android
|