diff --git a/CMakeLists.txt b/CMakeLists.txt
index 98de6dbd9bfb7521238c7c784b077456b35b5629..c46ad291f2e0e0ba09f12ce18a56400cb555d593 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -246,6 +246,7 @@ SET(HDRS_FACTOR
   include/core/factor/factor_pose_3d.h
   include/core/factor/factor_quaternion_absolute.h
   include/core/factor/factor_relative_2d_analytic.h
+  include/core/factor/factor_relative_pose_2d_with_extrinsics.h
   )
 SET(HDRS_FEATURE
   include/core/feature/feature_base.h
diff --git a/include/core/factor/factor_diff_drive.h b/include/core/factor/factor_diff_drive.h
index d936a99d6c1d64f29fda2c5aeca1806e0065c4d1..7b189e4424b419b655833020ff9a94c71952748e 100644
--- a/include/core/factor/factor_diff_drive.h
+++ b/include/core/factor/factor_diff_drive.h
@@ -14,6 +14,7 @@
 #include "core/factor/factor_autodiff.h"
 #include "core/frame/frame_base.h"
 #include "core/feature/feature_motion.h"
+#include "core/math/rotations.h"
 
 namespace
 {
@@ -126,10 +127,7 @@ inline bool FactorDiffDrive::operator ()(const T* const _p1, const T* const _o1,
     residuals = delta_corrected - delta_predicted;
 
     // angle remapping
-    while (residuals(2) > T(M_PI))
-        residuals(2) = residuals(2) - T(2. * M_PI);
-    while (residuals(2) <= T(-M_PI))
-        residuals(2) = residuals(2) + T(2. * M_PI);
+    pi2pi(residuals(2));
 
     // weighted residual
     residuals = getMeasurementSquareRootInformationUpper() * residuals;
diff --git a/include/core/factor/factor_odom_2d.h b/include/core/factor/factor_odom_2d.h
index e20f0f50f158b7ed04e2d09054fafee9300a6f29..1ce2a375c131cea5234a5e603cdf2808764bc4cc 100644
--- a/include/core/factor/factor_odom_2d.h
+++ b/include/core/factor/factor_odom_2d.h
@@ -4,6 +4,7 @@
 //Wolf includes
 #include "core/factor/factor_autodiff.h"
 #include "core/frame/frame_base.h"
+#include "core/math/rotations.h"
 
 //#include "ceres/jet.h"
 
@@ -72,10 +73,7 @@ inline bool FactorOdom2d::operator ()(const T* const _p1, const T* const _o1, co
 
     // Error
     er = expected_measurement - getMeasurement();
-    while (er(2) > T( M_PI ))
-        er(2) -=   T( 2 * M_PI );
-    while (er(2) < T( -M_PI ))
-        er(2) +=   T( 2 * M_PI );
+    er(2) = pi2pi(er(2));
 
     // Residuals
     res = getMeasurementSquareRootInformationUpper() * er;
diff --git a/include/core/factor/factor_odom_2d_closeloop.h b/include/core/factor/factor_odom_2d_closeloop.h
index e7d35aefa48b549174d060668cbee540edb6e9e2..85a1785440a03a15a899117b619d3d07aaa3eb39 100644
--- a/include/core/factor/factor_odom_2d_closeloop.h
+++ b/include/core/factor/factor_odom_2d_closeloop.h
@@ -4,6 +4,7 @@
 //Wolf includes
 #include "core/factor/factor_autodiff.h"
 #include "core/frame/frame_base.h"
+#include "core/math/rotations.h"
 
 //#include "ceres/jet.h"
 
@@ -71,10 +72,7 @@ inline bool FactorOdom2dCloseloop::operator ()(const T* const _p1, const T* cons
 
     // Error
     er = expected_measurement - getMeasurement().cast<T>();
-    while (er(2) > T( M_PI ))
-        er(2) -=   T( 2 * M_PI );
-    while (er(2) < T( -M_PI ))
-        er(2) +=   T( 2 * M_PI );
+    er(2)=pi2pi(er(2));
 
     // Residuals
     res = getMeasurementSquareRootInformationUpper().cast<T>() * er;
diff --git a/include/core/factor/factor_pose_2d.h b/include/core/factor/factor_pose_2d.h
index ce63c806017117e67d18c2e79c34468e6b16a61c..f11b674e7775c78c0f0f306be69c85c832e328de 100644
--- a/include/core/factor/factor_pose_2d.h
+++ b/include/core/factor/factor_pose_2d.h
@@ -54,11 +54,7 @@ inline bool FactorPose2d::operator ()(const T* const _p, const T* const _o, T* _
     Eigen::Matrix<T,3,1> er;
     er(0) = meas(0) - _p[0];
     er(1) = meas(1) - _p[1];
-    er(2) = meas(2) - _o[0];
-    while (er[2] > T(M_PI))
-        er(2) = er(2) - T(2*M_PI);
-    while (er(2) <= T(-M_PI))
-        er(2) = er(2) + T(2*M_PI);
+    er(2) = pi2pi(meas(2) - _o[0]);
 
     // residual
     Eigen::Map<Eigen::Matrix<T,3,1>> res(_residuals);
diff --git a/include/core/factor/factor_relative_2d_analytic.h b/include/core/factor/factor_relative_2d_analytic.h
index 5a67da1ff6c0e17614902ebc3195dd8124ec0ed6..2dba90bc6d4ead00634f601bf232d4471860c418 100644
--- a/include/core/factor/factor_relative_2d_analytic.h
+++ b/include/core/factor/factor_relative_2d_analytic.h
@@ -4,6 +4,7 @@
 //Wolf includes
 #include "core/factor/factor_analytic.h"
 #include "core/landmark/landmark_base.h"
+#include "core/math/rotations.h"
 #include <Eigen/StdVector>
 
 namespace wolf {
@@ -19,11 +20,11 @@ class FactorRelative2dAnalytic : public FactorAnalytic
          **/
         FactorRelative2dAnalytic(const std::string& _tp,
                                      const FeatureBasePtr& _ftr_ptr,
-                                     const FrameBasePtr& _frame_ptr,
+                                     const FrameBasePtr& _frame_other_ptr,
                                      const ProcessorBasePtr& _processor_ptr,
                                      bool _apply_loss_function,
                                      FactorStatus _status = FAC_ACTIVE) :
-            FactorAnalytic(_tp, _frame_ptr, nullptr, nullptr, nullptr, _processor_ptr, _apply_loss_function, _status, _frame_ptr->getP(), _frame_ptr->getO(), _ftr_ptr->getFrame()->getP(), _ftr_ptr->getFrame()->getO())
+            FactorAnalytic(_tp, _frame_other_ptr, nullptr, nullptr, nullptr, _processor_ptr, _apply_loss_function, _status, _frame_other_ptr->getP(), _frame_other_ptr->getO(), _ftr_ptr->getFrame()->getP(), _ftr_ptr->getFrame()->getO())
         {
             //
         }
@@ -45,11 +46,11 @@ class FactorRelative2dAnalytic : public FactorAnalytic
          **/
         FactorRelative2dAnalytic(const std::string& _tp,
                                      const FeatureBasePtr& _ftr_ptr,
-                                     const LandmarkBasePtr& _landmark_ptr,
+                                     const LandmarkBasePtr& _landmark_other_ptr,
                                      const ProcessorBasePtr& _processor_ptr,
                                      bool _apply_loss_function,
                                      FactorStatus _status = FAC_ACTIVE) :
-            FactorAnalytic(_tp, nullptr, nullptr, nullptr, _landmark_ptr, _processor_ptr, _apply_loss_function, _status, _ftr_ptr->getFrame()->getP(), _ftr_ptr->getFrame()->getO(), _landmark_ptr->getP(), _landmark_ptr->getO())
+            FactorAnalytic(_tp, nullptr, nullptr, nullptr, _landmark_other_ptr, _processor_ptr, _apply_loss_function, _status, _ftr_ptr->getFrame()->getP(), _ftr_ptr->getFrame()->getO(), _landmark_other_ptr->getP(), _landmark_other_ptr->getO())
         {
             //
         }
@@ -113,10 +114,7 @@ inline Eigen::VectorXd FactorRelative2dAnalytic::evaluateResiduals(
     expected_measurement(2) = _st_vector[3](0) - _st_vector[1](0);
     // Residual
     residual = expected_measurement - getMeasurement();
-    while (residual(2) > M_PI)
-        residual(2) = residual(2) - 2 * M_PI;
-    while (residual(2) <= -M_PI)
-        residual(2) = residual(2) + 2 * M_PI;
+    residual(2) = pi2pi(residual(2));
     residual = getMeasurementSquareRootInformationUpper() * residual;
     return residual;
 }
diff --git a/include/core/factor/factor_relative_pose_2d_with_extrinsics.h b/include/core/factor/factor_relative_pose_2d_with_extrinsics.h
new file mode 100644
index 0000000000000000000000000000000000000000..be13135c9c7e3e0899ac2bb9043f4dd1b7b60ef2
--- /dev/null
+++ b/include/core/factor/factor_relative_pose_2d_with_extrinsics.h
@@ -0,0 +1,119 @@
+#ifndef FACTOR_RELATIVE_POSE_2D_WITH_EXTRINSICS_THETA_H_
+#define FACTOR_RELATIVE_POSE_2D_WITH_EXTRINSICS_THETA_H_
+
+//Wolf includes
+#include "core/factor/factor_autodiff.h"
+#include "core/frame/frame_base.h"
+#include "core/math/rotations.h"
+
+//#include "ceres/jet.h"
+
+namespace wolf {
+
+WOLF_PTR_TYPEDEFS(FactorRelativePose2dWithExtrinsics);
+
+//class
+class FactorRelativePose2dWithExtrinsics : public FactorAutodiff<FactorRelativePose2dWithExtrinsics, 3, 2, 1, 2, 1, 2, 1>
+{
+    public:
+        FactorRelativePose2dWithExtrinsics(const FeatureBasePtr& _ftr_ptr,
+                                           const FrameBasePtr& _frame_other_ptr,
+                                           const ProcessorBasePtr& _processor_ptr,
+                                           bool _apply_loss_function,
+                                           FactorStatus _status = FAC_ACTIVE) :
+             FactorAutodiff<FactorRelativePose2dWithExtrinsics, 3, 2, 1, 2, 1, 2, 1>("FactorRelativePose2dWithExtrinsics",
+                                                                                     _frame_other_ptr, nullptr, nullptr, nullptr,
+                                                                                     _processor_ptr,
+                                                                                     _apply_loss_function,
+                                                                                     _status,
+                                                                                     _frame_other_ptr->getP(),
+                                                                                     _frame_other_ptr->getO(),
+                                                                                     _ftr_ptr->getFrame()->getP(),
+                                                                                     _ftr_ptr->getFrame()->getO(),
+                                                                                     _ftr_ptr->getCapture()->getSensorP(),
+                                                                                     _ftr_ptr->getCapture()->getSensorO())
+        {
+            assert(_ftr_ptr->getCapture()->getSensorP() != nullptr && "No extrinsics found!");
+            assert(_ftr_ptr->getCapture()->getSensorO() != nullptr && "No extrinsics found!");
+            //
+        }
+
+        virtual ~FactorRelativePose2dWithExtrinsics() = default;
+
+        virtual std::string getTopology() const override
+        {
+            return std::string("MOTION");
+        }
+
+        template<typename T>
+        bool operator ()(const T* const _p1, const T* const _o1, const T* const _p2, const T* const _o2, const T* const _sp, const T* const _so,
+                         T* _residuals) const;
+};
+
+template<typename T>
+inline bool FactorRelativePose2dWithExtrinsics::operator ()(const T* const _p1, const T* const _o1, const T* const _p2,
+                                          const T* const _o2, const T* const _ps, const T* const _os, T* _residuals) const
+{
+
+    // MAPS
+    Eigen::Map<Eigen::Matrix<T,3,1> > res(_residuals);
+    Eigen::Map<const Eigen::Matrix<T,2,1> > p1(_p1);
+    Eigen::Map<const Eigen::Matrix<T,2,1> > p2(_p2);
+    Eigen::Map<const Eigen::Matrix<T,2,1> > ps(_ps);
+    T o1 = _o1[0];
+    T o2 = _o2[0];
+    T os = _os[0];
+
+    Eigen::Matrix<T, 3, 1> expected_measurement;
+    Eigen::Matrix<T, 3, 1> er; // error
+
+    // Expected measurement
+    // r1_p_r1_s1 = ps
+    // r2_p_r2_s2 = ps
+    // r1_R_s1 = R(os)
+    // r2_R_s2 = R(os)
+    // w_R_r1 = R(o1)
+    // w_R_r2 = R(o2)
+    // ----------------------------------------
+
+    // s1_p_s1_s2 = s1_R_r1*r1_p_s1_r1 +
+    //              s1_R_r1*r1_R_w*w_p_r1_w +
+    //              s1_R_r1*r1_R_w*w_p_w_r2 +
+    //              s1_R_r1*r1_R_w*w_R_r2*r2_p_r2_s2
+
+    // s1_p_s1_s2 = s1_R_r1*(r1_p_s1_r1 + r1_R_w*(w_p_r1_w + w_p_w_r2 + w_R_r2*r2_p_r2_s2))
+
+    expected_measurement.head(2) = Eigen::Rotation2D<T>(-os)*(-ps + Eigen::Rotation2D<T>(-o1)*(-p1 + p2 + Eigen::Rotation2D<T>(o2)*ps));
+    expected_measurement(2) = o2 - o1;
+
+    // Error
+    er = expected_measurement - getMeasurement().cast<T>();
+    er(2) = pi2pi(er(2));
+
+    // Residuals
+    res = getMeasurementSquareRootInformationUpper().cast<T>() * er;
+
+    ////////////////////////////////////////////////////////
+    // print Jacobian. Uncomment this as you wish (remember to uncomment #include "ceres/jet.h" above):
+//    using ceres::Jet;
+//    Eigen::MatrixXs J(3,6);
+//    J.row(0) = ((Jet<Scalar, 6>)(er(0))).v;
+//    J.row(1) = ((Jet<Scalar, 6>)(er(1))).v;
+//    J.row(2) = ((Jet<Scalar, 6>)(er(2))).v;
+//    J.row(0) = ((Jet<Scalar, 6>)(res(0))).v;
+//    J.row(1) = ((Jet<Scalar, 6>)(res(1))).v;
+//    J.row(2) = ((Jet<Scalar, 6>)(res(2))).v;
+//    if (sizeof(er(0)) != sizeof(double))
+//    {
+//        std::cout << "FactorRelativePose2dWithExtrinsics::Jacobian(c" << id() << ") = \n " << J << std::endl;
+//        std::cout << "FactorRelativePose2dWithExtrinsics::Weighted Jacobian(c" << id() << ") = \n " << J << std::endl;
+//        std::cout << "FactorRelativePose2dWithExtrinsics::Sqrt Info(c" << id() << ") = \n" << getMeasurementSquareRootInformationUpper() << std::endl;
+//    }
+    ////////////////////////////////////////////////////////
+
+    return true;
+}
+
+} // namespace wolf
+
+#endif
diff --git a/include/core/math/rotations.h b/include/core/math/rotations.h
index 7c2ede927a796adfd35e7dd9240e0128f1192c11..a0a86c6ee4c3ef095725a2df2230c2fdb8de751f 100644
--- a/include/core/math/rotations.h
+++ b/include/core/math/rotations.h
@@ -27,7 +27,7 @@ pi2pi(const T _angle)
     T angle = _angle;
     while (angle > T( M_PI ))
         angle -=   T( 2 * M_PI );
-    while (angle < T( -M_PI ))
+    while (angle <= T( -M_PI ))
         angle +=   T( 2 * M_PI );
 
     return angle;
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index 501965cdff67102f807e482f87ca7eafb7109e54..dc7cfae429f38b83a414229f78a70b01cc5ba53f 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -177,6 +177,10 @@ target_link_libraries(gtest_factor_block_difference ${PROJECT_NAME})
 wolf_add_gtest(gtest_factor_diff_drive gtest_factor_diff_drive.cpp)
 target_link_libraries(gtest_factor_diff_drive ${PROJECT_NAME})
 
+# FactorOdom2d class test
+wolf_add_gtest(gtest_factor_odom_2d gtest_factor_odom_2d.cpp)
+target_link_libraries(gtest_factor_odom_2d ${PROJECT_NAME})
+
 # FactorOdom3d class test
 wolf_add_gtest(gtest_factor_odom_3d gtest_factor_odom_3d.cpp)
 target_link_libraries(gtest_factor_odom_3d ${PROJECT_NAME})
@@ -189,6 +193,14 @@ target_link_libraries(gtest_factor_pose_2d ${PROJECT_NAME})
 wolf_add_gtest(gtest_factor_pose_3d gtest_factor_pose_3d.cpp)
 target_link_libraries(gtest_factor_pose_3d ${PROJECT_NAME})
 
+# FactorRelativePose2dWithExtrinsics class test
+wolf_add_gtest(gtest_factor_relative_pose_2d_with_extrinsics gtest_factor_relative_pose_2d_with_extrinsics.cpp)
+target_link_libraries(gtest_factor_relative_pose_2d_with_extrinsics ${PROJECT_NAME})
+
+# FactorRelative2dAnalytic class test
+wolf_add_gtest(gtest_factor_relative_2d_analytic gtest_factor_relative_2d_analytic.cpp)
+target_link_libraries(gtest_factor_relative_2d_analytic ${PROJECT_NAME})
+
 # MakePosDef function test
 wolf_add_gtest(gtest_make_posdef gtest_make_posdef.cpp)
 target_link_libraries(gtest_make_posdef ${PROJECT_NAME})
diff --git a/test/gtest_factor_odom_2d.cpp b/test/gtest_factor_odom_2d.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..76d559a767f962ef7e82c41bffdded012dd4d04e
--- /dev/null
+++ b/test/gtest_factor_odom_2d.cpp
@@ -0,0 +1,109 @@
+#include "core/utils/utils_gtest.h"
+
+#include "core/factor/factor_odom_2d.h"
+#include "core/capture/capture_odom_2d.h"
+#include "core/math/rotations.h"
+
+#include "core/ceres_wrapper/ceres_manager.h"
+
+using namespace Eigen;
+using namespace wolf;
+using std::cout;
+using std::endl;
+
+// Input odometry data and covariance
+Matrix3d data_cov = 0.1*Matrix3d::Identity();
+
+// Problem and solver
+ProblemPtr problem_ptr = Problem::create("PO", 2);
+CeresManager ceres_mgr(problem_ptr);
+
+// Two frames
+FrameBasePtr frm0 = problem_ptr->emplaceFrame(KEY, Vector3d::Zero(), TimeStamp(0));
+FrameBasePtr frm1 = problem_ptr->emplaceFrame(KEY, Vector3d::Zero(), TimeStamp(1));
+
+// Capture, feature and factor from frm1 to frm0
+auto cap1 = CaptureBase::emplace<CaptureOdom2d>(frm1, 1, nullptr, Vector3d::Zero(), data_cov);
+auto fea1 = FeatureBase::emplace<FeatureBase>(cap1, "FeatureOdom2d", Vector3d::Zero(), data_cov);
+auto fac1 = FactorBase::emplace<FactorOdom2d>(fea1, fea1, frm0, nullptr, false); // create and add
+
+TEST(FactorOdom2d, check_tree)
+{
+    ASSERT_TRUE(problem_ptr->check(0));
+}
+
+TEST(FactorOdom2d, fix_0_solve)
+{
+    for (int i = 0; i < 1e3; i++)
+    {
+        // Random delta
+        Vector3d delta = Vector3d::Random() * 10;
+        pi2pi(delta(2));
+
+        // Random initial pose
+        Vector3d x0 = Vector3d::Random() * 10;
+        pi2pi(x0(2));
+
+        // x1 groundtruth
+        Vector3d x1;
+        x1.head<2>() = x0.head<2>() + Rotation2Dd(x0(2)) * delta.head<2>();
+        x1(2) = pi2pi(x0(2) + delta(2));
+
+        // Set states and measurement
+        frm0->setState(x0);
+        frm1->setState(x1);
+        cap1->setData(delta);
+        fea1->setMeasurement(delta);
+
+        // Fix frm0, perturb frm1
+        frm0->fix();
+        frm1->unfix();
+        frm1->perturb(5);
+
+        // solve for frm1
+        std::string report = ceres_mgr.solve(SolverManager::ReportVerbosity::BRIEF);
+
+        ASSERT_POSE2d_APPROX(frm1->getState(), x1, 1e-6);
+    }
+}
+
+TEST(FactorOdom2d, fix_1_solve)
+{
+    for (int i = 0; i < 1e3; i++)
+    {
+        // Random delta
+        Vector3d delta = Vector3d::Random() * 10;
+        pi2pi(delta(2));
+
+        // Random initial pose
+        Vector3d x0 = Vector3d::Random() * 10;
+        pi2pi(x0(2));
+
+        // x1 groundtruth
+        Vector3d x1;
+        x1.head<2>() = x0.head<2>() + Rotation2Dd(x0(2)) * delta.head<2>();
+        x1(2) = pi2pi(x0(2) + delta(2));
+
+        // Set states and measurement
+        frm0->setState(x0);
+        frm1->setState(x1);
+        cap1->setData(delta);
+        fea1->setMeasurement(delta);
+
+        // Fix frm1, perturb frm0
+        frm1->fix();
+        frm0->unfix();
+        frm0->perturb(5);
+
+        // solve for frm0
+        std::string report = ceres_mgr.solve(SolverManager::ReportVerbosity::BRIEF);
+
+        ASSERT_POSE2d_APPROX(frm0->getState(), x0, 1e-6);
+    }
+}
+
+int main(int argc, char **argv)
+{
+  testing::InitGoogleTest(&argc, argv);
+  return RUN_ALL_TESTS();
+}
diff --git a/test/gtest_factor_relative_2d_analytic.cpp b/test/gtest_factor_relative_2d_analytic.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..fd8e2296c887aeb7ce6a07579eec939964d29a92
--- /dev/null
+++ b/test/gtest_factor_relative_2d_analytic.cpp
@@ -0,0 +1,109 @@
+#include "core/utils/utils_gtest.h"
+
+#include "core/factor/factor_relative_2d_analytic.h"
+#include "core/capture/capture_odom_2d.h"
+#include "core/math/rotations.h"
+
+#include "core/ceres_wrapper/ceres_manager.h"
+
+using namespace Eigen;
+using namespace wolf;
+using std::cout;
+using std::endl;
+
+// Input odometry data and covariance
+Matrix3d data_cov = 0.1*Matrix3d::Identity();
+
+// Problem and solver
+ProblemPtr problem_ptr = Problem::create("PO", 2);
+CeresManager ceres_mgr(problem_ptr);
+
+// Two frames
+FrameBasePtr frm0 = problem_ptr->emplaceFrame(KEY, Vector3d::Zero(), TimeStamp(0));
+FrameBasePtr frm1 = problem_ptr->emplaceFrame(KEY, Vector3d::Zero(), TimeStamp(1));
+
+// Capture, feature and factor from frm1 to frm0
+auto cap1 = CaptureBase::emplace<CaptureOdom2d>(frm1, 1, nullptr, Vector3d::Zero(), data_cov);
+auto fea1 = FeatureBase::emplace<FeatureBase>(cap1, "FeatureOdom2d", Vector3d::Zero(), data_cov);
+auto fac1 = FactorBase::emplace<FactorRelative2dAnalytic>(fea1, "odom2d", fea1, frm0, nullptr, false); // create and add
+
+TEST(FactorRelative2dAnalytic, check_tree)
+{
+    ASSERT_TRUE(problem_ptr->check(0));
+}
+
+TEST(FactorRelative2dAnalytic, fix_0_solve)
+{
+    for (int i = 0; i < 1e3; i++)
+    {
+        // Random delta
+        Vector3d delta = Vector3d::Random() * 10;
+        pi2pi(delta(2));
+
+        // Random initial pose
+        Vector3d x0 = Vector3d::Random() * 10;
+        pi2pi(x0(2));
+
+        // x1 groundtruth
+        Vector3d x1;
+        x1.head<2>() = x0.head<2>() + Rotation2Dd(x0(2)) * delta.head<2>();
+        x1(2) = pi2pi(x0(2) + delta(2));
+
+        // Set states and measurement
+        frm0->setState(x0);
+        frm1->setState(x1);
+        cap1->setData(delta);
+        fea1->setMeasurement(delta);
+
+        // Fix frm0, perturb frm1
+        frm0->fix();
+        frm1->unfix();
+        frm1->perturb(5);
+
+        // solve for frm1
+        std::string report = ceres_mgr.solve(SolverManager::ReportVerbosity::BRIEF);
+
+        ASSERT_POSE2d_APPROX(frm1->getState(), x1, 1e-6);
+    }
+}
+
+TEST(FactorRelative2dAnalytic, fix_1_solve)
+{
+    for (int i = 0; i < 1e3; i++)
+    {
+        // Random delta
+        Vector3d delta = Vector3d::Random() * 10;
+        pi2pi(delta(2));
+
+        // Random initial pose
+        Vector3d x0 = Vector3d::Random() * 10;
+        pi2pi(x0(2));
+
+        // x1 groundtruth
+        Vector3d x1;
+        x1.head<2>() = x0.head<2>() + Rotation2Dd(x0(2)) * delta.head<2>();
+        x1(2) = pi2pi(x0(2) + delta(2));
+
+        // Set states and measurement
+        frm0->setState(x0);
+        frm1->setState(x1);
+        cap1->setData(delta);
+        fea1->setMeasurement(delta);
+
+        // Fix frm1, perturb frm0
+        frm1->fix();
+        frm0->unfix();
+        frm0->perturb(5);
+
+        // solve for frm0
+        std::string report = ceres_mgr.solve(SolverManager::ReportVerbosity::BRIEF);
+
+        ASSERT_POSE2d_APPROX(frm0->getState(), x0, 1e-6);
+    }
+}
+
+int main(int argc, char **argv)
+{
+  testing::InitGoogleTest(&argc, argv);
+  return RUN_ALL_TESTS();
+}
diff --git a/test/gtest_factor_relative_pose_2d_with_extrinsics.cpp b/test/gtest_factor_relative_pose_2d_with_extrinsics.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..547b4c9b7402a9b17eb695ea0e555869fb748a58
--- /dev/null
+++ b/test/gtest_factor_relative_pose_2d_with_extrinsics.cpp
@@ -0,0 +1,219 @@
+#include "core/utils/utils_gtest.h"
+
+#include "core/factor/factor_relative_pose_2d_with_extrinsics.h"
+#include "core/capture/capture_odom_2d.h"
+#include "core/sensor/sensor_odom_2d.h"
+#include "core/processor/processor_odom_2d.h"
+#include "core/math/rotations.h"
+
+#include "core/ceres_wrapper/ceres_manager.h"
+
+using namespace Eigen;
+using namespace wolf;
+using std::cout;
+using std::endl;
+
+std::string wolf_root = _WOLF_ROOT_DIR;
+
+// Input odometry data and covariance
+Matrix3d data_cov = 0.1*Matrix3d::Identity();
+
+// Problem and solver
+ProblemPtr problem_ptr = Problem::create("PO", 2);
+CeresManager ceres_mgr(problem_ptr);
+
+// Sensor
+auto sensor_odom2d = problem_ptr->installSensor("SensorOdom2d", "odom", Vector3d(0,0,0), wolf_root + "/test/yaml/sensor_odom_2d.yaml");
+auto processor_odom2d = problem_ptr->installProcessor("ProcessorOdom2d", "odom", sensor_odom2d, std::make_shared<ProcessorParamsOdom2d>());
+
+// Two frames
+FrameBasePtr frm0 = problem_ptr->emplaceFrame(KEY, Vector3d::Zero(), TimeStamp(0));
+FrameBasePtr frm1 = problem_ptr->emplaceFrame(KEY, Vector3d::Zero(), TimeStamp(1));
+
+// Capture, feature and factor from frm1 to frm0
+auto cap1 = CaptureBase::emplace<CaptureOdom2d>(frm1, 1, sensor_odom2d, Vector3d::Zero(), data_cov);
+auto fea1 = FeatureBase::emplace<FeatureBase>(cap1, "FeatureOdom2d", Vector3d::Zero(), data_cov);
+auto fac1 = FactorBase::emplace<FactorRelativePose2dWithExtrinsics>(fea1, fea1, frm0, processor_odom2d, false); // create and add
+
+TEST(FactorRelativePose2dWithExtrinsics, check_tree)
+{
+    ASSERT_TRUE(problem_ptr->check(0));
+}
+
+TEST(FactorRelativePose2dWithExtrinsics, fix_0_solve)
+{
+    for (int i = 0; i < 1e3; i++)
+    {
+        // Random delta
+        Vector3d delta = Vector3d::Random() * 10;
+        pi2pi(delta(2));
+
+        // Random initial pose
+        Vector3d x0 = Vector3d::Random() * 10;
+        pi2pi(x0(2));
+
+        // Random extrinsics
+        Vector3d xs = Vector3d::Random();
+        pi2pi(xs(2));
+
+        // x1 groundtruth
+        Vector3d x1;
+        x1(2) = pi2pi(x0(2) + delta(2));
+        x1.head<2>() = x0.head<2>() + Rotation2Dd(x0(2)) * (xs.head<2>() + Rotation2Dd(xs(2)) * delta.head<2>()) - Rotation2Dd(x1(2)) * xs.head<2>();
+
+        // Set states and measurement
+        frm0->setState(x0);
+        frm1->setState(x1);
+        cap1->setData(delta);
+        fea1->setMeasurement(delta);
+        sensor_odom2d->setState(xs);
+
+        // Perturb frm1, fix the rest
+        frm0->fix();
+        frm1->unfix();
+        sensor_odom2d->getP()->fix();
+        sensor_odom2d->getO()->fix();
+        frm1->perturb(5);
+
+        // solve for frm1
+        std::string report = ceres_mgr.solve(SolverManager::ReportVerbosity::BRIEF);
+
+        ASSERT_POSE2d_APPROX(frm1->getState(), x1, 1e-6);
+    }
+}
+
+TEST(FactorRelativePose2dWithExtrinsics, fix_1_solve)
+{
+    for (int i = 0; i < 1e3; i++)
+    {
+        // Random delta
+        Vector3d delta = Vector3d::Random() * 10;
+        pi2pi(delta(2));
+
+        // Random initial pose
+        Vector3d x0 = Vector3d::Random() * 10;
+        pi2pi(x0(2));
+
+        // Random extrinsics
+        Vector3d xs = Vector3d::Random();
+        pi2pi(xs(2));
+
+        // x1 groundtruth
+        Vector3d x1;
+        x1(2) = pi2pi(x0(2) + delta(2));
+        x1.head<2>() = x0.head<2>() + Rotation2Dd(x0(2)) * (xs.head<2>() + Rotation2Dd(xs(2)) * delta.head<2>()) - Rotation2Dd(x1(2)) * xs.head<2>();
+
+        // Set states and measurement
+        frm0->setState(x0);
+        frm1->setState(x1);
+        cap1->setData(delta);
+        fea1->setMeasurement(delta);
+        sensor_odom2d->setState(xs);
+
+        // Perturb frm0, fix the rest
+        frm1->fix();
+        frm0->unfix();
+        sensor_odom2d->getP()->fix();
+        sensor_odom2d->getO()->fix();
+        frm0->perturb(5);
+
+        // solve for frm0
+        std::string report = ceres_mgr.solve(SolverManager::ReportVerbosity::BRIEF);
+
+        ASSERT_POSE2d_APPROX(frm0->getState(), x0, 1e-6);
+    }
+}
+
+TEST(FactorRelativePose2dWithExtrinsics, extrinsics_p_solve)
+{
+    for (int i = 0; i < 1e3; i++)
+    {
+        // Random delta
+        Vector3d delta = Vector3d::Random() * 10;
+        pi2pi(delta(2));
+
+        // Random initial pose
+        Vector3d x0 = Vector3d::Random() * 10;
+        pi2pi(x0(2));
+
+        // Random extrinsics
+        Vector3d xs = Vector3d::Random();
+        pi2pi(xs(2));
+
+        // x1 groundtruth
+        Vector3d x1;
+        x1(2) = pi2pi(x0(2) + delta(2));
+        x1.head<2>() = x0.head<2>() + Rotation2Dd(x0(2)) * (xs.head<2>() + Rotation2Dd(xs(2)) * delta.head<2>()) - Rotation2Dd(x1(2)) * xs.head<2>();
+
+        // Set states and measurement
+        frm0->setState(x0);
+        frm1->setState(x1);
+        cap1->setData(delta);
+        fea1->setMeasurement(delta);
+        sensor_odom2d->setState(xs);
+
+        // Perturb sensor P, fix the rest
+        frm1->fix();
+        frm0->fix();
+        sensor_odom2d->getP()->unfix();
+        sensor_odom2d->getO()->fix();
+        sensor_odom2d->getP()->perturb(1);
+
+        // solve for frm0
+        std::string report = ceres_mgr.solve(SolverManager::ReportVerbosity::BRIEF);
+
+        ASSERT_POSE2d_APPROX(sensor_odom2d->getState(), xs, 1e-6);
+    }
+}
+
+TEST(FactorRelativePose2dWithExtrinsics, extrinsics_o_solve)
+{
+    for (int i = 0; i < 1e3; i++)
+    {
+        // Random delta
+        Vector3d delta = Vector3d::Random() * 10;
+        pi2pi(delta(2));
+
+        // Random initial pose
+        Vector3d x0 = Vector3d::Random() * 10;
+        pi2pi(x0(2));
+
+        // Random extrinsics
+        Vector3d xs = Vector3d::Random();
+        pi2pi(xs(2));
+
+        // x1 groundtruth
+        Vector3d x1;
+        x1(2) = pi2pi(x0(2) + delta(2));
+        x1.head<2>() = x0.head<2>() + Rotation2Dd(x0(2)) * (xs.head<2>() + Rotation2Dd(xs(2)) * delta.head<2>()) - Rotation2Dd(x1(2)) * xs.head<2>();
+
+        // Set states and measurement
+        frm0->setState(x0);
+        frm1->setState(x1);
+        cap1->setData(delta);
+        fea1->setMeasurement(delta);
+        sensor_odom2d->setState(xs);
+
+        //std::cout << "Sensor O: " << sensor_odom2d->getO()->getState().transpose() << std::endl;
+
+        // Perturb sensor O, fix the rest
+        frm1->fix();
+        frm0->fix();
+        sensor_odom2d->getP()->fix();
+        sensor_odom2d->getO()->unfix();
+        sensor_odom2d->getO()->perturb(.2);
+
+        //std::cout << "Sensor O (perturbed): " << sensor_odom2d->getO()->getState().transpose() << std::endl;
+
+        // solve for frm0
+        std::string report = ceres_mgr.solve(SolverManager::ReportVerbosity::BRIEF);
+
+        ASSERT_POSE2d_APPROX(sensor_odom2d->getState(), xs, 1e-6);
+    }
+}
+
+int main(int argc, char **argv)
+{
+  testing::InitGoogleTest(&argc, argv);
+  return RUN_ALL_TESTS();
+}