diff --git a/wpimath/src/main/java/edu/wpi/first/math/estimator/MerweScaledSigmaPoints.java b/wpimath/src/main/java/edu/wpi/first/math/estimator/MerweScaledSigmaPoints.java
index d3131a83e9..d159af5d19 100644
--- a/wpimath/src/main/java/edu/wpi/first/math/estimator/MerweScaledSigmaPoints.java
+++ b/wpimath/src/main/java/edu/wpi/first/math/estimator/MerweScaledSigmaPoints.java
@@ -15,7 +15,8 @@ import org.ejml.simple.SimpleMatrix;
* UnscentedKalmanFilter class.
*
*
It parametrizes the sigma points using alpha, beta, kappa terms, and is the version seen in
- * most publications. Unless you know better, this should be your default choice.
+ * most publications. S3SigmaPoints is generally preferred due to its greater performance with
+ * nearly identical accuracy.
*
*
States is the dimensionality of the state. 2*States+1 weights will be generated.
*
@@ -24,7 +25,7 @@ import org.ejml.simple.SimpleMatrix;
*
* @param The dimensionality of the state. 2 * States + 1 weights will be generated.
*/
-public class MerweScaledSigmaPoints {
+public class MerweScaledSigmaPoints implements SigmaPoints {
private final double m_alpha;
private final int m_kappa;
private final Nat m_states;
@@ -64,6 +65,7 @@ public class MerweScaledSigmaPoints {
*
* @return The number of sigma points for each variable in the state x.
*/
+ @Override
public int getNumSigmas() {
return 2 * m_states.getNum() + 1;
}
@@ -77,6 +79,7 @@ public class MerweScaledSigmaPoints {
* @return Two-dimensional array of sigma points. Each column contains all the sigmas for one
* dimension in the problem space. Ordered by Xi_0, Xi_{1..n}, Xi_{n+1..2n}.
*/
+ @Override
public Matrix squareRootSigmaPoints(Matrix x, Matrix s) {
double lambda = Math.pow(m_alpha, 2) * (m_states.getNum() + m_kappa) - m_states.getNum();
double eta = Math.sqrt(lambda + m_states.getNum());
@@ -125,6 +128,7 @@ public class MerweScaledSigmaPoints {
*
* @return the weight for each sigma point for the mean.
*/
+ @Override
public Matrix, N1> getWm() {
return m_wm;
}
@@ -135,6 +139,7 @@ public class MerweScaledSigmaPoints {
* @param element Element of vector to return.
* @return the element i's weight for the mean.
*/
+ @Override
public double getWm(int element) {
return m_wm.get(element, 0);
}
@@ -144,6 +149,7 @@ public class MerweScaledSigmaPoints {
*
* @return the weight for each sigma point for the covariance.
*/
+ @Override
public Matrix, N1> getWc() {
return m_wc;
}
@@ -154,6 +160,7 @@ public class MerweScaledSigmaPoints {
* @param element Element of vector to return.
* @return The element I's weight for the covariance.
*/
+ @Override
public double getWc(int element) {
return m_wc.get(element, 0);
}
diff --git a/wpimath/src/main/java/edu/wpi/first/math/estimator/MerweUKF.java b/wpimath/src/main/java/edu/wpi/first/math/estimator/MerweUKF.java
new file mode 100644
index 0000000000..d34ebe1ff9
--- /dev/null
+++ b/wpimath/src/main/java/edu/wpi/first/math/estimator/MerweUKF.java
@@ -0,0 +1,117 @@
+// 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.
+
+package edu.wpi.first.math.estimator;
+
+import edu.wpi.first.math.Matrix;
+import edu.wpi.first.math.Nat;
+import edu.wpi.first.math.Num;
+import edu.wpi.first.math.numbers.N1;
+import java.util.function.BiFunction;
+
+/**
+ * An Unscented Kalman Filter using sigma points and weights from Van der Merwe's 2004 dissertation.
+ * S3UKF is generally preferred due to its greater performance with nearly identical accuracy.
+ *
+ * @param Number of states.
+ * @param Number of inputs.
+ * @param Number of outputs.
+ */
+public class MerweUKF
+ extends UnscentedKalmanFilter {
+ /**
+ * Constructs a Merwe Unscented Kalman Filter.
+ *
+ * See https://docs.wpilib.org/en/stable/docs/software/advanced-controls/state-space/state-space-observers.html#process-and-measurement-noise-covariance-matrices
+ * for how to select the standard deviations.
+ *
+ * @param states A Nat representing the number of states.
+ * @param outputs A Nat representing the number of outputs.
+ * @param f A vector-valued function of x and u that returns the derivative of the state vector.
+ * @param h A vector-valued function of x and u that returns the measurement vector.
+ * @param stateStdDevs Standard deviations of model states.
+ * @param measurementStdDevs Standard deviations of measurements.
+ * @param nominalDt Nominal discretization timestep in seconds.
+ */
+ public MerweUKF(
+ Nat states,
+ Nat outputs,
+ BiFunction, Matrix, Matrix> f,
+ BiFunction, Matrix, Matrix> h,
+ Matrix stateStdDevs,
+ Matrix measurementStdDevs,
+ double nominalDt) {
+ super(
+ new MerweScaledSigmaPoints<>(states),
+ states,
+ outputs,
+ f,
+ h,
+ stateStdDevs,
+ measurementStdDevs,
+ (sigmas, Wm) -> sigmas.times(Matrix.changeBoundsUnchecked(Wm)),
+ (sigmas, Wm) -> sigmas.times(Matrix.changeBoundsUnchecked(Wm)),
+ Matrix::minus,
+ Matrix::minus,
+ Matrix::plus,
+ nominalDt);
+ }
+
+ /**
+ * Constructs a Merwe Unscented Kalman filter with custom mean, residual, and addition functions.
+ * Using custom functions for arithmetic can be useful if you have angles in the state or
+ * measurements, because they allow you to correctly account for the modular nature of angle
+ * arithmetic.
+ *
+ * See https://docs.wpilib.org/en/stable/docs/software/advanced-controls/state-space/state-space-observers.html#process-and-measurement-noise-covariance-matrices
+ * for how to select the standard deviations.
+ *
+ * @param states A Nat representing the number of states.
+ * @param outputs A Nat representing the number of outputs.
+ * @param f A vector-valued function of x and u that returns the derivative of the state vector.
+ * @param h A vector-valued function of x and u that returns the measurement vector.
+ * @param stateStdDevs Standard deviations of model states.
+ * @param measurementStdDevs Standard deviations of measurements.
+ * @param meanFuncX A function that computes the mean of NumSigmas state vectors using a given set
+ * of weights.
+ * @param meanFuncY A function that computes the mean of NumSigmas measurement vectors using a
+ * given set of weights.
+ * @param residualFuncX A function that computes the residual of two state vectors (i.e. it
+ * subtracts them.)
+ * @param residualFuncY A function that computes the residual of two measurement vectors (i.e. it
+ * subtracts them.)
+ * @param addFuncX A function that adds two state vectors.
+ * @param nominalDt Nominal discretization timestep in seconds.
+ */
+ public MerweUKF(
+ Nat states,
+ Nat outputs,
+ BiFunction, Matrix, Matrix> f,
+ BiFunction, Matrix, Matrix> h,
+ Matrix stateStdDevs,
+ Matrix measurementStdDevs,
+ BiFunction, Matrix, N1>, Matrix> meanFuncX,
+ BiFunction, Matrix, N1>, Matrix> meanFuncY,
+ BiFunction, Matrix, Matrix> residualFuncX,
+ BiFunction, Matrix, Matrix> residualFuncY,
+ BiFunction, Matrix, Matrix> addFuncX,
+ double nominalDt) {
+ super(
+ new MerweScaledSigmaPoints<>(states),
+ states,
+ outputs,
+ f,
+ h,
+ stateStdDevs,
+ measurementStdDevs,
+ meanFuncX,
+ meanFuncY,
+ residualFuncX,
+ residualFuncY,
+ addFuncX,
+ nominalDt);
+ }
+}
diff --git a/wpimath/src/main/java/edu/wpi/first/math/estimator/S3SigmaPoints.java b/wpimath/src/main/java/edu/wpi/first/math/estimator/S3SigmaPoints.java
new file mode 100644
index 0000000000..430296e109
--- /dev/null
+++ b/wpimath/src/main/java/edu/wpi/first/math/estimator/S3SigmaPoints.java
@@ -0,0 +1,172 @@
+// 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.
+
+package edu.wpi.first.math.estimator;
+
+import edu.wpi.first.math.Matrix;
+import edu.wpi.first.math.Nat;
+import edu.wpi.first.math.Num;
+import edu.wpi.first.math.numbers.N1;
+import org.ejml.simple.SimpleMatrix;
+
+/**
+ * Generates sigma points and weights according to Papakonstantinou's paper[1] for the
+ * UnscentedKalmanFilter class.
+ *
+ * It parameterizes the sigma points using alpha and beta terms. Unless you know better, this
+ * should be your default choice due to its high accuracy and performance.
+ *
+ *
[1] K. Papakonstantinou "A Scaled Spherical Simplex Filter (S3F) with a decreased n + 2 sigma
+ * points set size and equivalent 2n + 1 Unscented Kalman Filter (UKF) accuracy"
+ *
+ * @param The dimenstionality of the state. States + 2 weights will be generated.
+ */
+public class S3SigmaPoints implements SigmaPoints {
+ private final Nat m_states;
+ private final double m_alpha;
+ private Matrix, N1> m_wm;
+ private Matrix, N1> m_wc;
+
+ /**
+ * Constructs a generator for Papakonstantinou sigma points.
+ *
+ * @param states an instance of Num that represents the number of states.
+ * @param alpha Determines the spread of the sigma points around the mean. Usually a small
+ * positive value (1e-3).
+ * @param beta Incorporates prior knowledge of the distribution of the mean. For Gaussian
+ * distributions, beta = 2 is optimal.
+ */
+ @SuppressWarnings("this-escape")
+ public S3SigmaPoints(Nat states, double alpha, double beta) {
+ m_states = states;
+ m_alpha = alpha;
+
+ computeWeights(beta);
+ }
+
+ /**
+ * Constructs a generator for Papakonstantinou sigma points with default values for alpha, beta,
+ * and kappa.
+ *
+ * @param states an instance of Num that represents the number of states.
+ */
+ public S3SigmaPoints(Nat states) {
+ this(states, 1e-3, 2);
+ }
+
+ /**
+ * Returns number of sigma points for each variable in the state x.
+ *
+ * @return The number of sigma points for each variable in the state x.
+ */
+ @Override
+ public int getNumSigmas() {
+ return m_states.getNum() + 2;
+ }
+
+ /**
+ * Computes the sigma points for an unscented Kalman filter given the mean (x) and square-root
+ * covariance (s) of the filter.
+ *
+ * @param x An array of the means.
+ * @param s Square-root covariance of the filter.
+ * @return Two-dimensional array of sigma points. Each column contains all the sigmas for one
+ * dimension in the problem space. Ordered by Xi_0, Xi_{1..n}, Xi_{n+1..2n}.
+ */
+ @Override
+ public Matrix squareRootSigmaPoints(Matrix x, Matrix s) {
+ // table (1), equation (12)
+ double[] q = new double[m_states.getNum()];
+ for (int t = 1; t <= m_states.getNum(); ++t) {
+ q[t - 1] = m_alpha * Math.sqrt(t * (m_states.getNum() + 1) / (double) (t + 1));
+ }
+
+ Matrix C = new Matrix<>(new SimpleMatrix(m_states.getNum(), getNumSigmas()));
+ for (int row = 0; row < m_states.getNum(); ++row) {
+ C.set(row, 0, 0.0);
+ }
+ for (int col = 1; col < getNumSigmas(); ++col) {
+ for (int row = 0; row < m_states.getNum(); ++row) {
+ if (row < col - 2) {
+ C.set(row, col, 0.0);
+ } else if (row == col - 2) {
+ C.set(row, col, q[row]);
+ } else {
+ C.set(row, col, -q[row] / (row + 1));
+ }
+ }
+ }
+
+ Matrix sigmas = new Matrix<>(new SimpleMatrix(m_states.getNum(), getNumSigmas()));
+ for (int col = 0; col < getNumSigmas(); ++col) {
+ sigmas.setColumn(col, x.plus(s.times(C.extractColumnVector(col))));
+ }
+
+ return sigmas;
+ }
+
+ /**
+ * Computes the weights for the scaled unscented Kalman filter.
+ *
+ * @param beta Incorporates prior knowledge of the distribution of the mean.
+ */
+ private void computeWeights(double beta) {
+ double alpha_sq = m_alpha * m_alpha;
+
+ double c = 1.0 / (alpha_sq * (m_states.getNum() + 1));
+
+ Matrix, N1> wM = new Matrix<>(new SimpleMatrix(getNumSigmas(), 1));
+ Matrix, N1> wC = new Matrix<>(new SimpleMatrix(getNumSigmas(), 1));
+ wM.fill(c);
+ wC.fill(c);
+
+ wM.set(0, 0, 1.0 - 1.0 / alpha_sq);
+ wC.set(0, 0, 1.0 - 1.0 / alpha_sq + (1 - alpha_sq + beta));
+
+ this.m_wm = wM;
+ this.m_wc = wC;
+ }
+
+ /**
+ * Returns the weight for each sigma point for the mean.
+ *
+ * @return the weight for each sigma point for the mean.
+ */
+ @Override
+ public Matrix, N1> getWm() {
+ return m_wm;
+ }
+
+ /**
+ * Returns an element of the weight for each sigma point for the mean.
+ *
+ * @param element Element of vector to return.
+ * @return the element i's weight for the mean.
+ */
+ @Override
+ public double getWm(int element) {
+ return m_wm.get(element, 0);
+ }
+
+ /**
+ * Returns the weight for each sigma point for the covariance.
+ *
+ * @return the weight for each sigma point for the covariance.
+ */
+ @Override
+ public Matrix, N1> getWc() {
+ return m_wc;
+ }
+
+ /**
+ * Returns an element of the weight for each sigma point for the covariance.
+ *
+ * @param element Element of vector to return.
+ * @return The element I's weight for the covariance.
+ */
+ @Override
+ public double getWc(int element) {
+ return m_wc.get(element, 0);
+ }
+}
diff --git a/wpimath/src/main/java/edu/wpi/first/math/estimator/S3UKF.java b/wpimath/src/main/java/edu/wpi/first/math/estimator/S3UKF.java
new file mode 100644
index 0000000000..4de46faee3
--- /dev/null
+++ b/wpimath/src/main/java/edu/wpi/first/math/estimator/S3UKF.java
@@ -0,0 +1,117 @@
+// 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.
+
+package edu.wpi.first.math.estimator;
+
+import edu.wpi.first.math.Matrix;
+import edu.wpi.first.math.Nat;
+import edu.wpi.first.math.Num;
+import edu.wpi.first.math.numbers.N1;
+import java.util.function.BiFunction;
+
+/**
+ * An Unscented Kalman Filter using sigma points and weights from Papakonstantinou's paper. This is
+ * generally preferred over other UKF variants due to its high accuracy and performance.
+ *
+ * @param Number of states.
+ * @param Number of inputs.
+ * @param Number of outputs.
+ */
+public class S3UKF
+ extends UnscentedKalmanFilter {
+ /**
+ * Constructs a Scaled Spherical Simplex (S3) Unscented Kalman Filter.
+ *
+ * See https://docs.wpilib.org/en/stable/docs/software/advanced-controls/state-space/state-space-observers.html#process-and-measurement-noise-covariance-matrices
+ * for how to select the standard deviations.
+ *
+ * @param states A Nat representing the number of states.
+ * @param outputs A Nat representing the number of outputs.
+ * @param f A vector-valued function of x and u that returns the derivative of the state vector.
+ * @param h A vector-valued function of x and u that returns the measurement vector.
+ * @param stateStdDevs Standard deviations of model states.
+ * @param measurementStdDevs Standard deviations of measurements.
+ * @param nominalDt Nominal discretization timestep in seconds.
+ */
+ public S3UKF(
+ Nat states,
+ Nat outputs,
+ BiFunction, Matrix, Matrix> f,
+ BiFunction, Matrix, Matrix> h,
+ Matrix stateStdDevs,
+ Matrix measurementStdDevs,
+ double nominalDt) {
+ super(
+ new S3SigmaPoints<>(states),
+ states,
+ outputs,
+ f,
+ h,
+ stateStdDevs,
+ measurementStdDevs,
+ (sigmas, Wm) -> sigmas.times(Matrix.changeBoundsUnchecked(Wm)),
+ (sigmas, Wm) -> sigmas.times(Matrix.changeBoundsUnchecked(Wm)),
+ Matrix::minus,
+ Matrix::minus,
+ Matrix::plus,
+ nominalDt);
+ }
+
+ /**
+ * Constructs a Scaled Spherical Simplex (S3) Unscented Kalman filter with custom mean, residual,
+ * and addition functions. Using custom functions for arithmetic can be useful if you have angles
+ * in the state or measurements, because they allow you to correctly account for the modular
+ * nature of angle arithmetic.
+ *
+ * See https://docs.wpilib.org/en/stable/docs/software/advanced-controls/state-space/state-space-observers.html#process-and-measurement-noise-covariance-matrices
+ * for how to select the standard deviations.
+ *
+ * @param states A Nat representing the number of states.
+ * @param outputs A Nat representing the number of outputs.
+ * @param f A vector-valued function of x and u that returns the derivative of the state vector.
+ * @param h A vector-valued function of x and u that returns the measurement vector.
+ * @param stateStdDevs Standard deviations of model states.
+ * @param measurementStdDevs Standard deviations of measurements.
+ * @param meanFuncX A function that computes the mean of NumSigmas state vectors using a given set
+ * of weights.
+ * @param meanFuncY A function that computes the mean of NumSigmas measurement vectors using a
+ * given set of weights.
+ * @param residualFuncX A function that computes the residual of two state vectors (i.e. it
+ * subtracts them.)
+ * @param residualFuncY A function that computes the residual of two measurement vectors (i.e. it
+ * subtracts them.)
+ * @param addFuncX A function that adds two state vectors.
+ * @param nominalDt Nominal discretization timestep in seconds.
+ */
+ public S3UKF(
+ Nat states,
+ Nat outputs,
+ BiFunction, Matrix, Matrix> f,
+ BiFunction, Matrix, Matrix> h,
+ Matrix stateStdDevs,
+ Matrix measurementStdDevs,
+ BiFunction, Matrix, N1>, Matrix> meanFuncX,
+ BiFunction, Matrix, N1>, Matrix> meanFuncY,
+ BiFunction, Matrix, Matrix> residualFuncX,
+ BiFunction, Matrix, Matrix> residualFuncY,
+ BiFunction, Matrix, Matrix> addFuncX,
+ double nominalDt) {
+ super(
+ new S3SigmaPoints<>(states),
+ states,
+ outputs,
+ f,
+ h,
+ stateStdDevs,
+ measurementStdDevs,
+ meanFuncX,
+ meanFuncY,
+ residualFuncX,
+ residualFuncY,
+ addFuncX,
+ nominalDt);
+ }
+}
diff --git a/wpimath/src/main/java/edu/wpi/first/math/estimator/SigmaPoints.java b/wpimath/src/main/java/edu/wpi/first/math/estimator/SigmaPoints.java
new file mode 100644
index 0000000000..cb0ca2334b
--- /dev/null
+++ b/wpimath/src/main/java/edu/wpi/first/math/estimator/SigmaPoints.java
@@ -0,0 +1,64 @@
+// 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.
+
+package edu.wpi.first.math.estimator;
+
+import edu.wpi.first.math.Matrix;
+import edu.wpi.first.math.Num;
+import edu.wpi.first.math.numbers.N1;
+
+/**
+ * A sigma points generator for the UnscentedKalmanFilter class.
+ *
+ * @param The dimensionality of the state.
+ */
+public interface SigmaPoints {
+ /**
+ * Returns number of sigma points for each variable in the state x.
+ *
+ * @return The number of sigma points for each variable in the state x.
+ */
+ int getNumSigmas();
+
+ /**
+ * Computes the sigma points for an unscented Kalman filter given the mean (x) and square-root
+ * covariance (s) of the filter.
+ *
+ * @param x An array of the means.
+ * @param s Square-root covariance of the filter.
+ * @return Two-dimensional array of sigma points. Each column contains all the sigmas for one
+ * dimension in the problem space. Ordered by Xi_0, Xi_{1..n}, Xi_{n+1..2n}.
+ */
+ Matrix squareRootSigmaPoints(Matrix x, Matrix s);
+
+ /**
+ * Returns the weight for each sigma point for the mean.
+ *
+ * @return the weight for each sigma point for the mean.
+ */
+ Matrix, N1> getWm();
+
+ /**
+ * Returns an element of the weight for each sigma point for the mean.
+ *
+ * @param element Element of vector to return.
+ * @return the element i's weight for the mean.
+ */
+ double getWm(int element);
+
+ /**
+ * Returns the weight for each sigma point for the covariance.
+ *
+ * @return the weight for each sigma point for the covariance.
+ */
+ Matrix, N1> getWc();
+
+ /**
+ * Returns an element of the weight for each sigma point for the covariance.
+ *
+ * @param element Element of vector to return.
+ * @return The element I's weight for the covariance.
+ */
+ double getWc(int element);
+}
diff --git a/wpimath/src/main/java/edu/wpi/first/math/estimator/UnscentedKalmanFilter.java b/wpimath/src/main/java/edu/wpi/first/math/estimator/UnscentedKalmanFilter.java
index 0d402c7af9..455cd67d46 100644
--- a/wpimath/src/main/java/edu/wpi/first/math/estimator/UnscentedKalmanFilter.java
+++ b/wpimath/src/main/java/edu/wpi/first/math/estimator/UnscentedKalmanFilter.java
@@ -22,6 +22,11 @@ import org.ejml.simple.SimpleMatrix;
* true system state. This is useful because many states cannot be measured directly as a result of
* sensor noise, or because the state is "hidden".
*
+ * This class's constructors require a SigmaPoints generator. For convenience, {@link S3UKF} and
+ * {@link MerweUKF} subclasses are provided to create a suitable generator for you. S3UKF is
+ * generally preferred over MerweUKF because of its greater performance while maintaining nearly
+ * identical accuracy.
+ *
*
Kalman filters use a K gain matrix to determine whether to trust the model or measurements
* more. Kalman filter theory uses statistics to compute an optimal K gain which minimizes the sum
* of squares error in the state estimate. This K gain is used to correct the state estimate by some
@@ -64,7 +69,7 @@ public class UnscentedKalmanFilter m_sigmasF;
private double m_dt;
- private final MerweScaledSigmaPoints m_pts;
+ private final SigmaPoints m_pts;
/**
* Constructs an Unscented Kalman Filter.
@@ -73,6 +78,7 @@ public class UnscentedKalmanFilterhttps://docs.wpilib.org/en/stable/docs/software/advanced-controls/state-space/state-space-observers.html#process-and-measurement-noise-covariance-matrices
* for how to select the standard deviations.
*
+ * @param pts A sigma points and weights generator.
* @param states A Nat representing the number of states.
* @param outputs A Nat representing the number of outputs.
* @param f A vector-valued function of x and u that returns the derivative of the state vector.
@@ -82,6 +88,7 @@ public class UnscentedKalmanFilter pts,
Nat states,
Nat outputs,
BiFunction, Matrix, Matrix> f,
@@ -90,6 +97,7 @@ public class UnscentedKalmanFilter measurementStdDevs,
double nominalDt) {
this(
+ pts,
states,
outputs,
f,
@@ -113,16 +121,17 @@ public class UnscentedKalmanFilterhttps://docs.wpilib.org/en/stable/docs/software/advanced-controls/state-space/state-space-observers.html#process-and-measurement-noise-covariance-matrices
* for how to select the standard deviations.
*
+ * @param pts A sigma points and weights generator.
* @param states A Nat representing the number of states.
* @param outputs A Nat representing the number of outputs.
* @param f A vector-valued function of x and u that returns the derivative of the state vector.
* @param h A vector-valued function of x and u that returns the measurement vector.
* @param stateStdDevs Standard deviations of model states.
* @param measurementStdDevs Standard deviations of measurements.
- * @param meanFuncX A function that computes the mean of 2 * States + 1 state vectors using a
+ * @param meanFuncX A function that computes the mean of NumSigmas state vectors using a given set
+ * of weights.
+ * @param meanFuncY A function that computes the mean of NumSigmas measurement vectors using a
* given set of weights.
- * @param meanFuncY A function that computes the mean of 2 * States + 1 measurement vectors using
- * a given set of weights.
* @param residualFuncX A function that computes the residual of two state vectors (i.e. it
* subtracts them.)
* @param residualFuncY A function that computes the residual of two measurement vectors (i.e. it
@@ -131,6 +140,7 @@ public class UnscentedKalmanFilter pts,
Nat states,
Nat outputs,
BiFunction, Matrix, Matrix> f,
@@ -160,37 +170,36 @@ public class UnscentedKalmanFilter(states);
+ m_pts = pts;
reset();
}
- static
- Pair, Matrix> squareRootUnscentedTransform(
- Nat s,
- Nat dim,
- Matrix sigmas,
- Matrix, N1> Wm,
- Matrix, N1> Wc,
- BiFunction, Matrix, N1>, Matrix> meanFunc,
- BiFunction, Matrix, Matrix> residualFunc,
- Matrix squareRootR) {
- if (sigmas.getNumRows() != dim.getNum() || sigmas.getNumCols() != 2 * s.getNum() + 1) {
+ static Pair, Matrix> squareRootUnscentedTransform(
+ Nat covdim,
+ int numSigmas,
+ Matrix sigmas,
+ Matrix, N1> Wm,
+ Matrix, N1> Wc,
+ BiFunction, Matrix, N1>, Matrix> meanFunc,
+ BiFunction, Matrix, Matrix> residualFunc,
+ Matrix squareRootR) {
+ if (sigmas.getNumRows() != covdim.getNum() || sigmas.getNumCols() != numSigmas) {
throw new IllegalArgumentException(
- "Sigmas must be covDim by 2 * states + 1! Got "
+ "Sigmas must be covDim by numSigmas! Got "
+ sigmas.getNumRows()
+ " by "
+ sigmas.getNumCols());
}
- if (Wm.getNumRows() != 2 * s.getNum() + 1 || Wm.getNumCols() != 1) {
+ if (Wm.getNumRows() != numSigmas || Wm.getNumCols() != 1) {
throw new IllegalArgumentException(
- "Wm must be 2 * states + 1 by 1! Got " + Wm.getNumRows() + " by " + Wm.getNumCols());
+ "Wm must be numSigmas by 1! Got " + Wm.getNumRows() + " by " + Wm.getNumCols());
}
- if (Wc.getNumRows() != 2 * s.getNum() + 1 || Wc.getNumCols() != 1) {
+ if (Wc.getNumRows() != numSigmas || Wc.getNumCols() != 1) {
throw new IllegalArgumentException(
- "Wc must be 2 * states + 1 by 1! Got " + Wc.getNumRows() + " by " + Wc.getNumCols());
+ "Wc must be numSigmas by 1! Got " + Wc.getNumRows() + " by " + Wc.getNumCols());
}
// New mean is usually just the sum of the sigmas * weights:
@@ -208,13 +217,18 @@ public class UnscentedKalmanFilter Sbar = new Matrix<>(new SimpleMatrix(dim.getNum(), 2 * s.getNum() + dim.getNum()));
- for (int i = 0; i < 2 * s.getNum(); i++) {
+ //
+ // Note that we allow a custom function instead of the difference to allow
+ // angle wrapping. Furthermore, we allow an arbitrary number of sigma points to
+ // support similar methods such as the Scaled Spherical Simplex Filter (S3F).
+ Matrix Sbar =
+ new Matrix<>(new SimpleMatrix(covdim.getNum(), numSigmas - 1 + covdim.getNum()));
+ for (int i = 0; i < numSigmas - 1; i++) {
Sbar.setColumn(
i,
residualFunc.apply(sigmas.extractColumnVector(1 + i), x).times(Math.sqrt(Wc.get(1, 0))));
}
- Sbar.assignBlock(0, 2 * s.getNum(), squareRootR);
+ Sbar.assignBlock(0, numSigmas - 1, squareRootR);
QRDecompositionHouseholder_DDRM qr = new QRDecompositionHouseholder_DDRM();
var qrStorage = Sbar.transpose().getStorage();
@@ -355,7 +369,7 @@ public class UnscentedKalmanFilter(m_states, Nat.N1());
m_S = new Matrix<>(m_states, m_states);
- m_sigmasF = new Matrix<>(new SimpleMatrix(m_states.getNum(), 2 * m_states.getNum() + 1));
+ m_sigmasF = new Matrix<>(new SimpleMatrix(m_states.getNum(), m_pts.getNumSigmas()));
}
/**
@@ -397,7 +411,7 @@ public class UnscentedKalmanFilter sigmasH = new Matrix<>(new SimpleMatrix(rows.getNum(), 2 * m_states.getNum() + 1));
+ Matrix sigmasH = new Matrix<>(new SimpleMatrix(rows.getNum(), m_pts.getNumSigmas()));
var sigmas = m_pts.squareRootSigmaPoints(m_xHat, m_S);
for (int i = 0; i < m_pts.getNumSigmas(); i++) {
Matrix hRet = h.apply(sigmas.extractColumnVector(i), u);
@@ -521,8 +535,8 @@ public class UnscentedKalmanFilter>;
+template class EXPORT_TEMPLATE_DEFINE(WPILIB_DLLEXPORT)
+ UnscentedKalmanFilter<5, 3, 3, MerweScaledSigmaPoints<5>>;
+
+} // namespace frc
diff --git a/wpimath/src/main/native/cpp/estimator/UnscentedKalmanFilter.cpp b/wpimath/src/main/native/cpp/estimator/S3UKF.cpp
similarity index 71%
rename from wpimath/src/main/native/cpp/estimator/UnscentedKalmanFilter.cpp
rename to wpimath/src/main/native/cpp/estimator/S3UKF.cpp
index d5e869b6cb..d0302d7ca9 100644
--- a/wpimath/src/main/native/cpp/estimator/UnscentedKalmanFilter.cpp
+++ b/wpimath/src/main/native/cpp/estimator/S3UKF.cpp
@@ -2,13 +2,13 @@
// 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.
-#include "frc/estimator/UnscentedKalmanFilter.h"
+#include "frc/estimator/S3UKF.h"
namespace frc {
template class EXPORT_TEMPLATE_DEFINE(WPILIB_DLLEXPORT)
- UnscentedKalmanFilter<3, 3, 1>;
+ UnscentedKalmanFilter<3, 3, 1, S3SigmaPoints<3>>;
template class EXPORT_TEMPLATE_DEFINE(WPILIB_DLLEXPORT)
- UnscentedKalmanFilter<5, 3, 3>;
+ UnscentedKalmanFilter<5, 3, 3, S3SigmaPoints<5>>;
} // namespace frc
diff --git a/wpimath/src/main/native/include/frc/estimator/MerweScaledSigmaPoints.h b/wpimath/src/main/native/include/frc/estimator/MerweScaledSigmaPoints.h
index 2605d5e03e..1b5b5cfa1f 100644
--- a/wpimath/src/main/native/include/frc/estimator/MerweScaledSigmaPoints.h
+++ b/wpimath/src/main/native/include/frc/estimator/MerweScaledSigmaPoints.h
@@ -15,8 +15,8 @@ namespace frc {
* dissertation[1] for the UnscentedKalmanFilter class.
*
* It parametrizes the sigma points using alpha, beta, kappa terms, and is the
- * version seen in most publications. Unless you know better, this should be
- * your default choice.
+ * version seen in most publications. S3SigmaPoints is generally preferred due
+ * to its greater performance with nearly identical accuracy.
*
* [1] R. Van der Merwe "Sigma-Point Kalman Filters for Probabilistic
* Inference in Dynamic State-Space Models" (Doctoral dissertation)
@@ -27,6 +27,8 @@ namespace frc {
template
class MerweScaledSigmaPoints {
public:
+ static constexpr int NumSigmas = 2 * States + 1;
+
/**
* Constructs a generator for Van der Merwe scaled sigma points.
*
@@ -44,11 +46,6 @@ class MerweScaledSigmaPoints {
ComputeWeights(beta);
}
- /**
- * Returns number of sigma points for each variable in the state x.
- */
- int NumSigmas() { return 2 * States + 1; }
-
/**
* Computes the sigma points for an unscented Kalman filter given the mean
* (x) and square-root covariance (S) of the filter.
diff --git a/wpimath/src/main/native/include/frc/estimator/MerweUKF.h b/wpimath/src/main/native/include/frc/estimator/MerweUKF.h
new file mode 100644
index 0000000000..1b5165aeb4
--- /dev/null
+++ b/wpimath/src/main/native/include/frc/estimator/MerweUKF.h
@@ -0,0 +1,25 @@
+// 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.
+
+#pragma once
+
+#include
+
+#include "frc/estimator/MerweScaledSigmaPoints.h"
+#include "frc/estimator/UnscentedKalmanFilter.h"
+
+namespace frc {
+
+template
+using MerweUKF = UnscentedKalmanFilter>;
+
+// Because MerweUKF is a type alias and not a class, we have to use
+// UnscentedKalmanFilter instead
+extern template class EXPORT_TEMPLATE_DECLARE(WPILIB_DLLEXPORT)
+ UnscentedKalmanFilter<3, 3, 1, MerweScaledSigmaPoints<3>>;
+extern template class EXPORT_TEMPLATE_DECLARE(WPILIB_DLLEXPORT)
+ UnscentedKalmanFilter<5, 3, 3, MerweScaledSigmaPoints<5>>;
+
+} // namespace frc
diff --git a/wpimath/src/main/native/include/frc/estimator/S3SigmaPoints.h b/wpimath/src/main/native/include/frc/estimator/S3SigmaPoints.h
new file mode 100644
index 0000000000..a19cd072ac
--- /dev/null
+++ b/wpimath/src/main/native/include/frc/estimator/S3SigmaPoints.h
@@ -0,0 +1,135 @@
+// 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.
+
+#pragma once
+
+#include
+
+#include "frc/EigenCore.h"
+#include "frc/estimator/SigmaPoints.h"
+
+namespace frc {
+
+/**
+ * Generates sigma points and weights according to Papakonstantinou's paper[1]
+ * for the UnscentedKalmanFilter class.
+ *
+ * It parameterizes the sigma points using alpha and beta terms. Unless you know
+ * better, this should be your default choice due to its high accuracy and
+ * performance.
+ *
+ * [1] K. Papakonstantinou "A Scaled Spherical Simplex Filter (S3F) with a
+ * decreased n + 2 sigma points set size and equivalent 2n + 1 Unscented Kalman
+ * Filter (UKF) accuracy"
+ *
+ * @tparam States The dimenstionality of the state. States + 2 weights will be
+ * generated.
+ */
+template
+class S3SigmaPoints {
+ public:
+ static constexpr int NumSigmas = States + 2;
+
+ /**
+ * Constructs a generator for Papakonstantinou sigma points.
+ *
+ * @param alpha Determines the spread of the sigma points around the mean.
+ * Usually a small positive value (1e-3).
+ * @param beta Incorporates prior knowledge of the distribution of the mean.
+ * For Gaussian distributions, beta = 2 is optimal.
+ */
+ explicit S3SigmaPoints(double alpha = 1e-3, double beta = 2)
+ : m_alpha{alpha} {
+ ComputeWeights(beta);
+ }
+
+ /**
+ * Computes the sigma points for an unscented Kalman filter given the mean (x)
+ * and square-root covariance (S) of the filter.
+ *
+ * @param x An array of the means.
+ * @param S Square-root covariance of the filter.
+ *
+ * @return Two dimensional array of sigma points. Each column contains all of
+ * the sigmas for one dimension in the problem space. Ordered by Xi_0,
+ * Xi_{1..n+1}.
+ */
+ Matrixd SquareRootSigmaPoints(
+ const Vectord& x, const Matrixd& S) const {
+ // table (1), equation (12)
+ wpi::array q(wpi::empty_array);
+ for (size_t t = 1; t <= States; ++t) {
+ q[t - 1] = m_alpha * std::sqrt(static_cast(t * (States + 1)) /
+ static_cast(t + 1));
+ }
+
+ Matrixd C;
+ C.template block(0, 0) = Vectord::Constant(0.0);
+ for (int col = 1; col < NumSigmas; ++col) {
+ for (int row = 0; row < States; ++row) {
+ if (row < col - 2) {
+ C(row, col) = 0.0;
+ } else if (row == col - 2) {
+ C(row, col) = q[row];
+ } else {
+ C(row, col) = -q[row] / (row + 1);
+ }
+ }
+ }
+
+ Matrixd sigmas;
+ for (int col = 0; col < NumSigmas; ++col) {
+ sigmas.col(col) = x + S * C.col(col);
+ }
+
+ return sigmas;
+ }
+
+ /**
+ * Returns the weight for each sigma point for the mean.
+ */
+ const Vectord& Wm() const { return m_Wm; }
+
+ /**
+ * Returns an element of the weight for each sigma point for the mean.
+ *
+ * @param i Element of vector to return.
+ */
+ double Wm(int i) const { return m_Wm(i, 0); }
+
+ /**
+ * Returns the weight for each sigma point for the covariance.
+ */
+ const Vectord& Wc() const { return m_Wc; }
+
+ /**
+ * Returns an element of the weight for each sigma point for the covariance.
+ *
+ * @param i Element of vector to return.
+ */
+ double Wc(int i) const { return m_Wc(i, 0); }
+
+ private:
+ Vectord m_Wm;
+ Vectord m_Wc;
+ double m_alpha;
+
+ /**
+ * Computes the weights for the scaled unscented Kalman filter.
+ *
+ * @param beta Incorporates prior knowledge of the distribution of the mean.
+ */
+ void ComputeWeights(double beta) {
+ double alpha_sq = m_alpha * m_alpha;
+
+ double c = 1.0 / (alpha_sq * (States + 1));
+ m_Wm = Vectord::Constant(c);
+ m_Wc = Vectord::Constant(c);
+
+ m_Wm(0) = 1.0 - 1.0 / alpha_sq;
+ m_Wc(0) = 1.0 - 1.0 / alpha_sq + (1 - alpha_sq + beta);
+ }
+};
+
+} // namespace frc
diff --git a/wpimath/src/main/native/include/frc/estimator/S3UKF.h b/wpimath/src/main/native/include/frc/estimator/S3UKF.h
new file mode 100644
index 0000000000..e94f2af0c6
--- /dev/null
+++ b/wpimath/src/main/native/include/frc/estimator/S3UKF.h
@@ -0,0 +1,25 @@
+// 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.
+
+#pragma once
+
+#include
+
+#include "frc/estimator/S3SigmaPoints.h"
+#include "frc/estimator/UnscentedKalmanFilter.h"
+
+namespace frc {
+
+template
+using S3UKF =
+ UnscentedKalmanFilter>;
+
+// Because S3UKF is a type alias and not a class, we have to use
+// UnscentedKalmanFilter instead
+extern template class EXPORT_TEMPLATE_DECLARE(WPILIB_DLLEXPORT)
+ UnscentedKalmanFilter<3, 3, 1, S3SigmaPoints<3>>;
+extern template class EXPORT_TEMPLATE_DECLARE(WPILIB_DLLEXPORT)
+ UnscentedKalmanFilter<5, 3, 3, S3SigmaPoints<5>>;
+
+} // namespace frc
diff --git a/wpimath/src/main/native/include/frc/estimator/SigmaPoints.h b/wpimath/src/main/native/include/frc/estimator/SigmaPoints.h
new file mode 100644
index 0000000000..a459197c75
--- /dev/null
+++ b/wpimath/src/main/native/include/frc/estimator/SigmaPoints.h
@@ -0,0 +1,26 @@
+// 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.
+
+#pragma once
+
+#include
+
+#include "frc/EigenCore.h"
+
+namespace frc {
+
+template
+concept SigmaPoints =
+ requires(T t, Vectord x, Matrixd S, int i) {
+ { T::NumSigmas } -> std::convertible_to;
+ {
+ t.SquareRootSigmaPoints(x, S)
+ } -> std::same_as>;
+ { t.Wm() } -> std::convertible_to>;
+ { t.Wm(i) } -> std::same_as;
+ { t.Wc() } -> std::convertible_to>;
+ { t.Wc(i) } -> std::same_as;
+ } && std::default_initializable;
+
+} // namespace frc
diff --git a/wpimath/src/main/native/include/frc/estimator/UnscentedKalmanFilter.h b/wpimath/src/main/native/include/frc/estimator/UnscentedKalmanFilter.h
index 21838870a8..2d2ed82c62 100644
--- a/wpimath/src/main/native/include/frc/estimator/UnscentedKalmanFilter.h
+++ b/wpimath/src/main/native/include/frc/estimator/UnscentedKalmanFilter.h
@@ -13,7 +13,7 @@
#include "frc/EigenCore.h"
#include "frc/StateSpaceUtil.h"
-#include "frc/estimator/MerweScaledSigmaPoints.h"
+#include "frc/estimator/SigmaPoints.h"
#include "frc/estimator/UnscentedTransform.h"
#include "frc/system/Discretization.h"
#include "frc/system/NumericalIntegration.h"
@@ -28,6 +28,11 @@ namespace frc {
* be measured directly as a result of sensor noise, or because the state is
* "hidden".
*
+ * This class requires a SigmaPoints template parameter. For convenience, S3UKF
+ * and MerweUKF type aliases are provided to specify a suitable generator for
+ * you. S3UKF is generally preferred over MerweUKF because of its greater
+ * performance while maintaining nearly identical accuracy.
+ *
* Kalman filters use a K gain matrix to determine whether to trust the model or
* measurements more. Kalman filter theory uses statistics to compute an optimal
* K gain which minimizes the sum of squares error in the state estimate. This K
@@ -51,10 +56,13 @@ namespace frc {
* @tparam States Number of states.
* @tparam Inputs Number of inputs.
* @tparam Outputs Number of outputs.
+ * @tparam SigmaPoints Type used to generate sigma points and weights.
*/
-template
+template SigmaPoints>
class UnscentedKalmanFilter {
public:
+ static constexpr int NumSigmas = SigmaPoints::NumSigmas;
+
using StateVector = Vectord;
using InputVector = Vectord;
using OutputVector = Vectord;
@@ -87,12 +95,12 @@ class UnscentedKalmanFilter {
: m_f(std::move(f)), m_h(std::move(h)) {
m_contQ = MakeCovMatrix(stateStdDevs);
m_contR = MakeCovMatrix(measurementStdDevs);
- m_meanFuncX = [](const Matrixd& sigmas,
- const Vectord<2 * States + 1>& Wm) -> StateVector {
+ m_meanFuncX = [](const Matrixd& sigmas,
+ const Vectord& Wm) -> StateVector {
return sigmas * Wm;
};
- m_meanFuncY = [](const Matrixd& sigmas,
- const Vectord<2 * States + 1>& Wc) -> OutputVector {
+ m_meanFuncY = [](const Matrixd& sigmas,
+ const Vectord& Wc) -> OutputVector {
return sigmas * Wc;
};
m_residualFuncX = [](const StateVector& a,
@@ -141,11 +149,11 @@ class UnscentedKalmanFilter {
std::function f,
std::function h,
const StateArray& stateStdDevs, const OutputArray& measurementStdDevs,
- std::function&,
- const Vectord<2 * States + 1>&)>
+ std::function&,
+ const Vectord&)>
meanFuncX,
- std::function&,
- const Vectord<2 * States + 1>&)>
+ std::function&,
+ const Vectord&)>
meanFuncY,
std::function
residualFuncX,
@@ -257,7 +265,7 @@ class UnscentedKalmanFilter {
// Generate sigma points around the state mean
//
// equation (17)
- Matrixd sigmas =
+ Matrixd sigmas =
m_pts.SquareRootSigmaPoints(m_xHat, m_S);
// Project each sigma point forward in time according to the
@@ -267,7 +275,7 @@ class UnscentedKalmanFilter {
// sigmasF = 𝒳ₖ,ₖ₋₁ or just 𝒳 for readability
//
// equation (18)
- for (int i = 0; i < m_pts.NumSigmas(); ++i) {
+ for (int i = 0; i < NumSigmas; ++i) {
StateVector x = sigmas.template block(0, i);
m_sigmasF.template block(0, i) = RK4(m_f, x, u, dt);
}
@@ -276,7 +284,7 @@ class UnscentedKalmanFilter {
// to compute the prior state mean and covariance
//
// equations (18) (19) and (20)
- auto [xHat, S] = SquareRootUnscentedTransform(
+ auto [xHat, S] = SquareRootUnscentedTransform(
m_sigmasF, m_pts.Wm(), m_pts.Wc(), m_meanFuncX, m_residualFuncX,
discQ.template triangularView());
m_xHat = xHat;
@@ -327,8 +335,8 @@ class UnscentedKalmanFilter {
const InputVector& u, const Vectord& y,
std::function(const StateVector&, const InputVector&)> h,
const Matrixd& R) {
- auto meanFuncY = [](const Matrixd& sigmas,
- const Vectord<2 * States + 1>& Wc) -> Vectord {
+ auto meanFuncY = [](const Matrixd& sigmas,
+ const Vectord& Wc) -> Vectord {
return sigmas * Wc;
};
auto residualFuncX = [](const StateVector& a,
@@ -358,7 +366,7 @@ class UnscentedKalmanFilter {
* @param h A vector-valued function of x and u that returns the
* measurement vector.
* @param R Continuous measurement noise covariance matrix.
- * @param meanFuncY A function that computes the mean of 2 * States + 1
+ * @param meanFuncY A function that computes the mean of NumSigmas
* measurement vectors using a given set of weights.
* @param residualFuncY A function that computes the residual of two
* measurement vectors (i.e. it subtracts them.)
@@ -371,8 +379,8 @@ class UnscentedKalmanFilter {
const InputVector& u, const Vectord& y,
std::function(const StateVector&, const InputVector&)> h,
const Matrixd& R,
- std::function(const Matrixd&,
- const Vectord<2 * States + 1>&)>
+ std::function(const Matrixd&,
+ const Vectord&)>
meanFuncY,
std::function(const Vectord&, const Vectord&)>
residualFuncY,
@@ -392,10 +400,10 @@ class UnscentedKalmanFilter {
// This differs from equation (22) which uses
// the prior sigma points, regenerating them allows
// multiple measurement updates per time update
- Matrixd sigmasH;
- Matrixd sigmas =
+ Matrixd sigmasH;
+ Matrixd sigmas =
m_pts.SquareRootSigmaPoints(m_xHat, m_S);
- for (int i = 0; i < m_pts.NumSigmas(); ++i) {
+ for (int i = 0; i < NumSigmas; ++i) {
sigmasH.template block(0, i) =
h(sigmas.template block(0, i), u);
}
@@ -405,7 +413,7 @@ class UnscentedKalmanFilter {
// covariance.
//
// equations (23) (24) and (25)
- auto [yHat, Sy] = SquareRootUnscentedTransform(
+ auto [yHat, Sy] = SquareRootUnscentedTransform(
sigmasH, m_pts.Wm(), m_pts.Wc(), meanFuncY, residualFuncY,
discR.template triangularView());
@@ -419,7 +427,7 @@ class UnscentedKalmanFilter {
// equation (26)
Matrixd