diff --git a/fuse.repos b/fuse.repos
new file mode 100644
index 000000000..8ad27e25b
--- /dev/null
+++ b/fuse.repos
@@ -0,0 +1,9 @@
+repositories:
+ locusrobotics/tf2_2d:
+ type: git
+ url: https://github.com/locusrobotics/tf2_2d.git
+ version: rolling
+ covariance_geometry_ros:
+ type: git
+ url: https://github.com/giafranchini/covariance_geometry_ros.git
+ version: iron
\ No newline at end of file
diff --git a/fuse_constraints/CMakeLists.txt b/fuse_constraints/CMakeLists.txt
index 8640fe075..2031533ee 100644
--- a/fuse_constraints/CMakeLists.txt
+++ b/fuse_constraints/CMakeLists.txt
@@ -78,6 +78,7 @@ add_library(${PROJECT_NAME}
src/absolute_orientation_3d_stamped_euler_constraint.cpp
src/absolute_pose_2d_stamped_constraint.cpp
src/absolute_pose_3d_stamped_constraint.cpp
+ src/absolute_pose_3d_stamped_euler_constraint.cpp
src/marginal_constraint.cpp
src/marginal_cost_function.cpp
src/marginalize_variables.cpp
@@ -85,11 +86,15 @@ add_library(${PROJECT_NAME}
src/normal_delta_orientation_2d.cpp
src/normal_delta_pose_2d.cpp
src/normal_prior_orientation_2d.cpp
+ src/normal_prior_orientation_3d.cpp
src/normal_prior_pose_2d.cpp
+ src/normal_prior_pose_3d.cpp
+ src/normal_prior_pose_3d_euler.cpp
src/relative_constraint.cpp
src/relative_orientation_3d_stamped_constraint.cpp
src/relative_pose_2d_stamped_constraint.cpp
src/relative_pose_3d_stamped_constraint.cpp
+ src/relative_pose_3d_stamped_euler_constraint.cpp
src/uuid_ordering.cpp
src/variable_constraints.cpp
)
diff --git a/fuse_constraints/fuse_plugins.xml b/fuse_constraints/fuse_plugins.xml
index aa6b02c54..71e7b8d9a 100644
--- a/fuse_constraints/fuse_plugins.xml
+++ b/fuse_constraints/fuse_plugins.xml
@@ -5,12 +5,24 @@
the 2D angular acceleration.
+
+
+ A constraint that represents either prior information about a 3D angular acceleration, or a direct measurement of
+ the 3D angular acceleration.
+
+
A constraint that represents either prior information about a 2D linear acceleration, or a direct measurement of
the 2D linear acceleration.
+
+
+ A constraint that represents either prior information about a 3D linear acceleration, or a direct measurement of
+ the 3D linear acceleration.
+
+
A constraint that represents either prior information about a 2D orientation, or a direct measurement of the
@@ -35,12 +47,24 @@
the 2D angular velocity.
+
+
+ A constraint that represents either prior information about a 3D angular velocity, or a direct measurement of
+ the 3D angular velocity.
+
+
A constraint that represents either prior information about a 2D linear velocity, or a direct measurement of
the 2D linear velocity.
+
+
+ A constraint that represents either prior information about a 3D linear velocity, or a direct measurement of
+ the 3D linear velocity.
+
+
A constraint that represents either prior information about a 3D orientation, or a direct measurement of the
@@ -63,6 +87,12 @@
A constraint that represents either prior information about a 3D pose, or a direct measurement of the 3D pose.
+
+
+ A constraint that represents either prior information about a 3D pose, or a direct measurement of the 3D pose,
+ orientation parametrized in RPY.
+
+
A constraint that represents remaining marginal information on a set of variables.
@@ -118,4 +148,9 @@
A constraint that represents a measurement on the difference between two 3D poses.
+
+
+ A constraint that represents a measurement on the difference between two 3D poses, orientation parametrized in RPY.
+
+
diff --git a/fuse_constraints/include/fuse_constraints/absolute_constraint.hpp b/fuse_constraints/include/fuse_constraints/absolute_constraint.hpp
index 8bb4ac4c7..995e0e8ea 100644
--- a/fuse_constraints/include/fuse_constraints/absolute_constraint.hpp
+++ b/fuse_constraints/include/fuse_constraints/absolute_constraint.hpp
@@ -46,11 +46,15 @@
#include
#include
#include
+#include
#include
+#include
#include
#include
#include
#include
+#include
+#include
#include
#include
@@ -196,27 +200,39 @@ class AbsoluteConstraint : public fuse_core::Constraint
// Define unique names for the different variations of the absolute constraint
using AbsoluteAccelerationAngular2DStampedConstraint =
AbsoluteConstraint;
+using AbsoluteAccelerationAngular3DStampedConstraint =
+ AbsoluteConstraint;
using AbsoluteAccelerationLinear2DStampedConstraint =
AbsoluteConstraint;
+using AbsoluteAccelerationLinear3DStampedConstraint =
+ AbsoluteConstraint;
using AbsoluteOrientation2DStampedConstraint =
AbsoluteConstraint;
using AbsolutePosition2DStampedConstraint = AbsoluteConstraint;
using AbsolutePosition3DStampedConstraint = AbsoluteConstraint;
using AbsoluteVelocityAngular2DStampedConstraint =
AbsoluteConstraint;
+using AbsoluteVelocityAngular3DStampedConstraint =
+ AbsoluteConstraint;
using AbsoluteVelocityLinear2DStampedConstraint =
AbsoluteConstraint;
+using AbsoluteVelocityLinear3DStampedConstraint =
+ AbsoluteConstraint;
} // namespace fuse_constraints
// Include the template implementation
#include
BOOST_CLASS_EXPORT_KEY(fuse_constraints::AbsoluteAccelerationAngular2DStampedConstraint);
+BOOST_CLASS_EXPORT_KEY(fuse_constraints::AbsoluteAccelerationAngular3DStampedConstraint);
BOOST_CLASS_EXPORT_KEY(fuse_constraints::AbsoluteAccelerationLinear2DStampedConstraint);
+BOOST_CLASS_EXPORT_KEY(fuse_constraints::AbsoluteAccelerationLinear3DStampedConstraint);
BOOST_CLASS_EXPORT_KEY(fuse_constraints::AbsoluteOrientation2DStampedConstraint);
BOOST_CLASS_EXPORT_KEY(fuse_constraints::AbsolutePosition2DStampedConstraint);
BOOST_CLASS_EXPORT_KEY(fuse_constraints::AbsolutePosition3DStampedConstraint);
BOOST_CLASS_EXPORT_KEY(fuse_constraints::AbsoluteVelocityAngular2DStampedConstraint);
+BOOST_CLASS_EXPORT_KEY(fuse_constraints::AbsoluteVelocityAngular3DStampedConstraint);
BOOST_CLASS_EXPORT_KEY(fuse_constraints::AbsoluteVelocityLinear2DStampedConstraint);
+BOOST_CLASS_EXPORT_KEY(fuse_constraints::AbsoluteVelocityLinear3DStampedConstraint);
#endif // FUSE_CONSTRAINTS__ABSOLUTE_CONSTRAINT_HPP_
diff --git a/fuse_constraints/include/fuse_constraints/absolute_constraint_impl.hpp b/fuse_constraints/include/fuse_constraints/absolute_constraint_impl.hpp
index cbb689b54..197db9c5e 100644
--- a/fuse_constraints/include/fuse_constraints/absolute_constraint_impl.hpp
+++ b/fuse_constraints/include/fuse_constraints/absolute_constraint_impl.hpp
@@ -155,6 +155,18 @@ inline std::string AbsoluteConstraint
+inline std::string AbsoluteConstraint::type() const
+{
+ return "fuse_constraints::AbsoluteAccelerationAngular3DStampedConstraint";
+}
+
+template<>
+inline std::string AbsoluteConstraint::type() const
+{
+ return "fuse_constraints::AbsoluteAccelerationLinear3DStampedConstraint";
+}
+
template<>
inline std::string AbsoluteConstraint::type() const
{
@@ -185,6 +197,18 @@ inline std::string AbsoluteConstraint::
return "fuse_constraints::AbsoluteVelocityLinear2DStampedConstraint";
}
+template<>
+inline std::string AbsoluteConstraint::type() const
+{
+ return "fuse_constraints::AbsoluteVelocityAngular3DStampedConstraint";
+}
+
+template<>
+inline std::string AbsoluteConstraint::type() const
+{
+ return "fuse_constraints::AbsoluteVelocityLinear3DStampedConstraint";
+}
+
} // namespace fuse_constraints
#endif // FUSE_CONSTRAINTS__ABSOLUTE_CONSTRAINT_IMPL_HPP_
diff --git a/fuse_constraints/include/fuse_constraints/absolute_pose_3d_stamped_euler_constraint.hpp b/fuse_constraints/include/fuse_constraints/absolute_pose_3d_stamped_euler_constraint.hpp
new file mode 100644
index 000000000..672b52dac
--- /dev/null
+++ b/fuse_constraints/include/fuse_constraints/absolute_pose_3d_stamped_euler_constraint.hpp
@@ -0,0 +1,194 @@
+/*
+ * Software License Agreement (BSD License)
+ *
+ * Copyright (c) 2023, Giacomo Franchini
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ * * Neither the name of the copyright holder nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+
+#ifndef FUSE_CONSTRAINTS__ABSOLUTE_POSE_3D_STAMPED_EULER_CONSTRAINT_HPP_
+#define FUSE_CONSTRAINTS__ABSOLUTE_POSE_3D_STAMPED_EULER_CONSTRAINT_HPP_
+
+#include
+
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+
+
+namespace fuse_constraints
+{
+
+/**
+ * @brief A constraint that represents either prior information about a 3D pose, or a direct
+ * measurement of the 3D pose.
+ *
+ * A 3D pose is the combination of a 3D position and a 3D orientation variable. As a convenience,
+ * this class applies an absolute constraint on both variables at once. This type of constraint
+ * arises in many situations. In mapping it is common to define the very first pose as the origin.
+ * Some sensors, such as GPS, provide direct measurements of the robot's pose in the global frame.
+ * And localization systems often match laserscans or pointclouds to a prior map (scan-to-map
+ * measurements). This constraint holds the measured 3D pose: orientation is parametrized as roll-pitch-yaw
+ * Euler angles and the covariance represents the error around each translational and rotational axis.
+ * This constraint allows measurement of a subset of the pose components given in the variable.
+ */
+
+class AbsolutePose3DStampedEulerConstraint : public fuse_core::Constraint
+{
+public:
+ FUSE_CONSTRAINT_DEFINITIONS_WITH_EIGEN(AbsolutePose3DStampedEulerConstraint)
+
+ /**
+ * @brief Default constructor
+ */
+ AbsolutePose3DStampedEulerConstraint() = default;
+
+ /**
+ * @brief Create a constraint using a measurement/prior of the 3D pose
+ *
+ * @param[in] source The name of the sensor or motion model that generated this constraint
+ * @param[in] position The variable representing the position components of the pose
+ * @param[in] orientation The variable representing the orientation components of the pose
+ * @param[in] mean The measured/prior pose as a vector
+ * (6x1 vector: x, y, z, roll, pitch, yaw)
+ * @param[in] covariance The measurement/prior covariance (6x6 matrix: x, y, z, roll, pitch, yaw)
+ */
+ AbsolutePose3DStampedEulerConstraint(
+ const std::string & source,
+ const fuse_variables::Position3DStamped & position,
+ const fuse_variables::Orientation3DStamped & orientation,
+ const fuse_core::Vector6d & mean,
+ const fuse_core::Matrix6d & covariance);
+
+ /**
+ * @brief Create a constraint using a partial measurement/prior of the 3D pose
+ *
+ * @param[in] source The name of the sensor or motion model that generated this constraint
+ * @param[in] position The variable representing the position components of the pose
+ * @param[in] orientation The variable representing the orientation components of the pose
+ * @param[in] partial_mean The measured/prior pose as a vector
+ * (6x1 vector: x, y, z, roll, pitch, yaw)
+ * @param[in] partial_covariance The measurement/prior covariance (6x6 matrix: x, y, z, roll, pitch, yaw)
+ * @param[in] variable_indices The indices of the measured variables
+ */
+ AbsolutePose3DStampedEulerConstraint(
+ const std::string & source,
+ const fuse_variables::Position3DStamped & position,
+ const fuse_variables::Orientation3DStamped & orientation,
+ const fuse_core::Vector6d & partial_mean,
+ const fuse_core::MatrixXd & partial_covariance,
+ const std::vector & variable_indices);
+
+ /**
+ * @brief Destructor
+ */
+ virtual ~AbsolutePose3DStampedEulerConstraint() = default;
+
+ /**
+ * @brief Read-only access to the measured/prior vector of mean values.
+ *
+ * Order is (x, y, z, roll, pitch, yaw)
+ */
+ const fuse_core::Vector6d & mean() const {return mean_;}
+
+ /**
+ * @brief Read-only access to the square root information matrix.
+ *
+ * Order is (x, y, z, roll, pitch, yaw)
+ */
+ const fuse_core::MatrixXd & sqrtInformation() const {return sqrt_information_;}
+
+ /**
+ * @brief Compute the measurement covariance matrix.
+ *
+ * Order is (x, y, z, roll, pitch, yaw)
+ */
+ fuse_core::Matrix6d covariance() const;
+
+ /**
+ * @brief Print a human-readable description of the constraint to the provided stream.
+ *
+ * @param[out] stream The stream to write to. Defaults to stdout.
+ */
+ void print(std::ostream & stream = std::cout) const override;
+
+ /**
+ * @brief Construct an instance of this constraint's cost function
+ *
+ * The function caller will own the new cost function instance. It is the responsibility of the
+ * caller to delete the cost function object when it is no longer needed. If the pointer is
+ * provided to a Ceres::Problem object, the Ceres::Problem object will takes ownership of the
+ * pointer and delete it during destruction.
+ *
+ * @return A base pointer to an instance of a derived CostFunction.
+ */
+ ceres::CostFunction * costFunction() const override;
+
+protected:
+ fuse_core::Vector6d mean_; //!< The measured/prior mean vector for this variable
+ fuse_core::MatrixXd sqrt_information_; //!< The square root information matrix
+
+private:
+ // Allow Boost Serialization access to private methods
+ friend class boost::serialization::access;
+
+ /**
+ * @brief The Boost Serialize method that serializes all of the data members in to/out of the
+ * archive
+ *
+ * @param[in/out] archive - The archive object that holds the serialized class members
+ * @param[in] version - The version of the archive being read/written. Generally unused.
+ */
+ template
+ void serialize(Archive & archive, const unsigned int /* version */)
+ {
+ archive & boost::serialization::base_object(*this);
+ archive & mean_;
+ archive & sqrt_information_;
+ }
+};
+
+} // namespace fuse_constraints
+
+BOOST_CLASS_EXPORT_KEY(fuse_constraints::AbsolutePose3DStampedEulerConstraint);
+
+#endif // FUSE_CONSTRAINTS__ABSOLUTE_POSE_3D_STAMPED_EULER_CONSTRAINT_HPP_
diff --git a/fuse_constraints/include/fuse_constraints/normal_delta_pose_3d_euler_cost_functor.hpp b/fuse_constraints/include/fuse_constraints/normal_delta_pose_3d_euler_cost_functor.hpp
new file mode 100644
index 000000000..6ac3eb1c2
--- /dev/null
+++ b/fuse_constraints/include/fuse_constraints/normal_delta_pose_3d_euler_cost_functor.hpp
@@ -0,0 +1,167 @@
+/*
+ * Software License Agreement (BSD License)
+ *
+ * Copyright (c) 2023, Giacomo Franchini
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ * * Neither the name of the copyright holder nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+#ifndef FUSE_CONSTRAINTS__NORMAL_DELTA_POSE_3D_EULER_COST_FUNCTOR_HPP_
+#define FUSE_CONSTRAINTS__NORMAL_DELTA_POSE_3D_EULER_COST_FUNCTOR_HPP_
+
+#include
+#include
+
+#include
+#include
+#include
+
+
+namespace fuse_constraints
+{
+
+/**
+ * @brief Implements a cost function that models a difference between 3D pose variables.
+ *
+ * A single pose involves two variables: a 3D position and a 3D orientation. This cost function
+ * computes the difference using standard 3D transformation math:
+ *
+ * cost(x) = || A * [ (q1^-1 * (p2 - p1)) - b(0:2) ] ||^2
+ * || [ quat2rpy(q1^-1 * q2) - b(3:5) ] ||
+ *
+ * where p1 and p2 are the position variables, q1 and q2 are the quaternion orientation variables,
+ * and the matrix A and the vector b are fixed. In case the user is interested in implementing a
+ * cost function of the form:
+ *
+ * cost(X) = (X - mu)^T S^{-1} (X - mu)
+ *
+ * where, mu is a vector and S is a covariance matrix, then, A = S^{-1/2}, i.e the matrix A is the
+ * square root information matrix (the inverse of the covariance).
+ *
+ */
+class NormalDeltaPose3DEulerCostFunctor
+{
+public:
+ FUSE_MAKE_ALIGNED_OPERATOR_NEW()
+
+ /**
+ * @brief Constructor
+ *
+ * @param[in] A The residual weighting matrix, most likely the square root information matrix in
+ * order (dx, dy, dz, droll, dpitch, dyaw)
+ * @param[in] b The exposed pose difference in order (dx, dy, dz, droll, dpitch, dyaw)
+ */
+ NormalDeltaPose3DEulerCostFunctor(const fuse_core::MatrixXd & A, const fuse_core::Vector6d & b);
+
+ /**
+ * @brief Compute the cost values/residuals using the provided variable/parameter values
+ */
+ template
+ bool operator()(
+ const T * const position1,
+ const T * const orientation1,
+ const T * const position2,
+ const T * const orientation2,
+ T * residual) const;
+
+private:
+ fuse_core::MatrixXd A_;
+ fuse_core::Vector6d b_;
+};
+
+NormalDeltaPose3DEulerCostFunctor::NormalDeltaPose3DEulerCostFunctor(
+ const fuse_core::MatrixXd & A,
+ const fuse_core::Vector6d & b)
+: A_(A),
+ b_(b)
+{
+ CHECK_GT(A_.rows(), 0);
+ CHECK_EQ(A_.cols(), 6);
+}
+
+template
+bool NormalDeltaPose3DEulerCostFunctor::operator()(
+ const T * const position1,
+ const T * const orientation1,
+ const T * const position2,
+ const T * const orientation2,
+ T * residual) const
+{
+ T full_residuals[6];
+ T position_delta_rotated[3];
+ T orientation_delta[4];
+ T orientation_delta_rpy[3];
+
+ T orientation1_inverse[4]
+ {
+ orientation1[0],
+ -orientation1[1],
+ -orientation1[2],
+ -orientation1[3]
+ };
+
+ T position_delta[3]
+ {
+ position2[0] - position1[0],
+ position2[1] - position1[1],
+ position2[2] - position1[2]
+ };
+
+ // Compute the position residual
+ ceres::QuaternionRotatePoint(orientation1_inverse, position_delta, position_delta_rotated);
+ full_residuals[0] = position_delta_rotated[0] - T(b_(0));
+ full_residuals[1] = position_delta_rotated[1] - T(b_(1));
+ full_residuals[2] = position_delta_rotated[2] - T(b_(2));
+
+ // Compute the orientation residual
+ ceres::QuaternionProduct(orientation1_inverse, orientation2, orientation_delta);
+ orientation_delta_rpy[0] = fuse_core::getRoll(
+ orientation_delta[0], orientation_delta[1],
+ orientation_delta[2], orientation_delta[3]);
+ orientation_delta_rpy[1] = fuse_core::getPitch(
+ orientation_delta[0], orientation_delta[1],
+ orientation_delta[2], orientation_delta[3]);
+ orientation_delta_rpy[2] = fuse_core::getYaw(
+ orientation_delta[0], orientation_delta[1],
+ orientation_delta[2], orientation_delta[3]);
+ full_residuals[3] = orientation_delta_rpy[0] - T(b_(3));
+ full_residuals[4] = orientation_delta_rpy[1] - T(b_(4));
+ full_residuals[5] = orientation_delta_rpy[2] - T(b_(5));
+
+ // Scale the residuals by the square root information matrix to account for
+ // the measurement uncertainty.
+ Eigen::Map> full_residuals_map(full_residuals);
+ Eigen::Map> residuals_map(residual, A_.rows());
+ residuals_map = A_.template cast() * full_residuals_map;
+
+ return true;
+}
+
+} // namespace fuse_constraints
+
+#endif // FUSE_CONSTRAINTS__NORMAL_DELTA_POSE_3D_EULER_COST_FUNCTOR_HPP_
diff --git a/fuse_constraints/include/fuse_constraints/normal_prior_orientation_3d.hpp b/fuse_constraints/include/fuse_constraints/normal_prior_orientation_3d.hpp
new file mode 100644
index 000000000..4e6c4b60e
--- /dev/null
+++ b/fuse_constraints/include/fuse_constraints/normal_prior_orientation_3d.hpp
@@ -0,0 +1,100 @@
+/*
+ * Software License Agreement (BSD License)
+ *
+ * Copyright (c) 2023, Giacomo Franchini
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ * * Neither the name of the copyright holder nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+#ifndef FUSE_CONSTRAINTS__NORMAL_PRIOR_ORIENTATION_3D_HPP_
+#define FUSE_CONSTRAINTS__NORMAL_PRIOR_ORIENTATION_3D_HPP_
+
+#include
+
+#include
+
+
+namespace fuse_constraints
+{
+
+/**
+ * @brief Create a prior cost function on a 3D orientation variable (quaternion)
+ *
+ * The cost function is of the form:
+ *
+ * || ||^2
+ * cost(x) = || A * AngleAxis(b^-1 * q) ||
+ * || ||
+ *
+ * where the matrix A and the vector b are fixed, and q is the variable being measured, represented
+ * as a quaternion. The AngleAxis function converts a quaternion into a 3-vector of the form
+ * theta*k, where k is the unit vector axis of rotation and theta is the angle of rotation. The A
+ * matrix is applied to the angle-axis 3-vector.
+ *
+ * In case the user is interested in implementing a cost function of the form
+ *
+ * cost(X) = (X - mu)^T S^{-1} (X - mu)
+ *
+ * where, mu is a vector and S is a covariance matrix, then, A = S^{-1/2}, i.e the matrix A is the
+ * square root information matrix (the inverse of the covariance).
+ */
+class NormalPriorOrientation3D : public ceres::SizedCostFunction<3, 4>
+{
+public:
+ /**
+ * @brief Construct a cost function instance
+ *
+ * @param[in] A The residual weighting matrix, most likely the square root information matrix in
+ * order (qx, qy, qz)
+ * @param[in] b The orientation measurement or prior in order (qw, qx, qy, qz)
+ */
+ NormalPriorOrientation3D(const fuse_core::Matrix3d & A, const fuse_core::Vector4d & b);
+
+ /**
+ * @brief Destructor
+ */
+ virtual ~NormalPriorOrientation3D() = default;
+
+ /**
+ * @brief Compute the cost values/residuals, and optionally the Jacobians, using the provided
+ * variable/parameter values
+ */
+ virtual bool Evaluate(
+ double const * const * parameters,
+ double * residuals,
+ double ** jacobians) const;
+
+private:
+ fuse_core::Matrix3d A_; //!< The residual weighting matrix, most likely the square root
+ //!< information matrix
+ fuse_core::Vector4d b_; //!< The measured 3D orientation value
+};
+
+} // namespace fuse_constraints
+
+#endif // FUSE_CONSTRAINTS__NORMAL_PRIOR_ORIENTATION_3D_HPP_
diff --git a/fuse_constraints/include/fuse_constraints/normal_prior_pose_3d.hpp b/fuse_constraints/include/fuse_constraints/normal_prior_pose_3d.hpp
new file mode 100644
index 000000000..adc281aac
--- /dev/null
+++ b/fuse_constraints/include/fuse_constraints/normal_prior_pose_3d.hpp
@@ -0,0 +1,100 @@
+/*
+ * Software License Agreement (BSD License)
+ *
+ * Copyright (c) 2020, Locus Robotics
+ * Copyright (c) 2023, Giacomo Franchini
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ * * Neither the name of the copyright holder nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+#ifndef FUSE_CONSTRAINTS__NORMAL_PRIOR_POSE_3D_HPP_
+#define FUSE_CONSTRAINTS__NORMAL_PRIOR_POSE_3D_HPP_
+
+#include
+
+#include
+
+
+namespace fuse_constraints
+{
+
+/**
+ * @brief Create a prior cost function on both the position and orientation variables at once.
+ *
+ * The Ceres::NormalPrior cost function only supports a single variable. This is a convenience cost
+ * function that applies a prior constraint on both the position and orientation variables at once.
+ *
+ * The cost function is of the form:
+ *
+ * || [ x - b(0)] ||^2
+ * cost(x) = || A * [ y - b(1)] ||
+ * || [ z - b(2)] ||
+ * || [ quat2angleaxis(b(3-6)^-1 * q)] ||
+ *
+ * In case the user is interested in implementing a cost function of the form:
+ *
+ * cost(X) = (X - mu)^T S^{-1} (X - mu)
+ *
+ * where, mu is a vector and S is a covariance matrix, then, A = S^{-1/2}, i.e the matrix A is the
+ * square root information matrix (the inverse of the covariance).
+ */
+class NormalPriorPose3D : public ceres::SizedCostFunction<6, 3, 4>
+{
+public:
+ /**
+ * @brief Construct a cost function instance
+ *
+ * @param[in] A The residual weighting matrix, most likely the square root information matrix in
+ * order (x, y, z, roll, pitch, yaw)
+ * @param[in] b The pose measurement or prior in order (x, y, z, qw, qx, qy, qz)
+ */
+ NormalPriorPose3D(const fuse_core::Matrix6d & A, const fuse_core::Vector7d & b);
+
+ /**
+ * @brief Destructor
+ */
+ virtual ~NormalPriorPose3D() = default;
+
+ /**
+ * @brief Compute the cost values/residuals, and optionally the Jacobians, using the provided
+ * variable/parameter values
+ */
+ virtual bool Evaluate(
+ double const * const * parameters,
+ double * residuals,
+ double ** jacobians) const;
+
+private:
+ fuse_core::Matrix6d A_; //!< The residual weighting matrix, most likely the square root
+ //!< information matrix
+ fuse_core::Vector7d b_; //!< The measured 3D pose value
+};
+
+} // namespace fuse_constraints
+
+#endif // FUSE_CONSTRAINTS__NORMAL_PRIOR_POSE_3D_HPP_
diff --git a/fuse_constraints/include/fuse_constraints/normal_prior_pose_3d_euler.hpp b/fuse_constraints/include/fuse_constraints/normal_prior_pose_3d_euler.hpp
new file mode 100644
index 000000000..f2d1b3c64
--- /dev/null
+++ b/fuse_constraints/include/fuse_constraints/normal_prior_pose_3d_euler.hpp
@@ -0,0 +1,106 @@
+/*
+ * Software License Agreement (BSD License)
+ *
+ * Copyright (c) 2023, Giacomo Franchini
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ * * Neither the name of the copyright holder nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+#ifndef FUSE_CONSTRAINTS__NORMAL_PRIOR_POSE_3D_EULER_HPP_
+#define FUSE_CONSTRAINTS__NORMAL_PRIOR_POSE_3D_EULER_HPP_
+
+#include
+
+#include
+
+
+namespace fuse_constraints
+{
+
+/**
+ * @brief Create a prior cost function on both the position and orientation variables at once.
+ *
+ * The Ceres::NormalPrior cost function only supports a single variable. This is a convenience cost
+ * function that applies a prior constraint on both the position and orientation variables at once.
+ *
+ * The cost function is of the form:
+ *
+ * || [ x - b(0)] ||^2
+ * cost(x) = || A * [ y - b(1)] ||
+ * || [ z - b(2)] ||
+ * || [ quat2eul(q) - b(3:5) ] ||
+ *
+ * Here, the matrix A can be of variable size, thereby permitting the computation of errors for
+ * partial measurements. The vector b is a fixed-size 6x1. In case the user is interested in
+ * implementing a cost function of the form:
+ *
+ * cost(X) = (X - mu)^T S^{-1} (X - mu)
+ *
+ * where, mu is a vector and S is a covariance matrix, then, A = S^{-1/2}, i.e the matrix A is the
+ * square root information matrix (the inverse of the covariance).
+ */
+class NormalPriorPose3DEuler : public ceres::SizedCostFunction
+{
+public:
+ /**
+ * @brief Construct a cost function instance
+ *
+ * The residual weighting matrix can vary in size, as this cost functor can be used to compute
+ * costs for partial vectors. The number of rows of A will be the number of dimensions for which
+ * you want to compute the error, and the number of columns in A will be fixed at 6. For example,
+ * if we just want to use the values of x, y and yaw, then \p A will be of size 3x6.
+ *
+ * @param[in] A The residual weighting matrix, most likely the square root information matrix in
+ * order (x, y, z, roll, pitch, yaw)
+ * @param[in] b The pose measurement or prior in order (x, y, z, roll, pitch, yaw)
+ */
+ NormalPriorPose3DEuler(const fuse_core::MatrixXd & A, const fuse_core::Vector6d & b);
+
+ /**
+ * @brief Destructor
+ */
+ virtual ~NormalPriorPose3DEuler() = default;
+
+ /**
+ * @brief Compute the cost values/residuals, and optionally the Jacobians, using the provided
+ * variable/parameter values
+ */
+ virtual bool Evaluate(
+ double const * const * parameters,
+ double * residuals,
+ double ** jacobians) const;
+
+private:
+ fuse_core::MatrixXd A_; //!< The residual weighting matrix, most likely the square root
+ //!< information matrix
+ fuse_core::Vector6d b_; //!< The measured 3D pose value
+};
+
+} // namespace fuse_constraints
+
+#endif // FUSE_CONSTRAINTS__NORMAL_PRIOR_POSE_3D_EULER_HPP_
diff --git a/fuse_constraints/include/fuse_constraints/normal_prior_pose_3d_euler_cost_functor.hpp b/fuse_constraints/include/fuse_constraints/normal_prior_pose_3d_euler_cost_functor.hpp
new file mode 100644
index 000000000..085bc2fd6
--- /dev/null
+++ b/fuse_constraints/include/fuse_constraints/normal_prior_pose_3d_euler_cost_functor.hpp
@@ -0,0 +1,133 @@
+/*
+ * Software License Agreement (BSD License)
+ *
+ * Copyright (c) 2023, Giacomo Franchini
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ * * Neither the name of the copyright holder nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+#ifndef FUSE_CONSTRAINTS__NORMAL_PRIOR_POSE_3D_EULER_COST_FUNCTOR_HPP_
+#define FUSE_CONSTRAINTS__NORMAL_PRIOR_POSE_3D_EULER_COST_FUNCTOR_HPP_
+
+#include
+
+#include
+#include
+#include
+
+
+namespace fuse_constraints
+{
+
+/**
+ * @brief Create a prior cost function on both the 3D position and orientation variables at once.
+ *
+ * The Ceres::NormalPrior cost function only supports a single variable. This is a convenience cost
+ * function that applies a prior constraint on both the 3D position and orientation variables at
+ * once.
+ *
+ * The cost function is of the form:
+ *
+ * cost(x) = || A * [ p - b(0:2) ] ||^2
+ * || [ quat2eul(q) - b(3:5) ] ||
+ *
+ * where, the matrix A and the vector b are fixed, p is the position variable, and q is the
+ * orientation variable. In case the user is interested in implementing a cost function of the form
+ *
+ * cost(X) = (X - mu)^T S^{-1} (X - mu)
+ *
+ * where, mu is a vector and S is a covariance matrix, then, A = S^{-1/2}, i.e the matrix A is the
+ * square root information matrix (the inverse of the covariance).
+ */
+class NormalPriorPose3DEulerCostFunctor
+{
+public:
+ FUSE_MAKE_ALIGNED_OPERATOR_NEW()
+
+ /**
+ * @brief Construct a cost function instance
+ *
+ * @param[in] A The residual weighting matrix, most likely the square root information matrix in
+ * order (x, y, z, roll, pitch, yaw)
+ * @param[in] b The 3D pose measurement or prior in order (x, y, z, roll, pitch, yaw)
+ */
+ NormalPriorPose3DEulerCostFunctor(const fuse_core::MatrixXd & A, const fuse_core::Vector6d & b);
+
+ /**
+ * @brief Evaluate the cost function. Used by the Ceres optimization engine.
+ */
+ template
+ bool operator()(const T * const position, const T * const orientation, T * residual) const;
+
+private:
+ fuse_core::MatrixXd A_;
+ fuse_core::Vector6d b_;
+};
+
+NormalPriorPose3DEulerCostFunctor::NormalPriorPose3DEulerCostFunctor(
+ const fuse_core::MatrixXd & A,
+ const fuse_core::Vector6d & b)
+: A_(A),
+ b_(b)
+{
+ CHECK_GT(A_.rows(), 0);
+ CHECK_EQ(A_.cols(), 6);
+}
+
+template
+bool NormalPriorPose3DEulerCostFunctor::operator()(
+ const T * const position, const T * const orientation,
+ T * residual) const
+{
+ T full_residuals[6];
+ T orientation_rpy[3] = {
+ fuse_core::getRoll(orientation[0], orientation[1], orientation[2], orientation[3]),
+ fuse_core::getPitch(orientation[0], orientation[1], orientation[2], orientation[3]),
+ fuse_core::getYaw(orientation[0], orientation[1], orientation[2], orientation[3])
+ };
+
+ // Compute the position residual
+ full_residuals[0] = position[0] - T(b_(0));
+ full_residuals[1] = position[1] - T(b_(1));
+ full_residuals[2] = position[2] - T(b_(2));
+ // Compute the orientation residual
+ full_residuals[3] = orientation_rpy[0] - T(b_(3));
+ full_residuals[4] = orientation_rpy[1] - T(b_(4));
+ full_residuals[5] = orientation_rpy[2] - T(b_(5));
+
+ // Scale the residuals by the square root information matrix to account for
+ // the measurement uncertainty.
+ Eigen::Map> full_residuals_map(full_residuals);
+ Eigen::Map> residuals_map(residual, A_.rows());
+ residuals_map = A_.template cast() * full_residuals_map;
+ return true;
+}
+
+} // namespace fuse_constraints
+
+#endif // FUSE_CONSTRAINTS__NORMAL_PRIOR_POSE_3D_COST_FUNCTOR_HPP_
diff --git a/fuse_constraints/include/fuse_constraints/relative_pose_3d_stamped_euler_constraint.hpp b/fuse_constraints/include/fuse_constraints/relative_pose_3d_stamped_euler_constraint.hpp
new file mode 100644
index 000000000..424515bed
--- /dev/null
+++ b/fuse_constraints/include/fuse_constraints/relative_pose_3d_stamped_euler_constraint.hpp
@@ -0,0 +1,189 @@
+/*
+ * Software License Agreement (BSD License)
+ *
+ * Copyright (c) 2023, Giacomo Franchini
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ * * Neither the name of the copyright holder nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef FUSE_CONSTRAINTS__RELATIVE_POSE_3D_STAMPED_EULER_CONSTRAINT_HPP_
+#define FUSE_CONSTRAINTS__RELATIVE_POSE_3D_STAMPED_EULER_CONSTRAINT_HPP_
+
+#include
+
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+
+
+namespace fuse_constraints
+{
+
+/**
+ * @brief A constraint that represents a measurement on the difference between two 3D poses.
+ *
+ * This type of constraint arises in many situations. Many types of incremental odometry
+ * measurements (e.g., visual odometry) measure the change in the pose, not the pose directly. This
+ * constraint holds the measured 3D pose change and the measurement uncertainty/covariance.
+ */
+class RelativePose3DStampedEulerConstraint : public fuse_core::Constraint
+{
+public:
+ FUSE_CONSTRAINT_DEFINITIONS_WITH_EIGEN(RelativePose3DStampedEulerConstraint)
+
+ /**
+ * @brief Default constructor
+ */
+ RelativePose3DStampedEulerConstraint() = default;
+
+ /**
+ * @brief Create a constraint using a measurement/prior of the relative 3D pose
+ *
+ * @param[in] source The name of the sensor or motion model that generated this constraint
+ * @param[in] position1 The variable representing the position components of the first pose
+ * @param[in] orientation1 The variable representing the orientation components of the first pose
+ * @param[in] position2 The variable representing the position components of the second pose
+ * @param[in] orientation2 The variable representing the orientation components of the second pose
+ * @param[in] delta The measured change in the pose
+ * (6x1 vector: dx, dy, dz, droll, dpitch, dyaw)
+ * @param[in] covariance The measurement covariance (6x6 matrix: dx, dy, dz, droll, dpitch, dyaw)
+ */
+ RelativePose3DStampedEulerConstraint(
+ const std::string & source,
+ const fuse_variables::Position3DStamped & position1,
+ const fuse_variables::Orientation3DStamped & orientation1,
+ const fuse_variables::Position3DStamped & position2,
+ const fuse_variables::Orientation3DStamped & orientation2,
+ const fuse_core::Vector6d & delta,
+ const fuse_core::Matrix6d & covariance);
+
+ /**
+ * @brief Create a constraint using a measurement/prior of the relative 3D pose
+ *
+ * @param[in] source The name of the sensor or motion model that generated this constraint
+ * @param[in] position1 The variable representing the position components of the first pose
+ * @param[in] orientation1 The variable representing the orientation components of the first pose
+ * @param[in] position2 The variable representing the position components of the second pose
+ * @param[in] orientation2 The variable representing the orientation components of the second pose
+ * @param[in] partial_delta The measured change in the pose
+ * (6x1 vector: dx, dy, dz, droll, dpitch, dyaw)
+ * @param[in] partial_covariance The measurement subset covariance (max 6x6 matrix: x, y, z, droll, dpitch, dyaw)
+ * @param[in] variable_indices The indices of the measured variables
+ */
+ RelativePose3DStampedEulerConstraint(
+ const std::string & source,
+ const fuse_variables::Position3DStamped & position1,
+ const fuse_variables::Orientation3DStamped & orientation1,
+ const fuse_variables::Position3DStamped & position2,
+ const fuse_variables::Orientation3DStamped & orientation2,
+ const fuse_core::Vector6d & partial_delta,
+ const fuse_core::MatrixXd & partial_covariance,
+ const std::vector & variable_indices);
+
+ /**
+ * @brief Destructor
+ */
+ virtual ~RelativePose3DStampedEulerConstraint() = default;
+
+ /**
+ * @brief Read-only access to the measured pose change.
+ */
+ const fuse_core::Vector6d & delta() const {return delta_;}
+
+ /**
+ * @brief Read-only access to the square root information matrix.
+ */
+ const fuse_core::MatrixXd & sqrtInformation() const {return sqrt_information_;}
+
+ /**
+ * @brief Compute the measurement covariance matrix.
+ */
+ fuse_core::Matrix6d covariance() const;
+
+ /**
+ * @brief Print a human-readable description of the constraint to the provided stream.
+ *
+ * @param[out] stream The stream to write to. Defaults to stdout.
+ */
+ void print(std::ostream & stream = std::cout) const override;
+
+ /**
+ * @brief Access the cost function for this constraint
+ *
+ * The function caller will own the new cost function instance. It is the responsibility of the
+ * caller to delete the cost function object when it is no longer needed. If the pointer is
+ * provided to a Ceres::Problem object, the Ceres::Problem object will takes ownership of the
+ * pointer and delete it during destruction.
+ *
+ * @return A base pointer to an instance of a derived CostFunction.
+ */
+ ceres::CostFunction * costFunction() const override;
+
+protected:
+ fuse_core::Vector6d delta_; //!< The measured pose change (dx, dy, dz, droll, dpitch, dyaw)
+ fuse_core::MatrixXd sqrt_information_; //!< The square root information matrix (derived from the
+ //!< covariance matrix)
+
+private:
+ // Allow Boost Serialization access to private methods
+ friend class boost::serialization::access;
+
+ /**
+ * @brief The Boost Serialize method that serializes all of the data members in to/out of the
+ * archive
+ *
+ * @param[in/out] archive - The archive object that holds the serialized class members
+ * @param[in] version - The version of the archive being read/written. Generally unused.
+ */
+ template
+ void serialize(Archive & archive, const unsigned int /* version */)
+ {
+ archive & boost::serialization::base_object(*this);
+ archive & delta_;
+ archive & sqrt_information_;
+ }
+};
+
+} // namespace fuse_constraints
+
+BOOST_CLASS_EXPORT_KEY(fuse_constraints::RelativePose3DStampedEulerConstraint);
+
+#endif // FUSE_CONSTRAINTS__RELATIVE_POSE_3D_STAMPED_EULER_CONSTRAINT_HPP_
diff --git a/fuse_constraints/src/absolute_constraint.cpp b/fuse_constraints/src/absolute_constraint.cpp
index 9aa7593fc..68dd8a7a1 100644
--- a/fuse_constraints/src/absolute_constraint.cpp
+++ b/fuse_constraints/src/absolute_constraint.cpp
@@ -36,19 +36,29 @@
#include
BOOST_CLASS_EXPORT_IMPLEMENT(fuse_constraints::AbsoluteAccelerationAngular2DStampedConstraint);
+BOOST_CLASS_EXPORT_IMPLEMENT(fuse_constraints::AbsoluteAccelerationAngular3DStampedConstraint);
BOOST_CLASS_EXPORT_IMPLEMENT(fuse_constraints::AbsoluteAccelerationLinear2DStampedConstraint);
+BOOST_CLASS_EXPORT_IMPLEMENT(fuse_constraints::AbsoluteAccelerationLinear3DStampedConstraint);
BOOST_CLASS_EXPORT_IMPLEMENT(fuse_constraints::AbsoluteOrientation2DStampedConstraint);
BOOST_CLASS_EXPORT_IMPLEMENT(fuse_constraints::AbsolutePosition2DStampedConstraint);
BOOST_CLASS_EXPORT_IMPLEMENT(fuse_constraints::AbsolutePosition3DStampedConstraint);
BOOST_CLASS_EXPORT_IMPLEMENT(fuse_constraints::AbsoluteVelocityAngular2DStampedConstraint);
+BOOST_CLASS_EXPORT_IMPLEMENT(fuse_constraints::AbsoluteVelocityAngular3DStampedConstraint);
BOOST_CLASS_EXPORT_IMPLEMENT(fuse_constraints::AbsoluteVelocityLinear2DStampedConstraint);
+BOOST_CLASS_EXPORT_IMPLEMENT(fuse_constraints::AbsoluteVelocityLinear3DStampedConstraint);
PLUGINLIB_EXPORT_CLASS(
fuse_constraints::AbsoluteAccelerationAngular2DStampedConstraint,
fuse_core::Constraint);
+PLUGINLIB_EXPORT_CLASS(
+ fuse_constraints::AbsoluteAccelerationAngular3DStampedConstraint,
+ fuse_core::Constraint);
PLUGINLIB_EXPORT_CLASS(
fuse_constraints::AbsoluteAccelerationLinear2DStampedConstraint,
fuse_core::Constraint);
+PLUGINLIB_EXPORT_CLASS(
+ fuse_constraints::AbsoluteAccelerationLinear3DStampedConstraint,
+ fuse_core::Constraint);
PLUGINLIB_EXPORT_CLASS(
fuse_constraints::AbsoluteOrientation2DStampedConstraint,
fuse_core::Constraint);
@@ -61,6 +71,12 @@ PLUGINLIB_EXPORT_CLASS(
PLUGINLIB_EXPORT_CLASS(
fuse_constraints::AbsoluteVelocityAngular2DStampedConstraint,
fuse_core::Constraint);
+PLUGINLIB_EXPORT_CLASS(
+ fuse_constraints::AbsoluteVelocityAngular3DStampedConstraint,
+ fuse_core::Constraint);
PLUGINLIB_EXPORT_CLASS(
fuse_constraints::AbsoluteVelocityLinear2DStampedConstraint,
fuse_core::Constraint);
+PLUGINLIB_EXPORT_CLASS(
+ fuse_constraints::AbsoluteVelocityLinear3DStampedConstraint,
+ fuse_core::Constraint);
diff --git a/fuse_constraints/src/absolute_pose_3d_stamped_constraint.cpp b/fuse_constraints/src/absolute_pose_3d_stamped_constraint.cpp
index cc64e8d75..324b7fbc6 100644
--- a/fuse_constraints/src/absolute_pose_3d_stamped_constraint.cpp
+++ b/fuse_constraints/src/absolute_pose_3d_stamped_constraint.cpp
@@ -2,6 +2,7 @@
* Software License Agreement (BSD License)
*
* Copyright (c) 2018, Locus Robotics
+ * Copyright (c) 2023, Giacomo Franchini
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -31,14 +32,13 @@
* ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
-#include
#include
#include
#include
#include
-#include
+#include
#include
namespace fuse_constraints
@@ -74,8 +74,7 @@ void AbsolutePose3DStampedConstraint::print(std::ostream & stream) const
ceres::CostFunction * AbsolutePose3DStampedConstraint::costFunction() const
{
- return new ceres::AutoDiffCostFunction(
- new NormalPriorPose3DCostFunctor(sqrt_information_, mean_));
+ return new NormalPriorPose3D(sqrt_information_, mean_);
}
} // namespace fuse_constraints
diff --git a/fuse_constraints/src/absolute_pose_3d_stamped_euler_constraint.cpp b/fuse_constraints/src/absolute_pose_3d_stamped_euler_constraint.cpp
new file mode 100644
index 000000000..8afee0c88
--- /dev/null
+++ b/fuse_constraints/src/absolute_pose_3d_stamped_euler_constraint.cpp
@@ -0,0 +1,141 @@
+/*
+ * Software License Agreement (BSD License)
+ *
+ * Copyright (c) 2023, Giacomo Franchini
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ * * Neither the name of the copyright holder nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+#include
+
+#include
+
+#include
+
+#include
+#include
+#include
+
+namespace fuse_constraints
+{
+
+AbsolutePose3DStampedEulerConstraint::AbsolutePose3DStampedEulerConstraint(
+ const std::string & source,
+ const fuse_variables::Position3DStamped & position,
+ const fuse_variables::Orientation3DStamped & orientation,
+ const fuse_core::Vector6d & mean,
+ const fuse_core::Matrix6d & covariance)
+: fuse_core::Constraint(source, {position.uuid(), orientation.uuid()}), // NOLINT
+ mean_(mean),
+ sqrt_information_(covariance.inverse().llt().matrixU())
+{
+}
+
+AbsolutePose3DStampedEulerConstraint::AbsolutePose3DStampedEulerConstraint(
+ const std::string & source,
+ const fuse_variables::Position3DStamped & position,
+ const fuse_variables::Orientation3DStamped & orientation,
+ const fuse_core::Vector6d & partial_mean,
+ const fuse_core::MatrixXd & partial_covariance,
+ const std::vector & variable_indices)
+: fuse_core::Constraint(source, {position.uuid(), orientation.uuid()}), // NOLINT
+ mean_(partial_mean)
+{
+ // Compute the partial sqrt information matrix of the provided cov matrix
+ fuse_core::MatrixXd partial_sqrt_information = partial_covariance.inverse().llt().matrixU();
+
+ // Assemble a sqrt information matrix from the provided values, but in proper Variable order
+ //
+ // What are we doing here? The constraint equation is defined as: cost(x) = ||A * (x - b)||^2
+ // If we are measuring a subset of dimensions, we only want to produce costs for the measured
+ // dimensions. But the variable vectors will be full sized. We can make this all work out by
+ // creating a non-square A, where each row computes a cost for one measured dimensions,
+ // and the columns are in the order defined by the variable.
+
+ // Fill in the rows of the sqrt information matrix corresponding to the measured dimensions
+ sqrt_information_ = fuse_core::MatrixXd::Zero(variable_indices.size(), 6);
+ for (size_t i = 0; i < variable_indices.size(); ++i) {
+ sqrt_information_.col(variable_indices[i]) = partial_sqrt_information.col(i);
+ }
+}
+
+fuse_core::Matrix6d AbsolutePose3DStampedEulerConstraint::covariance() const
+{
+ if (sqrt_information_.rows() == 6) {
+ return (sqrt_information_.transpose() * sqrt_information_).inverse();
+ }
+ // Otherwise we need to compute the pseudoinverse
+ // cov = (sqrt_info' * sqrt_info)^-1
+ // With some linear algebra, we can swap the transpose and the inverse.
+ // cov = (sqrt_info^-1) * (sqrt_info^-1)'
+ // But sqrt_info _may_ not be square. So we need to compute the pseudoinverse instead.
+ // Eigen doesn't have a pseudoinverse function (for probably very legitimate reasons).
+ // So we set the right hand side to identity, then solve using one of Eigen's many decompositions.
+ auto I = fuse_core::MatrixXd::Identity(sqrt_information_.rows(), sqrt_information_.cols());
+ fuse_core::MatrixXd pinv = sqrt_information_.colPivHouseholderQr().solve(I);
+ return pinv * pinv.transpose();
+}
+
+void AbsolutePose3DStampedEulerConstraint::print(std::ostream & stream) const
+{
+ stream << type() << "\n"
+ << " source: " << source() << "\n"
+ << " uuid: " << uuid() << "\n"
+ << " position variable: " << variables().at(0) << "\n"
+ << " orientation variable: " << variables().at(1) << "\n"
+ << " mean: " << mean().transpose() << "\n"
+ << " sqrt_info: " << sqrtInformation() << "\n";
+
+ if (loss()) {
+ stream << " loss: ";
+ loss()->print(stream);
+ }
+}
+
+ceres::CostFunction * AbsolutePose3DStampedEulerConstraint::costFunction() const
+{
+ return new NormalPriorPose3DEuler(sqrt_information_, mean_);
+
+ // Here we return a cost function that computes the analytic derivatives/jacobians, but we could
+ // use automatic differentiation as follows:
+
+ // return new ceres::AutoDiffCostFunction(
+ // new NormalPriorPose3DEulerCostFunctor(sqrt_information_, mean_),
+ // sqrt_information_.rows());
+
+ // And including the followings:
+ // #include
+ // #include
+}
+
+} // namespace fuse_constraints
+
+BOOST_CLASS_EXPORT_IMPLEMENT(fuse_constraints::AbsolutePose3DStampedEulerConstraint);
+PLUGINLIB_EXPORT_CLASS(
+ fuse_constraints::AbsolutePose3DStampedEulerConstraint,
+ fuse_core::Constraint);
diff --git a/fuse_constraints/src/normal_prior_orientation_3d.cpp b/fuse_constraints/src/normal_prior_orientation_3d.cpp
new file mode 100644
index 000000000..e8c44bab0
--- /dev/null
+++ b/fuse_constraints/src/normal_prior_orientation_3d.cpp
@@ -0,0 +1,96 @@
+/*
+ * Software License Agreement (BSD License)
+ *
+ * Copyright (c) 2023, Giacomo Franchini
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ * * Neither the name of the copyright holder nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+#include
+#include
+
+#include
+#include
+
+namespace fuse_constraints
+{
+
+NormalPriorOrientation3D::NormalPriorOrientation3D(
+ const fuse_core::Matrix3d & A,
+ const fuse_core::Vector4d & b)
+: A_(A),
+ b_(b)
+{
+}
+
+bool NormalPriorOrientation3D::Evaluate(
+ double const * const * parameters,
+ double * residuals,
+ double ** jacobians) const
+{
+ double variable[4] =
+ {
+ parameters[0][0],
+ parameters[0][1],
+ parameters[0][2],
+ parameters[0][3],
+ };
+
+ double observation_inverse[4] =
+ {
+ b_(0),
+ -b_(1),
+ -b_(2),
+ -b_(3)
+ };
+
+ double difference[4];
+ double j_product[16];
+ double j_quat2angle[12];
+
+ // TODO(giafranchini): these jacobians should be populated only if jacobians[1] != nullptr
+ fuse_core::quaternionProduct(observation_inverse, variable, difference, j_product);
+ fuse_core::quaternionToAngleAxis(difference, residuals, j_quat2angle); // orientation angle-axis
+
+ // Scale the residuals by the square root information matrix to account for the measurement
+ // uncertainty.
+ Eigen::Map residuals_map(residuals);
+ residuals_map.applyOnTheLeft(A_);
+
+ if (jacobians != nullptr) {
+ if (jacobians[0] != nullptr) {
+ Eigen::Map> j_map(jacobians[0]);
+ Eigen::Map j_product_map(j_product);
+ Eigen::Map> j_quat2angle_map(j_quat2angle);
+ j_map = A_ * j_quat2angle_map * j_product_map;
+ }
+ }
+ return true;
+}
+
+} // namespace fuse_constraints
diff --git a/fuse_constraints/src/normal_prior_pose_3d.cpp b/fuse_constraints/src/normal_prior_pose_3d.cpp
new file mode 100644
index 000000000..c3743a3f3
--- /dev/null
+++ b/fuse_constraints/src/normal_prior_pose_3d.cpp
@@ -0,0 +1,107 @@
+/*
+ * Software License Agreement (BSD License)
+ *
+ * Copyright (c) 2023, Giacomo Franchini
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ * * Neither the name of the copyright holder nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+#include
+#include
+
+#include
+#include
+
+namespace fuse_constraints
+{
+
+NormalPriorPose3D::NormalPriorPose3D(const fuse_core::Matrix6d & A, const fuse_core::Vector7d & b)
+: A_(A),
+ b_(b)
+{
+}
+
+bool NormalPriorPose3D::Evaluate(
+ double const * const * parameters,
+ double * residuals,
+ double ** jacobians) const
+{
+
+ double variable[4] =
+ {
+ parameters[1][0],
+ parameters[1][1],
+ parameters[1][2],
+ parameters[1][3],
+ };
+
+ double observation_inverse[4] =
+ {
+ b_(3),
+ -b_(4),
+ -b_(5),
+ -b_(6)
+ };
+
+ double difference[4];
+ double j_product[16];
+ double j_quat2angle[12];
+
+ // Compute position residuals
+ residuals[0] = parameters[0][0] - b_[0];
+ residuals[1] = parameters[0][1] - b_[1];
+ residuals[2] = parameters[0][2] - b_[2];
+ // Compute orientation residuals
+
+ // TODO(giafranchini): this is repeated code, it should be better to include normal_prior_orientation_3d.hpp
+ // and use that to compute residuals and jacobians.
+ fuse_core::quaternionProduct(observation_inverse, variable, difference, j_product);
+ fuse_core::quaternionToAngleAxis(difference, &residuals[3], j_quat2angle); // orientation angle-axis
+
+ // Scale the residuals by the square root information matrix to account for the measurement
+ // uncertainty.
+ Eigen::Map residuals_map(residuals);
+ residuals_map.applyOnTheLeft(A_);
+
+ if (jacobians != nullptr) {
+ // Jacobian of the residuals wrt position parameter block
+ if (jacobians[0] != nullptr) {
+ Eigen::Map(jacobians[0], num_residuals(), 3) = A_.leftCols<3>();
+ }
+ // Jacobian of the residuals wrt orientation parameter block
+ if (jacobians[1] != nullptr) {
+ Eigen::Map j_product_map(j_product);
+ Eigen::Map> j_quat2angle_map(j_quat2angle);
+ Eigen::Map j1_map(jacobians[1], num_residuals(), 4);
+ j1_map = A_.rightCols<3>() * j_quat2angle_map * j_product_map;
+ }
+ }
+ return true;
+}
+
+} // namespace fuse_constraints
diff --git a/fuse_constraints/src/normal_prior_pose_3d_euler.cpp b/fuse_constraints/src/normal_prior_pose_3d_euler.cpp
new file mode 100644
index 000000000..033083baa
--- /dev/null
+++ b/fuse_constraints/src/normal_prior_pose_3d_euler.cpp
@@ -0,0 +1,97 @@
+/*
+ * Software License Agreement (BSD License)
+ *
+ * Copyright (c) 2023, Giacomo Franchini
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ * * Neither the name of the copyright holder nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+#include
+#include
+#include
+
+#include
+#include
+#include
+
+namespace fuse_constraints
+{
+
+NormalPriorPose3DEuler::NormalPriorPose3DEuler(
+ const fuse_core::MatrixXd & A,
+ const fuse_core::Vector6d & b)
+: A_(A),
+ b_(b)
+{
+ CHECK_GT(A_.rows(), 0);
+ CHECK_EQ(A_.cols(), 6);
+ set_num_residuals(A_.rows());
+}
+
+bool NormalPriorPose3DEuler::Evaluate(
+ double const * const * parameters,
+ double * residuals,
+ double ** jacobians) const
+{
+ fuse_core::Vector6d full_residuals;
+ double orientation_rpy[3];
+ double j_quat2rpy[12];
+
+ fuse_core::quaternion2rpy(parameters[1], orientation_rpy, j_quat2rpy);
+
+ // Compute the position residual
+ full_residuals(0) = parameters[0][0] - b_(0);
+ full_residuals(1) = parameters[0][1] - b_(1);
+ full_residuals(2) = parameters[0][2] - b_(2);
+
+ // Compute the orientation residual
+ full_residuals(3) = orientation_rpy[0] - b_(3);
+ full_residuals(4) = orientation_rpy[1] - b_(4);
+ full_residuals(5) = orientation_rpy[2] - b_(5);
+
+ // Scale the residuals by the square root information matrix to account for
+ // the measurement uncertainty.
+ Eigen::Map> residuals_map(residuals, A_.rows());
+ residuals_map = A_ * full_residuals;
+
+ if (jacobians != nullptr) {
+ // Jacobian wrt position
+ if (jacobians[0] != nullptr) {
+ Eigen::Map(jacobians[0], num_residuals(), 3) = A_.leftCols<3>();
+ }
+ // Jacobian wrt orientation
+ if (jacobians[1] != nullptr) {
+ Eigen::Map> j_quat2angle_map(j_quat2rpy);
+ Eigen::Map(jacobians[1], num_residuals(), 4) =
+ A_.rightCols<3>() * j_quat2angle_map;
+ }
+ }
+ return true;
+}
+
+} // namespace fuse_constraints
diff --git a/fuse_constraints/src/relative_pose_3d_stamped_constraint.cpp b/fuse_constraints/src/relative_pose_3d_stamped_constraint.cpp
index 7d627e117..1aefeb5fe 100644
--- a/fuse_constraints/src/relative_pose_3d_stamped_constraint.cpp
+++ b/fuse_constraints/src/relative_pose_3d_stamped_constraint.cpp
@@ -74,6 +74,7 @@ void RelativePose3DStampedConstraint::print(std::ostream & stream) const
ceres::CostFunction * RelativePose3DStampedConstraint::costFunction() const
{
+ // TODO(giafranchini): implement cost function with analytical derivatives
return new ceres::AutoDiffCostFunction(
new NormalDeltaPose3DCostFunctor(sqrt_information_, delta_));
}
diff --git a/fuse_constraints/src/relative_pose_3d_stamped_euler_constraint.cpp b/fuse_constraints/src/relative_pose_3d_stamped_euler_constraint.cpp
new file mode 100644
index 000000000..a2bc7706a
--- /dev/null
+++ b/fuse_constraints/src/relative_pose_3d_stamped_euler_constraint.cpp
@@ -0,0 +1,135 @@
+/*
+ * Software License Agreement (BSD License)
+ *
+ * Copyright (c) 2023, Giacomo Franchini
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ * * Neither the name of the copyright holder nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+#include
+
+#include
+
+#include
+#include
+#include
+#include
+
+namespace fuse_constraints
+{
+
+RelativePose3DStampedEulerConstraint::RelativePose3DStampedEulerConstraint(
+ const std::string & source,
+ const fuse_variables::Position3DStamped & position1,
+ const fuse_variables::Orientation3DStamped & orientation1,
+ const fuse_variables::Position3DStamped & position2,
+ const fuse_variables::Orientation3DStamped & orientation2,
+ const fuse_core::Vector6d & delta,
+ const fuse_core::Matrix6d & covariance)
+: fuse_core::Constraint(
+ source,
+ {position1.uuid(), orientation1.uuid(), position2.uuid(), orientation2.uuid()}), // NOLINT
+ delta_(delta),
+ sqrt_information_(covariance.inverse().llt().matrixU())
+{
+}
+
+RelativePose3DStampedEulerConstraint::RelativePose3DStampedEulerConstraint(
+ const std::string & source,
+ const fuse_variables::Position3DStamped & position1,
+ const fuse_variables::Orientation3DStamped & orientation1,
+ const fuse_variables::Position3DStamped & position2,
+ const fuse_variables::Orientation3DStamped & orientation2,
+ const fuse_core::Vector6d & partial_delta,
+ const fuse_core::MatrixXd & partial_covariance,
+ const std::vector & variable_indices)
+: fuse_core::Constraint(
+ source,
+ {position1.uuid(), orientation1.uuid(), position2.uuid(), orientation2.uuid()}), // NOLINT
+ delta_(partial_delta)
+{
+ // Compute the partial sqrt information matrix of the provided cov matrix
+ fuse_core::MatrixXd partial_sqrt_information = partial_covariance.inverse().llt().matrixU();
+
+ // Assemble a sqrt information matrix from the provided values, but in proper Variable order
+ //
+ // What are we doing here? The constraint equation is defined as: cost(x) = ||A * (x - b)||^2
+ // If we are measuring a subset of dimensions, we only want to produce costs for the measured
+ // dimensions. But the variable vectors will be full sized. We can make this all work out by
+ // creating a non-square A, where each row computes a cost for one measured dimensions,
+ // and the columns are in the order defined by the variable.
+
+ // Fill in the rows of the sqrt information matrix corresponding to the measured dimensions
+ sqrt_information_ = fuse_core::MatrixXd::Zero(variable_indices.size(), 6);
+ for (size_t i = 0; i < variable_indices.size(); ++i) {
+ sqrt_information_.col(variable_indices[i]) = partial_sqrt_information.col(i);
+ }
+}
+
+fuse_core::Matrix6d RelativePose3DStampedEulerConstraint::covariance() const
+{
+ // We need to compute the pseudoinverse
+ // cov = (sqrt_info' * sqrt_info)^-1
+ // With some linear algebra, we can swap the transpose and the inverse.
+ // cov = (sqrt_info^-1) * (sqrt_info^-1)'
+ // But sqrt_info _may_ not be square. So we need to compute the pseudoinverse instead.
+ // Eigen doesn't have a pseudoinverse function (for probably very legitimate reasons).
+ // So we set the right hand side to identity, then solve using one of Eigen's many decompositions.
+ auto I = fuse_core::MatrixXd::Identity(sqrt_information_.rows(), sqrt_information_.cols());
+ fuse_core::MatrixXd pinv = sqrt_information_.colPivHouseholderQr().solve(I);
+ return pinv * pinv.transpose();
+}
+
+void RelativePose3DStampedEulerConstraint::print(std::ostream & stream) const
+{
+ stream << type() << "\n"
+ << " source: " << source() << "\n"
+ << " uuid: " << uuid() << "\n"
+ << " position1 variable: " << variables().at(0) << "\n"
+ << " orientation1 variable: " << variables().at(1) << "\n"
+ << " position2 variable: " << variables().at(2) << "\n"
+ << " orientation2 variable: " << variables().at(3) << "\n"
+ << " delta: " << delta().transpose() << "\n"
+ << " sqrt_info: " << sqrtInformation() << "\n";
+}
+
+ceres::CostFunction * RelativePose3DStampedEulerConstraint::costFunction() const
+{
+ // TODO(giafranchini): implement cost function with analytical Jacobians
+ return new ceres::AutoDiffCostFunction(
+ new NormalDeltaPose3DEulerCostFunctor(sqrt_information_, delta_),
+ sqrt_information_.rows());
+}
+
+} // namespace fuse_constraints
+
+BOOST_CLASS_EXPORT_IMPLEMENT(fuse_constraints::RelativePose3DStampedEulerConstraint);
+PLUGINLIB_EXPORT_CLASS(
+ fuse_constraints::RelativePose3DStampedEulerConstraint,
+ fuse_core::Constraint);
diff --git a/fuse_constraints/test/CMakeLists.txt b/fuse_constraints/test/CMakeLists.txt
index 426322c5c..ec69a351b 100644
--- a/fuse_constraints/test/CMakeLists.txt
+++ b/fuse_constraints/test/CMakeLists.txt
@@ -6,13 +6,17 @@ set(TEST_TARGETS
test_absolute_orientation_3d_stamped_euler_constraint
test_absolute_pose_2d_stamped_constraint
test_absolute_pose_3d_stamped_constraint
+ test_absolute_pose_3d_stamped_euler_constraint
test_marginal_constraint
test_marginalize_variables
test_normal_delta_pose_2d
test_normal_prior_pose_2d
+ test_normal_prior_pose_3d
+ test_normal_prior_pose_3d_euler
test_relative_constraint
test_relative_pose_2d_stamped_constraint
test_relative_pose_3d_stamped_constraint
+ test_relative_pose_3d_stamped_euler_constraint
test_uuid_ordering
test_variable_constraints
)
diff --git a/fuse_constraints/test/cost_function_gtest.hpp b/fuse_constraints/test/cost_function_gtest.hpp
index 3687d0504..9fd98e470 100644
--- a/fuse_constraints/test/cost_function_gtest.hpp
+++ b/fuse_constraints/test/cost_function_gtest.hpp
@@ -38,6 +38,7 @@
#include
#include
+#include
/**
* @brief A helper function to compare a expected and actual cost function.
@@ -52,7 +53,8 @@
*/
static void ExpectCostFunctionsAreEqual(
const ceres::CostFunction & cost_function,
- const ceres::CostFunction & actual_cost_function)
+ const ceres::CostFunction & actual_cost_function,
+ const double tol = 1e-16)
{
EXPECT_EQ(cost_function.num_residuals(), actual_cost_function.num_residuals());
const size_t num_residuals = cost_function.num_residuals();
@@ -68,8 +70,22 @@ static void ExpectCostFunctionsAreEqual(
}
std::unique_ptr parameters(new double[num_parameters]);
- for (size_t i = 0; i < num_parameters; ++i) {
- parameters[i] = static_cast(i) + 1.0;
+ if ((num_parameters == 7) && (parameter_block_sizes[0] == 3) &&
+ (parameter_block_sizes[1] == 4))
+ {
+ // Special case for parameters[1] as quaternion
+ for (size_t i = 0; i < 3; i++) {
+ parameters[i] = static_cast(i) + 1.0;
+ }
+ Eigen::Quaterniond q = Eigen::Quaterniond::UnitRandom();
+ parameters[3] = q.w();
+ parameters[4] = q.x();
+ parameters[5] = q.y();
+ parameters[6] = q.z();
+ } else {
+ for (size_t i = 0; i < num_parameters; ++i) {
+ parameters[i] = static_cast(i) + 1.0;
+ }
}
std::unique_ptr residuals(new double[num_residuals]);
@@ -96,7 +112,7 @@ static void ExpectCostFunctionsAreEqual(
parameter_blocks.get(), actual_residuals.get(),
nullptr));
for (size_t i = 0; i < num_residuals; ++i) {
- EXPECT_DOUBLE_EQ(residuals[i], actual_residuals[i]) << "residual id: " << i;
+ EXPECT_NEAR(residuals[i], actual_residuals[i], tol) << "residual id: " << i;
}
EXPECT_TRUE(
@@ -108,11 +124,11 @@ static void ExpectCostFunctionsAreEqual(
parameter_blocks.get(), actual_residuals.get(),
actual_jacobian_blocks.get()));
for (size_t i = 0; i < num_residuals; ++i) {
- EXPECT_DOUBLE_EQ(residuals[i], actual_residuals[i]) << "residual : " << i;
+ EXPECT_NEAR(residuals[i], actual_residuals[i], tol) << "residual : " << i;
}
for (size_t i = 0; i < num_residuals * num_parameters; ++i) {
- EXPECT_DOUBLE_EQ(jacobians[i], actual_jacobians[i])
+ EXPECT_NEAR(jacobians[i], actual_jacobians[i], tol)
<< "jacobian : " << i << " " << jacobians[i] << " " << actual_jacobians[i];
}
}
diff --git a/fuse_constraints/test/test_absolute_orientation_3d_stamped_euler_constraint.cpp b/fuse_constraints/test/test_absolute_orientation_3d_stamped_euler_constraint.cpp
index 9787d83f6..5b79011fc 100644
--- a/fuse_constraints/test/test_absolute_orientation_3d_stamped_euler_constraint.cpp
+++ b/fuse_constraints/test/test_absolute_orientation_3d_stamped_euler_constraint.cpp
@@ -2,6 +2,7 @@
* Software License Agreement (BSD License)
*
* Copyright (c) 2018, Locus Robotics
+ * Copyright (c) 2023, Giacomo Franchini
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@@ -150,7 +151,8 @@ TEST(AbsoluteOrientation3DStampedEulerConstraint, OptimizationFull)
ceres::Solve(options, &problem, &summary);
// Check
- Eigen::Quaterniond expected = Eigen::AngleAxisd(0.5, Eigen::Vector3d::UnitZ()) *
+ Eigen::Quaterniond expected =
+ Eigen::AngleAxisd(0.5, Eigen::Vector3d::UnitZ()) *
Eigen::AngleAxisd(1.5, Eigen::Vector3d::UnitY()) *
Eigen::AngleAxisd(1.0, Eigen::Vector3d::UnitX());
EXPECT_NEAR(expected.w(), orientation_variable->w(), 5.0e-3);
@@ -229,7 +231,8 @@ TEST(AbsoluteOrientation3DStampedEulerConstraint, OptimizationPartial)
ceres::Solve(options, &problem, &summary);
// Check
- Eigen::Quaterniond expected = Eigen::AngleAxisd(0.5, Eigen::Vector3d::UnitZ()) *
+ Eigen::Quaterniond expected =
+ Eigen::AngleAxisd(0.5, Eigen::Vector3d::UnitZ()) *
Eigen::AngleAxisd(1.5, Eigen::Vector3d::UnitY()) *
Eigen::AngleAxisd(1.0, Eigen::Vector3d::UnitX());
EXPECT_NEAR(expected.w(), orientation_variable->w(), 5.0e-3);
diff --git a/fuse_constraints/test/test_absolute_pose_3d_stamped_euler_constraint.cpp b/fuse_constraints/test/test_absolute_pose_3d_stamped_euler_constraint.cpp
new file mode 100644
index 000000000..5d605f42d
--- /dev/null
+++ b/fuse_constraints/test/test_absolute_pose_3d_stamped_euler_constraint.cpp
@@ -0,0 +1,501 @@
+/*
+ * Software License Agreement (BSD License)
+ *
+ * Copyright (c) 2023, Giacomo Franchini
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ * * Neither the name of the copyright holder nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+#include
+#include
+#include
+#include
+
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+using fuse_variables::Orientation3DStamped;
+using fuse_variables::Position3DStamped;
+using fuse_constraints::AbsolutePose3DStampedEulerConstraint;
+
+
+TEST(AbsolutePose3DStampedEulerConstraint, Constructor)
+{
+ // Construct a constraint just to make sure it compiles.
+ Position3DStamped position_variable(rclcpp::Time(1234, 5678), fuse_core::uuid::generate("walle"));
+ Orientation3DStamped orientation_variable(rclcpp::Time(1234, 5678),
+ fuse_core::uuid::generate("walle"));
+
+ fuse_core::Vector6d mean;
+ mean << 1.0, 2.0, 3.0, 0.1, 0.1, 1.0;
+
+ // Generated PD matrix using Octave: R = rand(6, 6); A = R * R' (use format long g to get the
+ // required precision)
+ fuse_core::Matrix6d cov;
+ /* *INDENT-OFF* */
+ cov << 2.0847236144069, 1.10752598122138, 1.02943174290333, 1.96120532313878, 1.96735470687891, 1.5153042667951, // NOLINT
+ 1.10752598122138, 1.39176289439125, 0.643422499737987, 1.35471905449013, 1.18353784377297, 1.28979625492894, // NOLINT
+ 1.02943174290333, 0.643422499737987, 1.26701658550187, 1.23641771365403, 1.55169301761377, 1.34706781598061, // NOLINT
+ 1.96120532313878, 1.35471905449013, 1.23641771365403, 2.39750866789926, 2.06887486311147, 2.04350823837035, // NOLINT
+ 1.96735470687891, 1.18353784377297, 1.55169301761377, 2.06887486311147, 2.503913946461, 1.73844731158092, // NOLINT
+ 1.5153042667951, 1.28979625492894, 1.34706781598061, 2.04350823837035, 1.73844731158092, 2.15326088526198; // NOLINT
+ /* *INDENT-ON* */
+
+ EXPECT_NO_THROW(
+ AbsolutePose3DStampedEulerConstraint constraint(
+ "test", position_variable, orientation_variable,
+ mean, cov));
+}
+
+TEST(AbsolutePose3DStampedEulerConstraint, ConstructorPartial)
+{
+ // Construct a constraint just to make sure it compiles.
+ Position3DStamped position_variable(rclcpp::Time(1234, 5678), fuse_core::uuid::generate("walle"));
+ Orientation3DStamped orientation_variable(rclcpp::Time(1234, 5678),
+ fuse_core::uuid::generate("walle"));
+
+ std::vector variable_indices {0, 2, 3, 4, 5};
+
+ fuse_core::Vector6d mean_partial;
+ mean_partial << 1.0, 0.0, 3.0, 0.1, 0.1, 1.0;
+
+ fuse_core::Matrix5d cov_partial;
+ /* *INDENT-OFF* */
+ cov_partial << 2.0847236144069, 1.02943174290333, 1.96120532313878, 1.96735470687891, 1.5153042667951, // NOLINT
+ 1.02943174290333, 1.26701658550187, 1.23641771365403, 1.55169301761377, 1.34706781598061, // NOLINT
+ 1.96120532313878, 1.23641771365403, 2.39750866789926, 2.06887486311147, 2.04350823837035, // NOLINT
+ 1.96735470687891, 1.55169301761377, 2.06887486311147, 2.503913946461, 1.73844731158092, // NOLINT
+ 1.5153042667951, 1.34706781598061, 2.04350823837035, 1.73844731158092, 2.15326088526198; // NOLINT
+ /* *INDENT-ON* */
+
+ EXPECT_NO_THROW(
+ AbsolutePose3DStampedEulerConstraint constraint(
+ "test", position_variable, orientation_variable,
+ mean_partial, cov_partial, variable_indices));
+}
+
+TEST(AbsolutePose3DStampedEulerConstraint, Covariance)
+{
+ // Verify the covariance <--> sqrt information conversions are correct
+ Position3DStamped position_variable(rclcpp::Time(1234, 5678), fuse_core::uuid::generate("mo"));
+ Orientation3DStamped orientation_variable(rclcpp::Time(1234, 5678), fuse_core::uuid::generate(
+ "mo"));
+
+ fuse_core::Vector6d mean;
+ mean << 1.0, 2.0, 3.0, 0.1, 0.1, 1.0;
+
+ // Generated PD matrix using Octiave: R = rand(6, 6); A = R * R' (use format long g to get the
+ // required precision)
+ fuse_core::Matrix6d cov;
+ /* *INDENT-OFF* */
+ cov << 2.0847236144069, 1.10752598122138, 1.02943174290333, 1.96120532313878, 1.96735470687891, 1.5153042667951, // NOLINT
+ 1.10752598122138, 1.39176289439125, 0.643422499737987, 1.35471905449013, 1.18353784377297, 1.28979625492894, // NOLINT
+ 1.02943174290333, 0.643422499737987, 1.26701658550187, 1.23641771365403, 1.55169301761377, 1.34706781598061, // NOLINT
+ 1.96120532313878, 1.35471905449013, 1.23641771365403, 2.39750866789926, 2.06887486311147, 2.04350823837035, // NOLINT
+ 1.96735470687891, 1.18353784377297, 1.55169301761377, 2.06887486311147, 2.503913946461, 1.73844731158092, // NOLINT
+ 1.5153042667951, 1.28979625492894, 1.34706781598061, 2.04350823837035, 1.73844731158092, 2.15326088526198; // NOLINT
+ /* *INDENT-ON* */
+
+ AbsolutePose3DStampedEulerConstraint constraint("test", position_variable, orientation_variable,
+ mean,
+ cov);
+
+ // Define the expected matrices (used Octave to compute sqrt_info: 'chol(inv(A))')
+ fuse_core::Matrix6d expected_sqrt_info;
+ /* *INDENT-OFF* */
+ expected_sqrt_info << 2.12658752275893, 1.20265444927878, 4.71225672571804, 1.43587520991272, -4.12764062992821, -3.19509486240291, // NOLINT
+ 0.0, 2.41958656956248, 5.93151964116945, 3.72535320852517, -4.23326858606213, -5.27776664777548, // NOLINT
+ 0.0, 0.0, 3.82674686590005, 2.80341171946161, -2.68168478581452, -2.8894384435255, // NOLINT
+ 0.0, 0.0, 0.0, 1.83006791372784, -0.696917410192509, -1.17412835464633, // NOLINT
+ 0.0, 0.0, 0.0, 0.0, 0.953302832761324, -0.769654414882847, // NOLINT
+ 0.0, 0.0, 0.0, 0.0, 0.0, 0.681477739760948; // NOLINT
+ /* *INDENT-ON* */
+ fuse_core::Matrix6d expected_cov = cov;
+
+ // Compare
+ EXPECT_MATRIX_NEAR(expected_cov, constraint.covariance(), 1.0e-9);
+ EXPECT_MATRIX_NEAR(expected_sqrt_info, constraint.sqrtInformation(), 1.0e-9);
+}
+
+TEST(AbsolutePose3DStampedEulerConstraint, CovariancePartial)
+{
+ // Verify the covariance <--> sqrt information conversions are correct
+ Position3DStamped position_variable(rclcpp::Time(1234, 5678), fuse_core::uuid::generate("mo"));
+ Orientation3DStamped orientation_variable(rclcpp::Time(1234, 5678), fuse_core::uuid::generate(
+ "mo"));
+
+ std::vector variable_indices {0, 2, 3, 4, 5};
+
+ fuse_core::Vector6d mean;
+ mean << 1.0, 0.0, 3.0, 0.1, 0.1, 1.0;
+
+ // Generated PD matrix using Octiave: R = rand(6, 6); A = R * R' (use format long g to get the
+ // required precision)
+ fuse_core::Matrix5d cov;
+ /* *INDENT-OFF* */
+ cov << 2.0847236144069, 1.02943174290333, 1.96120532313878, 1.96735470687891, 1.5153042667951, // NOLINT
+ 1.02943174290333, 1.26701658550187, 1.23641771365403, 1.55169301761377, 1.34706781598061, // NOLINT
+ 1.96120532313878, 1.23641771365403, 2.39750866789926, 2.06887486311147, 2.04350823837035, // NOLINT
+ 1.96735470687891, 1.55169301761377, 2.06887486311147, 2.503913946461, 1.73844731158092, // NOLINT
+ 1.5153042667951, 1.34706781598061, 2.04350823837035, 1.73844731158092, 2.15326088526198; // NOLINT
+ /* *INDENT-ON* */
+
+ AbsolutePose3DStampedEulerConstraint constraint("test", position_variable, orientation_variable,
+ mean, cov, variable_indices);
+
+ // Define the expected matrices (used Octave to compute sqrt_info: 'chol(inv(A))')
+ fuse_core::Matrix expected_sqrt_info;
+ /* *INDENT-OFF* */
+ expected_sqrt_info << 1.90431982, 0.0, 1.57962714, -0.37235015, -1.81200356, -0.51202133, // NOLINT
+ 0. , 0.0, 3.82674687, 2.80341172 , -2.68168479, -2.88943844, // NOLINT
+ 0. , 0.0, 0. , 1.83006791 , -0.69691741, -1.17412835, // NOLINT
+ 0. , 0.0, 0. , 0. , 0.95330283, -0.76965441, // NOLINT
+ 0. , 0.0, 0. , 0. , 0. , 0.68147774; // NOLINT
+ /* *INDENT-ON* */
+
+ fuse_core::Matrix6d expected_cov;
+ /* *INDENT-OFF* */
+ expected_cov << 2.0847236144069, 0.0, 1.02943174290333, 1.96120532313878, 1.96735470687891, 1.5153042667951, // NOLINT
+ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, // NOLINT
+ 1.02943174290333, 0.0, 1.26701658550187, 1.23641771365403, 1.55169301761377, 1.34706781598061, // NOLINT
+ 1.96120532313878, 0.0, 1.23641771365403, 2.39750866789926, 2.06887486311147, 2.04350823837035, // NOLINT
+ 1.96735470687891, 0.0, 1.55169301761377, 2.06887486311147, 2.503913946461, 1.73844731158092, // NOLINT
+ 1.5153042667951, 0.0, 1.34706781598061, 2.04350823837035, 1.73844731158092, 2.15326088526198; // NOLINT
+ /* *INDENT-ON* */
+
+ // Compare
+ EXPECT_MATRIX_NEAR(expected_cov, constraint.covariance(), 1.0e-8);
+ EXPECT_MATRIX_NEAR(expected_sqrt_info, constraint.sqrtInformation(), 1.0e-8);
+}
+
+TEST(AbsolutePose3DStampedEulerConstraint, Optimization)
+{
+ // Optimize a single pose and single constraint, verify the expected value and covariance are
+ // generated. Create the variables
+ auto position_variable = Position3DStamped::make_shared(
+ rclcpp::Time(1, 0), fuse_core::uuid::generate("spra"));
+ position_variable->x() = 1.5;
+ position_variable->y() = -3.0;
+ position_variable->z() = 10.0;
+
+ auto orientation_variable = Orientation3DStamped::make_shared(
+ rclcpp::Time(1, 0), fuse_core::uuid::generate("spra"));
+ orientation_variable->w() = 0.952;
+ orientation_variable->x() = 0.038;
+ orientation_variable->y() = -0.189;
+ orientation_variable->z() = 0.239;
+
+ // Create an absolute pose constraint
+ fuse_core::Vector6d mean;
+ mean << 1.0, 2.0, 3.0, 0.0, 0.0, 0.0;
+
+ fuse_core::Matrix6d cov;
+ /* *INDENT-OFF* */
+ cov << 1.0, 0.1, 0.2, 0.3, 0.4, 0.5,
+ 0.1, 2.0, 0.6, 0.5, 0.4, 0.3,
+ 0.2, 0.6, 3.0, 0.2, 0.1, 0.2,
+ 0.3, 0.5, 0.2, 4.0, 0.3, 0.4,
+ 0.4, 0.4, 0.1, 0.3, 5.0, 0.5,
+ 0.5, 0.3, 0.2, 0.4, 0.5, 6.0;
+ /* *INDENT-ON* */
+
+ auto constraint = AbsolutePose3DStampedEulerConstraint::make_shared(
+ "test",
+ *position_variable,
+ *orientation_variable,
+ mean,
+ cov);
+
+ // Build the problem
+ ceres::Problem::Options problem_options;
+ problem_options.loss_function_ownership = fuse_core::Loss::Ownership;
+ ceres::Problem problem(problem_options);
+ problem.AddParameterBlock(
+ position_variable->data(),
+ position_variable->size(),
+#if !CERES_SUPPORTS_MANIFOLDS
+ position_variable->localParameterization());
+#else
+ position_variable->manifold());
+#endif
+ problem.AddParameterBlock(
+ orientation_variable->data(),
+ orientation_variable->size(),
+#if !CERES_SUPPORTS_MANIFOLDS
+ orientation_variable->localParameterization());
+#else
+ orientation_variable->manifold());
+#endif
+ std::vector parameter_blocks;
+ parameter_blocks.push_back(position_variable->data());
+ parameter_blocks.push_back(orientation_variable->data());
+
+ problem.AddResidualBlock(
+ constraint->costFunction(),
+ constraint->lossFunction(),
+ parameter_blocks);
+
+ // Run the solver
+ ceres::Solver::Options options;
+ ceres::Solver::Summary summary;
+ ceres::Solve(options, &problem, &summary);
+
+ // Check
+ EXPECT_NEAR(1.0, position_variable->x(), 1.0e-5);
+ EXPECT_NEAR(2.0, position_variable->y(), 1.0e-5);
+ EXPECT_NEAR(3.0, position_variable->z(), 1.0e-5);
+ EXPECT_NEAR(1.0, orientation_variable->w(), 1.0e-3);
+ EXPECT_NEAR(0.0, orientation_variable->x(), 1.0e-3);
+ EXPECT_NEAR(0.0, orientation_variable->y(), 1.0e-3);
+ EXPECT_NEAR(0.0, orientation_variable->z(), 1.0e-3);
+
+ // Compute the covariance
+ std::vector> covariance_blocks;
+ covariance_blocks.emplace_back(position_variable->data(), position_variable->data());
+ covariance_blocks.emplace_back(orientation_variable->data(), orientation_variable->data());
+ covariance_blocks.emplace_back(position_variable->data(), orientation_variable->data());
+
+ ceres::Covariance::Options cov_options;
+ ceres::Covariance covariance(cov_options);
+ covariance.Compute(covariance_blocks, &problem);
+ fuse_core::MatrixXd cov_pos_pos(position_variable->size(), position_variable->size());
+ covariance.GetCovarianceBlock(
+ position_variable->data(),
+ position_variable->data(), cov_pos_pos.data());
+
+ fuse_core::MatrixXd cov_or_or(orientation_variable->localSize(),
+ orientation_variable->localSize());
+ covariance.GetCovarianceBlockInTangentSpace(
+ orientation_variable->data(), orientation_variable->data(), cov_or_or.data());
+
+ fuse_core::MatrixXd cov_pos_or(position_variable->localSize(), orientation_variable->localSize());
+ covariance.GetCovarianceBlockInTangentSpace(
+ position_variable->data(), orientation_variable->data(), cov_pos_or.data());
+
+ // Assemble the full covariance from the covariance blocks
+ fuse_core::Matrix6d actual_covariance;
+ actual_covariance << cov_pos_pos, cov_pos_or, cov_pos_or.transpose(), cov_or_or;
+
+ // Define the expected covariance
+ fuse_core::Matrix6d expected_covariance;
+ /* *INDENT-OFF* */
+ expected_covariance <<
+ 1.0, 0.1, 0.2, 0.3, 0.4, 0.5,
+ 0.1, 2.0, 0.6, 0.5, 0.4, 0.3,
+ 0.2, 0.6, 3.0, 0.2, 0.1, 0.2,
+ 0.3, 0.5, 0.2, 4.0, 0.3, 0.4,
+ 0.4, 0.4, 0.1, 0.3, 5.0, 0.5,
+ 0.5, 0.3, 0.2, 0.4, 0.5, 6.0;
+ /* *INDENT-ON* */
+
+ // High tolerance here, but also high values of covariance
+ EXPECT_MATRIX_NEAR(expected_covariance, actual_covariance, 1.0e-2);
+}
+
+TEST(AbsolutePose3DStampedEulerConstraint, OptimizationPartial)
+{
+ // Optimize a single pose and single constraint, verify the expected value and covariance are
+ // generated. Create the variables. Version for partial measurements
+ auto position_variable = Position3DStamped::make_shared(
+ rclcpp::Time(1, 0), fuse_core::uuid::generate("spra"));
+ position_variable->x() = 1.5;
+ position_variable->y() = 1.0;
+ position_variable->z() = 10.0;
+
+ auto orientation_variable = Orientation3DStamped::make_shared(
+ rclcpp::Time(1, 0), fuse_core::uuid::generate("spra"));
+ orientation_variable->w() = 0.952;
+ orientation_variable->x() = 0.038;
+ orientation_variable->y() = -0.189;
+ orientation_variable->z() = 0.239;
+
+ // Create an absolute pose constraint
+ std::vector variable_indices {0, 2, 3, 4, 5};
+
+ fuse_core::Vector6d mean;
+ mean << 1.0, 0.0, 3.0, 0.0, 0.0, 0.0;
+
+ fuse_core::Matrix5d cov;
+ /* *INDENT-OFF* */
+ cov << 1.0, 0.2, 0.3, 0.4, 0.5,
+ 0.2, 3.0, 0.2, 0.1, 0.2,
+ 0.3, 0.2, 4.0, 0.3, 0.4,
+ 0.4, 0.1, 0.3, 5.0, 0.5,
+ 0.5, 0.2, 0.4, 0.5, 6.0;
+ /* *INDENT-ON* */
+
+ auto constraint = AbsolutePose3DStampedEulerConstraint::make_shared(
+ "test",
+ *position_variable,
+ *orientation_variable,
+ mean,
+ cov,
+ variable_indices);
+
+ // Build the problem
+ ceres::Problem::Options problem_options;
+ problem_options.loss_function_ownership = fuse_core::Loss::Ownership;
+ ceres::Problem problem(problem_options);
+ problem.AddParameterBlock(
+ position_variable->data(),
+ position_variable->size(),
+#if !CERES_SUPPORTS_MANIFOLDS
+ position_variable->localParameterization());
+#else
+ position_variable->manifold());
+#endif
+ problem.AddParameterBlock(
+ orientation_variable->data(),
+ orientation_variable->size(),
+#if !CERES_SUPPORTS_MANIFOLDS
+ orientation_variable->localParameterization());
+#else
+ orientation_variable->manifold());
+#endif
+ std::vector parameter_blocks;
+ parameter_blocks.push_back(position_variable->data());
+ parameter_blocks.push_back(orientation_variable->data());
+
+ problem.AddResidualBlock(
+ constraint->costFunction(),
+ constraint->lossFunction(),
+ parameter_blocks);
+
+ // Run the solver
+ ceres::Solver::Options options;
+ ceres::Solver::Summary summary;
+ ceres::Solve(options, &problem, &summary);
+
+ // Check
+ EXPECT_NEAR(1.0, position_variable->x(), 1.0e-5);
+ EXPECT_NEAR(1.0, position_variable->y(), 1.0e-5); // This is not measured so it will not change
+ EXPECT_NEAR(3.0, position_variable->z(), 1.0e-5);
+ EXPECT_NEAR(1.0, orientation_variable->w(), 1.0e-3);
+ EXPECT_NEAR(0.0, orientation_variable->x(), 1.0e-3);
+ EXPECT_NEAR(0.0, orientation_variable->y(), 1.0e-3);
+ EXPECT_NEAR(0.0, orientation_variable->z(), 1.0e-3);
+
+ // Compute the covariance
+ std::vector> covariance_blocks;
+ covariance_blocks.emplace_back(position_variable->data(), position_variable->data());
+ covariance_blocks.emplace_back(orientation_variable->data(), orientation_variable->data());
+ covariance_blocks.emplace_back(position_variable->data(), orientation_variable->data());
+
+ ceres::Covariance::Options cov_options;
+ cov_options.algorithm_type = ceres::DENSE_SVD;
+ ceres::Covariance covariance(cov_options);
+ covariance.Compute(covariance_blocks, &problem);
+ fuse_core::MatrixXd cov_pos_pos(position_variable->size(), position_variable->size());
+ covariance.GetCovarianceBlock(
+ position_variable->data(),
+ position_variable->data(), cov_pos_pos.data());
+
+ fuse_core::MatrixXd cov_or_or(orientation_variable->localSize(),
+ orientation_variable->localSize());
+ covariance.GetCovarianceBlockInTangentSpace(
+ orientation_variable->data(), orientation_variable->data(), cov_or_or.data());
+
+ fuse_core::MatrixXd cov_pos_or(position_variable->localSize(), orientation_variable->localSize());
+ covariance.GetCovarianceBlockInTangentSpace(
+ position_variable->data(), orientation_variable->data(), cov_pos_or.data());
+
+ // Assemble the full covariance from the covariance blocks
+ fuse_core::Matrix6d actual_covariance;
+ actual_covariance << cov_pos_pos, cov_pos_or, cov_pos_or.transpose(), cov_or_or;
+
+ // Define the expected covariance
+ fuse_core::Matrix6d expected_covariance;
+ /* *INDENT-OFF* */
+ expected_covariance <<
+ 1.0, 0.0, 0.2, 0.3, 0.4, 0.5,
+ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
+ 0.2, 0.0, 3.0, 0.2, 0.1, 0.2,
+ 0.3, 0.0, 0.2, 4.0, 0.3, 0.4,
+ 0.4, 0.0, 0.1, 0.3, 5.0, 0.5,
+ 0.5, 0.0, 0.2, 0.4, 0.5, 6.0;
+ /* *INDENT-ON* */
+
+ // High tolerance here, but also high values of covariance
+ EXPECT_MATRIX_NEAR(expected_covariance, actual_covariance, 1.0e-2);
+}
+
+TEST(AbsolutePose3DStampedEulerConstraint, Serialization)
+{
+ // Construct a constraint
+ Position3DStamped position_variable(rclcpp::Time(1234, 5678), fuse_core::uuid::generate("walle"));
+ Orientation3DStamped orientation_variable(rclcpp::Time(1234, 5678),
+ fuse_core::uuid::generate("walle"));
+
+ fuse_core::Vector6d mean;
+ mean << 1.0, 2.0, 3.0, 0.1, 0.1, 1.0;
+
+ // Generated PD matrix using Octave: R = rand(6, 6); A = R * R' (use format long g to get the
+ // required precision)
+ fuse_core::Matrix6d cov;
+ /* *INDENT-OFF* */
+ cov << 2.0847236144069, 1.10752598122138, 1.02943174290333, 1.96120532313878, 1.96735470687891, 1.5153042667951, // NOLINT
+ 1.10752598122138, 1.39176289439125, 0.643422499737987, 1.35471905449013, 1.18353784377297, 1.28979625492894, // NOLINT
+ 1.02943174290333, 0.643422499737987, 1.26701658550187, 1.23641771365403, 1.55169301761377, 1.34706781598061, // NOLINT
+ 1.96120532313878, 1.35471905449013, 1.23641771365403, 2.39750866789926, 2.06887486311147, 2.04350823837035, // NOLINT
+ 1.96735470687891, 1.18353784377297, 1.55169301761377, 2.06887486311147, 2.503913946461, 1.73844731158092, // NOLINT
+ 1.5153042667951, 1.28979625492894, 1.34706781598061, 2.04350823837035, 1.73844731158092, 2.15326088526198; // NOLINT
+ /* *INDENT-ON* */
+
+ AbsolutePose3DStampedEulerConstraint expected("test", position_variable, orientation_variable,
+ mean,
+ cov);
+
+ // Serialize the constraint into an archive
+ std::stringstream stream;
+ {
+ fuse_core::TextOutputArchive archive(stream);
+ expected.serialize(archive);
+ }
+
+ // Deserialize a new constraint from that same stream
+ AbsolutePose3DStampedEulerConstraint actual;
+ {
+ fuse_core::TextInputArchive archive(stream);
+ actual.deserialize(archive);
+ }
+
+ // Compare
+ EXPECT_EQ(expected.uuid(), actual.uuid());
+ EXPECT_EQ(expected.variables(), actual.variables());
+ EXPECT_MATRIX_EQ(expected.mean(), actual.mean());
+ EXPECT_MATRIX_EQ(expected.sqrtInformation(), actual.sqrtInformation());
+}
diff --git a/fuse_constraints/test/test_normal_prior_pose_3d.cpp b/fuse_constraints/test/test_normal_prior_pose_3d.cpp
new file mode 100644
index 000000000..f3ba07a3d
--- /dev/null
+++ b/fuse_constraints/test/test_normal_prior_pose_3d.cpp
@@ -0,0 +1,93 @@
+/*
+ * Software License Agreement (BSD License)
+ *
+ * Copyright (c) 2023, Giacomo Franchini
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ * * Neither the name of the copyright holder nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+#include
+#include
+#include
+
+#include
+#include
+
+#include "cost_function_gtest.hpp"
+#include
+#include
+#include
+
+/**
+ * @brief Test fixture that initializes a full pose 3d mean and sqrt information matrix.
+ */
+class NormalPriorPose3DTestFixture : public ::testing::Test
+{
+public:
+ //!< The automatic differentiation cost function type for the pose 3d cost functor
+ using AutoDiffNormalPriorPose3D =
+ ceres::AutoDiffCostFunction;
+
+ /**
+ * @brief Constructor
+ */
+ NormalPriorPose3DTestFixture()
+ {
+ full_sqrt_information = covariance.inverse().llt().matrixU();
+ }
+
+ const fuse_core::Matrix6d covariance =
+ fuse_core::Vector6d(
+ 1e-3, 1e-3, 1e-3,
+ 1e-3, 1e-3, 1e-3).asDiagonal(); //!< The full pose 3d covariance for the x,
+ //!< y, z, roll, pitch and yaw components
+ Eigen::Matrix full_sqrt_information; //!< The full pose 3d sqrt information matrix for the x, y
+ //!< z, roll, pitch, and yaw components
+ Eigen::Vector full_mean{1.0, 2.0, 1.0, 1.0, 0.0, 0.0, 0.0}; //!< The full pose 3d mean
+ //!< components: x, y z,
+ //!< qw, qx, qy, qz
+};
+
+TEST_F(NormalPriorPose3DTestFixture, AnalyticAndAutoDiffCostFunctionsAreEqual)
+{
+ // Create cost function
+ auto q = Eigen::Quaterniond::UnitRandom();
+ full_mean << 1.0, 2.0, 1.0, q.w(), q.x(), q.y(), q.z(); // Create automatic differentiation cost function
+ const fuse_constraints::NormalPriorPose3D cost_function{full_sqrt_information, full_mean};
+ const auto num_residuals = full_sqrt_information.rows();
+
+ AutoDiffNormalPriorPose3D autodiff_cost_function(
+ new fuse_constraints::NormalPriorPose3DCostFunctor(full_sqrt_information, full_mean),
+ num_residuals);
+
+ // Compare the expected, automatic differentiation, cost function and the actual one
+ // N.B. in ExpectCostFunctionsAreEqual constructor, the first argument is the expected cost function
+ // and the second argument is the actual cost function
+ ExpectCostFunctionsAreEqual(cost_function, autodiff_cost_function, 1e-14);
+}
diff --git a/fuse_constraints/test/test_normal_prior_pose_3d_euler.cpp b/fuse_constraints/test/test_normal_prior_pose_3d_euler.cpp
new file mode 100644
index 000000000..50301c277
--- /dev/null
+++ b/fuse_constraints/test/test_normal_prior_pose_3d_euler.cpp
@@ -0,0 +1,207 @@
+/*
+ * Software License Agreement (BSD License)
+ *
+ * Copyright (c) 2023, Giacomo Franchini
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ * * Neither the name of the copyright holder nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+#include
+#include
+#include
+
+#include
+#include
+
+#include "cost_function_gtest.hpp"
+#include
+#include
+#include
+
+/**
+ * @brief Test fixture that initializes a full pose 3d mean and sqrt information matrix.
+ */
+class NormalPriorPose3DEulerTestFixture : public ::testing::Test
+{
+public:
+ //!< The automatic differentiation cost function type for the pose 3d cost functor
+ using AutoDiffNormalPriorPose3DEuler =
+ ceres::AutoDiffCostFunction;
+
+ /**
+ * @brief Constructor
+ */
+ NormalPriorPose3DEulerTestFixture()
+ {
+ full_sqrt_information = covariance.inverse().llt().matrixU();
+ }
+
+ const fuse_core::Matrix6d covariance =
+ fuse_core::Vector6d(
+ 1e-3, 1e-3, 1e-3,
+ 1e-3, 1e-3, 1e-3).asDiagonal(); //!< The full pose 3d covariance for the x,
+ //!< y, z, roll, pitch and yaw components
+ Eigen::Matrix full_sqrt_information; //!< The full pose 3d sqrt information matrix for the x, y
+ //!< z, roll, pitch, and yaw components
+ Eigen::Vector full_mean{1.0, 2.0, 1.0, 0.0, 0.0, 0.0}; //!< The full pose 3d mean
+ //!< components: x, y z,
+ //!< roll, pitch, and yaw
+};
+
+TEST_F(NormalPriorPose3DEulerTestFixture, AnalyticAndAutoDiffCostFunctionsAreEqualForFullResiduals)
+{
+ // Create cost function
+ auto rpy = Eigen::Vector3d::Random();
+ full_mean << 1.0, 2.0, 1.0, rpy.x(), rpy.y(), rpy.z();
+ const fuse_constraints::NormalPriorPose3DEuler cost_function{full_sqrt_information, full_mean};
+ const auto num_residuals = full_sqrt_information.rows();
+
+ AutoDiffNormalPriorPose3DEuler autodiff_cost_function(
+ new fuse_constraints::NormalPriorPose3DEulerCostFunctor(full_sqrt_information, full_mean),
+ num_residuals);
+
+ // Compare the expected, automatic differentiation, cost function and the actual one
+ // N.B. in ExpectCostFunctionsAreEqual constructor, the first argument is the expected cost function
+ // and the second argument is the actual cost function
+ ExpectCostFunctionsAreEqual(cost_function, autodiff_cost_function, 1e-13);
+}
+
+TEST_F(
+ NormalPriorPose3DEulerTestFixture,
+ AnalyticAndAutoDiffCostFunctionsAreEqualForPartialPositionResiduals)
+{
+ // Create cost function for a subset of residuals
+ // Version with y position = 0
+ std::vector indices = {0, 2, 3, 4, 5};
+ auto rpy = Eigen::Vector3d::Random();
+ full_mean << 1.0, 0.0, 1.0, rpy.x(), rpy.y(), rpy.z();
+ fuse_core::Matrix partial_sqrt_information;
+ for (size_t i = 0; i < indices.size(); ++i) {
+ partial_sqrt_information.row(i) = full_sqrt_information.row(indices[i]);
+ }
+
+ std::cout << "full_mean: " << full_mean << std::endl;
+ std::cout << "partial_sqrt_information: " << partial_sqrt_information << std::endl;
+
+ const fuse_constraints::NormalPriorPose3DEuler cost_function{partial_sqrt_information, full_mean};
+
+ // Create automatic differentiation cost function
+ const auto num_residuals = partial_sqrt_information.rows();
+
+ AutoDiffNormalPriorPose3DEuler autodiff_cost_function(
+ new fuse_constraints::NormalPriorPose3DEulerCostFunctor(partial_sqrt_information, full_mean),
+ num_residuals);
+
+ ExpectCostFunctionsAreEqual(cost_function, autodiff_cost_function, 1e-13);
+}
+
+TEST_F(
+ NormalPriorPose3DEulerTestFixture,
+ AnalyticAndAutoDiffCostFunctionsAreEqualForPartialOrientationResiduals)
+{
+ // Create cost function for a subset of residuals
+ // Version with roll, pitch = 0
+ std::vector indices = {0, 1, 2, 5};
+ Eigen::Vector3d rpy = Eigen::Vector3d::Random();
+ rpy(0) = 0.0;
+ rpy(1) = 0.0;
+ full_mean << 1.0, 0.0, 1.0, rpy.x(), rpy.y(), rpy.z();
+ fuse_core::Matrix partial_sqrt_information;
+
+ for (size_t i = 0; i < indices.size(); ++i) {
+ partial_sqrt_information.row(i) = full_sqrt_information.row(indices[i]);
+ }
+
+ const fuse_constraints::NormalPriorPose3DEuler cost_function{partial_sqrt_information, full_mean};
+
+ // Create automatic differentiation cost function
+ const auto num_residuals = partial_sqrt_information.rows();
+
+ AutoDiffNormalPriorPose3DEuler autodiff_cost_function(
+ new fuse_constraints::NormalPriorPose3DEulerCostFunctor(partial_sqrt_information, full_mean),
+ num_residuals);
+
+ ExpectCostFunctionsAreEqual(cost_function, autodiff_cost_function, 1e-13);
+}
+
+TEST_F(
+ NormalPriorPose3DEulerTestFixture,
+ AnalyticAndAutoDiffCostFunctionsAreEqualForPartialResidualsPositionOnly)
+{
+ // Create cost function for a subset of residuals
+ // Version with z = 0, orientation = 0
+ std::vector indices = {0, 1};
+ Eigen::Vector3d rpy {0.0, 0.0, 0.0};
+ full_mean << 0.1, 0.5, 0.0, rpy.x(), rpy.y(), rpy.z();
+ fuse_core::Matrix partial_sqrt_information;
+
+ for (size_t i = 0; i < indices.size(); ++i) {
+ partial_sqrt_information.row(i) = full_sqrt_information.row(indices[i]);
+ }
+
+ const fuse_constraints::NormalPriorPose3DEuler cost_function{partial_sqrt_information, full_mean};
+
+ // Create automatic differentiation cost function
+ const auto num_residuals = partial_sqrt_information.rows();
+
+ AutoDiffNormalPriorPose3DEuler autodiff_cost_function(
+ new fuse_constraints::NormalPriorPose3DEulerCostFunctor(partial_sqrt_information, full_mean),
+ num_residuals);
+
+ ExpectCostFunctionsAreEqual(cost_function, autodiff_cost_function, 1e-13);
+}
+
+TEST_F(
+ NormalPriorPose3DEulerTestFixture,
+ AnalyticAndAutoDiffCostFunctionsAreEqualForPartialResidualsOrientationOnly)
+{
+ // Create cost function for a subset of residuals
+ // Version with position = 0, roll = 0
+ std::vector indices = {4, 5};
+ Eigen::Vector3d rpy = Eigen::Vector3d::Random();
+ rpy(0) = 0.0;
+ full_mean << 0.0, 0.0, 0.0, rpy.x(), rpy.y(), rpy.z();
+ fuse_core::Matrix partial_sqrt_information;
+
+ for (size_t i = 0; i < indices.size(); ++i) {
+ partial_sqrt_information.row(i) = full_sqrt_information.row(indices[i]);
+ }
+
+ const fuse_constraints::NormalPriorPose3DEuler cost_function{partial_sqrt_information, full_mean};
+
+ // Create automatic differentiation cost function
+ const auto num_residuals = partial_sqrt_information.rows();
+
+ AutoDiffNormalPriorPose3DEuler autodiff_cost_function(
+ new fuse_constraints::NormalPriorPose3DEulerCostFunctor(partial_sqrt_information, full_mean),
+ num_residuals);
+
+ ExpectCostFunctionsAreEqual(cost_function, autodiff_cost_function, 1e-13);
+}
diff --git a/fuse_constraints/test/test_relative_pose_3d_stamped_euler_constraint.cpp b/fuse_constraints/test/test_relative_pose_3d_stamped_euler_constraint.cpp
new file mode 100644
index 000000000..ee0724d21
--- /dev/null
+++ b/fuse_constraints/test/test_relative_pose_3d_stamped_euler_constraint.cpp
@@ -0,0 +1,731 @@
+/*
+ * Software License Agreement (BSD License)
+ *
+ * Copyright (c) 2023, Giacomo Franchini
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ * * Neither the name of the copyright holder nor the names of its
+ * contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+#include
+#include
+#include
+#include
+
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+using fuse_variables::Orientation3DStamped;
+using fuse_variables::Position3DStamped;
+using fuse_constraints::AbsolutePose3DStampedConstraint;
+using fuse_constraints::AbsolutePose3DStampedEulerConstraint;
+using fuse_constraints::RelativePose3DStampedEulerConstraint;
+
+
+TEST(RelativePose3DStampedEulerConstraint, Constructor)
+{
+ // Construct a constraint just to make sure it compiles.
+ Orientation3DStamped orientation1(rclcpp::Time(1234, 5678), fuse_core::uuid::generate("r5d4"));
+ Position3DStamped position1(rclcpp::Time(1234, 5678), fuse_core::uuid::generate("r5d4"));
+ Orientation3DStamped orientation2(rclcpp::Time(1235, 5678), fuse_core::uuid::generate("r5d4"));
+ Position3DStamped position2(rclcpp::Time(1235, 5678), fuse_core::uuid::generate("r5d4"));
+
+ fuse_core::Vector6d delta;
+ delta << 1.0, 2.0, 3.0, 0.988, 0.094, 0.079;
+
+ // Generated PD matrix using Octave: R = rand(6, 6); A = R * R' (use format long g to get the
+ // required precision)
+ fuse_core::Matrix6d cov;
+ /* *INDENT-OFF* */
+ cov << 2.0847236144069, 1.10752598122138, 1.02943174290333, 1.96120532313878, 1.96735470687891, 1.5153042667951, // NOLINT
+ 1.10752598122138, 1.39176289439125, 0.643422499737987, 1.35471905449013, 1.18353784377297, 1.28979625492894, // NOLINT
+ 1.02943174290333, 0.643422499737987, 1.26701658550187, 1.23641771365403, 1.55169301761377, 1.34706781598061, // NOLINT
+ 1.96120532313878, 1.35471905449013, 1.23641771365403, 2.39750866789926, 2.06887486311147, 2.04350823837035, // NOLINT
+ 1.96735470687891, 1.18353784377297, 1.55169301761377, 2.06887486311147, 2.503913946461, 1.73844731158092, // NOLINT
+ 1.5153042667951, 1.28979625492894, 1.34706781598061, 2.04350823837035, 1.73844731158092, 2.15326088526198; // NOLINT
+ /* *INDENT-ON* */
+
+ EXPECT_NO_THROW(
+ RelativePose3DStampedEulerConstraint constraint(
+ "test", position1, orientation1, position2,
+ orientation2, delta, cov));
+}
+
+TEST(RelativePose3DStampedEulerConstraint, ConstructorPartial)
+{
+ // Construct a constraint just to make sure it compiles.
+ Orientation3DStamped orientation1(rclcpp::Time(1234, 5678), fuse_core::uuid::generate("r5d4"));
+ Position3DStamped position1(rclcpp::Time(1234, 5678), fuse_core::uuid::generate("r5d4"));
+ Orientation3DStamped orientation2(rclcpp::Time(1235, 5678), fuse_core::uuid::generate("r5d4"));
+ Position3DStamped position2(rclcpp::Time(1235, 5678), fuse_core::uuid::generate("r5d4"));
+
+ std::vector indices {0, 1, 3, 4, 5};
+ fuse_core::Vector6d delta;
+ delta << 1.0, 2.0, 0.0, 0.988, 0.094, 0.079;
+
+ // Generated PD matrix using Octave: R = rand(6, 6); A = R * R' (use format long g to get the
+ // required precision)
+ fuse_core::Matrix5d cov;
+ /* *INDENT-OFF* */
+ cov << 2.0847236144069, 1.10752598122138, 1.96120532313878, 1.96735470687891, 1.5153042667951, // NOLINT
+ 1.10752598122138, 1.39176289439125, 1.35471905449013, 1.18353784377297, 1.28979625492894, // NOLINT
+ 1.96120532313878, 1.35471905449013, 2.39750866789926, 2.06887486311147, 2.04350823837035, // NOLINT
+ 1.96735470687891, 1.18353784377297, 2.06887486311147, 2.503913946461, 1.73844731158092, // NOLINT
+ 1.5153042667951, 1.28979625492894, 2.04350823837035, 1.73844731158092, 2.15326088526198; // NOLINT
+ /* *INDENT-ON* */
+
+ EXPECT_NO_THROW(
+ RelativePose3DStampedEulerConstraint constraint(
+ "test", position1, orientation1, position2,
+ orientation2, delta, cov, indices));
+}
+
+TEST(RelativePose3DStampedEulerConstraint, Covariance)
+{
+ // Verify the covariance <--> sqrt information conversions are correct
+ Orientation3DStamped orientation1(rclcpp::Time(1234, 5678), fuse_core::uuid::generate("r5d4"));
+ Position3DStamped position1(rclcpp::Time(1234, 5678), fuse_core::uuid::generate("r5d4"));
+ Orientation3DStamped orientation2(rclcpp::Time(1235, 5678), fuse_core::uuid::generate("r5d4"));
+ Position3DStamped position2(rclcpp::Time(1235, 5678), fuse_core::uuid::generate("r5d4"));
+ fuse_core::Vector6d delta;
+ delta << 1.0, 2.0, 3.0, 0.988, 0.094, 0.079;
+
+ // Generated PD matrix using Octiave: R = rand(6, 6); A = R * R' (use format long g to get the
+ // required precision)
+ fuse_core::Matrix6d cov;
+ /* *INDENT-OFF* */
+ cov << 2.0847236144069, 1.10752598122138, 1.02943174290333, 1.96120532313878, 1.96735470687891, 1.5153042667951, // NOLINT
+ 1.10752598122138, 1.39176289439125, 0.643422499737987, 1.35471905449013, 1.18353784377297, 1.28979625492894, // NOLINT
+ 1.02943174290333, 0.643422499737987, 1.26701658550187, 1.23641771365403, 1.55169301761377, 1.34706781598061, // NOLINT
+ 1.96120532313878, 1.35471905449013, 1.23641771365403, 2.39750866789926, 2.06887486311147, 2.04350823837035, // NOLINT
+ 1.96735470687891, 1.18353784377297, 1.55169301761377, 2.06887486311147, 2.503913946461, 1.73844731158092, // NOLINT
+ 1.5153042667951, 1.28979625492894, 1.34706781598061, 2.04350823837035, 1.73844731158092, 2.15326088526198; // NOLINT
+ /* *INDENT-ON* */
+
+ RelativePose3DStampedEulerConstraint constraint(
+ "test",
+ position1,
+ orientation1,
+ position2,
+ orientation2,
+ delta,
+ cov);
+
+ // Define the expected matrices (used Octave to compute sqrt_info: 'chol(inv(A))')
+ fuse_core::Matrix6d expected_sqrt_info;
+ /* *INDENT-OFF* */
+ expected_sqrt_info << 2.12658752275893, 1.20265444927878, 4.71225672571804, 1.43587520991272, -4.12764062992821, -3.19509486240291, // NOLINT
+ 0.0, 2.41958656956248, 5.93151964116945, 3.72535320852517, -4.23326858606213, -5.27776664777548, // NOLINT
+ 0.0, 0.0, 3.82674686590005, 2.80341171946161, -2.68168478581452, -2.8894384435255, // NOLINT
+ 0.0, 0.0, 0.0, 1.83006791372784, -0.696917410192509, -1.17412835464633, // NOLINT
+ 0.0, 0.0, 0.0, 0.0, 0.953302832761324, -0.769654414882847, // NOLINT
+ 0.0, 0.0, 0.0, 0.0, 0.0, 0.681477739760948; // NOLINT
+ /* *INDENT-ON* */
+ fuse_core::Matrix6d expected_cov = cov;
+
+ // Compare
+ EXPECT_MATRIX_NEAR(expected_cov, constraint.covariance(), 1.0e-9);
+ EXPECT_MATRIX_NEAR(expected_sqrt_info, constraint.sqrtInformation(), 1.0e-9);
+}
+
+TEST(RelativePose3DStampedEulerConstraint, CovariancePartial)
+{
+ // Verify the covariance <--> sqrt information conversions are correct
+ Orientation3DStamped orientation1(rclcpp::Time(1234, 5678), fuse_core::uuid::generate("r5d4"));
+ Position3DStamped position1(rclcpp::Time(1234, 5678), fuse_core::uuid::generate("r5d4"));
+ Orientation3DStamped orientation2(rclcpp::Time(1235, 5678), fuse_core::uuid::generate("r5d4"));
+ Position3DStamped position2(rclcpp::Time(1235, 5678), fuse_core::uuid::generate("r5d4"));
+
+ std::vector indices {0, 1, 3, 4, 5};
+ fuse_core::Vector6d delta;
+ delta << 1.0, 2.0, 0.0, 0.988, 0.094, 0.079;
+
+ // Generated PD matrix using Octiave: R = rand(6, 6); A = R * R' (use format long g to get the
+ // required precision)
+ fuse_core::Matrix5d cov;
+ /* *INDENT-OFF* */
+ cov << 2.0847236144069, 1.10752598122138, 1.96120532313878, 1.96735470687891, 1.5153042667951, // NOLINT
+ 1.10752598122138, 1.39176289439125, 1.35471905449013, 1.18353784377297, 1.28979625492894, // NOLINT
+ 1.96120532313878, 1.35471905449013, 2.39750866789926, 2.06887486311147, 2.04350823837035, // NOLINT
+ 1.96735470687891, 1.18353784377297, 2.06887486311147, 2.503913946461, 1.73844731158092, // NOLINT
+ 1.5153042667951, 1.28979625492894, 2.04350823837035, 1.73844731158092, 2.15326088526198; // NOLINT
+ /* *INDENT-ON* */
+
+ RelativePose3DStampedEulerConstraint constraint(
+ "test",
+ position1,
+ orientation1,
+ position2,
+ orientation2,
+ delta,
+ cov,
+ indices);
+
+ // Define the expected matrices (used Octave to compute sqrt_info: 'chol(inv(A))')
+ fuse_core::Matrix expected_sqrt_info;
+ /* *INDENT-OFF* */
+ expected_sqrt_info << 1.7687, -0.1286, 0.0, -1.3877, -0.6508, 0.6747, // NOLINT
+ 0.0, 1.3117, 0.0, -0.3361, -0.0415, -0.4332, // NOLINT
+ 0.0, 0.0, 0.0, 1.8301, -0.6969, -1.1741, // NOLINT
+ 0.0, 0.0, 0.0, 0.0, 0.9533, -0.7697, // NOLINT
+ 0.0, 0.0, 0.0, 0.0, 0.0, 0.6815; // NOLINT
+ /* *INDENT-ON* */
+ /* *INDENT-ON* */
+ fuse_core::Matrix6d expected_cov;
+
+ expected_cov << 2.0847236144069, 1.10752598122138, 0.0, 1.96120532313878, 1.96735470687891,
+ 1.5153042667951, // NOLINT
+ 1.10752598122138, 1.39176289439125, 0.0, 1.35471905449013, 1.18353784377297, 1.28979625492894, // NOLINT
+ 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, // NOLINT
+ 1.96120532313878, 1.35471905449013, 0.0, 2.39750866789926, 2.06887486311147, 2.04350823837035, // NOLINT
+ 1.96735470687891, 1.18353784377297, 0.0, 2.06887486311147, 2.503913946461, 1.73844731158092, // NOLINT
+ 1.5153042667951, 1.28979625492894, 0.0, 2.04350823837035, 1.73844731158092, 2.15326088526198; // NOLINT
+
+ // Compare
+ EXPECT_MATRIX_NEAR(expected_cov, constraint.covariance(), 1.0e-9);
+ EXPECT_MATRIX_NEAR(expected_sqrt_info, constraint.sqrtInformation(), 1.0e-4);
+}
+
+TEST(RelativePose3DStampedEulerConstraint, Optimization)
+{
+ // Optimize a two-pose system with a pose prior and a relative pose constraint
+ // Verify the expected poses and covariances are generated.
+ // Create two poses
+ auto position1 =
+ Position3DStamped::make_shared(rclcpp::Time(1, 0), fuse_core::uuid::generate("spra"));
+ position1->x() = 1.5;
+ position1->y() = -3.0;
+ position1->z() = 10.0;
+
+ auto orientation1 = Orientation3DStamped::make_shared(
+ rclcpp::Time(1, 0), fuse_core::uuid::generate("spra"));
+ orientation1->w() = 0.952;
+ orientation1->x() = 0.038;
+ orientation1->y() = -0.189;
+ orientation1->z() = 0.239;
+
+ auto position2 =
+ Position3DStamped::make_shared(rclcpp::Time(2, 0), fuse_core::uuid::generate("spra"));
+ position2->x() = -1.5;
+ position2->y() = 3.0;
+ position2->z() = -10.0;
+
+ auto orientation2 = Orientation3DStamped::make_shared(
+ rclcpp::Time(2, 0), fuse_core::uuid::generate("spra"));
+ orientation2->w() = 0.944;
+ orientation2->x() = -0.128;
+ orientation2->y() = 0.145;
+ orientation2->z() = -0.269;
+
+ // Create an absolute pose constraint at the origin
+ fuse_core::Vector7d mean_origin;
+ mean_origin << 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0;
+ fuse_core::Matrix6d cov_origin = fuse_core::Matrix6d::Identity();
+ auto prior = AbsolutePose3DStampedConstraint::make_shared(
+ "test",
+ *position1,
+ *orientation1,
+ mean_origin,
+ cov_origin);
+
+ // Create a relative pose constraint for 1m in the x direction
+ fuse_core::Vector6d mean_delta;
+ mean_delta << 1.0, 0.0, 0.0, 0.0, 0.0, 0.0;
+ fuse_core::Matrix6d cov_delta = fuse_core::Matrix6d::Identity();
+ auto relative = RelativePose3DStampedEulerConstraint::make_shared(
+ "test",
+ *position1,
+ *orientation1,
+ *position2,
+ *orientation2,
+ mean_delta,
+ cov_delta);
+
+ // Build the problem
+ ceres::Problem::Options problem_options;
+ problem_options.loss_function_ownership = fuse_core::Loss::Ownership;
+ ceres::Problem problem(problem_options);
+ problem.AddParameterBlock(
+ orientation1->data(),
+ orientation1->size(),
+#if !CERES_SUPPORTS_MANIFOLDS
+ orientation1->localParameterization());
+#else
+ orientation1->manifold());
+#endif
+ problem.AddParameterBlock(
+ position1->data(),
+ position1->size(),
+#if !CERES_SUPPORTS_MANIFOLDS
+ position1->localParameterization());
+#else
+ position1->manifold());
+#endif
+ problem.AddParameterBlock(
+ orientation2->data(),
+ orientation2->size(),
+#if !CERES_SUPPORTS_MANIFOLDS
+ orientation2->localParameterization());
+#else
+ orientation2->manifold());
+#endif
+ problem.AddParameterBlock(
+ position2->data(),
+ position2->size(),
+#if !CERES_SUPPORTS_MANIFOLDS
+ position2->localParameterization());
+#else
+ position2->manifold());
+#endif
+ std::vector prior_parameter_blocks;
+ prior_parameter_blocks.push_back(position1->data());
+ prior_parameter_blocks.push_back(orientation1->data());
+ problem.AddResidualBlock(
+ prior->costFunction(),
+ prior->lossFunction(),
+ prior_parameter_blocks);
+ std::vector relative_parameter_blocks;
+ relative_parameter_blocks.push_back(position1->data());
+ relative_parameter_blocks.push_back(orientation1->data());
+ relative_parameter_blocks.push_back(position2->data());
+ relative_parameter_blocks.push_back(orientation2->data());
+ problem.AddResidualBlock(
+ relative->costFunction(),
+ relative->lossFunction(),
+ relative_parameter_blocks);
+
+ // Run the solver
+ ceres::Solver::Options options;
+ ceres::Solver::Summary summary;
+ ceres::Solve(options, &problem, &summary);
+
+ // Check
+ EXPECT_NEAR(0.0, position1->x(), 1.0e-5);
+ EXPECT_NEAR(0.0, position1->y(), 1.0e-5);
+ EXPECT_NEAR(0.0, position1->z(), 1.0e-5);
+ EXPECT_NEAR(1.0, orientation1->w(), 1.0e-3);
+ EXPECT_NEAR(0.0, orientation1->x(), 1.0e-3);
+ EXPECT_NEAR(0.0, orientation1->y(), 1.0e-3);
+ EXPECT_NEAR(0.0, orientation1->z(), 1.0e-3);
+ EXPECT_NEAR(1.0, position2->x(), 1.0e-5);
+ EXPECT_NEAR(0.0, position2->y(), 1.0e-5);
+ EXPECT_NEAR(0.0, position2->z(), 1.0e-5);
+ EXPECT_NEAR(1.0, orientation2->w(), 1.0e-3);
+ EXPECT_NEAR(0.0, orientation2->x(), 1.0e-3);
+ EXPECT_NEAR(0.0, orientation2->y(), 1.0e-3);
+ EXPECT_NEAR(0.0, orientation2->z(), 1.0e-3);
+
+ // Compute the marginal covariance for pose1
+ {
+ std::vector> covariance_blocks;
+ covariance_blocks.emplace_back(position1->data(), position1->data());
+ covariance_blocks.emplace_back(orientation1->data(), orientation1->data());
+ covariance_blocks.emplace_back(position1->data(), orientation1->data());
+
+ ceres::Covariance::Options cov_options;
+ ceres::Covariance covariance(cov_options);
+ covariance.Compute(covariance_blocks, &problem);
+
+ fuse_core::MatrixXd cov_pos_pos(position1->size(), position1->size());
+ covariance.GetCovarianceBlock(position1->data(), position1->data(), cov_pos_pos.data());
+
+ fuse_core::MatrixXd cov_or_or(3, 3);
+ covariance.GetCovarianceBlockInTangentSpace(
+ orientation1->data(),
+ orientation1->data(), cov_or_or.data());
+
+ fuse_core::MatrixXd cov_pos_or(position1->size(), 3);
+ covariance.GetCovarianceBlockInTangentSpace(
+ position1->data(),
+ orientation1->data(), cov_pos_or.data());
+
+ // Assemble the full covariance from the covariance blocks
+ fuse_core::Matrix6d actual_covariance;
+ actual_covariance << cov_pos_pos, cov_pos_or, cov_pos_or.transpose(), cov_or_or;
+
+ // Define the expected covariance
+ fuse_core::Matrix6d expected_covariance;
+ /* *INDENT-OFF* */
+ expected_covariance <<
+ 1.0, 0.0, 0.0, 0.0, 0.0, 0.0,
+ 0.0, 1.0, 0.0, 0.0, 0.0, 0.0,
+ 0.0, 0.0, 1.0, 0.0, 0.0, 0.0,
+ 0.0, 0.0, 0.0, 1.0, 0.0, 0.0,
+ 0.0, 0.0, 0.0, 0.0, 1.0, 0.0,
+ 0.0, 0.0, 0.0, 0.0, 0.0, 1.0;
+ /* *INDENT-ON* */
+
+ EXPECT_MATRIX_NEAR(expected_covariance, actual_covariance, 1.0e-9);
+ }
+
+ // Compute the marginal covariance for pose2
+ {
+ std::vector> covariance_blocks;
+ covariance_blocks.emplace_back(position2->data(), position2->data());
+ covariance_blocks.emplace_back(position2->data(), orientation2->data());
+ covariance_blocks.emplace_back(orientation2->data(), orientation2->data());
+
+ ceres::Covariance::Options cov_options;
+ ceres::Covariance covariance(cov_options);
+ covariance.Compute(covariance_blocks, &problem);
+
+ fuse_core::MatrixXd cov_pos_pos(position2->size(), position2->size());
+ covariance.GetCovarianceBlock(position2->data(), position2->data(), cov_pos_pos.data());
+
+ fuse_core::MatrixXd cov_or_or(3, 3);
+ covariance.GetCovarianceBlockInTangentSpace(
+ orientation2->data(),
+ orientation2->data(), cov_or_or.data());
+
+ fuse_core::MatrixXd cov_pos_or(position1->size(), 3);
+ covariance.GetCovarianceBlockInTangentSpace(
+ position2->data(),
+ orientation2->data(), cov_pos_or.data());
+
+ // Assemble the full covariance from the covariance blocks
+ fuse_core::Matrix6d actual_covariance;
+ actual_covariance << cov_pos_pos, cov_pos_or, cov_pos_or.transpose(), cov_or_or;
+
+ // Define the expected covariance
+ fuse_core::Matrix6d expected_covariance;
+ /* *INDENT-OFF* */
+ expected_covariance <<
+ 2.0, 0.0, 0.0, 0.0, 0.0, 0.0,
+ 0.0, 3.0, 0.0, 0.0, 0.0, 1.0,
+ 0.0, 0.0, 3.0, 0.0, -1.0, 0.0,
+ 0.0, 0.0, 0.0, 2.0, 0.0, 0.0,
+ 0.0, 0.0, -1.0, 0.0, 2.0, 0.0,
+ 0.0, 1.0, 0.0, 0.0, 0.0, 2.0;
+ /* *INDENT-ON* */
+
+ // High tolerance here, but also high values of covariance
+ EXPECT_MATRIX_NEAR(expected_covariance, actual_covariance, 1.0e-2);
+ }
+}
+
+TEST(RelativePose3DStampedEulerConstraint, OptimizationPartial)
+{
+ // Optimize a two-pose system with a pose prior and a relative pose constraint
+ // Verify the expected poses and covariances are generated.
+ // Create two poses
+ auto position1 =
+ Position3DStamped::make_shared(rclcpp::Time(1, 0), fuse_core::uuid::generate("spra"));
+ position1->x() = 1.5;
+ position1->y() = -3.0;
+ position1->z() = 10.0;
+
+ auto orientation1 = Orientation3DStamped::make_shared(
+ rclcpp::Time(1, 0), fuse_core::uuid::generate("spra"));
+ orientation1->w() = 0.952;
+ orientation1->x() = 0.038;
+ orientation1->y() = -0.189;
+ orientation1->z() = 0.239;
+
+ auto position2 =
+ Position3DStamped::make_shared(rclcpp::Time(2, 0), fuse_core::uuid::generate("spra"));
+ position2->x() = -1.5;
+ position2->y() = 3.0;
+ position2->z() = -10.0;
+
+ auto orientation2 = Orientation3DStamped::make_shared(
+ rclcpp::Time(2, 0), fuse_core::uuid::generate("spra"));
+ orientation2->w() = 0.944;
+ orientation2->x() = -0.128;
+ orientation2->y() = 0.145;
+ orientation2->z() = -0.269;
+
+ std::vector indices {0, 1, 3, 4, 5};
+
+ // Create an absolute pose constraint at the origin
+ fuse_core::Vector7d mean_origin;
+ mean_origin << 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0;
+ fuse_core::Matrix6d cov_origin = fuse_core::Matrix6d::Identity();
+ auto prior = AbsolutePose3DStampedConstraint::make_shared(
+ "test",
+ *position1,
+ *orientation1,
+ mean_origin,
+ cov_origin);
+
+ // Create a relative pose constraint for 1m in the x direction
+ fuse_core::Vector6d mean_delta;
+ mean_delta << 1.0, 0.0, 0.0, 0.0, 0.0, 0.0;
+ fuse_core::Matrix5d cov_delta = fuse_core::Matrix5d::Identity();
+ auto relative = RelativePose3DStampedEulerConstraint::make_shared(
+ "test",
+ *position1,
+ *orientation1,
+ *position2,
+ *orientation2,
+ mean_delta,
+ cov_delta,
+ indices);
+
+ // Create a relative pose constraint for 1m in the y direction
+ std::vector indices_y {1, 2, 3, 4, 5};
+ fuse_core::Vector6d mean_delta_y;
+ mean_delta_y << 0.0, 1.0, 0.0, 0.0, 0.0, 0.0;
+ fuse_core::Matrix5d cov_delta_y = fuse_core::Matrix5d::Identity();
+ auto relative_y = RelativePose3DStampedEulerConstraint::make_shared(
+ "test",
+ *position1,
+ *orientation1,
+ *position2,
+ *orientation2,
+ mean_delta_y,
+ cov_delta_y,
+ indices_y);
+
+ // Build the problem
+ ceres::Problem::Options problem_options;
+ problem_options.loss_function_ownership = fuse_core::Loss::Ownership;
+ ceres::Problem problem(problem_options);
+ problem.AddParameterBlock(
+ orientation1->data(),
+ orientation1->size(),
+#if !CERES_SUPPORTS_MANIFOLDS
+ orientation1->localParameterization());
+#else
+ orientation1->manifold());
+#endif
+ problem.AddParameterBlock(
+ position1->data(),
+ position1->size(),
+#if !CERES_SUPPORTS_MANIFOLDS
+ position1->localParameterization());
+#else
+ position1->manifold());
+#endif
+ problem.AddParameterBlock(
+ orientation2->data(),
+ orientation2->size(),
+#if !CERES_SUPPORTS_MANIFOLDS
+ orientation2->localParameterization());
+#else
+ orientation2->manifold());
+#endif
+ problem.AddParameterBlock(
+ position2->data(),
+ position2->size(),
+#if !CERES_SUPPORTS_MANIFOLDS
+ position2->localParameterization());
+#else
+ position2->manifold());
+#endif
+ std::vector prior_parameter_blocks;
+ prior_parameter_blocks.push_back(position1->data());
+ prior_parameter_blocks.push_back(orientation1->data());
+ problem.AddResidualBlock(
+ prior->costFunction(),
+ prior->lossFunction(),
+ prior_parameter_blocks);
+ std::vector relative_parameter_blocks;
+ relative_parameter_blocks.push_back(position1->data());
+ relative_parameter_blocks.push_back(orientation1->data());
+ relative_parameter_blocks.push_back(position2->data());
+ relative_parameter_blocks.push_back(orientation2->data());
+ problem.AddResidualBlock(
+ relative->costFunction(),
+ relative->lossFunction(),
+ relative_parameter_blocks);
+ std::vector relative_parameter_blocks_y;
+ relative_parameter_blocks_y.push_back(position1->data());
+ relative_parameter_blocks_y.push_back(orientation1->data());
+ relative_parameter_blocks_y.push_back(position2->data());
+ relative_parameter_blocks_y.push_back(orientation2->data());
+ problem.AddResidualBlock(
+ relative_y->costFunction(),
+ relative_y->lossFunction(),
+ relative_parameter_blocks_y);
+
+ // Run the solver
+ ceres::Solver::Options options;
+ ceres::Solver::Summary summary;
+ ceres::Solve(options, &problem, &summary);
+
+ // Check
+ EXPECT_NEAR(0.0, position1->x(), 1.0e-5);
+ EXPECT_NEAR(0.0, position1->y(), 1.0e-5);
+ EXPECT_NEAR(0.0, position1->z(), 1.0e-5);
+ EXPECT_NEAR(1.0, orientation1->w(), 1.0e-3);
+ EXPECT_NEAR(0.0, orientation1->x(), 1.0e-3);
+ EXPECT_NEAR(0.0, orientation1->y(), 1.0e-3);
+ EXPECT_NEAR(0.0, orientation1->z(), 1.0e-3);
+ EXPECT_NEAR(1.0, position2->x(), 1.0e-5);
+ EXPECT_NEAR(0.5, position2->y(), 1.0e-5);
+ EXPECT_NEAR(0.0, position2->z(), 1.0e-5);
+ EXPECT_NEAR(1.0, orientation2->w(), 1.0e-3);
+ EXPECT_NEAR(0.0, orientation2->x(), 1.0e-3);
+ EXPECT_NEAR(0.0, orientation2->y(), 1.0e-3);
+ EXPECT_NEAR(0.0, orientation2->z(), 1.0e-3);
+
+ // Compute the marginal covariance for pose1
+ {
+ std::vector> covariance_blocks;
+ covariance_blocks.emplace_back(position1->data(), position1->data());
+ covariance_blocks.emplace_back(orientation1->data(), orientation1->data());
+ covariance_blocks.emplace_back(position1->data(), orientation1->data());
+
+ ceres::Covariance::Options cov_options;
+ cov_options.algorithm_type = ceres::DENSE_SVD;
+ ceres::Covariance covariance(cov_options);
+ covariance.Compute(covariance_blocks, &problem);
+
+ fuse_core::MatrixXd cov_pos_pos(position1->size(), position1->size());
+ covariance.GetCovarianceBlock(position1->data(), position1->data(), cov_pos_pos.data());
+
+ fuse_core::MatrixXd cov_or_or(orientation1->localSize(), orientation1->localSize());
+ covariance.GetCovarianceBlockInTangentSpace(
+ orientation1->data(),
+ orientation1->data(), cov_or_or.data());
+
+ fuse_core::MatrixXd cov_pos_or(position1->localSize(), orientation1->localSize());
+ covariance.GetCovarianceBlockInTangentSpace(
+ position1->data(),
+ orientation1->data(), cov_pos_or.data());
+
+ // Assemble the full covariance from the covariance blocks
+ fuse_core::Matrix6d actual_covariance;
+ actual_covariance << cov_pos_pos, cov_pos_or, cov_pos_or.transpose(), cov_or_or;
+
+ // Define the expected covariance
+ fuse_core::Matrix6d expected_covariance;
+ /* *INDENT-OFF* */
+ expected_covariance <<
+ 1.0, 0.0, 0.0, 0.0, 0.0, 0.0,
+ 0.0, 1.0, 0.0, 0.0, 0.0, 0.0,
+ 0.0, 0.0, 1.0, 0.0, 0.0, 0.0,
+ 0.0, 0.0, 0.0, 1.0, 0.0, 0.0,
+ 0.0, 0.0, 0.0, 0.0, 1.0, 0.0,
+ 0.0, 0.0, 0.0, 0.0, 0.0, 1.0;
+ /* *INDENT-ON* */
+
+ EXPECT_MATRIX_NEAR(expected_covariance, actual_covariance, 1.0e-9);
+ }
+
+ // Compute the marginal covariance for pose2
+ {
+ std::vector> covariance_blocks;
+ covariance_blocks.emplace_back(position2->data(), position2->data());
+ covariance_blocks.emplace_back(position2->data(), orientation2->data());
+ covariance_blocks.emplace_back(orientation2->data(), orientation2->data());
+
+ ceres::Covariance::Options cov_options;
+ cov_options.algorithm_type = ceres::DENSE_SVD;
+ ceres::Covariance covariance(cov_options);
+ covariance.Compute(covariance_blocks, &problem);
+
+ fuse_core::MatrixXd cov_pos_pos(position2->size(), position2->size());
+ covariance.GetCovarianceBlock(position2->data(), position2->data(), cov_pos_pos.data());
+
+ fuse_core::MatrixXd cov_or_or(orientation1->localSize(), orientation1->localSize());
+ covariance.GetCovarianceBlockInTangentSpace(
+ orientation2->data(),
+ orientation2->data(), cov_or_or.data());
+
+ fuse_core::MatrixXd cov_pos_or(position1->localSize(), orientation1->localSize());
+ covariance.GetCovarianceBlockInTangentSpace(
+ position2->data(),
+ orientation2->data(), cov_pos_or.data());
+
+ // Assemble the full covariance from the covariance blocks
+ fuse_core::Matrix6d actual_covariance;
+ actual_covariance << cov_pos_pos, cov_pos_or, cov_pos_or.transpose(), cov_or_or;
+
+ // Define the expected covariance
+ fuse_core::Matrix6d expected_covariance;
+ /* *INDENT-OFF* */
+ expected_covariance <<
+ 2.25, -0.5, 0.0, 0.0, 0.0, -0.5,
+ -0.5, 2.5, 0.0, 0.0, 0.0, 1.0,
+ 0.0, 0.0, 3.25, 0.5, -1.0, 0.0,
+ 0.0, 0.0, 0.5, 1.5, 0.0, 0.0,
+ 0.0, 0.0, -1.0, 0.0, 1.5, 0.0,
+ -0.5, 1.0, 0.0, 0.0, 0.0, 1.5;
+ /* *INDENT-ON* */
+
+ EXPECT_MATRIX_NEAR(expected_covariance, actual_covariance, 1.0e-2);
+ }
+}
+
+TEST(RelativePose3DStampedEulerConstraint, Serialization)
+{
+ // Construct a constraint
+ Orientation3DStamped orientation1(rclcpp::Time(1234, 5678), fuse_core::uuid::generate("r5d4"));
+ Position3DStamped position1(rclcpp::Time(1234, 5678), fuse_core::uuid::generate("r5d4"));
+ Orientation3DStamped orientation2(rclcpp::Time(1235, 5678), fuse_core::uuid::generate("r5d4"));
+ Position3DStamped position2(rclcpp::Time(1235, 5678), fuse_core::uuid::generate("r5d4"));
+
+ fuse_core::Vector6d delta;
+ delta << 1.0, 2.0, 3.0, 0.988, 0.094, 0.079;
+
+ // Generated PD matrix using Octave: R = rand(6, 6); A = R * R' (use format long g to get the
+ // required precision)
+ fuse_core::Matrix6d cov;
+ /* *INDENT-OFF* */
+ cov << 2.0847236144069, 1.10752598122138, 1.02943174290333, 1.96120532313878, 1.96735470687891, 1.5153042667951, // NOLINT
+ 1.10752598122138, 1.39176289439125, 0.643422499737987, 1.35471905449013, 1.18353784377297, 1.28979625492894, // NOLINT
+ 1.02943174290333, 0.643422499737987, 1.26701658550187, 1.23641771365403, 1.55169301761377, 1.34706781598061, // NOLINT
+ 1.96120532313878, 1.35471905449013, 1.23641771365403, 2.39750866789926, 2.06887486311147, 2.04350823837035, // NOLINT
+ 1.96735470687891, 1.18353784377297, 1.55169301761377, 2.06887486311147, 2.503913946461, 1.73844731158092, // NOLINT
+ 1.5153042667951, 1.28979625492894, 1.34706781598061, 2.04350823837035, 1.73844731158092, 2.15326088526198; // NOLINT
+ /* *INDENT-ON* */
+
+ RelativePose3DStampedEulerConstraint expected("test", position1, orientation1, position2,
+ orientation2,
+ delta, cov);
+
+ // Serialize the constraint into an archive
+ std::stringstream stream;
+ {
+ fuse_core::TextOutputArchive archive(stream);
+ expected.serialize(archive);
+ }
+
+ // Deserialize a new constraint from that same stream
+ RelativePose3DStampedEulerConstraint actual;
+ {
+ fuse_core::TextInputArchive archive(stream);
+ actual.deserialize(archive);
+ }
+
+ // Compare
+ EXPECT_EQ(expected.uuid(), actual.uuid());
+ EXPECT_EQ(expected.variables(), actual.variables());
+ EXPECT_MATRIX_EQ(expected.delta(), actual.delta());
+ EXPECT_MATRIX_EQ(expected.sqrtInformation(), actual.sqrtInformation());
+}
diff --git a/fuse_core/include/fuse_core/constraint.hpp b/fuse_core/include/fuse_core/constraint.hpp
index 1abd6d16d..db4f4b4a8 100644
--- a/fuse_core/include/fuse_core/constraint.hpp
+++ b/fuse_core/include/fuse_core/constraint.hpp
@@ -89,15 +89,15 @@
void serialize(fuse_core::BinaryOutputArchive & archive) const override \
{ \
archive << *this; \
- } /* NOLINT */ \
+ } /* NOLINT */ \
void serialize(fuse_core::TextOutputArchive & archive) const override \
{ \
archive << *this; \
- } /* NOLINT */ \
+ } /* NOLINT */ \
void deserialize(fuse_core::BinaryInputArchive & archive) override \
{ \
archive >> *this; \
- } /* NOLINT */ \
+ } /* NOLINT */ \
void deserialize(fuse_core::TextInputArchive & archive) override \
{ \
archive >> *this; \
@@ -124,8 +124,8 @@
static std::string type() \
{ \
return boost::typeindex::stl_type_index::type_id<__VA_ARGS__>().pretty_name(); \
- } /* NOLINT */ \
- }; /* NOLINT */ \
+ } /* NOLINT */ \
+ }; /* NOLINT */ \
std::string type() const override \
{ \
return detail::type(); \
diff --git a/fuse_core/include/fuse_core/eigen.hpp b/fuse_core/include/fuse_core/eigen.hpp
index 6d9ced923..447325539 100644
--- a/fuse_core/include/fuse_core/eigen.hpp
+++ b/fuse_core/include/fuse_core/eigen.hpp
@@ -54,6 +54,7 @@ using Vector6d = Eigen::Matrix;
using Vector7d = Eigen::Matrix;
using Vector8d = Eigen::Matrix;
using Vector9d = Eigen::Matrix;
+using Vector15d = Eigen::Matrix;
using MatrixXd = Eigen::Matrix;
using Matrix1d = Eigen::Matrix;
@@ -65,6 +66,7 @@ using Matrix6d = Eigen::Matrix;
using Matrix7d = Eigen::Matrix;
using Matrix8d = Eigen::Matrix;
using Matrix9d = Eigen::Matrix;
+using Matrix15d = Eigen::Matrix;
template
using Matrix = Eigen::Matrix;
diff --git a/fuse_core/include/fuse_core/graph.hpp b/fuse_core/include/fuse_core/graph.hpp
index a9f366c84..c0a918e3e 100644
--- a/fuse_core/include/fuse_core/graph.hpp
+++ b/fuse_core/include/fuse_core/graph.hpp
@@ -74,15 +74,15 @@
void serialize(fuse_core::BinaryOutputArchive & archive) const override \
{ \
archive << *this; \
- } /* NOLINT */ \
+ } /* NOLINT */ \
void serialize(fuse_core::TextOutputArchive & archive) const override \
{ \
archive << *this; \
- } /* NOLINT */ \
+ } /* NOLINT */ \
void deserialize(fuse_core::BinaryInputArchive & archive) override \
{ \
archive >> *this; \
- } /* NOLINT */ \
+ } /* NOLINT */ \
void deserialize(fuse_core::TextInputArchive & archive) override \
{ \
archive >> *this; \
@@ -109,8 +109,8 @@
static std::string type() \
{ \
return boost::typeindex::stl_type_index::type_id<__VA_ARGS__>().pretty_name(); \
- } /* NOLINT */ \
- }; /* NOLINT */ \
+ } /* NOLINT */ \
+ }; /* NOLINT */ \
std::string type() const override \
{ \
return detail::type(); \
diff --git a/fuse_core/include/fuse_core/loss.hpp b/fuse_core/include/fuse_core/loss.hpp
index 8465e574f..bf0fb4eb5 100644
--- a/fuse_core/include/fuse_core/loss.hpp
+++ b/fuse_core/include/fuse_core/loss.hpp
@@ -81,15 +81,15 @@
void serialize(fuse_core::BinaryOutputArchive & archive) const override \
{ \
archive << *this; \
- } /* NOLINT */ \
+ } /* NOLINT */ \
void serialize(fuse_core::TextOutputArchive & archive) const override \
{ \
archive << *this; \
- } /* NOLINT */ \
+ } /* NOLINT */ \
void deserialize(fuse_core::BinaryInputArchive & archive) override \
{ \
archive >> *this; \
- } /* NOLINT */ \
+ } /* NOLINT */ \
void deserialize(fuse_core::TextInputArchive & archive) override \
{ \
archive >> *this; \
@@ -116,8 +116,8 @@
static std::string type() \
{ \
return boost::typeindex::stl_type_index::type_id<__VA_ARGS__>().pretty_name(); \
- } /* NOLINT */ \
- }; /* NOLINT */ \
+ } /* NOLINT */ \
+ }; /* NOLINT */ \
std::string type() const override \
{ \
return detail::type(); \
diff --git a/fuse_core/include/fuse_core/util.hpp b/fuse_core/include/fuse_core/util.hpp
index b3bdc4a55..1b10f088c 100644
--- a/fuse_core/include/fuse_core/util.hpp
+++ b/fuse_core/include/fuse_core/util.hpp
@@ -35,9 +35,11 @@
#define FUSE_CORE__UTIL_HPP_
#include
+#include
#include