diff --git a/src/software/sensor_fusion/filter/kalman_filter_test.cpp b/src/software/sensor_fusion/filter/kalman_filter_test.cpp index 4602d1b845..7a3c223359 100644 --- a/src/software/sensor_fusion/filter/kalman_filter_test.cpp +++ b/src/software/sensor_fusion/filter/kalman_filter_test.cpp @@ -4,75 +4,293 @@ #include -TEST(KalmanFilterTest, BasicScalarUpdate) +struct KalmanParamsMath { - constexpr int DimX = 1; - constexpr int DimY = 1; - constexpr int DimU = 1; + double state_estimate; + double state_covariance; + double process_model; + double process_covariance; + double control_model; + double measurement_model; + double measurement_covariance; + double control_input; + double measurement_input; +}; - KalmanFilter filter{}; +template +struct KalmanParamsBehaviour +{ + Eigen::Vector state_estimate; + Eigen::Matrix state_covariance; + Eigen::Matrix process_model; + Eigen::Matrix process_covariance; + Eigen::Matrix control_model; + Eigen::Matrix measurement_model; + Eigen::Matrix measurement_covariance; + Eigen::Matrix control_input; + Eigen::Matrix measurement_input; + Eigen::Vector expected_state_estimate; + Eigen::Matrix expected_state_covariance; +}; - filter.state_estimate << 1.0; - filter.state_covariance << 1.0; - filter.process_model << 1.0; - filter.process_covariance << 0.0; - filter.control_model << 0.0; - filter.measurement_model << 1.0; - filter.measurement_covariance << 1.0; +class MathTests1D : public testing::TestWithParam +{ + protected: + static constexpr int DimX = 1; + static constexpr int DimY = 1; + static constexpr int DimU = 1; + + KalmanFilter filter{}; - // Predict with zero control input Eigen::Matrix control_input; - control_input << 0.0; + Eigen::Matrix measurement_input; + + KalmanParamsMath p = {}; + + void SetUp() override + { + p = GetParam(); + + filter.state_estimate << p.state_estimate; + filter.state_covariance << p.state_covariance; + filter.process_model << p.process_model; + filter.process_covariance << p.process_covariance; + filter.control_model << p.control_model; + filter.measurement_model << p.measurement_model; + filter.measurement_covariance << p.measurement_covariance; + + // Control input + control_input << p.control_input; + measurement_input << p.measurement_input; + } +}; + +TEST_P(MathTests1D, Math1D) +{ + // Predict Stage 1x1 Matrix Equations + double predict_state_estimate = + p.process_model * p.state_estimate + p.control_model * p.control_input; + double predict_state_covariance = + p.process_model * p.process_model * p.state_covariance + p.process_covariance; filter.predict(control_input); + EXPECT_DOUBLE_EQ(filter.state_estimate(0), predict_state_estimate); + EXPECT_DOUBLE_EQ(filter.state_covariance(0, 0), predict_state_covariance); + + // Update Stage 1x1 Matrix Equations - // Measurement says state is 3 - Eigen::Matrix measurement; - measurement << 3.0; + double innovation_covariance = + p.measurement_model * p.measurement_model * predict_state_covariance + + p.measurement_covariance; + double kalman_gain; - filter.update(measurement); + if (innovation_covariance == 0) + { + // Kalman gain should default to 0 if the matrix is uninvertible + kalman_gain = 0; + } + else + { + kalman_gain = + predict_state_covariance * p.measurement_model / + (p.measurement_model * p.measurement_model * predict_state_covariance + + p.measurement_covariance); + } - // With equal prior and measurement uncertainty: - // estimate should move halfway toward measurement - EXPECT_NEAR(filter.state_estimate(0), 2.0, std::numeric_limits::epsilon()); + double update_state_estimate = + predict_state_estimate + + kalman_gain * + (p.measurement_input - p.measurement_model * predict_state_estimate); + double update_state_covariance = + std::pow((1 - kalman_gain * p.measurement_model), 2) * predict_state_covariance + + std::pow(kalman_gain, 2) * p.measurement_covariance; - // Covariance should reduce from 1 -> 0.5 - EXPECT_NEAR(filter.state_covariance(0, 0), 0.5, - std::numeric_limits::epsilon()); + filter.update(measurement_input); + EXPECT_DOUBLE_EQ(filter.state_estimate(0), update_state_estimate); + EXPECT_DOUBLE_EQ(filter.state_covariance(0, 0), update_state_covariance); } -TEST(KalmanFilterTest, IgnoreNoisyMeasurement) -{ - constexpr int DimX = 1; - constexpr int DimY = 1; - constexpr int DimU = 1; +INSTANTIATE_TEST_SUITE_P( + Math1D, MathTests1D, + ::testing::Values( + // Random Values + KalmanParamsMath{1.9, 3.2, 11.7, 5.3, 2.1, 1.7, 0.2, 4.1, 5.4}, + + // Negative Numbers, No Uncertainty + KalmanParamsMath{-3.5, -0.1, -1.0, 0.0, -1.0, -1.0, 0.0, 3.4, -2.3}, + // Handles innovation covariance = 0 (matrix is uninvertible) + KalmanParamsMath{-3.5, -0.1, -1.0, 0.0, -1.0, 0.0, 0.0, 3.4, -2.3})); + +template +class BehaviourTests + : public testing::TestWithParam> +{ + protected: KalmanFilter filter{}; - filter.state_estimate << 5.0; - filter.state_covariance << 7.0; - filter.process_model << 1.0; - filter.process_covariance << 0.0; - filter.control_model << 0.0; - filter.measurement_model << 1.0; - filter.measurement_covariance << 1e20; // Huge measurement noise + Eigen::Vector expected_state_estimate; + Eigen::Matrix expected_state_covariance; - // Predict with zero control input Eigen::Matrix control_input; - control_input << 0.0; + Eigen::Matrix measurement_input; + KalmanParamsBehaviour p = {}; + + void SetUp() override + { + p = this->GetParam(); + + filter.state_estimate = p.state_estimate; + filter.state_covariance = p.state_covariance; + filter.process_model = p.process_model; + filter.process_covariance = p.process_covariance; + filter.control_model = p.control_model; + filter.measurement_model = p.measurement_model; + filter.measurement_covariance = p.measurement_covariance; + + // Input + control_input = p.control_input; + measurement_input = p.measurement_input; + + // Expected Values + expected_state_estimate = p.expected_state_estimate; + expected_state_covariance = p.expected_state_covariance; + } +}; + +using BehaviourTests1D = BehaviourTests<1, 1, 1>; +TEST_P(BehaviourTests1D, Behaviour1D) +{ filter.predict(control_input); + filter.update(measurement_input); - // Noisy measurement says state is 3 - Eigen::Matrix measurement; - measurement << 3.0; + EXPECT_TRUE(filter.state_estimate.isApprox(expected_state_estimate, + std::numeric_limits::epsilon())); + EXPECT_TRUE(filter.state_covariance.isApprox(expected_state_covariance, + std::numeric_limits::epsilon())); +} - filter.update(measurement); +INSTANTIATE_TEST_SUITE_P( + Behaviour1D, BehaviourTests1D, + ::testing::Values( + // High measurement noise and no control input, estimate should trust prediction + KalmanParamsBehaviour<1, 1, 1>{ + Eigen::Vector{5.0}, Eigen::Matrix{7.0}, + Eigen::Matrix{1.0}, Eigen::Matrix{0.0}, + Eigen::Matrix{2.0}, Eigen::Matrix{1.0}, + Eigen::Matrix{1e20}, Eigen::Matrix{0}, + Eigen::Matrix{4.0}, Eigen::Matrix{5.0}, + Eigen::Matrix{7.0}}, + // High process noise, estimate should trust measurements no matter control + KalmanParamsBehaviour<1, 1, 1>{ + Eigen::Vector{2.0}, Eigen::Matrix{6.0}, + Eigen::Matrix{1.0}, Eigen::Matrix{1e20}, + Eigen::Matrix{1.0}, Eigen::Matrix{1.0}, + Eigen::Matrix{2.0}, Eigen::Matrix{0.0}, + Eigen::Matrix{4.0}, Eigen::Matrix{4.0}, + Eigen::Matrix{2.0}}, + KalmanParamsBehaviour<1, 1, 1>{ + Eigen::Vector{2.0}, Eigen::Matrix{6.0}, + Eigen::Matrix{1.0}, Eigen::Matrix{1e20}, + Eigen::Matrix{1.0}, Eigen::Matrix{1.0}, + Eigen::Matrix{2.0}, Eigen::Matrix{90.0}, + Eigen::Matrix{4.0}, Eigen::Matrix{4.0}, + Eigen::Matrix{2.0}}, + // Equal measurement and process noise, estimate should weigh each equally + KalmanParamsBehaviour<1, 1, 1>{ + Eigen::Vector{-3.4}, Eigen::Matrix{1.0}, + Eigen::Matrix{1.0}, Eigen::Matrix{0.0}, + Eigen::Matrix{0.0}, Eigen::Matrix{1.0}, + Eigen::Matrix{1.0}, Eigen::Matrix{0.0}, + Eigen::Matrix{-2.6}, Eigen::Matrix{-3.0}, + Eigen::Matrix{0.5}})); - // With high measurement noise, estimate should trust prediction - EXPECT_NEAR(filter.state_estimate(0), 5.0, std::numeric_limits::epsilon()); +using BehaviourTests2D = BehaviourTests<2, 1, 3>; +TEST_P(BehaviourTests2D, Behaviour2D) +{ + filter.predict(control_input); + filter.update(measurement_input); - // Covariance should stay relatively unchanged - EXPECT_NEAR(filter.state_covariance(0, 0), 7.0, - std::numeric_limits::epsilon()); + EXPECT_TRUE(filter.state_estimate.isApprox(expected_state_estimate, + std::numeric_limits::epsilon())); + EXPECT_TRUE(filter.state_covariance.isApprox(expected_state_covariance, + std::numeric_limits::epsilon())); } + +INSTANTIATE_TEST_SUITE_P( + Behaviour2D, BehaviourTests2D, + ::testing::Values( + // Control model of zero, control inputs should not have an effect on state + KalmanParamsBehaviour<2, 1, 3>{ + Eigen::Vector{3.0, 4.0}, + Eigen::Matrix{{7.0, 4.0}, {3.0, 5.0}}, + Eigen::Matrix::Identity(), Eigen::Matrix::Zero(), + Eigen::Matrix::Zero(), Eigen::Matrix{{1.0, 0.0}}, + Eigen::Matrix{1e20}, Eigen::Matrix{300, 400, 700}, + Eigen::Matrix{3.0}, Eigen::Matrix{3.0, 4.0}, + Eigen::Matrix{{7.0, 4.0}, {3.0, 5.0}}}, + // Process model and covariance of zero, the filter should forget its past + // state and collapse to zero + KalmanParamsBehaviour<2, 1, 3>{ + Eigen::Vector{0.0, 0.0}, + Eigen::Matrix{{0.5, 0.0}, {0.0, 0.5}}, + Eigen::Matrix::Zero(), Eigen::Matrix::Zero(), + Eigen::Matrix{{1.0, 0.0, 2.0}, {1.0, 0.0, 2.0}}, + Eigen::Matrix{{1.0, 1.0}}, Eigen::Matrix{0.5}, + Eigen::Matrix{0.0, 0.0, 0.0}, Eigen::Matrix{10.0}, + Eigen::Matrix{0.0, 0.0}, Eigen::Matrix::Zero()}, + // Measurement model of zero, measurements should not have an effect on state + KalmanParamsBehaviour<2, 1, 3>{ + Eigen::Vector{2.5, 3.5}, + Eigen::Matrix{{0.5, 0.0}, {0.0, 0.5}}, + Eigen::Matrix::Identity(), + Eigen::Matrix::Identity(), + Eigen::Matrix{{1.0, 0.0, 2.0}, {1.0, 0.0, 2.0}}, + Eigen::Matrix{{0.0, 0.0}}, Eigen::Matrix{0.5}, + Eigen::Matrix{0.0, 0.0, 0.0}, + Eigen::Matrix{300.0}, Eigen::Matrix{2.5, 3.5}, + Eigen::Matrix{{1.5, 0.0}, {0.0, 1.5}}})); + +using BehaviourTests3D = BehaviourTests<3, 3, 3>; +TEST_P(BehaviourTests3D, Behaviour3D) +{ + filter.predict(control_input); + filter.update(measurement_input); + + // Tolerance to account for the calculation drift of the larger matrix + constexpr double tolerance = 1e-10; + + EXPECT_TRUE(filter.state_estimate.isApprox(expected_state_estimate, tolerance)); + EXPECT_TRUE(filter.state_covariance.isApprox(expected_state_covariance, tolerance)); +} + +INSTANTIATE_TEST_SUITE_P( + Behaviour3D, BehaviourTests3D, + ::testing::Values( + // With one sensor having high measurement noise, filter should ignore prior data + KalmanParamsBehaviour<3, 3, 3>{ + Eigen::Vector{1.0, 3.0, 4.0}, + Eigen::Matrix::Identity(), + Eigen::Matrix::Identity(), Eigen::Matrix::Zero(), + Eigen::Matrix::Zero(), Eigen::Matrix::Identity(), + Eigen::Matrix{ + {1.0, 0.0, 0.0}, {0.0, 1.0, 0.0}, {0.0, 0.0, 1e15}}, + Eigen::Vector{0.0, 0.0, 0.0}, + Eigen::Vector{5.0, 7.0, 100.0}, + Eigen::Vector{3.0, 5.0, 4.0}, + Eigen::Matrix{ + {0.5, 0.0, 0.0}, {0.0, 0.5, 0.0}, {0.0, 0.0, 1.0}}}, + // With zero measurement error, the filter should trust the measurement + KalmanParamsBehaviour<3, 3, 3>{ + Eigen::Vector{1.0, 3.0, 4.0}, + Eigen::Matrix::Identity(), + Eigen::Matrix::Identity(), Eigen::Matrix::Zero(), + Eigen::Matrix::Zero(), Eigen::Matrix::Identity(), + Eigen::Matrix{ + {0.0, 0.0, 0.0}, {0.0, 0.0, 0.0}, {0.0, 0.0, 0.0}}, + Eigen::Vector{0.0, 0.0, 0.0}, + Eigen::Vector{20.0, 30.0, 50.0}, + Eigen::Vector{20.0, 30.0, 50.0}, + Eigen::Matrix{ + {0.0, 0.0, 0.0}, {0.0, 0.0, 0.0}, {0.0, 0.0, 0.0}}}));