31 #include <Eigen/Eigenvalues>
36 template <
typename Derived>
38 Derived
const &mat = m.derived();
39 if (mat.rows() != mat.cols()) {
43 if (!mat.isApprox(mat.transpose())) {
48 return (mat.eigenvalues().real().array() > 0.).all();
51 template <
typename Derived>
52 inline void checkSigmaPoints(MatrixBase<Derived>
const &m) {
53 SECTION(
"Sigma points") {
54 Derived
const &mat = m.derived();
56 INFO(
"Checking that there are at least two unique sigma points in "
57 "the collection of sigma points.");
58 REQUIRE_FALSE((mat.rowwise().minCoeff().array() ==
59 mat.rowwise().maxCoeff().array())
65 template <
typename Derived>
66 inline void thenCheckCovariance(MatrixBase<Derived>
const &c) {
67 Derived
const &cov = c.derived();
68 THEN(
"Covariance should be symmetric") {
69 REQUIRE(cov.isApprox(cov.transpose()));
70 AND_THEN(
"Covariance should be positive-definite") {
76 template <
typename Reconstruction>
77 inline void thenCheckReconstruction(Reconstruction
const &recon) {
78 THEN(
"Reconstructed covariance should satisfy its invariants") {
79 thenCheckCovariance(recon.getCov());
82 template <
typename Reconstruction>
83 inline void andThenCheckReconstruction(Reconstruction
const &recon) {
84 AND_THEN(
"Reconstructed covariance should satisfy its invariants") {
85 thenCheckCovariance(recon.getCov());
88 template <
typename GeneratorType>
89 inline void generatorChecks(GeneratorType
const &gen) {
90 static const auto epsilon = NumTraits<double>::dummy_precision();
93 CAPTURE(gen.getWeightsForMean().transpose());
95 INFO(
"Weights should be non-zero");
96 REQUIRE(((gen.getWeightsForMean().array() > epsilon) ||
97 (gen.getWeightsForMean().array() < -epsilon))
100 #ifdef DEMAND_NONNEGATIVE_WEIGHTS
101 CHECK((gen.getWeightsForMean().array() >= 0.).all());
104 CAPTURE(gen.getWeightsForCov().transpose());
106 INFO(
"Weights should be non-zero");
107 REQUIRE(((gen.getWeightsForCov().array() > epsilon) ||
108 (gen.getWeightsForCov().array() < -epsilon))
111 #ifdef DEMAND_NONNEGATIVE_WEIGHTS
112 CHECK((gen.getWeightsForCov().array() >= 0.).all());
116 checkSigmaPoints(gen.getSigmaPoints());
119 template <
typename GeneratorType>
120 inline void thenCheckSigmaPointGenerator(GeneratorType
const &gen) {
121 THEN(
"Sigma point generator should have reasonable output") {
122 generatorChecks(gen);
125 template <
typename GeneratorType>
126 inline void andThenCheckSigmaPointGenerator(GeneratorType
const &gen) {
127 AND_THEN(
"Sigma point generator should have reasonable output") {
128 generatorChecks(gen);
132 TEST_CASE(
"Sigma point reconstruction validity") {
134 static const auto DIM = 3;
136 Matrix3d cov(Vector3d::Constant(10).asDiagonal());
138 auto checkReconstruction = [&](Eigen::Vector3d
const &mean) {
141 using Reconstructor =
146 auto gen = Generator(mean, cov, params);
147 thenCheckSigmaPointGenerator(gen);
149 static const auto numSigmaPoints = Generator::NumSigmaPoints;
150 for (std::size_t i = 0; i < numSigmaPoints; ++i) {
151 CAPTURE(gen.getSigmaPoint(i));
155 AND_WHEN(
"reconstructed from the sigma points") {
156 auto recon = Reconstructor(gen, gen.getSigmaPoints());
157 thenCheckReconstruction(recon);
158 THEN(
"the reconstructed distribution should be approximately equal "
160 CAPTURE(gen.getSigmaPoints());
161 CAPTURE(recon.getMean());
162 for (std::size_t i = 0; i < DIM; ++i) {
164 REQUIRE(recon.getMean()[i] == Approx(mean[i]));
166 CAPTURE(recon.getCov());
167 REQUIRE(recon.getCov().isApprox(cov));
172 WHEN(
"Starting with a zero mean") { checkReconstruction(Vector3d::Zero()); }
174 WHEN(
"Starting with a non-zero mean") {
175 checkReconstruction(Vector3d(1, 2, 3));
177 WHEN(
"Starting with a small non-zero mean") {
178 checkReconstruction(Vector3d(0.1, 0.2, 0.3));
182 enum class Axis : std::size_t { X = 0, Y = 1, Z = 2 };
184 inline double zeroOrValueForAxis(Axis rotationAxis, Axis currentAxis,
186 return ((rotationAxis == currentAxis) ? value : 0.);
189 inline Vector3d ZeroVec3dExceptAtAxis(Axis rotationAxis,
double value) {
190 return Vector3d(zeroOrValueForAxis(rotationAxis, Axis::X, value),
191 zeroOrValueForAxis(rotationAxis, Axis::Y, value),
192 zeroOrValueForAxis(rotationAxis, Axis::Z, value));
195 inline Vector3d Vec3dSmallValueAt(Axis rotationAxis) {
196 return ZeroVec3dExceptAtAxis(rotationAxis, SMALL_VALUE);
199 inline Vector3d Vec3dUnit(Axis rotationAxis) {
200 return ZeroVec3dExceptAtAxis(rotationAxis, 1.);
203 inline double getSignCorrect(
bool positive) {
return (positive ? 1. : -1.); }
205 template <
typename MeasurementType,
typename InProgressType>
208 InProgressType &inProgress, Axis
const rotationAxis,
209 bool const positive =
true) {
210 const double signCorrect = getSignCorrect(positive);
211 Vector3d rotationVector = Vec3dSmallValueAt(rotationAxis) * signCorrect;
212 CAPTURE(rotationVector.transpose());
214 Vector3d residual = kalmanMeas.getResidual(data->
state);
215 CAPTURE(residual.transpose());
217 AND_THEN(
"residual directly computed by measurement should be zero, except "
218 "for the single axis of rotation, which should be of magnitude "
220 CHECK(residual.isApprox(rotationVector));
223 CAPTURE(inProgress.deltaz);
224 AND_THEN(
"computed deltaz should equal residual") {
225 CHECK(residual.isApprox(inProgress.deltaz));
228 AND_THEN(
"delta z (residual/propagated mean residual) should be "
229 "approximately 0, except for the single axis of rotation, which "
230 "should be magnitude SMALL_VALUE") {
231 REQUIRE(inProgress.deltaz[0] == Approx(rotationVector[0]));
232 REQUIRE(inProgress.deltaz[1] == Approx(rotationVector[1]));
233 REQUIRE(inProgress.deltaz[2] == Approx(rotationVector[2]));
236 CAPTURE(inProgress.stateCorrection.transpose());
237 REQUIRE(inProgress.stateCorrectionFinite);
238 AND_THEN(
"state correction should have a rotation component with small "
239 "absolute element (sign matching rotation vector) corresponding "
240 "to rotation axis") {
241 for (std::size_t i = 0; i < 3; ++i) {
242 const auto stateIndex = i + 3;
244 if (static_cast<std::size_t>(rotationAxis) == i) {
247 REQUIRE((signCorrect * inProgress.stateCorrection[stateIndex]) >
249 REQUIRE((signCorrect * inProgress.stateCorrection[stateIndex]) <
253 REQUIRE(inProgress.stateCorrection[stateIndex] == Approx(0.));
258 AND_THEN(
"state correction should have an angular velocity component "
259 "with zero or small abs (sign matching rotation) corresponding to "
262 for (std::size_t i = 0; i < 3; ++i) {
263 const auto stateIndex = i + 9;
265 if (static_cast<std::size_t>(rotationAxis) == i) {
268 REQUIRE(signCorrect * inProgress.stateCorrection[stateIndex] >=
272 REQUIRE(inProgress.stateCorrection[stateIndex] == Approx(0.));
276 AND_THEN(
"state correction should not contain any translational/linear "
277 "velocity components") {
278 REQUIRE(inProgress.stateCorrection.template head<3>().isZero());
279 REQUIRE(inProgress.stateCorrection.template segment<3>(6).isZero());
281 AND_WHEN(
"the correction is applied") {
282 auto errorCovarianceCorrectionWasFinite = inProgress.finishCorrection();
283 THEN(
"the new error covariance should be finite") {
284 REQUIRE(errorCovarianceCorrectionWasFinite);
285 AND_THEN(
"State error should have decreased") {
287 data->originalStateError.array())
295 template <
typename MeasurementType>
297 unscentedSmallSingleAxisChecks(
TestData *data, MeasurementType &kalmanMeas,
298 Axis rotationAxis,
bool positive =
true) {
301 kalman::beginUnscentedCorrection(data->
state, kalmanMeas, params);
303 andThenCheckSigmaPointGenerator(inProgress.sigmaPoints);
304 AND_THEN(
"transformed sigma points should not all equal 0") {
305 CAPTURE(inProgress.transformedPoints.transpose());
306 REQUIRE_FALSE(inProgress.transformedPoints.isZero());
308 checkSigmaPoints(inProgress.transformedPoints);
309 andThenCheckReconstruction(inProgress.reconstruction);
315 template <
typename MeasurementType>
316 inline void checkEffectiveIdentityMeasurement(
TestData *data,
317 MeasurementType &kalmanMeas) {
318 AND_THEN(
"residual should be zero") {
319 CAPTURE(kalmanMeas.getResidual(data->
state));
320 REQUIRE(kalmanMeas.getResidual(data->
state).isZero());
322 auto inProgress = kalman::beginUnscentedCorrection(data->
state, kalmanMeas);
324 andThenCheckSigmaPointGenerator(inProgress.sigmaPoints);
325 AND_THEN(
"transformed sigma points should not all equal 0") {
326 REQUIRE_FALSE(inProgress.transformedPoints.isZero());
328 checkSigmaPoints(inProgress.transformedPoints);
330 auto &recon = inProgress.reconstruction;
331 AND_THEN(
"propagated predicted measurement mean should be zero") {
332 CAPTURE(recon.getMean().transpose());
333 REQUIRE(recon.getMean().isZero());
335 AND_THEN(
"state correction should be finite - specifically, zero") {
336 CAPTURE(inProgress.stateCorrection.transpose());
337 REQUIRE(inProgress.stateCorrection.array().allFinite());
338 REQUIRE(inProgress.stateCorrection.isZero());
341 const Quaterniond origQuat = data->
state.getQuaternion();
342 AND_WHEN(
"the correction is applied") {
343 inProgress.finishCorrection();
344 THEN(
"state should be unchanged") {
348 CAPTURE(data->
state.getQuaternion());
349 REQUIRE(data->
state.getQuaternion().coeffs().isApprox(
352 THEN(
"State error should have decreased") {
354 data->originalStateError.array())
360 template <
typename F,
typename... Args>
361 inline void allSmallSingleAxisRotations(F &&f, Args &&... args) {
362 WHEN(
"filtering in a small positive rotation about x") {
363 std::forward<F>(f)(std::forward<Args>(args)..., Axis::X,
true);
366 WHEN(
"filtering in a small positive rotation about y") {
367 std::forward<F>(f)(std::forward<Args>(args)..., Axis::Y,
true);
370 WHEN(
"filtering in a small positive rotation about z") {
371 std::forward<F>(f)(std::forward<Args>(args)..., Axis::Z,
true);
374 WHEN(
"filtering in a small negative rotation about x") {
375 std::forward<F>(f)(std::forward<Args>(args)..., Axis::X,
false);
377 WHEN(
"filtering in a small negative rotation about y") {
378 std::forward<F>(f)(std::forward<Args>(args)..., Axis::Y,
false);
381 WHEN(
"filtering in a small negative rotation about z") {
382 std::forward<F>(f)(std::forward<Args>(args)..., Axis::Z,
false);
388 "unscented with identity calibration output",
"[ukf]",
392 TEST_CASE(
"unscented with identity calibration output",
"[ukf]") {
393 using TypeParam = kalman::SplitQ;
398 unique_ptr<TestData> data(
new TestData);
399 GIVEN(
"an identity state") {
400 WHEN(
"filtering in an identity measurement") {
401 Quaterniond xformedMeas = data->
xform(Quaterniond::Identity());
403 THEN(
"the transformed measurement should equal the "
405 CAPTURE(xformedMeas);
406 REQUIRE(xformedMeas.isApprox(Quaterniond::Identity()));
408 MeasurementType kalmanMeas{xformedMeas, data->imuVariance};
409 checkEffectiveIdentityMeasurement(data.get(), kalmanMeas);
412 allSmallSingleAxisRotations([&](Axis rotationAxis,
bool positive) {
413 const double signCorrect = getSignCorrect(positive);
414 auto radians = signCorrect * SMALL_VALUE;
416 auto axisVec = Vec3dUnit(rotationAxis);
417 CAPTURE(axisVec.transpose());
418 Quaterniond smallRotation =
419 Quaterniond(AngleAxisd(radians, axisVec));
421 CAPTURE(smallRotation);
422 Quaterniond xformedMeas = data->
xform(smallRotation);
423 CAPTURE(xformedMeas);
425 THEN(
"the transformed measurement should equal the "
427 REQUIRE(xformedMeas.isApprox(smallRotation));
429 MeasurementType kalmanMeas{xformedMeas, data->imuVariance};
430 unscentedSmallSingleAxisChecks(data.get(), kalmanMeas,
431 rotationAxis, positive);
436 auto runIncrementalSmallRotChecksNonIdentityState = [&](Axis rotationAxis,
438 const double signCorrect = getSignCorrect(positive);
439 auto radians = signCorrect * SMALL_VALUE;
441 auto axisVec = Vec3dUnit(rotationAxis);
442 CAPTURE(axisVec.transpose());
443 Quaterniond smallRotation = Quaterniond(AngleAxisd(radians, axisVec)) *
444 data->
state.getQuaternion();
446 CAPTURE(smallRotation);
447 Quaterniond xformedMeas = data->
xform(smallRotation);
448 CAPTURE(xformedMeas);
450 THEN(
"the transformed measurement should equal the "
452 REQUIRE(xformedMeas.isApprox(smallRotation));
454 MeasurementType kalmanMeas{xformedMeas, data->imuVariance};
455 unscentedSmallSingleAxisChecks(data.get(), kalmanMeas, rotationAxis,
459 GIVEN(
"a state rotated about y") {
460 Quaterniond stateRotation(AngleAxisd(M_PI / 4., Vector3d::UnitY()));
462 allSmallSingleAxisRotations(
463 runIncrementalSmallRotChecksNonIdentityState);
465 GIVEN(
"a state rotated about x") {
466 Quaterniond stateRotation(AngleAxisd(M_PI / 4., Vector3d::UnitX()));
468 allSmallSingleAxisRotations(
469 runIncrementalSmallRotChecksNonIdentityState);
473 TEST_CASE(
"unscented with small x rotation calibration output",
"[ukf]") {
475 unique_ptr<TestData> data(
new TestData);
476 CAPTURE(SMALL_VALUE);
478 Quaterniond(AngleAxisd(SMALL_VALUE, Vector3d::UnitX()));
480 INFO(
"This calibration value means that the camera is rotated negative "
481 "about its x wrt. the IMU/room");
482 INFO(
"Equivalently, that quaternion calibration value takes "
483 "points/orientations in room space and moves them to camera space");
484 GIVEN(
"an identity state in camera space") {
485 CAPTURE(data->
state.getQuaternion());
486 WHEN(
"filtering in a measurement that's the inverse of the room to "
487 "camera rotation (a measurement matching state)") {
490 Quaterniond xformedMeas = data->
xform(meas);
492 THEN(
"the transformed measurement should be approximately the "
494 CAPTURE(xformedMeas);
495 REQUIRE(xformedMeas.isApprox(Quaterniond::Identity()));
497 MeasurementType kalmanMeas{xformedMeas, data->imuVariance};
498 checkEffectiveIdentityMeasurement(data.get(), kalmanMeas);
502 GIVEN(
"an identity state in room space") {
504 CAPTURE(data->
state.getQuaternion());
505 WHEN(
"filtering in an identity measurement (a measurement matching "
507 Quaterniond xformedMeas = data->
xform(Quaterniond::Identity());
509 THEN(
"the transformed measurement should be approximately the "
510 "room to camera rotation") {
511 CAPTURE(xformedMeas);
514 MeasurementType kalmanMeas{xformedMeas, data->imuVariance};
515 checkEffectiveIdentityMeasurement(data.get(), kalmanMeas);
518 allSmallSingleAxisRotations([&](Axis rotationAxis,
bool positive) {
519 const double signCorrect = getSignCorrect(positive);
520 auto radians = signCorrect * SMALL_VALUE;
522 auto axisVec = Vec3dUnit(rotationAxis);
523 CAPTURE(axisVec.transpose());
524 Quaterniond smallRelRotationInCameraSpace =
525 Quaterniond(AngleAxisd(radians, axisVec)) *
526 data->
state.getQuaternion();
527 CAPTURE(smallRelRotationInCameraSpace);
529 smallRelRotationInCameraSpace;
530 CAPTURE(smallRotation);
531 THEN(
"the transformed measurement should equal the original "
532 "measurement as computed in camera space") {
533 Quaterniond xformedMeas = data->
xform(smallRotation);
534 CAPTURE(xformedMeas);
535 REQUIRE(xformedMeas.isApprox(smallRelRotationInCameraSpace));
539 MeasurementType kalmanMeas{xformedMeas, data->imuVariance};
540 CAPTURE(kalmanMeas.getResidual(data->
state));
541 unscentedSmallSingleAxisChecks(data.get(), kalmanMeas,
542 rotationAxis, positive);
548 TEST_CASE(
"conceptual transformation orders") {
549 Quaterniond positiveX(AngleAxisd(0.5, Vector3d::UnitX()));
550 Quaterniond positiveY(AngleAxisd(0.5, Vector3d::UnitY()));
551 Vector3d yVec = Vector3d::UnitY();
553 "Quaternion composition/multiplication order matches "
554 "transformation order (transformation and quat mult are associative)") {
555 CAPTURE((positiveX * (positiveY * yVec)).transpose());
556 CAPTURE(((positiveX * positiveY) * yVec).transpose());
557 REQUIRE((positiveX * (positiveY * yVec))
558 .isApprox((positiveX * positiveY) * yVec));
560 SECTION(
"Behavior of rotations") {
561 INFO(
"positive X is a transformation taking points from a coordinate "
562 "system pitched up, to a level coordinate system.");
563 CAPTURE((positiveX * yVec).transpose());
564 REQUIRE((positiveX * yVec).z() > 0.);
Header for a flexible Unscented-style Kalman filter correction from a measurement.
StateVector const & stateVector() const
xhat
Header-only framework for building Kalman-style filters, prediction, and sensor fusion.
void setQuaternion(Eigen::Quaterniond const &quaternion)
Intended for startup use.
EIGEN_MAKE_ALIGNED_OPERATOR_NEW Eigen::Quaterniond roomToCameraRotation
Quantity computed from the "camera pose" (room calibration output)
StateSquareMatrix const & errorCovariance() const
P.
CATCH_TYPELIST_DESCRIBED_TESTCASE("identity calibration output","[.][ekf]", kalman::SplitQ, kalman::QLastWithSplitInnovation)
TEST_CASE("unscented with identity calibration output","[ukf]")
Eigen::Quaterniond xform(Eigen::Quaterniond const &quat)
void commonSmallSingleAxisChecks(TestData *data, MeasurementType &kalmanMeas, InProgressType &inProgress, Axis const rotationAxis, bool const positive=true)
bool isPositiveDefinite(MatrixBase< Derived > const &m)