diff --git a/src/core/include/mp-units/cartesian_tensor.h b/src/core/include/mp-units/cartesian_tensor.h index 08b065a2f..af7eb80c5 100644 --- a/src/core/include/mp-units/cartesian_tensor.h +++ b/src/core/include/mp-units/cartesian_tensor.h @@ -351,10 +351,25 @@ MP_UNITS_EXPORT template requires requires(const T& t, const U& u) { t * u; } [[nodiscard]] constexpr auto tensor_product(const cartesian_vector& lhs, const cartesian_vector& rhs) { - ::mp_units::cartesian_tensor res; - for (std::size_t i = 0; i < N; ++i) - for (std::size_t j = 0; j < N; ++j) res._data_[i * N + j] = lhs[i] * rhs[j]; - return res; + return ::mp_units::detail::cartesian_tensor_from(std::make_index_sequence{}, + [&](std::size_t idx) { return lhs[idx / N] * rhs[idx % N]; }); +} + +// Explicit conversions between dimensions (there is no implicit cross-dimension conversion). `embed` +// places the 2×2 tensor in the top-left block of a 3×3, zero-filling the new row and column; +// `project` keeps that top-left 2×2 block. The zero is the additive identity derived from a component +// (`x - x`), so no value-initialization of `T` is required. +MP_UNITS_EXPORT template +[[nodiscard]] constexpr cartesian_tensor embed(const cartesian_tensor& t) +{ + const auto z = t(0, 0) - t(0, 0); + return cartesian_tensor{t(0, 0), t(0, 1), z, t(1, 0), t(1, 1), z, z, z, z}; +} + +MP_UNITS_EXPORT template +[[nodiscard]] constexpr cartesian_tensor project(const cartesian_tensor& t) +{ + return cartesian_tensor{t(0, 0), t(0, 1), t(1, 0), t(1, 1)}; } } // namespace mp_units diff --git a/src/core/include/mp-units/cartesian_vector.h b/src/core/include/mp-units/cartesian_vector.h index 856e5b406..99b177b1f 100644 --- a/src/core/include/mp-units/cartesian_vector.h +++ b/src/core/include/mp-units/cartesian_vector.h @@ -400,6 +400,22 @@ template requires requires { typename std::common_type_t; } cartesian_vector(Arg, Args...) -> cartesian_vector, 1 + sizeof...(Args)>; +// Explicit conversions between dimensions (there is no implicit cross-dimension conversion). `embed` +// is the canonical inclusion of the plane into space, zero-filling the new coordinate; `project` is +// the canonical projection onto the plane, dropping the last coordinate. The zero is the additive +// identity derived from a component (`x - x`), so no value-initialization of `T` is required. +MP_UNITS_EXPORT template +[[nodiscard]] constexpr cartesian_vector embed(const cartesian_vector& v) +{ + return cartesian_vector{v[0], v[1], v[0] - v[0]}; +} + +MP_UNITS_EXPORT template +[[nodiscard]] constexpr cartesian_vector project(const cartesian_vector& v) +{ + return cartesian_vector{v[0], v[1]}; +} + } // namespace mp_units template diff --git a/test/runtime/cartesian_tensor_test.cpp b/test/runtime/cartesian_tensor_test.cpp index 95d0cf0fc..3a7a757af 100644 --- a/test/runtime/cartesian_tensor_test.cpp +++ b/test/runtime/cartesian_tensor_test.cpp @@ -403,3 +403,45 @@ TEST_CASE("cartesian_tensor with a complex representation", "[tensor][complex]") REQUIRE(inner_product(id, v) == v); // I . v == v } } + +namespace { +template +concept tensor_embeddable = requires(T t) { embed(t); }; +template +concept tensor_projectable = requires(T t) { project(t); }; +} // namespace +// embed only lifts 2x2->3x3 and project only lowers 3x3->2x2 (each defined for one source dimension) +static_assert(tensor_embeddable> && !tensor_embeddable>); +static_assert(tensor_projectable> && !tensor_projectable>); + +TEST_CASE("cartesian_tensor embed/project between 2x2 and 3x3", "[tensor]") +{ + SECTION("embed places the 2x2 in the top-left block, zero elsewhere") + { + cartesian_tensor t3 = embed(cartesian_tensor{1.0, 2.0, 3.0, 4.0}); // [[1, 2], [3, 4]] + static_assert(std::is_same_v>); + REQUIRE(t3(0, 0) == 1.0); + REQUIRE(t3(0, 1) == 2.0); + REQUIRE(t3(1, 0) == 3.0); + REQUIRE(t3(1, 1) == 4.0); + REQUIRE(t3(0, 2) == 0.0); + REQUIRE(t3(2, 0) == 0.0); + REQUIRE(t3(2, 2) == 0.0); + } + + SECTION("project keeps the top-left 2x2 block") + { + cartesian_tensor t2 = project(cartesian_tensor{1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0}); + static_assert(std::is_same_v>); + REQUIRE(t2(0, 0) == 1.0); + REQUIRE(t2(0, 1) == 2.0); + REQUIRE(t2(1, 0) == 4.0); + REQUIRE(t2(1, 1) == 5.0); + } + + SECTION("project . embed is the identity on 2x2") + { + cartesian_tensor t2{1.0, 2.0, 3.0, 4.0}; + REQUIRE(project(embed(t2)) == t2); + } +} diff --git a/test/runtime/cartesian_vector_test.cpp b/test/runtime/cartesian_vector_test.cpp index ee3b05664..013cabd59 100644 --- a/test/runtime/cartesian_vector_test.cpp +++ b/test/runtime/cartesian_vector_test.cpp @@ -558,3 +558,38 @@ TEST_CASE("cartesian_vector with a complex representation", "[vector][complex]") REQUIRE(t[0] == c{2.0, 2.0}); } } + +namespace { +template +concept embeddable = requires(V v) { embed(v); }; +template +concept projectable = requires(V v) { project(v); }; +} // namespace +// embed only lifts 2D->3D and project only lowers 3D->2D (each defined for one source dimension) +static_assert(embeddable> && !embeddable>); +static_assert(projectable> && !projectable>); + +TEST_CASE("cartesian_vector embed/project between 2D and 3D", "[vector]") +{ + SECTION("embed zero-fills the new coordinate") + { + cartesian_vector v3 = embed(cartesian_vector{1.0, 2.0}); + static_assert(std::is_same_v>); + REQUIRE(v3[0] == 1.0); + REQUIRE(v3[1] == 2.0); + REQUIRE(v3[2] == 0.0); + } + + SECTION("project drops the last coordinate") + { + cartesian_vector v2 = project(cartesian_vector{1.0, 2.0, 3.0}); + static_assert(std::is_same_v>); + REQUIRE(v2[0] == 1.0); + REQUIRE(v2[1] == 2.0); + } + + SECTION("project . embed is the identity on 2D") + { + REQUIRE(project(embed(cartesian_vector{1.0, 2.0})) == cartesian_vector{1.0, 2.0}); + } +}