2020-12-26 14:12:05 -08:00
|
|
|
// Copyright (c) FIRST and other WPILib contributors.
|
|
|
|
|
// Open Source Software; you can modify and/or share it under the terms of
|
|
|
|
|
// the WPILib BSD license file in the root directory of this project.
|
2020-08-14 23:40:33 -07:00
|
|
|
|
[wpimath] Fix UnscentedKalmanFilter and improve math docs (#7850)
Throughout the code the state sqrt covariance S and innovation covariance Sy are maintained as upper triangular cholesky factors of those covariance matrices. The original paper defines P=S*S', so S should be lower triangular. The functions in the paper reflect this. In the code implementation, the sqrt covariance matrices are upper triangular, but the algorithm expects them to be lower triangular.
This bug was likely missed because the incorrect version of the filter is able to converge for some systems where all the states are observed, and the test case is set up such that all states are observed.
To fix the bug, a couple things needed to be changed:
all instances of rankUpdate() needed to be changed to use the lower triangular cholesky factor,
In the unscented transform, when S is found via QR decomposition, we need to take the transpose because R is upper triangular,
P() and SetP() functions need to be modified to be P=S*S' instead of P=S'*S, and P.llt().matrixL() instead of P.llt().matrixU() respectively.
Each part of the algorithm has also had the comments changed to clarify exactly which equation from the paper it implements.
Co-authored-by: Tyler Veness <calcmogul@gmail.com>
2025-03-14 13:05:15 -04:00
|
|
|
#include <algorithm>
|
2020-08-14 23:40:33 -07:00
|
|
|
#include <cmath>
|
[wpimath] Fix UnscentedKalmanFilter and improve math docs (#7850)
Throughout the code the state sqrt covariance S and innovation covariance Sy are maintained as upper triangular cholesky factors of those covariance matrices. The original paper defines P=S*S', so S should be lower triangular. The functions in the paper reflect this. In the code implementation, the sqrt covariance matrices are upper triangular, but the algorithm expects them to be lower triangular.
This bug was likely missed because the incorrect version of the filter is able to converge for some systems where all the states are observed, and the test case is set up such that all states are observed.
To fix the bug, a couple things needed to be changed:
all instances of rankUpdate() needed to be changed to use the lower triangular cholesky factor,
In the unscented transform, when S is found via QR decomposition, we need to take the transpose because R is upper triangular,
P() and SetP() functions need to be modified to be P=S*S' instead of P=S'*S, and P.llt().matrixL() instead of P.llt().matrixU() respectively.
Each part of the algorithm has also had the comments changed to clarify exactly which equation from the paper it implements.
Co-authored-by: Tyler Veness <calcmogul@gmail.com>
2025-03-14 13:05:15 -04:00
|
|
|
#include <numbers>
|
2024-09-20 17:43:39 -07:00
|
|
|
#include <vector>
|
2020-08-14 23:40:33 -07:00
|
|
|
|
2023-08-28 15:13:34 -07:00
|
|
|
#include <Eigen/QR>
|
|
|
|
|
#include <gtest/gtest.h>
|
|
|
|
|
|
2025-11-07 19:56:21 -05:00
|
|
|
#include "wpi/math/estimator/AngleStatistics.hpp"
|
|
|
|
|
#include "wpi/math/estimator/S3UKF.hpp"
|
2025-11-07 19:57:55 -05:00
|
|
|
#include "wpi/math/linalg/EigenCore.hpp"
|
2025-11-07 19:56:21 -05:00
|
|
|
#include "wpi/math/system/Discretization.hpp"
|
|
|
|
|
#include "wpi/math/system/NumericalIntegration.hpp"
|
|
|
|
|
#include "wpi/math/system/NumericalJacobian.hpp"
|
|
|
|
|
#include "wpi/math/system/plant/DCMotor.hpp"
|
|
|
|
|
#include "wpi/math/system/plant/LinearSystemId.hpp"
|
|
|
|
|
#include "wpi/math/trajectory/TrajectoryGenerator.hpp"
|
2025-11-07 19:57:55 -05:00
|
|
|
#include "wpi/math/util/StateSpaceUtil.hpp"
|
2025-11-07 19:56:21 -05:00
|
|
|
#include "wpi/units/moment_of_inertia.hpp"
|
2020-08-14 23:40:33 -07:00
|
|
|
|
|
|
|
|
namespace {
|
|
|
|
|
|
[wpimath] Fix UnscentedKalmanFilter and improve math docs (#7850)
Throughout the code the state sqrt covariance S and innovation covariance Sy are maintained as upper triangular cholesky factors of those covariance matrices. The original paper defines P=S*S', so S should be lower triangular. The functions in the paper reflect this. In the code implementation, the sqrt covariance matrices are upper triangular, but the algorithm expects them to be lower triangular.
This bug was likely missed because the incorrect version of the filter is able to converge for some systems where all the states are observed, and the test case is set up such that all states are observed.
To fix the bug, a couple things needed to be changed:
all instances of rankUpdate() needed to be changed to use the lower triangular cholesky factor,
In the unscented transform, when S is found via QR decomposition, we need to take the transpose because R is upper triangular,
P() and SetP() functions need to be modified to be P=S*S' instead of P=S'*S, and P.llt().matrixL() instead of P.llt().matrixU() respectively.
Each part of the algorithm has also had the comments changed to clarify exactly which equation from the paper it implements.
Co-authored-by: Tyler Veness <calcmogul@gmail.com>
2025-03-14 13:05:15 -04:00
|
|
|
// First test system, differential drive
|
2025-11-07 20:00:05 -05:00
|
|
|
wpi::math::Vectord<5> DriveDynamics(const wpi::math::Vectord<5>& x,
|
2025-11-07 20:01:58 -05:00
|
|
|
const wpi::math::Vectord<2>& u) {
|
2025-11-07 20:00:05 -05:00
|
|
|
auto motors = wpi::math::DCMotor::CIM(2);
|
2020-08-14 23:40:33 -07:00
|
|
|
|
2022-06-08 22:19:01 -07:00
|
|
|
// constexpr double Glow = 15.32; // Low gear ratio
|
2020-08-14 23:40:33 -07:00
|
|
|
constexpr double Ghigh = 7.08; // High gear ratio
|
|
|
|
|
constexpr auto rb = 0.8382_m / 2.0; // Robot radius
|
|
|
|
|
constexpr auto r = 0.0746125_m; // Wheel radius
|
|
|
|
|
constexpr auto m = 63.503_kg; // Robot mass
|
|
|
|
|
constexpr auto J = 5.6_kg_sq_m; // Robot moment of inertia
|
|
|
|
|
|
|
|
|
|
auto C1 = -std::pow(Ghigh, 2) * motors.Kt /
|
2025-11-07 20:00:05 -05:00
|
|
|
(motors.Kv * motors.R * wpi::units::math::pow<2>(r));
|
2020-08-14 23:40:33 -07:00
|
|
|
auto C2 = Ghigh * motors.Kt / (motors.R * r);
|
2025-11-07 20:00:05 -05:00
|
|
|
auto k1 = (1 / m + wpi::units::math::pow<2>(rb) / J);
|
|
|
|
|
auto k2 = (1 / m - wpi::units::math::pow<2>(rb) / J);
|
2020-08-14 23:40:33 -07:00
|
|
|
|
2025-11-07 20:00:05 -05:00
|
|
|
wpi::units::meters_per_second_t vl{x(3)};
|
|
|
|
|
wpi::units::meters_per_second_t vr{x(4)};
|
|
|
|
|
wpi::units::volt_t Vl{u(0)};
|
|
|
|
|
wpi::units::volt_t Vr{u(1)};
|
2020-08-14 23:40:33 -07:00
|
|
|
|
|
|
|
|
auto v = 0.5 * (vl + vr);
|
2025-11-07 20:00:05 -05:00
|
|
|
return wpi::math::Vectord<5>{
|
2021-10-25 08:58:12 -07:00
|
|
|
v.value() * std::cos(x(2)), v.value() * std::sin(x(2)),
|
|
|
|
|
((vr - vl) / (2.0 * rb)).value(),
|
|
|
|
|
k1.value() * ((C1 * vl).value() + (C2 * Vl).value()) +
|
|
|
|
|
k2.value() * ((C1 * vr).value() + (C2 * Vr).value()),
|
|
|
|
|
k2.value() * ((C1 * vl).value() + (C2 * Vl).value()) +
|
|
|
|
|
k1.value() * ((C1 * vr).value() + (C2 * Vr).value())};
|
2020-08-14 23:40:33 -07:00
|
|
|
}
|
|
|
|
|
|
2025-11-07 20:00:05 -05:00
|
|
|
wpi::math::Vectord<3> DriveLocalMeasurementModel(
|
2025-11-07 20:01:58 -05:00
|
|
|
const wpi::math::Vectord<5>& x,
|
|
|
|
|
[[maybe_unused]] const wpi::math::Vectord<2>& u) {
|
2025-11-07 20:00:05 -05:00
|
|
|
return wpi::math::Vectord<3>{x(2), x(3), x(4)};
|
2020-08-14 23:40:33 -07:00
|
|
|
}
|
|
|
|
|
|
2025-11-07 20:00:05 -05:00
|
|
|
wpi::math::Vectord<5> DriveGlobalMeasurementModel(
|
2025-11-07 20:01:58 -05:00
|
|
|
const wpi::math::Vectord<5>& x,
|
|
|
|
|
[[maybe_unused]] const wpi::math::Vectord<2>& u) {
|
2025-11-07 20:00:05 -05:00
|
|
|
return wpi::math::Vectord<5>{x(0), x(1), x(2), x(3), x(4)};
|
2020-08-14 23:40:33 -07:00
|
|
|
}
|
|
|
|
|
|
2025-07-20 22:18:31 -07:00
|
|
|
TEST(S3UKFTest, DriveInit) {
|
2021-08-19 00:23:48 -07:00
|
|
|
constexpr auto dt = 5_ms;
|
2020-08-14 23:40:33 -07:00
|
|
|
|
2025-11-07 20:00:05 -05:00
|
|
|
wpi::math::S3UKF<5, 2, 3> observer{DriveDynamics,
|
2025-11-07 20:01:58 -05:00
|
|
|
DriveLocalMeasurementModel,
|
|
|
|
|
{0.5, 0.5, 10.0, 1.0, 1.0},
|
|
|
|
|
{0.0001, 0.01, 0.01},
|
|
|
|
|
wpi::math::AngleMean<5, 5 + 2>(2),
|
|
|
|
|
wpi::math::AngleMean<3, 5 + 2>(0),
|
|
|
|
|
wpi::math::AngleResidual<5>(2),
|
|
|
|
|
wpi::math::AngleResidual<3>(0),
|
|
|
|
|
wpi::math::AngleAdd<5>(2),
|
|
|
|
|
dt};
|
2025-11-07 20:00:05 -05:00
|
|
|
wpi::math::Vectord<2> u{12.0, 12.0};
|
2020-08-14 23:40:33 -07:00
|
|
|
observer.Predict(u, dt);
|
|
|
|
|
|
[wpimath] Fix UnscentedKalmanFilter and improve math docs (#7850)
Throughout the code the state sqrt covariance S and innovation covariance Sy are maintained as upper triangular cholesky factors of those covariance matrices. The original paper defines P=S*S', so S should be lower triangular. The functions in the paper reflect this. In the code implementation, the sqrt covariance matrices are upper triangular, but the algorithm expects them to be lower triangular.
This bug was likely missed because the incorrect version of the filter is able to converge for some systems where all the states are observed, and the test case is set up such that all states are observed.
To fix the bug, a couple things needed to be changed:
all instances of rankUpdate() needed to be changed to use the lower triangular cholesky factor,
In the unscented transform, when S is found via QR decomposition, we need to take the transpose because R is upper triangular,
P() and SetP() functions need to be modified to be P=S*S' instead of P=S'*S, and P.llt().matrixL() instead of P.llt().matrixU() respectively.
Each part of the algorithm has also had the comments changed to clarify exactly which equation from the paper it implements.
Co-authored-by: Tyler Veness <calcmogul@gmail.com>
2025-03-14 13:05:15 -04:00
|
|
|
auto localY = DriveLocalMeasurementModel(observer.Xhat(), u);
|
2020-08-14 23:40:33 -07:00
|
|
|
observer.Correct(u, localY);
|
|
|
|
|
|
[wpimath] Fix UnscentedKalmanFilter and improve math docs (#7850)
Throughout the code the state sqrt covariance S and innovation covariance Sy are maintained as upper triangular cholesky factors of those covariance matrices. The original paper defines P=S*S', so S should be lower triangular. The functions in the paper reflect this. In the code implementation, the sqrt covariance matrices are upper triangular, but the algorithm expects them to be lower triangular.
This bug was likely missed because the incorrect version of the filter is able to converge for some systems where all the states are observed, and the test case is set up such that all states are observed.
To fix the bug, a couple things needed to be changed:
all instances of rankUpdate() needed to be changed to use the lower triangular cholesky factor,
In the unscented transform, when S is found via QR decomposition, we need to take the transpose because R is upper triangular,
P() and SetP() functions need to be modified to be P=S*S' instead of P=S'*S, and P.llt().matrixL() instead of P.llt().matrixU() respectively.
Each part of the algorithm has also had the comments changed to clarify exactly which equation from the paper it implements.
Co-authored-by: Tyler Veness <calcmogul@gmail.com>
2025-03-14 13:05:15 -04:00
|
|
|
auto globalY = DriveGlobalMeasurementModel(observer.Xhat(), u);
|
2025-11-07 20:00:05 -05:00
|
|
|
auto R = wpi::math::MakeCovMatrix(0.01, 0.01, 0.0001, 0.01, 0.01);
|
2025-11-07 20:01:58 -05:00
|
|
|
observer.Correct<5>(
|
|
|
|
|
u, globalY, DriveGlobalMeasurementModel, R,
|
|
|
|
|
wpi::math::AngleMean<5, 5 + 2>(2), wpi::math::AngleResidual<5>(2),
|
|
|
|
|
wpi::math::AngleResidual<5>(2), wpi::math::AngleAdd<5>(2));
|
2020-08-14 23:40:33 -07:00
|
|
|
}
|
|
|
|
|
|
2025-07-20 22:18:31 -07:00
|
|
|
TEST(S3UKFTest, DriveConvergence) {
|
2021-08-19 00:23:48 -07:00
|
|
|
constexpr auto dt = 5_ms;
|
2020-08-14 23:40:33 -07:00
|
|
|
constexpr auto rb = 0.8382_m / 2.0; // Robot radius
|
|
|
|
|
|
2025-11-07 20:00:05 -05:00
|
|
|
wpi::math::S3UKF<5, 2, 3> observer{DriveDynamics,
|
2025-11-07 20:01:58 -05:00
|
|
|
DriveLocalMeasurementModel,
|
|
|
|
|
{0.5, 0.5, 10.0, 1.0, 1.0},
|
|
|
|
|
{0.0001, 0.5, 0.5},
|
|
|
|
|
wpi::math::AngleMean<5, 5 + 2>(2),
|
|
|
|
|
wpi::math::AngleMean<3, 5 + 2>(0),
|
|
|
|
|
wpi::math::AngleResidual<5>(2),
|
|
|
|
|
wpi::math::AngleResidual<3>(0),
|
|
|
|
|
wpi::math::AngleAdd<5>(2),
|
|
|
|
|
dt};
|
|
|
|
|
|
|
|
|
|
auto waypoints = std::vector<wpi::math::Pose2d>{
|
|
|
|
|
wpi::math::Pose2d{2.75_m, 22.521_m, 0_rad},
|
|
|
|
|
wpi::math::Pose2d{24.73_m, 19.68_m, 5.846_rad}};
|
2025-11-07 20:00:05 -05:00
|
|
|
auto trajectory = wpi::math::TrajectoryGenerator::GenerateTrajectory(
|
2020-08-14 23:40:33 -07:00
|
|
|
waypoints, {8.8_mps, 0.1_mps_sq});
|
|
|
|
|
|
2025-11-07 20:00:05 -05:00
|
|
|
wpi::math::Vectord<5> r = wpi::math::Vectord<5>::Zero();
|
|
|
|
|
wpi::math::Vectord<2> u = wpi::math::Vectord<2>::Zero();
|
2020-08-14 23:40:33 -07:00
|
|
|
|
2025-11-07 20:00:05 -05:00
|
|
|
auto B = wpi::math::NumericalJacobianU<5, 5, 2>(
|
2025-11-07 20:01:58 -05:00
|
|
|
DriveDynamics, wpi::math::Vectord<5>::Zero(),
|
|
|
|
|
wpi::math::Vectord<2>::Zero());
|
2020-08-14 23:40:33 -07:00
|
|
|
|
2025-11-07 20:00:05 -05:00
|
|
|
observer.SetXhat(wpi::math::Vectord<5>{
|
2021-10-25 08:58:12 -07:00
|
|
|
trajectory.InitialPose().Translation().X().value(),
|
|
|
|
|
trajectory.InitialPose().Translation().Y().value(),
|
|
|
|
|
trajectory.InitialPose().Rotation().Radians().value(), 0.0, 0.0});
|
2020-08-14 23:40:33 -07:00
|
|
|
|
|
|
|
|
auto trueXhat = observer.Xhat();
|
|
|
|
|
|
|
|
|
|
auto totalTime = trajectory.TotalTime();
|
2021-10-25 08:58:12 -07:00
|
|
|
for (size_t i = 0; i < (totalTime / dt).value(); ++i) {
|
2020-08-14 23:40:33 -07:00
|
|
|
auto ref = trajectory.Sample(dt * i);
|
2025-11-07 20:00:05 -05:00
|
|
|
wpi::units::meters_per_second_t vl =
|
2021-10-25 08:58:12 -07:00
|
|
|
ref.velocity * (1 - (ref.curvature * rb).value());
|
2025-11-07 20:00:05 -05:00
|
|
|
wpi::units::meters_per_second_t vr =
|
2021-10-25 08:58:12 -07:00
|
|
|
ref.velocity * (1 + (ref.curvature * rb).value());
|
2020-08-14 23:40:33 -07:00
|
|
|
|
2025-11-07 20:00:05 -05:00
|
|
|
wpi::math::Vectord<5> nextR{
|
2021-10-25 08:58:12 -07:00
|
|
|
ref.pose.Translation().X().value(), ref.pose.Translation().Y().value(),
|
|
|
|
|
ref.pose.Rotation().Radians().value(), vl.value(), vr.value()};
|
2020-08-14 23:40:33 -07:00
|
|
|
|
2025-11-07 20:01:58 -05:00
|
|
|
auto localY =
|
|
|
|
|
DriveLocalMeasurementModel(trueXhat, wpi::math::Vectord<2>::Zero());
|
|
|
|
|
observer.Correct(
|
|
|
|
|
u, localY + wpi::math::MakeWhiteNoiseVector(0.0001, 0.5, 0.5));
|
2020-08-14 23:40:33 -07:00
|
|
|
|
2025-11-07 20:00:05 -05:00
|
|
|
wpi::math::Vectord<5> rdot = (nextR - r) / dt.value();
|
2025-11-07 20:01:58 -05:00
|
|
|
u = B.householderQr().solve(
|
|
|
|
|
rdot - DriveDynamics(r, wpi::math::Vectord<2>::Zero()));
|
2020-08-14 23:40:33 -07:00
|
|
|
|
|
|
|
|
observer.Predict(u, dt);
|
|
|
|
|
|
|
|
|
|
r = nextR;
|
2025-11-07 20:00:05 -05:00
|
|
|
trueXhat = wpi::math::RK4(DriveDynamics, trueXhat, u, dt);
|
2020-08-14 23:40:33 -07:00
|
|
|
}
|
|
|
|
|
|
[wpimath] Fix UnscentedKalmanFilter and improve math docs (#7850)
Throughout the code the state sqrt covariance S and innovation covariance Sy are maintained as upper triangular cholesky factors of those covariance matrices. The original paper defines P=S*S', so S should be lower triangular. The functions in the paper reflect this. In the code implementation, the sqrt covariance matrices are upper triangular, but the algorithm expects them to be lower triangular.
This bug was likely missed because the incorrect version of the filter is able to converge for some systems where all the states are observed, and the test case is set up such that all states are observed.
To fix the bug, a couple things needed to be changed:
all instances of rankUpdate() needed to be changed to use the lower triangular cholesky factor,
In the unscented transform, when S is found via QR decomposition, we need to take the transpose because R is upper triangular,
P() and SetP() functions need to be modified to be P=S*S' instead of P=S'*S, and P.llt().matrixL() instead of P.llt().matrixU() respectively.
Each part of the algorithm has also had the comments changed to clarify exactly which equation from the paper it implements.
Co-authored-by: Tyler Veness <calcmogul@gmail.com>
2025-03-14 13:05:15 -04:00
|
|
|
auto localY = DriveLocalMeasurementModel(trueXhat, u);
|
2020-08-14 23:40:33 -07:00
|
|
|
observer.Correct(u, localY);
|
|
|
|
|
|
[wpimath] Fix UnscentedKalmanFilter and improve math docs (#7850)
Throughout the code the state sqrt covariance S and innovation covariance Sy are maintained as upper triangular cholesky factors of those covariance matrices. The original paper defines P=S*S', so S should be lower triangular. The functions in the paper reflect this. In the code implementation, the sqrt covariance matrices are upper triangular, but the algorithm expects them to be lower triangular.
This bug was likely missed because the incorrect version of the filter is able to converge for some systems where all the states are observed, and the test case is set up such that all states are observed.
To fix the bug, a couple things needed to be changed:
all instances of rankUpdate() needed to be changed to use the lower triangular cholesky factor,
In the unscented transform, when S is found via QR decomposition, we need to take the transpose because R is upper triangular,
P() and SetP() functions need to be modified to be P=S*S' instead of P=S'*S, and P.llt().matrixL() instead of P.llt().matrixU() respectively.
Each part of the algorithm has also had the comments changed to clarify exactly which equation from the paper it implements.
Co-authored-by: Tyler Veness <calcmogul@gmail.com>
2025-03-14 13:05:15 -04:00
|
|
|
auto globalY = DriveGlobalMeasurementModel(trueXhat, u);
|
2025-11-07 20:00:05 -05:00
|
|
|
auto R = wpi::math::MakeCovMatrix(0.01, 0.01, 0.0001, 0.5, 0.5);
|
[wpimath] Fix UnscentedKalmanFilter and improve math docs (#7850)
Throughout the code the state sqrt covariance S and innovation covariance Sy are maintained as upper triangular cholesky factors of those covariance matrices. The original paper defines P=S*S', so S should be lower triangular. The functions in the paper reflect this. In the code implementation, the sqrt covariance matrices are upper triangular, but the algorithm expects them to be lower triangular.
This bug was likely missed because the incorrect version of the filter is able to converge for some systems where all the states are observed, and the test case is set up such that all states are observed.
To fix the bug, a couple things needed to be changed:
all instances of rankUpdate() needed to be changed to use the lower triangular cholesky factor,
In the unscented transform, when S is found via QR decomposition, we need to take the transpose because R is upper triangular,
P() and SetP() functions need to be modified to be P=S*S' instead of P=S'*S, and P.llt().matrixL() instead of P.llt().matrixU() respectively.
Each part of the algorithm has also had the comments changed to clarify exactly which equation from the paper it implements.
Co-authored-by: Tyler Veness <calcmogul@gmail.com>
2025-03-14 13:05:15 -04:00
|
|
|
observer.Correct<5>(u, globalY, DriveGlobalMeasurementModel, R,
|
2025-11-07 20:01:58 -05:00
|
|
|
wpi::math::AngleMean<5, 5 + 2>(2),
|
|
|
|
|
wpi::math::AngleResidual<5>(2),
|
2025-11-07 20:00:05 -05:00
|
|
|
wpi::math::AngleResidual<5>(2), wpi::math::AngleAdd<5>(2)
|
2020-11-28 17:35:35 -05:00
|
|
|
|
|
|
|
|
);
|
2020-08-14 23:40:33 -07:00
|
|
|
|
|
|
|
|
auto finalPosition = trajectory.Sample(trajectory.TotalTime());
|
2022-06-08 22:19:01 -07:00
|
|
|
EXPECT_NEAR(finalPosition.pose.Translation().X().value(), observer.Xhat(0),
|
|
|
|
|
0.055);
|
|
|
|
|
EXPECT_NEAR(finalPosition.pose.Translation().Y().value(), observer.Xhat(1),
|
|
|
|
|
0.15);
|
|
|
|
|
EXPECT_NEAR(finalPosition.pose.Rotation().Radians().value(), observer.Xhat(2),
|
2025-07-20 22:18:31 -07:00
|
|
|
0.00015);
|
2022-06-08 22:19:01 -07:00
|
|
|
EXPECT_NEAR(0.0, observer.Xhat(3), 0.1);
|
|
|
|
|
EXPECT_NEAR(0.0, observer.Xhat(4), 0.1);
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-20 22:18:31 -07:00
|
|
|
TEST(S3UKFTest, LinearUKF) {
|
2025-11-07 20:00:05 -05:00
|
|
|
constexpr wpi::units::second_t dt = 20_ms;
|
2025-11-07 20:01:58 -05:00
|
|
|
auto plant =
|
|
|
|
|
wpi::math::LinearSystemId::IdentifyVelocitySystem<wpi::units::meters>(
|
|
|
|
|
0.02_V / 1_mps, 0.006_V / 1_mps_sq);
|
2025-11-07 20:00:05 -05:00
|
|
|
wpi::math::S3UKF<1, 1, 1> observer{
|
|
|
|
|
[&](const wpi::math::Vectord<1>& x, const wpi::math::Vectord<1>& u) {
|
[wpimath] Fix UnscentedKalmanFilter and improve math docs (#7850)
Throughout the code the state sqrt covariance S and innovation covariance Sy are maintained as upper triangular cholesky factors of those covariance matrices. The original paper defines P=S*S', so S should be lower triangular. The functions in the paper reflect this. In the code implementation, the sqrt covariance matrices are upper triangular, but the algorithm expects them to be lower triangular.
This bug was likely missed because the incorrect version of the filter is able to converge for some systems where all the states are observed, and the test case is set up such that all states are observed.
To fix the bug, a couple things needed to be changed:
all instances of rankUpdate() needed to be changed to use the lower triangular cholesky factor,
In the unscented transform, when S is found via QR decomposition, we need to take the transpose because R is upper triangular,
P() and SetP() functions need to be modified to be P=S*S' instead of P=S'*S, and P.llt().matrixL() instead of P.llt().matrixU() respectively.
Each part of the algorithm has also had the comments changed to clarify exactly which equation from the paper it implements.
Co-authored-by: Tyler Veness <calcmogul@gmail.com>
2025-03-14 13:05:15 -04:00
|
|
|
return plant.A() * x + plant.B() * u;
|
|
|
|
|
},
|
2025-11-07 20:00:05 -05:00
|
|
|
[&](const wpi::math::Vectord<1>& x, const wpi::math::Vectord<1>& u) {
|
[wpimath] Fix UnscentedKalmanFilter and improve math docs (#7850)
Throughout the code the state sqrt covariance S and innovation covariance Sy are maintained as upper triangular cholesky factors of those covariance matrices. The original paper defines P=S*S', so S should be lower triangular. The functions in the paper reflect this. In the code implementation, the sqrt covariance matrices are upper triangular, but the algorithm expects them to be lower triangular.
This bug was likely missed because the incorrect version of the filter is able to converge for some systems where all the states are observed, and the test case is set up such that all states are observed.
To fix the bug, a couple things needed to be changed:
all instances of rankUpdate() needed to be changed to use the lower triangular cholesky factor,
In the unscented transform, when S is found via QR decomposition, we need to take the transpose because R is upper triangular,
P() and SetP() functions need to be modified to be P=S*S' instead of P=S'*S, and P.llt().matrixL() instead of P.llt().matrixU() respectively.
Each part of the algorithm has also had the comments changed to clarify exactly which equation from the paper it implements.
Co-authored-by: Tyler Veness <calcmogul@gmail.com>
2025-03-14 13:05:15 -04:00
|
|
|
return plant.CalculateY(x, u);
|
|
|
|
|
},
|
|
|
|
|
{0.05},
|
|
|
|
|
{1.0},
|
|
|
|
|
dt};
|
|
|
|
|
|
2025-11-07 20:00:05 -05:00
|
|
|
wpi::math::Matrixd<1, 1> discA;
|
|
|
|
|
wpi::math::Matrixd<1, 1> discB;
|
|
|
|
|
wpi::math::DiscretizeAB<1, 1>(plant.A(), plant.B(), dt, &discA, &discB);
|
[wpimath] Fix UnscentedKalmanFilter and improve math docs (#7850)
Throughout the code the state sqrt covariance S and innovation covariance Sy are maintained as upper triangular cholesky factors of those covariance matrices. The original paper defines P=S*S', so S should be lower triangular. The functions in the paper reflect this. In the code implementation, the sqrt covariance matrices are upper triangular, but the algorithm expects them to be lower triangular.
This bug was likely missed because the incorrect version of the filter is able to converge for some systems where all the states are observed, and the test case is set up such that all states are observed.
To fix the bug, a couple things needed to be changed:
all instances of rankUpdate() needed to be changed to use the lower triangular cholesky factor,
In the unscented transform, when S is found via QR decomposition, we need to take the transpose because R is upper triangular,
P() and SetP() functions need to be modified to be P=S*S' instead of P=S'*S, and P.llt().matrixL() instead of P.llt().matrixU() respectively.
Each part of the algorithm has also had the comments changed to clarify exactly which equation from the paper it implements.
Co-authored-by: Tyler Veness <calcmogul@gmail.com>
2025-03-14 13:05:15 -04:00
|
|
|
|
2025-11-07 20:00:05 -05:00
|
|
|
wpi::math::Vectord<1> ref{100.0};
|
|
|
|
|
wpi::math::Vectord<1> u{0.0};
|
[wpimath] Fix UnscentedKalmanFilter and improve math docs (#7850)
Throughout the code the state sqrt covariance S and innovation covariance Sy are maintained as upper triangular cholesky factors of those covariance matrices. The original paper defines P=S*S', so S should be lower triangular. The functions in the paper reflect this. In the code implementation, the sqrt covariance matrices are upper triangular, but the algorithm expects them to be lower triangular.
This bug was likely missed because the incorrect version of the filter is able to converge for some systems where all the states are observed, and the test case is set up such that all states are observed.
To fix the bug, a couple things needed to be changed:
all instances of rankUpdate() needed to be changed to use the lower triangular cholesky factor,
In the unscented transform, when S is found via QR decomposition, we need to take the transpose because R is upper triangular,
P() and SetP() functions need to be modified to be P=S*S' instead of P=S'*S, and P.llt().matrixL() instead of P.llt().matrixU() respectively.
Each part of the algorithm has also had the comments changed to clarify exactly which equation from the paper it implements.
Co-authored-by: Tyler Veness <calcmogul@gmail.com>
2025-03-14 13:05:15 -04:00
|
|
|
|
|
|
|
|
for (int i = 0; i < 2.0 / dt.value(); ++i) {
|
|
|
|
|
observer.Predict(u, dt);
|
|
|
|
|
|
|
|
|
|
u = discB.householderQr().solve(ref - discA * ref);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
EXPECT_NEAR(ref(0, 0), observer.Xhat(0), 5);
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-20 22:18:31 -07:00
|
|
|
TEST(S3UKFTest, RoundTripP) {
|
2022-06-08 22:19:01 -07:00
|
|
|
constexpr auto dt = 5_ms;
|
|
|
|
|
|
2025-11-07 20:00:05 -05:00
|
|
|
wpi::math::S3UKF<2, 2, 2> observer{
|
2025-11-07 20:01:58 -05:00
|
|
|
[](const wpi::math::Vectord<2>& x, const wpi::math::Vectord<2>& u) {
|
|
|
|
|
return x;
|
|
|
|
|
},
|
|
|
|
|
[](const wpi::math::Vectord<2>& x, const wpi::math::Vectord<2>& u) {
|
|
|
|
|
return x;
|
|
|
|
|
},
|
2022-06-08 22:19:01 -07:00
|
|
|
{0.0, 0.0},
|
|
|
|
|
{0.0, 0.0},
|
|
|
|
|
dt};
|
|
|
|
|
|
2025-11-07 20:00:05 -05:00
|
|
|
wpi::math::Matrixd<2, 2> P({{2, 1}, {1, 2}});
|
2022-06-08 22:19:01 -07:00
|
|
|
observer.SetP(P);
|
|
|
|
|
|
|
|
|
|
ASSERT_TRUE(observer.P().isApprox(P));
|
2020-08-14 23:40:33 -07:00
|
|
|
}
|
[wpimath] Fix UnscentedKalmanFilter and improve math docs (#7850)
Throughout the code the state sqrt covariance S and innovation covariance Sy are maintained as upper triangular cholesky factors of those covariance matrices. The original paper defines P=S*S', so S should be lower triangular. The functions in the paper reflect this. In the code implementation, the sqrt covariance matrices are upper triangular, but the algorithm expects them to be lower triangular.
This bug was likely missed because the incorrect version of the filter is able to converge for some systems where all the states are observed, and the test case is set up such that all states are observed.
To fix the bug, a couple things needed to be changed:
all instances of rankUpdate() needed to be changed to use the lower triangular cholesky factor,
In the unscented transform, when S is found via QR decomposition, we need to take the transpose because R is upper triangular,
P() and SetP() functions need to be modified to be P=S*S' instead of P=S'*S, and P.llt().matrixL() instead of P.llt().matrixU() respectively.
Each part of the algorithm has also had the comments changed to clarify exactly which equation from the paper it implements.
Co-authored-by: Tyler Veness <calcmogul@gmail.com>
2025-03-14 13:05:15 -04:00
|
|
|
|
|
|
|
|
// Second system, single motor feedforward estimator
|
2025-11-07 20:00:05 -05:00
|
|
|
wpi::math::Vectord<4> MotorDynamics(const wpi::math::Vectord<4>& x,
|
2025-11-07 20:01:58 -05:00
|
|
|
const wpi::math::Vectord<1>& u) {
|
[wpimath] Fix UnscentedKalmanFilter and improve math docs (#7850)
Throughout the code the state sqrt covariance S and innovation covariance Sy are maintained as upper triangular cholesky factors of those covariance matrices. The original paper defines P=S*S', so S should be lower triangular. The functions in the paper reflect this. In the code implementation, the sqrt covariance matrices are upper triangular, but the algorithm expects them to be lower triangular.
This bug was likely missed because the incorrect version of the filter is able to converge for some systems where all the states are observed, and the test case is set up such that all states are observed.
To fix the bug, a couple things needed to be changed:
all instances of rankUpdate() needed to be changed to use the lower triangular cholesky factor,
In the unscented transform, when S is found via QR decomposition, we need to take the transpose because R is upper triangular,
P() and SetP() functions need to be modified to be P=S*S' instead of P=S'*S, and P.llt().matrixL() instead of P.llt().matrixU() respectively.
Each part of the algorithm has also had the comments changed to clarify exactly which equation from the paper it implements.
Co-authored-by: Tyler Veness <calcmogul@gmail.com>
2025-03-14 13:05:15 -04:00
|
|
|
double v = x(1);
|
|
|
|
|
double kV = x(2);
|
|
|
|
|
double kA = x(3);
|
|
|
|
|
|
|
|
|
|
double V = u(0);
|
|
|
|
|
|
|
|
|
|
double a = -kV / kA * v + 1.0 / kA * V;
|
2025-11-07 20:00:05 -05:00
|
|
|
return wpi::math::Vectord<4>{v, a, 0.0, 0.0};
|
[wpimath] Fix UnscentedKalmanFilter and improve math docs (#7850)
Throughout the code the state sqrt covariance S and innovation covariance Sy are maintained as upper triangular cholesky factors of those covariance matrices. The original paper defines P=S*S', so S should be lower triangular. The functions in the paper reflect this. In the code implementation, the sqrt covariance matrices are upper triangular, but the algorithm expects them to be lower triangular.
This bug was likely missed because the incorrect version of the filter is able to converge for some systems where all the states are observed, and the test case is set up such that all states are observed.
To fix the bug, a couple things needed to be changed:
all instances of rankUpdate() needed to be changed to use the lower triangular cholesky factor,
In the unscented transform, when S is found via QR decomposition, we need to take the transpose because R is upper triangular,
P() and SetP() functions need to be modified to be P=S*S' instead of P=S'*S, and P.llt().matrixL() instead of P.llt().matrixU() respectively.
Each part of the algorithm has also had the comments changed to clarify exactly which equation from the paper it implements.
Co-authored-by: Tyler Veness <calcmogul@gmail.com>
2025-03-14 13:05:15 -04:00
|
|
|
}
|
|
|
|
|
|
2025-11-07 20:00:05 -05:00
|
|
|
wpi::math::Vectord<3> MotorMeasurementModel(const wpi::math::Vectord<4>& x,
|
2025-11-07 20:01:58 -05:00
|
|
|
const wpi::math::Vectord<1>& u) {
|
[wpimath] Fix UnscentedKalmanFilter and improve math docs (#7850)
Throughout the code the state sqrt covariance S and innovation covariance Sy are maintained as upper triangular cholesky factors of those covariance matrices. The original paper defines P=S*S', so S should be lower triangular. The functions in the paper reflect this. In the code implementation, the sqrt covariance matrices are upper triangular, but the algorithm expects them to be lower triangular.
This bug was likely missed because the incorrect version of the filter is able to converge for some systems where all the states are observed, and the test case is set up such that all states are observed.
To fix the bug, a couple things needed to be changed:
all instances of rankUpdate() needed to be changed to use the lower triangular cholesky factor,
In the unscented transform, when S is found via QR decomposition, we need to take the transpose because R is upper triangular,
P() and SetP() functions need to be modified to be P=S*S' instead of P=S'*S, and P.llt().matrixL() instead of P.llt().matrixU() respectively.
Each part of the algorithm has also had the comments changed to clarify exactly which equation from the paper it implements.
Co-authored-by: Tyler Veness <calcmogul@gmail.com>
2025-03-14 13:05:15 -04:00
|
|
|
double p = x(0);
|
|
|
|
|
double v = x(1);
|
|
|
|
|
double kV = x(2);
|
|
|
|
|
double kA = x(3);
|
|
|
|
|
|
|
|
|
|
double V = u(0);
|
|
|
|
|
|
|
|
|
|
double a = -kV / kA * v + 1.0 / kA * V;
|
2025-11-07 20:00:05 -05:00
|
|
|
return wpi::math::Vectord<3>{p, v, a};
|
[wpimath] Fix UnscentedKalmanFilter and improve math docs (#7850)
Throughout the code the state sqrt covariance S and innovation covariance Sy are maintained as upper triangular cholesky factors of those covariance matrices. The original paper defines P=S*S', so S should be lower triangular. The functions in the paper reflect this. In the code implementation, the sqrt covariance matrices are upper triangular, but the algorithm expects them to be lower triangular.
This bug was likely missed because the incorrect version of the filter is able to converge for some systems where all the states are observed, and the test case is set up such that all states are observed.
To fix the bug, a couple things needed to be changed:
all instances of rankUpdate() needed to be changed to use the lower triangular cholesky factor,
In the unscented transform, when S is found via QR decomposition, we need to take the transpose because R is upper triangular,
P() and SetP() functions need to be modified to be P=S*S' instead of P=S'*S, and P.llt().matrixL() instead of P.llt().matrixU() respectively.
Each part of the algorithm has also had the comments changed to clarify exactly which equation from the paper it implements.
Co-authored-by: Tyler Veness <calcmogul@gmail.com>
2025-03-14 13:05:15 -04:00
|
|
|
}
|
|
|
|
|
|
2025-11-07 20:00:05 -05:00
|
|
|
wpi::math::Vectord<1> MotorControlInput(double t) {
|
|
|
|
|
return wpi::math::Vectord<1>{
|
[wpimath] Fix UnscentedKalmanFilter and improve math docs (#7850)
Throughout the code the state sqrt covariance S and innovation covariance Sy are maintained as upper triangular cholesky factors of those covariance matrices. The original paper defines P=S*S', so S should be lower triangular. The functions in the paper reflect this. In the code implementation, the sqrt covariance matrices are upper triangular, but the algorithm expects them to be lower triangular.
This bug was likely missed because the incorrect version of the filter is able to converge for some systems where all the states are observed, and the test case is set up such that all states are observed.
To fix the bug, a couple things needed to be changed:
all instances of rankUpdate() needed to be changed to use the lower triangular cholesky factor,
In the unscented transform, when S is found via QR decomposition, we need to take the transpose because R is upper triangular,
P() and SetP() functions need to be modified to be P=S*S' instead of P=S'*S, and P.llt().matrixL() instead of P.llt().matrixU() respectively.
Each part of the algorithm has also had the comments changed to clarify exactly which equation from the paper it implements.
Co-authored-by: Tyler Veness <calcmogul@gmail.com>
2025-03-14 13:05:15 -04:00
|
|
|
std::clamp(8 * std::sin(std::numbers::pi * std::sqrt(2.0) * t) +
|
|
|
|
|
6 * std::sin(std::numbers::pi * std::sqrt(3.0) * t) +
|
|
|
|
|
4 * std::sin(std::numbers::pi * std::sqrt(5.0) * t),
|
|
|
|
|
-12.0, 12.0)};
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-20 22:18:31 -07:00
|
|
|
TEST(S3UKFTest, MotorConvergence) {
|
2025-11-07 20:00:05 -05:00
|
|
|
constexpr wpi::units::second_t dt = 10_ms;
|
[wpimath] Fix UnscentedKalmanFilter and improve math docs (#7850)
Throughout the code the state sqrt covariance S and innovation covariance Sy are maintained as upper triangular cholesky factors of those covariance matrices. The original paper defines P=S*S', so S should be lower triangular. The functions in the paper reflect this. In the code implementation, the sqrt covariance matrices are upper triangular, but the algorithm expects them to be lower triangular.
This bug was likely missed because the incorrect version of the filter is able to converge for some systems where all the states are observed, and the test case is set up such that all states are observed.
To fix the bug, a couple things needed to be changed:
all instances of rankUpdate() needed to be changed to use the lower triangular cholesky factor,
In the unscented transform, when S is found via QR decomposition, we need to take the transpose because R is upper triangular,
P() and SetP() functions need to be modified to be P=S*S' instead of P=S'*S, and P.llt().matrixL() instead of P.llt().matrixU() respectively.
Each part of the algorithm has also had the comments changed to clarify exactly which equation from the paper it implements.
Co-authored-by: Tyler Veness <calcmogul@gmail.com>
2025-03-14 13:05:15 -04:00
|
|
|
constexpr int steps = 500;
|
|
|
|
|
constexpr double true_kV = 3;
|
|
|
|
|
constexpr double true_kA = 0.2;
|
|
|
|
|
|
|
|
|
|
constexpr double pos_stddev = 0.02;
|
|
|
|
|
constexpr double vel_stddev = 0.1;
|
|
|
|
|
constexpr double accel_stddev = 0.1;
|
|
|
|
|
|
2025-11-07 20:00:05 -05:00
|
|
|
std::vector<wpi::math::Vectord<4>> states(steps + 1);
|
|
|
|
|
std::vector<wpi::math::Vectord<1>> inputs(steps);
|
|
|
|
|
std::vector<wpi::math::Vectord<3>> measurements(steps);
|
|
|
|
|
states[0] = wpi::math::Vectord<4>{{0.0}, {0.0}, {true_kV}, {true_kA}};
|
[wpimath] Fix UnscentedKalmanFilter and improve math docs (#7850)
Throughout the code the state sqrt covariance S and innovation covariance Sy are maintained as upper triangular cholesky factors of those covariance matrices. The original paper defines P=S*S', so S should be lower triangular. The functions in the paper reflect this. In the code implementation, the sqrt covariance matrices are upper triangular, but the algorithm expects them to be lower triangular.
This bug was likely missed because the incorrect version of the filter is able to converge for some systems where all the states are observed, and the test case is set up such that all states are observed.
To fix the bug, a couple things needed to be changed:
all instances of rankUpdate() needed to be changed to use the lower triangular cholesky factor,
In the unscented transform, when S is found via QR decomposition, we need to take the transpose because R is upper triangular,
P() and SetP() functions need to be modified to be P=S*S' instead of P=S'*S, and P.llt().matrixL() instead of P.llt().matrixU() respectively.
Each part of the algorithm has also had the comments changed to clarify exactly which equation from the paper it implements.
Co-authored-by: Tyler Veness <calcmogul@gmail.com>
2025-03-14 13:05:15 -04:00
|
|
|
|
2025-11-07 20:00:05 -05:00
|
|
|
constexpr wpi::math::Matrixd<4, 4> A{{0.0, 1.0, 0.0, 0.0},
|
2025-11-07 20:01:58 -05:00
|
|
|
{0.0, -true_kV / true_kA, 0.0, 0.0},
|
|
|
|
|
{0.0, 0.0, 0.0, 0.0},
|
|
|
|
|
{0.0, 0.0, 0.0, 0.0}};
|
2025-11-07 20:00:05 -05:00
|
|
|
constexpr wpi::math::Matrixd<4, 1> B{{0.0}, {1.0 / true_kA}, {0.0}, {0.0}};
|
[wpimath] Fix UnscentedKalmanFilter and improve math docs (#7850)
Throughout the code the state sqrt covariance S and innovation covariance Sy are maintained as upper triangular cholesky factors of those covariance matrices. The original paper defines P=S*S', so S should be lower triangular. The functions in the paper reflect this. In the code implementation, the sqrt covariance matrices are upper triangular, but the algorithm expects them to be lower triangular.
This bug was likely missed because the incorrect version of the filter is able to converge for some systems where all the states are observed, and the test case is set up such that all states are observed.
To fix the bug, a couple things needed to be changed:
all instances of rankUpdate() needed to be changed to use the lower triangular cholesky factor,
In the unscented transform, when S is found via QR decomposition, we need to take the transpose because R is upper triangular,
P() and SetP() functions need to be modified to be P=S*S' instead of P=S'*S, and P.llt().matrixL() instead of P.llt().matrixU() respectively.
Each part of the algorithm has also had the comments changed to clarify exactly which equation from the paper it implements.
Co-authored-by: Tyler Veness <calcmogul@gmail.com>
2025-03-14 13:05:15 -04:00
|
|
|
|
2025-11-07 20:00:05 -05:00
|
|
|
wpi::math::Matrixd<4, 4> discA;
|
|
|
|
|
wpi::math::Matrixd<4, 1> discB;
|
|
|
|
|
wpi::math::DiscretizeAB(A, B, dt, &discA, &discB);
|
[wpimath] Fix UnscentedKalmanFilter and improve math docs (#7850)
Throughout the code the state sqrt covariance S and innovation covariance Sy are maintained as upper triangular cholesky factors of those covariance matrices. The original paper defines P=S*S', so S should be lower triangular. The functions in the paper reflect this. In the code implementation, the sqrt covariance matrices are upper triangular, but the algorithm expects them to be lower triangular.
This bug was likely missed because the incorrect version of the filter is able to converge for some systems where all the states are observed, and the test case is set up such that all states are observed.
To fix the bug, a couple things needed to be changed:
all instances of rankUpdate() needed to be changed to use the lower triangular cholesky factor,
In the unscented transform, when S is found via QR decomposition, we need to take the transpose because R is upper triangular,
P() and SetP() functions need to be modified to be P=S*S' instead of P=S'*S, and P.llt().matrixL() instead of P.llt().matrixU() respectively.
Each part of the algorithm has also had the comments changed to clarify exactly which equation from the paper it implements.
Co-authored-by: Tyler Veness <calcmogul@gmail.com>
2025-03-14 13:05:15 -04:00
|
|
|
|
|
|
|
|
for (int i = 0; i < steps; ++i) {
|
|
|
|
|
inputs[i] = MotorControlInput(i * dt.value());
|
|
|
|
|
states[i + 1] = discA * states[i] + discB * inputs[i];
|
|
|
|
|
measurements[i] =
|
|
|
|
|
MotorMeasurementModel(states[i + 1], inputs[i]) +
|
2025-11-07 20:00:05 -05:00
|
|
|
wpi::math::MakeWhiteNoiseVector(pos_stddev, vel_stddev, accel_stddev);
|
[wpimath] Fix UnscentedKalmanFilter and improve math docs (#7850)
Throughout the code the state sqrt covariance S and innovation covariance Sy are maintained as upper triangular cholesky factors of those covariance matrices. The original paper defines P=S*S', so S should be lower triangular. The functions in the paper reflect this. In the code implementation, the sqrt covariance matrices are upper triangular, but the algorithm expects them to be lower triangular.
This bug was likely missed because the incorrect version of the filter is able to converge for some systems where all the states are observed, and the test case is set up such that all states are observed.
To fix the bug, a couple things needed to be changed:
all instances of rankUpdate() needed to be changed to use the lower triangular cholesky factor,
In the unscented transform, when S is found via QR decomposition, we need to take the transpose because R is upper triangular,
P() and SetP() functions need to be modified to be P=S*S' instead of P=S'*S, and P.llt().matrixL() instead of P.llt().matrixU() respectively.
Each part of the algorithm has also had the comments changed to clarify exactly which equation from the paper it implements.
Co-authored-by: Tyler Veness <calcmogul@gmail.com>
2025-03-14 13:05:15 -04:00
|
|
|
}
|
|
|
|
|
|
2025-11-07 20:00:05 -05:00
|
|
|
wpi::math::Vectord<4> P0{0.001, 0.001, 10, 10};
|
[wpimath] Fix UnscentedKalmanFilter and improve math docs (#7850)
Throughout the code the state sqrt covariance S and innovation covariance Sy are maintained as upper triangular cholesky factors of those covariance matrices. The original paper defines P=S*S', so S should be lower triangular. The functions in the paper reflect this. In the code implementation, the sqrt covariance matrices are upper triangular, but the algorithm expects them to be lower triangular.
This bug was likely missed because the incorrect version of the filter is able to converge for some systems where all the states are observed, and the test case is set up such that all states are observed.
To fix the bug, a couple things needed to be changed:
all instances of rankUpdate() needed to be changed to use the lower triangular cholesky factor,
In the unscented transform, when S is found via QR decomposition, we need to take the transpose because R is upper triangular,
P() and SetP() functions need to be modified to be P=S*S' instead of P=S'*S, and P.llt().matrixL() instead of P.llt().matrixU() respectively.
Each part of the algorithm has also had the comments changed to clarify exactly which equation from the paper it implements.
Co-authored-by: Tyler Veness <calcmogul@gmail.com>
2025-03-14 13:05:15 -04:00
|
|
|
|
2025-11-07 20:00:05 -05:00
|
|
|
wpi::math::S3UKF<4, 1, 3> observer{
|
2025-11-07 20:01:58 -05:00
|
|
|
MotorDynamics, MotorMeasurementModel,
|
|
|
|
|
wpi::util::array{0.1, 1.0, 1e-10, 1e-10},
|
2025-11-07 20:00:05 -05:00
|
|
|
wpi::util::array{pos_stddev, vel_stddev, accel_stddev}, dt};
|
[wpimath] Fix UnscentedKalmanFilter and improve math docs (#7850)
Throughout the code the state sqrt covariance S and innovation covariance Sy are maintained as upper triangular cholesky factors of those covariance matrices. The original paper defines P=S*S', so S should be lower triangular. The functions in the paper reflect this. In the code implementation, the sqrt covariance matrices are upper triangular, but the algorithm expects them to be lower triangular.
This bug was likely missed because the incorrect version of the filter is able to converge for some systems where all the states are observed, and the test case is set up such that all states are observed.
To fix the bug, a couple things needed to be changed:
all instances of rankUpdate() needed to be changed to use the lower triangular cholesky factor,
In the unscented transform, when S is found via QR decomposition, we need to take the transpose because R is upper triangular,
P() and SetP() functions need to be modified to be P=S*S' instead of P=S'*S, and P.llt().matrixL() instead of P.llt().matrixU() respectively.
Each part of the algorithm has also had the comments changed to clarify exactly which equation from the paper it implements.
Co-authored-by: Tyler Veness <calcmogul@gmail.com>
2025-03-14 13:05:15 -04:00
|
|
|
|
2025-11-07 20:00:05 -05:00
|
|
|
observer.SetXhat(wpi::math::Vectord<4>{0.0, 0.0, 2.0, 2.0});
|
[wpimath] Fix UnscentedKalmanFilter and improve math docs (#7850)
Throughout the code the state sqrt covariance S and innovation covariance Sy are maintained as upper triangular cholesky factors of those covariance matrices. The original paper defines P=S*S', so S should be lower triangular. The functions in the paper reflect this. In the code implementation, the sqrt covariance matrices are upper triangular, but the algorithm expects them to be lower triangular.
This bug was likely missed because the incorrect version of the filter is able to converge for some systems where all the states are observed, and the test case is set up such that all states are observed.
To fix the bug, a couple things needed to be changed:
all instances of rankUpdate() needed to be changed to use the lower triangular cholesky factor,
In the unscented transform, when S is found via QR decomposition, we need to take the transpose because R is upper triangular,
P() and SetP() functions need to be modified to be P=S*S' instead of P=S'*S, and P.llt().matrixL() instead of P.llt().matrixU() respectively.
Each part of the algorithm has also had the comments changed to clarify exactly which equation from the paper it implements.
Co-authored-by: Tyler Veness <calcmogul@gmail.com>
2025-03-14 13:05:15 -04:00
|
|
|
observer.SetP(P0.asDiagonal());
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < steps; ++i) {
|
|
|
|
|
observer.Predict(inputs[i], dt);
|
|
|
|
|
observer.Correct(inputs[i], measurements[i]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
EXPECT_NEAR(true_kV, observer.Xhat(2), true_kV * 0.5);
|
|
|
|
|
EXPECT_NEAR(true_kA, observer.Xhat(3), true_kA * 0.5);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
} // namespace
|