OSVR Framework (Internal Development Docs)  0.6-1962-g59773924
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Macros Modules Pages
TestIMU_UKF.cpp
Go to the documentation of this file.
1 
11 // Copyright 2016 Sensics, Inc.
12 //
13 // Licensed under the Apache License, Version 2.0 (the "License");
14 // you may not use this file except in compliance with the License.
15 // You may obtain a copy of the License at
16 //
17 // http://www.apache.org/licenses/LICENSE-2.0
18 //
19 // Unless required by applicable law or agreed to in writing, software
20 // distributed under the License is distributed on an "AS IS" BASIS,
21 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
22 // See the License for the specific language governing permissions and
23 // limitations under the License.
24 
25 // Internal Includes
26 #include "TestIMU_Common.h"
27 
29 
30 // Library/third-party includes
31 #include <Eigen/Eigenvalues>
32 
33 // Standard includes
34 // - none
35 
36 template <typename Derived>
37 inline bool isPositiveDefinite(MatrixBase<Derived> const &m) {
38  Derived const &mat = m.derived();
39  if (mat.rows() != mat.cols()) {
41  return false;
42  }
43  if (!mat.isApprox(mat.transpose())) {
45  return false;
46  }
47  // Having all positive eigenvalues is equivalent to being positive definite.
48  return (mat.eigenvalues().real().array() > 0.).all();
49 }
50 
51 template <typename Derived>
52 inline void checkSigmaPoints(MatrixBase<Derived> const &m) {
53  SECTION("Sigma points") {
54  Derived const &mat = m.derived();
55  {
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())
60  .all());
61  }
62  }
63 }
64 
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") {
71  REQUIRE(isPositiveDefinite(cov));
72  }
73  }
74 }
75 
76 template <typename Reconstruction>
77 inline void thenCheckReconstruction(Reconstruction const &recon) {
78  THEN("Reconstructed covariance should satisfy its invariants") {
79  thenCheckCovariance(recon.getCov());
80  }
81 }
82 template <typename Reconstruction>
83 inline void andThenCheckReconstruction(Reconstruction const &recon) {
84  AND_THEN("Reconstructed covariance should satisfy its invariants") {
85  thenCheckCovariance(recon.getCov());
86  }
87 }
88 template <typename GeneratorType>
89 inline void generatorChecks(GeneratorType const &gen) {
90  static const auto epsilon = NumTraits<double>::dummy_precision();
91 
92  SECTION("Weights") {
93  CAPTURE(gen.getWeightsForMean().transpose());
94  {
95  INFO("Weights should be non-zero");
96  REQUIRE(((gen.getWeightsForMean().array() > epsilon) ||
97  (gen.getWeightsForMean().array() < -epsilon))
98  .all());
99  }
100 #ifdef DEMAND_NONNEGATIVE_WEIGHTS
101  CHECK((gen.getWeightsForMean().array() >= 0.).all());
102 #endif
103 
104  CAPTURE(gen.getWeightsForCov().transpose());
105  {
106  INFO("Weights should be non-zero");
107  REQUIRE(((gen.getWeightsForCov().array() > epsilon) ||
108  (gen.getWeightsForCov().array() < -epsilon))
109  .all());
110  }
111 #ifdef DEMAND_NONNEGATIVE_WEIGHTS
112  CHECK((gen.getWeightsForCov().array() >= 0.).all());
113 #endif
114  }
115 
116  checkSigmaPoints(gen.getSigmaPoints());
117 }
118 
119 template <typename GeneratorType>
120 inline void thenCheckSigmaPointGenerator(GeneratorType const &gen) {
121  THEN("Sigma point generator should have reasonable output") {
122  generatorChecks(gen);
123  }
124 }
125 template <typename GeneratorType>
126 inline void andThenCheckSigmaPointGenerator(GeneratorType const &gen) {
127  AND_THEN("Sigma point generator should have reasonable output") {
128  generatorChecks(gen);
129  }
130 }
131 
132 TEST_CASE("Sigma point reconstruction validity") {
133 
134  static const auto DIM = 3;
135  using namespace osvr::kalman;
136  Matrix3d cov(Vector3d::Constant(10).asDiagonal());
137 
138  auto checkReconstruction = [&](Eigen::Vector3d const &mean) {
139  using namespace osvr::kalman;
140  using Generator = SigmaPointGenerator<DIM>;
141  using Reconstructor =
143  const auto params = SigmaPointParameters();
144  CAPTURE(mean);
145  CAPTURE(cov);
146  auto gen = Generator(mean, cov, params);
147  thenCheckSigmaPointGenerator(gen);
148 #if 0
149  static const auto numSigmaPoints = Generator::NumSigmaPoints;
150  for (std::size_t i = 0; i < numSigmaPoints; ++i) {
151  CAPTURE(gen.getSigmaPoint(i));
152  CHECK(false);
153  }
154 #endif
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 "
159  "to the input") {
160  CAPTURE(gen.getSigmaPoints());
161  CAPTURE(recon.getMean());
162  for (std::size_t i = 0; i < DIM; ++i) {
163  CAPTURE(i);
164  REQUIRE(recon.getMean()[i] == Approx(mean[i]));
165  }
166  CAPTURE(recon.getCov());
167  REQUIRE(recon.getCov().isApprox(cov));
168  }
169  }
170  };
171 
172  WHEN("Starting with a zero mean") { checkReconstruction(Vector3d::Zero()); }
173 
174  WHEN("Starting with a non-zero mean") {
175  checkReconstruction(Vector3d(1, 2, 3));
176  }
177  WHEN("Starting with a small non-zero mean") {
178  checkReconstruction(Vector3d(0.1, 0.2, 0.3));
179  }
180 }
181 
182 enum class Axis : std::size_t { X = 0, Y = 1, Z = 2 };
183 
184 inline double zeroOrValueForAxis(Axis rotationAxis, Axis currentAxis,
185  double value) {
186  return ((rotationAxis == currentAxis) ? value : 0.);
187 }
188 
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));
193 }
194 
195 inline Vector3d Vec3dSmallValueAt(Axis rotationAxis) {
196  return ZeroVec3dExceptAtAxis(rotationAxis, SMALL_VALUE);
197 }
198 
199 inline Vector3d Vec3dUnit(Axis rotationAxis) {
200  return ZeroVec3dExceptAtAxis(rotationAxis, 1.);
201 }
202 
203 inline double getSignCorrect(bool positive) { return (positive ? 1. : -1.); }
204 
205 template <typename MeasurementType, typename InProgressType>
206 inline void
207 commonSmallSingleAxisChecks(TestData *data, MeasurementType &kalmanMeas,
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());
213 
214  Vector3d residual = kalmanMeas.getResidual(data->state);
215  CAPTURE(residual.transpose());
216 
217  AND_THEN("residual directly computed by measurement should be zero, except "
218  "for the single axis of rotation, which should be of magnitude "
219  "SMALL_VALUE") {
220  CHECK(residual.isApprox(rotationVector));
221  }
222 
223  CAPTURE(inProgress.deltaz);
224  AND_THEN("computed deltaz should equal residual") {
225  CHECK(residual.isApprox(inProgress.deltaz));
226  }
227 
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]));
234  }
235 
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;
243  CAPTURE(stateIndex);
244  if (static_cast<std::size_t>(rotationAxis) == i) {
247  REQUIRE((signCorrect * inProgress.stateCorrection[stateIndex]) >
248  0);
249  REQUIRE((signCorrect * inProgress.stateCorrection[stateIndex]) <
250  SMALL_VALUE);
251  } else {
253  REQUIRE(inProgress.stateCorrection[stateIndex] == Approx(0.));
254  }
255  }
256  }
257 
258  AND_THEN("state correction should have an angular velocity component "
259  "with zero or small abs (sign matching rotation) corresponding to "
260  "rotation axis") {
261 
262  for (std::size_t i = 0; i < 3; ++i) {
263  const auto stateIndex = i + 9;
264  CAPTURE(stateIndex);
265  if (static_cast<std::size_t>(rotationAxis) == i) {
268  REQUIRE(signCorrect * inProgress.stateCorrection[stateIndex] >=
269  0.);
270  } else {
272  REQUIRE(inProgress.stateCorrection[stateIndex] == Approx(0.));
273  }
274  }
275  }
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());
280  }
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") {
286  REQUIRE((data->state.errorCovariance().array() <=
287  data->originalStateError.array())
288  .all());
289  }
290  thenCheckCovariance(data->state.errorCovariance());
291  }
292  }
293 }
294 
295 template <typename MeasurementType>
296 inline void
297 unscentedSmallSingleAxisChecks(TestData *data, MeasurementType &kalmanMeas,
298  Axis rotationAxis, bool positive = true) {
299  const auto params = kalman::SigmaPointParameters();
300  auto inProgress =
301  kalman::beginUnscentedCorrection(data->state, kalmanMeas, params);
302 
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());
307  }
308  checkSigmaPoints(inProgress.transformedPoints);
309  andThenCheckReconstruction(inProgress.reconstruction);
310 
311  commonSmallSingleAxisChecks(data, kalmanMeas, inProgress, rotationAxis,
312  positive);
313 }
314 
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());
321  }
322  auto inProgress = kalman::beginUnscentedCorrection(data->state, kalmanMeas);
323 
324  andThenCheckSigmaPointGenerator(inProgress.sigmaPoints);
325  AND_THEN("transformed sigma points should not all equal 0") {
326  REQUIRE_FALSE(inProgress.transformedPoints.isZero());
327  }
328  checkSigmaPoints(inProgress.transformedPoints);
329 
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());
334  }
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());
339  }
340 
341  const Quaterniond origQuat = data->state.getQuaternion();
342  AND_WHEN("the correction is applied") {
343  inProgress.finishCorrection();
344  THEN("state should be unchanged") {
345  CAPTURE(data->state.stateVector().transpose());
346  REQUIRE(data->state.stateVector().isZero());
347  CAPTURE(origQuat);
348  CAPTURE(data->state.getQuaternion());
349  REQUIRE(data->state.getQuaternion().coeffs().isApprox(
350  origQuat.coeffs()));
351  }
352  THEN("State error should have decreased") {
353  REQUIRE((data->state.errorCovariance().array() <=
354  data->originalStateError.array())
355  .all());
356  }
357  }
358 }
359 
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);
364  }
365 
366  WHEN("filtering in a small positive rotation about y") {
367  std::forward<F>(f)(std::forward<Args>(args)..., Axis::Y, true);
368  }
369 
370  WHEN("filtering in a small positive rotation about z") {
371  std::forward<F>(f)(std::forward<Args>(args)..., Axis::Z, true);
372  }
373 
374  WHEN("filtering in a small negative rotation about x") {
375  std::forward<F>(f)(std::forward<Args>(args)..., Axis::X, false);
376  }
377  WHEN("filtering in a small negative rotation about y") {
378  std::forward<F>(f)(std::forward<Args>(args)..., Axis::Y, false);
379  }
380 
381  WHEN("filtering in a small negative rotation about z") {
382  std::forward<F>(f)(std::forward<Args>(args)..., Axis::Z, false);
383  }
384 }
385 
386 #if 0
388  "unscented with identity calibration output", "[ukf]",
389  /*kalman::QFirst, kalman::QLast,*/ kalman::SplitQ) {
390 
391 #endif
392 TEST_CASE("unscented with identity calibration output", "[ukf]") {
393  using TypeParam = kalman::SplitQ;
394 
395  const auto params = kalman::SigmaPointParameters();
396  using MeasurementType = OrientationMeasurementUsingPolicy<TypeParam>;
397  // using MeasurementType = TypeParam;
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());
402 
403  THEN("the transformed measurement should equal the "
404  "measurement") {
405  CAPTURE(xformedMeas);
406  REQUIRE(xformedMeas.isApprox(Quaterniond::Identity()));
407 
408  MeasurementType kalmanMeas{xformedMeas, data->imuVariance};
409  checkEffectiveIdentityMeasurement(data.get(), kalmanMeas);
410  }
411  }
412  allSmallSingleAxisRotations([&](Axis rotationAxis, bool positive) {
413  const double signCorrect = getSignCorrect(positive);
414  auto radians = signCorrect * SMALL_VALUE;
415  CAPTURE(radians);
416  auto axisVec = Vec3dUnit(rotationAxis);
417  CAPTURE(axisVec.transpose());
418  Quaterniond smallRotation =
419  Quaterniond(AngleAxisd(radians, axisVec));
420 
421  CAPTURE(smallRotation);
422  Quaterniond xformedMeas = data->xform(smallRotation);
423  CAPTURE(xformedMeas);
424 
425  THEN("the transformed measurement should equal the "
426  "measurement") {
427  REQUIRE(xformedMeas.isApprox(smallRotation));
429  MeasurementType kalmanMeas{xformedMeas, data->imuVariance};
430  unscentedSmallSingleAxisChecks(data.get(), kalmanMeas,
431  rotationAxis, positive);
432  }
433  });
434  }
435 
436  auto runIncrementalSmallRotChecksNonIdentityState = [&](Axis rotationAxis,
437  bool positive) {
438  const double signCorrect = getSignCorrect(positive);
439  auto radians = signCorrect * SMALL_VALUE;
440  CAPTURE(radians);
441  auto axisVec = Vec3dUnit(rotationAxis);
442  CAPTURE(axisVec.transpose());
443  Quaterniond smallRotation = Quaterniond(AngleAxisd(radians, axisVec)) *
444  data->state.getQuaternion();
445 
446  CAPTURE(smallRotation);
447  Quaterniond xformedMeas = data->xform(smallRotation);
448  CAPTURE(xformedMeas);
449 
450  THEN("the transformed measurement should equal the "
451  "measurement") {
452  REQUIRE(xformedMeas.isApprox(smallRotation));
454  MeasurementType kalmanMeas{xformedMeas, data->imuVariance};
455  unscentedSmallSingleAxisChecks(data.get(), kalmanMeas, rotationAxis,
456  positive);
457  }
458  };
459  GIVEN("a state rotated about y") {
460  Quaterniond stateRotation(AngleAxisd(M_PI / 4., Vector3d::UnitY()));
461  data->state.setQuaternion(stateRotation);
462  allSmallSingleAxisRotations(
463  runIncrementalSmallRotChecksNonIdentityState);
464  }
465  GIVEN("a state rotated about x") {
466  Quaterniond stateRotation(AngleAxisd(M_PI / 4., Vector3d::UnitX()));
467  data->state.setQuaternion(stateRotation);
468  allSmallSingleAxisRotations(
469  runIncrementalSmallRotChecksNonIdentityState);
470  }
471 }
472 
473 TEST_CASE("unscented with small x rotation calibration output", "[ukf]") {
475  unique_ptr<TestData> data(new TestData);
476  CAPTURE(SMALL_VALUE);
477  data->roomToCameraRotation =
478  Quaterniond(AngleAxisd(SMALL_VALUE, Vector3d::UnitX()));
479  CAPTURE(data->roomToCameraRotation);
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)") {
488  Quaterniond meas = data->roomToCameraRotation.inverse();
489  CAPTURE(meas);
490  Quaterniond xformedMeas = data->xform(meas);
491 
492  THEN("the transformed measurement should be approximately the "
493  "identity") {
494  CAPTURE(xformedMeas);
495  REQUIRE(xformedMeas.isApprox(Quaterniond::Identity()));
496 
497  MeasurementType kalmanMeas{xformedMeas, data->imuVariance};
498  checkEffectiveIdentityMeasurement(data.get(), kalmanMeas);
499  }
500  }
501  }
502  GIVEN("an identity state in room space") {
504  CAPTURE(data->state.getQuaternion());
505  WHEN("filtering in an identity measurement (a measurement matching "
506  "state)") {
507  Quaterniond xformedMeas = data->xform(Quaterniond::Identity());
508 
509  THEN("the transformed measurement should be approximately the "
510  "room to camera rotation") {
511  CAPTURE(xformedMeas);
512  REQUIRE(xformedMeas.isApprox(data->roomToCameraRotation));
513 
514  MeasurementType kalmanMeas{xformedMeas, data->imuVariance};
515  checkEffectiveIdentityMeasurement(data.get(), kalmanMeas);
516  }
517  }
518  allSmallSingleAxisRotations([&](Axis rotationAxis, bool positive) {
519  const double signCorrect = getSignCorrect(positive);
520  auto radians = signCorrect * SMALL_VALUE;
521  CAPTURE(radians);
522  auto axisVec = Vec3dUnit(rotationAxis);
523  CAPTURE(axisVec.transpose());
524  Quaterniond smallRelRotationInCameraSpace =
525  Quaterniond(AngleAxisd(radians, axisVec)) *
526  data->state.getQuaternion();
527  CAPTURE(smallRelRotationInCameraSpace);
528  Quaterniond smallRotation = data->roomToCameraRotation.inverse() *
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));
536 
539  MeasurementType kalmanMeas{xformedMeas, data->imuVariance};
540  CAPTURE(kalmanMeas.getResidual(data->state));
541  unscentedSmallSingleAxisChecks(data.get(), kalmanMeas,
542  rotationAxis, positive);
543  }
544  });
545  }
546 }
547 
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();
552  SECTION(
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));
559  }
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.);
565  }
566 }
Header for a flexible Unscented-style Kalman filter correction from a measurement.
StateVector const & stateVector() const
xhat
Definition: PoseState.h:177
Header-only framework for building Kalman-style filters, prediction, and sensor fusion.
void setQuaternion(Eigen::Quaterniond const &quaternion)
Intended for startup use.
Definition: PoseState.h:189
EIGEN_MAKE_ALIGNED_OPERATOR_NEW Eigen::Quaterniond roomToCameraRotation
Quantity computed from the "camera pose" (room calibration output)
StateSquareMatrix const & errorCovariance() const
P.
Definition: PoseState.h:183
CATCH_TYPELIST_DESCRIBED_TESTCASE("identity calibration output","[.][ekf]", kalman::SplitQ, kalman::QLastWithSplitInnovation)
Definition: TestIMU_EKF.cpp:39
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)
Definition: TestIMU_UKF.cpp:37
BodyState state