|
38 | 38 | #include <vector> |
39 | 39 |
|
40 | 40 | #include <fuse_core/eigen_gtest.hpp> |
| 41 | +#include <fuse_core/util.hpp> |
41 | 42 | #include <fuse_models/omnidirectional_3d_predict.hpp> |
42 | 43 | #include <ceres/jet.h> |
43 | 44 |
|
@@ -711,3 +712,63 @@ TEST(Predict, VelocityDecaysGeometricallyOverMultipleSteps) |
711 | 712 | // AND after 1 second velocity has decayed to vel0 * exp(-k * 1.0) |
712 | 713 | EXPECT_NEAR(vel_linear.x(), 0.5 * std::exp(-k * 1.0), 1e-6); |
713 | 714 | } |
| 715 | + |
| 716 | +TEST(Predict, predictJacobians) |
| 717 | +{ |
| 718 | + // GIVEN a state away from gimbal lock (small angles keep the RPY parameterization well conditioned) |
| 719 | + double const dt = 0.1; |
| 720 | + const fuse_core::Vector3d position1(0.1, -0.2, 0.3); |
| 721 | + const fuse_core::Vector3d vel_linear1(1.0, 0.2, -0.1); |
| 722 | + const fuse_core::Vector3d vel_angular1(0.3, -0.2, 0.5); |
| 723 | + const fuse_core::Vector3d acc_linear1(0.5, -0.4, 0.2); |
| 724 | + const Eigen::Quaterniond orientation1 = Eigen::AngleAxisd(0.3, Eigen::Vector3d::UnitZ()) * |
| 725 | + Eigen::AngleAxisd(-0.1, Eigen::Vector3d::UnitY()) * |
| 726 | + Eigen::AngleAxisd(0.2, Eigen::Vector3d::UnitX()); |
| 727 | + |
| 728 | + // Extract RPY with the same convention predict() uses so the autodiff input matches the analytic input. |
| 729 | + double const quat[4] = { orientation1.w(), orientation1.x(), orientation1.y(), orientation1.z() }; |
| 730 | + double rpy[3]; |
| 731 | + fuse_core::quaternion2rpy(quat, rpy); |
| 732 | + |
| 733 | + // WHEN computing the analytic 15x15 state Jacobian (orientation columns in RPY space) |
| 734 | + fuse_core::Vector3d position2; |
| 735 | + fuse_core::Vector3d vel_linear2; |
| 736 | + fuse_core::Vector3d vel_angular2; |
| 737 | + fuse_core::Vector3d acc_linear2; |
| 738 | + Eigen::Quaterniond orientation2; |
| 739 | + fuse_core::Matrix15d jacobian_analytic; |
| 740 | + fuse_models::predict(position1, orientation1, vel_linear1, vel_angular1, acc_linear1, dt, position2, orientation2, |
| 741 | + vel_linear2, vel_angular2, acc_linear2, jacobian_analytic); |
| 742 | + |
| 743 | + // AND the same Jacobian by autodiff through the templated (RPY-in, RPY-out) overload |
| 744 | + using Jet = ceres::Jet<double, 15>; |
| 745 | + const std::array<Jet, 15> x{ |
| 746 | + Jet(position1.x(), 0), Jet(position1.y(), 1), Jet(position1.z(), 2), Jet(rpy[0], 3), |
| 747 | + Jet(rpy[1], 4), Jet(rpy[2], 5), Jet(vel_linear1.x(), 6), Jet(vel_linear1.y(), 7), |
| 748 | + Jet(vel_linear1.z(), 8), Jet(vel_angular1.x(), 9), Jet(vel_angular1.y(), 10), Jet(vel_angular1.z(), 11), |
| 749 | + Jet(acc_linear1.x(), 12), Jet(acc_linear1.y(), 13), Jet(acc_linear1.z(), 14) |
| 750 | + }; |
| 751 | + std::array<Jet, 3> position2_jet; |
| 752 | + std::array<Jet, 3> orientation2_jet; |
| 753 | + std::array<Jet, 3> vel_linear2_jet; |
| 754 | + std::array<Jet, 3> vel_angular2_jet; |
| 755 | + std::array<Jet, 3> acc_linear2_jet; |
| 756 | + fuse_models::predict(x.data(), x.data() + 3, x.data() + 6, x.data() + 9, x.data() + 12, Jet(dt), position2_jet.data(), |
| 757 | + orientation2_jet.data(), vel_linear2_jet.data(), vel_angular2_jet.data(), |
| 758 | + acc_linear2_jet.data()); |
| 759 | + |
| 760 | + fuse_core::Matrix15d jacobian_autodiff; |
| 761 | + const std::array<std::array<Jet, 3> const*, 5> outputs{ &position2_jet, &orientation2_jet, &vel_linear2_jet, |
| 762 | + &vel_angular2_jet, &acc_linear2_jet }; |
| 763 | + for (int block = 0; block < 5; ++block) |
| 764 | + { |
| 765 | + for (int row = 0; row < 3; ++row) |
| 766 | + { |
| 767 | + jacobian_autodiff.row(block * 3 + row) = (*outputs[block])[row].v.transpose(); |
| 768 | + } |
| 769 | + } |
| 770 | + |
| 771 | + // THEN the analytic Jacobian matches autodiff. This guards the quaternion->RPY conversion of the |
| 772 | + // orientation columns: the prior truncated assembly would fail this check. |
| 773 | + EXPECT_MATRIX_NEAR(jacobian_autodiff, jacobian_analytic, 1e-9); |
| 774 | +} |
0 commit comments