diff --git a/sophus/se2.hpp b/sophus/se2.hpp index 60d4654c..f68ca5ef 100644 --- a/sophus/se2.hpp +++ b/sophus/se2.hpp @@ -6,7 +6,7 @@ #include "so2.hpp" namespace Sophus { -template +template class SE2; using SE2d = SE2; using SE2f = SE2; @@ -15,27 +15,33 @@ using SE2f = SE2; namespace Eigen { namespace internal { -template -struct traits> { +template +struct traits> { + static constexpr int Options = Options_; + static constexpr bool PreserveInvariant = PreserveInvariant_; using Scalar = Scalar_; using TranslationType = Sophus::Vector2; - using SO2Type = Sophus::SO2; + using SO2Type = Sophus::SO2; }; -template -struct traits, Options>> - : traits> { +template +struct traits< + Map, Options_>> + : traits> { using Scalar = Scalar_; - using TranslationType = Map, Options>; - using SO2Type = Map, Options>; + using TranslationType = Map, Options_>; + using SO2Type = + Map, Options_>; }; -template -struct traits const, Options>> - : traits const> { +template +struct traits< + Map const, Options_>> + : traits const> { using Scalar = Scalar_; - using TranslationType = Map const, Options>; - using SO2Type = Map const, Options>; + using TranslationType = Map const, Options_>; + using SO2Type = + Map const, Options_>; }; } // namespace internal } // namespace Eigen @@ -78,6 +84,8 @@ class SE2Base { using Hyperplane = Hyperplane2; using Tangent = Vector; using Adjoint = Matrix; + static constexpr bool PreserveInvariant = + Eigen::internal::traits::PreserveInvariant; /// For binary operations the return type is determined with the /// ScalarBinaryOpTraits feature of Eigen. This allows mixing concrete and Map @@ -88,7 +96,9 @@ class SE2Base { Scalar, typename OtherDerived::Scalar>::ReturnType; template - using SE2Product = SE2>; + using SE2Product = + SE2, Eigen::internal::traits::Options, + PreserveInvariant && OtherDerived::PreserveInvariant>; template using PointProduct = Vector2>; @@ -115,9 +125,12 @@ class SE2Base { /// Returns copy of instance casted to NewScalarType. /// template - SOPHUS_FUNC SE2 cast() const { - return SE2(so2().template cast(), - translation().template cast()); + SOPHUS_FUNC SE2::Options, + PreserveInvariant> + cast() const { + return SE2::Options, + PreserveInvariant>(so2().template cast(), + translation().template cast()); } /// Returns derivative of this * exp(x) wrt x at x=0. @@ -156,9 +169,12 @@ class SE2Base { /// Returns group inverse. /// - SOPHUS_FUNC SE2 inverse() const { - SO2 const invR = so2().inverse(); - return SE2(invR, invR * (translation() * Scalar(-1))); + SOPHUS_FUNC + SE2::Options, PreserveInvariant> + inverse() const { + auto const invR = so2().inverse(); + return SE2::Options, + PreserveInvariant>(invR, invR * (translation() * Scalar(-1))); } /// Logarithmic map @@ -388,12 +404,13 @@ class SE2Base { }; /// SE2 using default storage; derived from SE2Base. -template -class SE2 : public SE2Base> { +template +class SE2 : public SE2Base> { public: - using Base = SE2Base>; + using Base = SE2Base>; static int constexpr DoF = Base::DoF; static int constexpr num_parameters = Base::num_parameters; + static constexpr bool PreserveInvariant = PreserveInvariant_; using Scalar = Scalar_; using Transformation = typename Base::Transformation; @@ -401,7 +418,7 @@ class SE2 : public SE2Base> { using HomogeneousPoint = typename Base::HomogeneousPoint; using Tangent = typename Base::Tangent; using Adjoint = typename Base::Adjoint; - using SO2Member = SO2; + using SO2Member = SO2; using TranslationMember = Vector2; using Base::operator=; @@ -448,7 +465,7 @@ class SE2 : public SE2Base> { /// of 1. /// SOPHUS_FUNC - SE2(typename SO2::Transformation const& rotation_matrix, + SE2(typename SO2Member::Transformation const& rotation_matrix, Point const& translation) : so2_(rotation_matrix), translation_(translation) {} @@ -606,9 +623,10 @@ class SE2 : public SE2Base> { /// ``expmat(.)`` being the matrix exponential and ``hat(.)`` the hat-operator /// of SE(2), see below. /// - SOPHUS_FUNC static SE2 exp(Tangent const& a) { + SOPHUS_FUNC static SE2 exp( + Tangent const& a) { Scalar theta = a[2]; - SO2 so2 = SO2::exp(theta); + SO2Member so2 = SO2Member::exp(theta); Scalar sin_theta_by_theta; Scalar one_minus_cos_theta_by_theta; using std::abs; @@ -626,16 +644,18 @@ class SE2 : public SE2Base> { Vector2 trans( sin_theta_by_theta * a[0] - one_minus_cos_theta_by_theta * a[1], one_minus_cos_theta_by_theta * a[0] + sin_theta_by_theta * a[1]); - return SE2(so2, trans); + return SE2(so2, trans); } /// Returns closest SE3 given arbitrary 4x4 matrix. /// template - static SOPHUS_FUNC std::enable_if_t::value, SE2> + static SOPHUS_FUNC std::enable_if_t::value, + SE2> fitToSE2(Matrix3 const& T) { - return SE2(SO2::fitToSO2(T.template block<2, 2>(0, 0)), - T.template block<2, 1>(0, 2)); + return SE2( + SO2Member::fitToSO2(T.template block<2, 2>(0, 0)), + T.template block<2, 1>(0, 2)); } /// Returns the ith infinitesimal generators of SE(2). @@ -709,7 +729,7 @@ class SE2 : public SE2Base> { /// Construct pure rotation. /// static SOPHUS_FUNC SE2 rot(Scalar const& x) { - return SE2(SO2(x), Sophus::Vector2::Zero()); + return SE2(SO2Member(x), Sophus::Vector2::Zero()); } /// Draw uniform sample from SE(2) manifold. @@ -719,7 +739,7 @@ class SE2 : public SE2Base> { template static SE2 sampleUniform(UniformRandomBitGenerator& generator) { std::uniform_real_distribution uniform(Scalar(-1), Scalar(1)); - return SE2(SO2::sampleUniform(generator), + return SE2(SO2Member::sampleUniform(generator), Vector2(uniform(generator), uniform(generator))); } @@ -727,11 +747,11 @@ class SE2 : public SE2Base> { /// template static SOPHUS_FUNC SE2 trans(T0 const& x, T1 const& y) { - return SE2(SO2(), Vector2(x, y)); + return SE2(SO2Member(), Vector2(x, y)); } static SOPHUS_FUNC SE2 trans(Vector2 const& xy) { - return SE2(SO2(), xy); + return SE2(SO2Member(), xy); } /// Construct x-axis translation. @@ -774,8 +794,8 @@ class SE2 : public SE2Base> { TranslationMember translation_; }; -template -SOPHUS_FUNC SE2::SE2() +template +SOPHUS_FUNC SE2::SE2() : translation_(TranslationMember::Zero()) { static_assert(std::is_standard_layout::value, "Assume standard layout for the use of offset of check below."); @@ -795,11 +815,13 @@ namespace Eigen { /// Specialization of Eigen::Map for ``SE2``; derived from SE2Base. /// /// Allows us to wrap SE2 objects around POD array. -template -class Map, Options> - : public Sophus::SE2Base, Options>> { +template +class Map, Options> + : public Sophus::SE2Base< + Map, Options>> { public: - using Base = Sophus::SE2Base, Options>>; + using Base = Sophus::SE2Base< + Map, Options>>; using Scalar = Scalar_; using Transformation = typename Base::Transformation; using Point = typename Base::Point; @@ -814,15 +836,22 @@ class Map, Options> SOPHUS_FUNC explicit Map(Scalar* coeffs) : so2_(coeffs), - translation_(coeffs + Sophus::SO2::num_parameters) {} + translation_(coeffs + Sophus::SO2::num_parameters) { + } /// Mutator of SO3 /// - SOPHUS_FUNC Map, Options>& so2() { return so2_; } + SOPHUS_FUNC + Map, Options>& so2() { + return so2_; + } /// Accessor of SO3 /// - SOPHUS_FUNC Map, Options> const& so2() const { + SOPHUS_FUNC + Map, Options> const& so2() + const { return so2_; } @@ -839,18 +868,21 @@ class Map, Options> } protected: - Map, Options> so2_; + Map, Options> so2_; Map, Options> translation_; }; /// Specialization of Eigen::Map for ``SE2 const``; derived from SE2Base. /// /// Allows us to wrap SE2 objects around POD array. -template -class Map const, Options> - : public Sophus::SE2Base const, Options>> { +template +class Map const, Options> + : public Sophus::SE2Base< + Map const, + Options>> { public: - using Base = Sophus::SE2Base const, Options>>; + using Base = Sophus::SE2Base< + Map const, Options>>; using Scalar = Scalar_; using Transformation = typename Base::Transformation; using Point = typename Base::Point; @@ -863,23 +895,28 @@ class Map const, Options> SOPHUS_FUNC explicit Map(Scalar const* coeffs) : so2_(coeffs), - translation_(coeffs + Sophus::SO2::num_parameters) {} + translation_(coeffs + Sophus::SO2::num_parameters) { + } /// Accessor of SO3 /// - SOPHUS_FUNC Map const, Options> const& so2() const { + SOPHUS_FUNC + Map const, Options> const& + so2() const { return so2_; } /// Accessor of translation vector /// - SOPHUS_FUNC Map const, Options> const& translation() - const { + SOPHUS_FUNC + Map const, Options> const& translation() const { return translation_; } protected: - Map const, Options> const so2_; + Map const, Options> const + so2_; Map const, Options> const translation_; }; } // namespace Eigen diff --git a/sophus/so2.hpp b/sophus/so2.hpp index 8c05916a..8bfd5a80 100644 --- a/sophus/so2.hpp +++ b/sophus/so2.hpp @@ -13,7 +13,7 @@ #include "types.hpp" namespace Sophus { -template +template class SO2; using SO2d = SO2; using SO2f = SO2; @@ -22,25 +22,30 @@ using SO2f = SO2; namespace Eigen { namespace internal { -template -struct traits> { +template +struct traits> { static constexpr int Options = Options_; + static constexpr bool PreserveInvariant = PreserveInvariant_; using Scalar = Scalar_; using ComplexType = Sophus::Vector2; }; -template -struct traits, Options_>> - : traits> { +template +struct traits< + Map, Options_>> + : traits> { static constexpr int Options = Options_; + static constexpr bool PreserveInvariant = PreserveInvariant_; using Scalar = Scalar_; using ComplexType = Map, Options>; }; -template -struct traits const, Options_>> - : traits const> { +template +struct traits< + Map const, Options_>> + : traits const> { static constexpr int Options = Options_; + static constexpr bool PreserveInvariant = PreserveInvariant_; using Scalar = Scalar_; using ComplexType = Map const, Options>; }; @@ -98,6 +103,10 @@ class SO2Base { using Hyperplane = Hyperplane2; using Tangent = Scalar; using Adjoint = Scalar; + static constexpr bool PreserveInvariant = + Eigen::internal::traits::PreserveInvariant; + static constexpr auto Normalize = std::bool_constant{}; + static constexpr auto DontNormalize = std::bool_constant{}; /// For binary operations the return type is determined with the /// ScalarBinaryOpTraits feature of Eigen. This allows mixing concrete and Map @@ -108,7 +117,8 @@ class SO2Base { Scalar, typename OtherDerived::Scalar>::ReturnType; template - using SO2Product = SO2>; + using SO2Product = SO2, Options, + PreserveInvariant && OtherDerived::PreserveInvariant>; template using PointProduct = Vector2>; @@ -129,8 +139,16 @@ class SO2Base { /// Returns copy of instance casted to NewScalarType. /// template - SOPHUS_FUNC SO2 cast() const { - return SO2(unit_complex().template cast()); + SOPHUS_FUNC SO2 cast() const { + return SO2( + unit_complex().template cast()); + } + + /// Cast to SO2 type with different invariant-preserving property + /// + template + SOPHUS_FUNC operator SO2() const { + return SO2(unit_complex()); } /// This provides unsafe read/write access to internal data. SO(2) is @@ -146,8 +164,10 @@ class SO2Base { /// Returns group inverse. /// - SOPHUS_FUNC SO2 inverse() const { - return SO2(unit_complex().x(), -unit_complex().y()); + SOPHUS_FUNC SO2 inverse() const { + // Conjugation preserves the norm, thus no re-normalization is necessary + return SO2( + unit_complex().x(), -unit_complex().y(), DontNormalize); } /// Logarithmic map @@ -209,6 +229,7 @@ class SO2Base { template SOPHUS_FUNC SO2Product operator*( SO2Base const& other) const { + using Product = SO2Product; using ResultT = ReturnScalar; Scalar const lhs_real = unit_complex().x(); Scalar const lhs_imag = unit_complex().y(); @@ -218,20 +239,30 @@ class SO2Base { ResultT const result_real = lhs_real * rhs_real - lhs_imag * rhs_imag; ResultT const result_imag = lhs_real * rhs_imag + lhs_imag * rhs_real; - ResultT const squared_norm = - result_real * result_real + result_imag * result_imag; - // We can assume that the squared-norm is close to 1 since we deal with a - // unit complex number. Due to numerical precision issues, there might - // be a small drift after pose concatenation. Hence, we need to renormalizes - // the complex number here. - // Since squared-norm is close to 1, we do not need to calculate the costly - // square-root, but can use an approximation around 1 (see - // http://stackoverflow.com/a/12934750 for details). - if (squared_norm != ResultT(1.0)) { - ResultT const scale = ResultT(2.0) / (ResultT(1.0) + squared_norm); - return SO2Product(result_real * scale, result_imag * scale); + if constexpr (Product::PreserveInvariant) { + ResultT const squared_norm = + result_real * result_real + result_imag * result_imag; + // We can assume that the squared-norm is close to 1 since we deal with + // a unit complex number. Due to numerical precision issues, there might + // be a small drift after pose concatenation. Hence, we need to + // renormalize the complex number here. Since squared-norm is close to + // 1, we do not need to calculate the costly square-root, but can use an + // approximation around 1 (see http://stackoverflow.com/a/12934750 for + // details). + if (squared_norm != ResultT(1.0)) { + ResultT const scale = ResultT(2.0) / (ResultT(1.0) + squared_norm); + ResultT const scaled_real = result_real * scale; + ResultT const scaled_imag = result_imag * scale; + ResultT const scaled_squared_norm = + scaled_real * scaled_real + scaled_imag * scaled_imag; + if (scaled_squared_norm != ResultT(1.0)) { + return Product(scaled_real, scaled_imag, Normalize); + } + return Product(scaled_real, scaled_imag, DontNormalize); + } + return Product(result_real, result_imag, DontNormalize); } - return SO2Product(result_real, result_imag); + return Product(result_real, result_imag, DontNormalize); } /// Group action on 2-points. @@ -334,7 +365,9 @@ class SO2Base { /// SOPHUS_FUNC void setComplex(Point const& complex) { unit_complex_nonconst() = complex; - normalize(); + if constexpr (PreserveInvariant) { + normalize(); + } } /// Accessor of unit quaternion. @@ -355,12 +388,13 @@ class SO2Base { }; /// SO2 using default storage; derived from SO2Base. -template -class SO2 : public SO2Base> { +template +class SO2 : public SO2Base> { public: - using Base = SO2Base>; + using Base = SO2Base>; static int constexpr DoF = Base::DoF; static int constexpr num_parameters = Base::num_parameters; + static constexpr bool PreserveInvariant = PreserveInvariant_; using Scalar = Scalar_; using Transformation = typename Base::Transformation; @@ -371,7 +405,7 @@ class SO2 : public SO2Base> { using ComplexMember = Vector2; /// ``Base`` is friend so unit_complex_nonconst can be accessed from ``Base``. - friend class SO2Base>; + friend class SO2Base>; using Base::operator=; @@ -394,7 +428,11 @@ class SO2 : public SO2Base> { /// template SOPHUS_FUNC SO2(SO2Base const& other) - : unit_complex_(other.unit_complex()) {} + : unit_complex_(other.unit_complex()) { + if constexpr (PreserveInvariant && !OtherDerived::PreserveInvariant) { + Base::normalize(); + } + } /// Constructor from rotation matrix /// @@ -410,15 +448,39 @@ class SO2 : public SO2Base> { /// Constructor from pair of real and imaginary number. /// + /// Normalization is performed based on PreserveInvariant + /// /// Precondition: The pair must not be close to zero. /// SOPHUS_FUNC SO2(Scalar const& real, Scalar const& imag) : unit_complex_(real, imag) { - Base::normalize(); + if (PreserveInvariant) { + Base::normalize(); + } + } + + /// Constructor from pair of real and imaginary number with optional + /// normalization + /// + /// Normalization is performed based on normalize flag + /// SO2Base::Normalize and SO2Base::DontNormalize constants can be used as a + /// shorthand for calling this constructor + /// + /// Precondition: The pair must not be close to zero. + /// + template + SO2(Scalar const& real, Scalar const& imag, + const std::bool_constant) + : unit_complex_(real, imag) { + if (normalize) { + Base::normalize(); + } } /// Constructor from 2-vector. /// + /// Normalization is performed based on PreserveInvariant + /// /// Precondition: The vector must not be close to zero. /// template @@ -426,13 +488,16 @@ class SO2 : public SO2Base> { : unit_complex_(complex) { static_assert(std::is_same::value, "must be same Scalar type"); - Base::normalize(); + if constexpr (PreserveInvariant) { + Base::normalize(); + } } /// Constructor from an rotation angle. /// SOPHUS_FUNC explicit SO2(Scalar theta) { - unit_complex_nonconst() = SO2::exp(theta).unit_complex(); + unit_complex_nonconst() = + SO2::exp(theta).unit_complex(); } /// Accessor of unit complex number @@ -450,10 +515,12 @@ class SO2 : public SO2Base> { /// with ``expmat(.)`` being the matrix exponential and ``hat(.)`` being the /// hat()-operator of SO(2). /// - SOPHUS_FUNC static SO2 exp(Tangent const& theta) { + SOPHUS_FUNC static SO2 exp( + Tangent const& theta) { using std::cos; using std::sin; - return SO2(cos(theta), sin(theta)); + return SO2(cos(theta), sin(theta), + Base::DontNormalize); } /// Returns derivative of exp(x) wrt. x. @@ -578,12 +645,15 @@ namespace Eigen { /// /// Allows us to wrap SO2 objects around POD array (e.g. external c style /// complex number / tuple). -template -class Map, Options> - : public Sophus::SO2Base, Options>> { +template +class Map, Options> + : public Sophus::SO2Base< + Map, Options>> { public: - using Base = Sophus::SO2Base, Options>>; + using Base = Sophus::SO2Base< + Map, Options>>; using Scalar = Scalar_; + static constexpr bool PreserveInvariant = PreserveInvariant_; using Transformation = typename Base::Transformation; using Point = typename Base::Point; @@ -592,7 +662,8 @@ class Map, Options> using Adjoint = typename Base::Adjoint; /// ``Base`` is friend so unit_complex_nonconst can be accessed from ``Base``. - friend class Sophus::SO2Base, Options>>; + friend class Sophus::SO2Base< + Map, Options>>; using Base::operator=; using Base::operator*=; @@ -623,12 +694,16 @@ class Map, Options> /// /// Allows us to wrap SO2 objects around POD array (e.g. external c style /// complex number / tuple). -template -class Map const, Options> - : public Sophus::SO2Base const, Options>> { +template +class Map const, Options> + : public Sophus::SO2Base< + Map const, + Options>> { public: - using Base = Sophus::SO2Base const, Options>>; + using Base = Sophus::SO2Base< + Map const, Options>>; using Scalar = Scalar_; + static constexpr bool PreserveInvariant = PreserveInvariant_; using Transformation = typename Base::Transformation; using Point = typename Base::Point; using HomogeneousPoint = typename Base::HomogeneousPoint; @@ -642,8 +717,8 @@ class Map const, Options> /// Accessor of unit complex number. /// - SOPHUS_FUNC Map const, Options> const& unit_complex() - const { + SOPHUS_FUNC + Map const, Options> const& unit_complex() const { return unit_complex_; } diff --git a/test/core/test_se2.cpp b/test/core/test_se2.cpp index c0e5d8f6..8d6705e7 100644 --- a/test/core/test_se2.cpp +++ b/test/core/test_se2.cpp @@ -9,12 +9,16 @@ namespace Eigen { template class Map>; template class Map const>; +template class Map>; +template class Map const>; } // namespace Eigen namespace Sophus { template class SE2; template class SE2; +template class SE2; +template class SE2; #if SOPHUS_CERES template class SE2>; #endif @@ -72,6 +76,7 @@ class Tests { passed &= testRawDataAcces(); passed &= testMutatingAccessors(); passed &= testConstructors(); + passed &= testNormalizationModes(); passed &= testFit(); processTestResult(passed); } @@ -216,6 +221,72 @@ class Tests { return passed; } + bool testNormalizationModes() { + bool passed = true; + // Proactively preserving SO2 norm invariant (default) + using SE2N = SE2; + // Non-enforcing norm invariant + using SE2U = SE2; + using SO2U = SO2; + + SE2U unnormalized(SO2U(Scalar(2), Scalar(0), SO2U::DontNormalize), + Point(Scalar(3), Scalar(4))); + SE2N normalized_set_complex; + SE2U unnormalized_set_complex; + normalized_set_complex.so2().setComplex(Point(Scalar(2), Scalar(0))); + unnormalized_set_complex.so2().setComplex(Point(Scalar(2), Scalar(0))); + SE2N normalized = unnormalized; + + SOPHUS_TEST_APPROX(passed, normalized.so2().unit_complex().norm(), + Scalar(1), Constants::epsilon(), ""); + SOPHUS_TEST_APPROX(passed, + normalized_set_complex.so2().unit_complex().norm(), + Scalar(1), Constants::epsilon(), ""); + SOPHUS_TEST_EQUAL(passed, unnormalized_set_complex.so2().unit_complex().x(), + Scalar(2), + "Unnormalized SE2 setComplex preserves magnitude"); + + unnormalized.normalize(); + SOPHUS_TEST_APPROX(passed, unnormalized.so2().unit_complex().norm(), + Scalar(1), Constants::epsilon(), ""); + + SE2U inverse = SE2U(SO2U(Scalar(2), Scalar(0), SO2U::DontNormalize), + Point(Scalar(1), Scalar(0))) + .inverse(); + SE2N normalized_inverse = + SE2N(SO2U(Scalar(2), Scalar(0), SO2U::DontNormalize), + Point(Scalar(1), Scalar(0))) + .inverse(); + SOPHUS_TEST_APPROX(passed, normalized_inverse.so2().unit_complex().norm(), + Scalar(1), Constants::epsilon(), ""); + SOPHUS_TEST_EQUAL(passed, inverse.so2().unit_complex().x(), Scalar(2), + "Unnormalized inverse preserves rotation magnitude"); + + static_assert( + std::is_same_v, SE2N>); + static_assert( + std::is_same_v, SE2U>); + static_assert( + std::is_same_v, SE2U>); + static_assert( + std::is_same_v, SE2U>); + + SE2N normalized_product = + SE2N::trans(Scalar(0), Scalar(0)) * + SE2N(SO2U(Scalar(2), Scalar(0), SO2U::DontNormalize), + Point(Scalar(0), Scalar(0))); + SE2U unnormalized_product = + SE2N::trans(Scalar(0), Scalar(0)) * + SE2U(SO2U(Scalar(2), Scalar(0), SO2U::DontNormalize), + Point(Scalar(0), Scalar(0))); + SOPHUS_TEST_APPROX(passed, normalized_product.so2().unit_complex().norm(), + Scalar(1), Constants::epsilon(), ""); + SOPHUS_TEST_APPROX(passed, unnormalized_product.so2().unit_complex().norm(), + Scalar(2), Constants::epsilon(), ""); + + return passed; + } + template std::enable_if_t::value, bool> testFit() { bool passed = true; diff --git a/test/core/test_so2.cpp b/test/core/test_so2.cpp index 9039c3a6..03c92393 100644 --- a/test/core/test_so2.cpp +++ b/test/core/test_so2.cpp @@ -8,12 +8,16 @@ namespace Eigen { template class Map>; template class Map const>; +template class Map>; +template class Map const>; } // namespace Eigen namespace Sophus { template class SO2; template class SO2; +template class SO2; +template class SO2; #if SOPHUS_CERES template class SO2>; #endif @@ -53,6 +57,7 @@ class Tests { passed &= testUnity(); passed &= testRawDataAcces(); passed &= testConstructors(); + passed &= testNormalizationModes(); passed &= testFit(); processTestResult(passed); } @@ -144,6 +149,70 @@ class Tests { return passed; } + bool testNormalizationModes() { + bool passed = true; + // Proactively preserving norm invariant (default) + using SO2N = SO2; + // Non-enforcing norm invariant + using SO2U = SO2; + + Vector2 diverged(Scalar(2), Scalar(0)); + SO2N normalized(diverged); + SO2U unnormalized(diverged); + SO2N normalized_set_complex; + SO2U unnormalized_set_complex; + normalized_set_complex.setComplex(diverged); + unnormalized_set_complex.setComplex(diverged); + + SOPHUS_TEST_APPROX(passed, normalized.unit_complex().norm(), Scalar(1), + Constants::epsilon(), ""); + SOPHUS_TEST_EQUAL(passed, unnormalized.unit_complex().x(), diverged.x(), + "Unnormalized constructor preserves real part"); + SOPHUS_TEST_EQUAL(passed, unnormalized.unit_complex().y(), diverged.y(), + "Unnormalized constructor preserves imaginary part"); + SOPHUS_TEST_APPROX(passed, normalized_set_complex.unit_complex().norm(), + Scalar(1), Constants::epsilon(), ""); + SOPHUS_TEST_EQUAL(passed, unnormalized_set_complex.unit_complex().x(), + diverged.x(), + "Unnormalized setComplex preserves real part"); + SOPHUS_TEST_EQUAL(passed, unnormalized_set_complex.unit_complex().y(), + diverged.y(), + "Unnormalized setComplex preserves imaginary part"); + + SO2N normalized_from_unnormalized = unnormalized; + SOPHUS_TEST_APPROX(passed, + normalized_from_unnormalized.unit_complex().norm(), + Scalar(1), Constants::epsilon(), + "Copy to normalized performs normalization"); + + unnormalized.normalize(); + SOPHUS_TEST_APPROX(passed, unnormalized.unit_complex().norm(), Scalar(1), + Constants::epsilon(), ""); + + static_assert( + std::is_same_v, SO2N>); + static_assert( + std::is_same_v, SO2U>); + static_assert( + std::is_same_v, SO2U>); + static_assert( + std::is_same_v, SO2U>); + + SO2N normalized_product = + SO2N(Scalar(2), Scalar(0)) * normalized_from_unnormalized; + SO2U unnormalized_product = + SO2U(Scalar(2), Scalar(0), SO2U::DontNormalize) * + SO2U(Scalar(1), Scalar(0), SO2U::DontNormalize); + SOPHUS_TEST_APPROX(passed, normalized_product.unit_complex().norm(), + Scalar(1), Constants::epsilon(), ""); + SOPHUS_TEST_EQUAL(passed, unnormalized_product.unit_complex().x(), + Scalar(2), ""); + SOPHUS_TEST_EQUAL(passed, unnormalized_product.unit_complex().y(), + Scalar(0), ""); + + return passed; + } + template std::enable_if_t::value, bool> testFit() { bool passed = true;