From 8fd725bc016d6cab9e1d7223b07d99bc7ea8746e Mon Sep 17 00:00:00 2001 From: Mateusz Pusz Date: Sun, 5 Jul 2026 23:26:36 +0200 Subject: [PATCH] feat: generalize polar/spherical facades to any tuple-like vector rep The polar_vector/spherical_vector conversion facades were tied to cartesian_vector. Generalize them to accept and produce any structured-bindings-compliant vector representation of the matching dimension that models the exported utility::Vector concept: - from-vector construction reads the dimension via std::tuple_size and the components via structured bindings - to_cartesian() takes the destination representation as a parameter, using parenthesized init and falling back to braced init for initializer_list-only reps such as Blaze's StaticVector - both sides are gated by detail::VectorRepOf Add the tuple protocol (std::tuple_size / std::tuple_element and an ADL-found get) for fixed-size Eigen vectors in the eigen integration, so Eigen::Vector2d/Vector3d work directly as a Cartesian representation. Blaze's StaticVector already ships the protocol, so its integration needs nothing. Add a per-backend polar_spherical_integration_test exercising both backends, and a how-to guide section. Co-Authored-By: Claude Opus 4.8 --- .../polar_and_spherical_coordinates.md | 50 ++++++ .../include/mp-units/integrations/eigen.h | 38 ++++ .../include/mp-units/utility/polar_vector.h | 92 +++++++--- .../mp-units/utility/spherical_vector.h | 46 +++-- test/runtime/CMakeLists.txt | 21 +++ .../polar_spherical_integration_test.cpp | 167 ++++++++++++++++++ test/runtime/polar_spherical_test.cpp | 21 +++ 7 files changed, 393 insertions(+), 42 deletions(-) create mode 100644 test/runtime/polar_spherical_integration_test.cpp diff --git a/docs/how_to_guides/advanced_usage/polar_and_spherical_coordinates.md b/docs/how_to_guides/advanced_usage/polar_and_spherical_coordinates.md index bed2dbc6a..951554e0e 100644 --- a/docs/how_to_guides/advanced_usage/polar_and_spherical_coordinates.md +++ b/docs/how_to_guides/advanced_usage/polar_and_spherical_coordinates.md @@ -15,6 +15,16 @@ the only correct implementation would convert to Cartesian and back on every cal than hide that cost behind an operator that looks cheap, the facades leave the Cartesian round-trip explicit. +The Cartesian side is not tied to `cartesian_vector`. Construction accepts any vector +quantity of the right dimension whose representation is tuple-like (the dimension is read +from `std::tuple_size`, the components with `get`) and offers the `magnitude` customization +point, and `to_cartesian()` takes the destination representation as a parameter, +defaulting to `cartesian_vector`. A Blaze `StaticVector` works on both sides with nothing +extra, because Blaze already ships that tuple protocol for it. An Eigen fixed-size vector +works too, because `mp-units/integrations/eigen.h` adds the tuple protocol that Eigen itself +does not provide. See [Using an Eigen or Blaze vector](#using-an-eigen-or-blaze-vector) +below. + ## Why not a polar quantity? A `quantity`'s representation has to support addition and subtraction (it must be @@ -201,6 +211,46 @@ This is the only extension point the facades expose, and it lives with them rath the core customization-point header. It is specific to these converters, not a general library facility. +## Using an Eigen or Blaze vector + +The facades read a vector representation's dimension from `std::tuple_size` and its +components with `get`, and `to_cartesian()` builds whatever representation you name. So +they work with any linear algebra backend whose vector is tuple-like and provides the +`magnitude` customization point, on both sides of the conversion: + +```cpp +#include +#include +#include + +using namespace mp_units; +using namespace mp_units::si::unit_symbols; + +// an Eigen vector quantity -> spherical and back +quantity xyz{Eigen::Vector3d{3.0, 4.0, 0.0}, si::metre}; +utility::spherical_vector s{xyz}; // r = 5 m, theta = pi/2, phi = atan2(4, 3) +Eigen::Vector3d back = s.to_cartesian().numerical_value_in(m); +``` + +The two supported backends need different amounts of glue, and the reason is instructive: + +- **Blaze** ships the tuple protocol (`std::tuple_size`, `std::tuple_element`, and a `get` + found by argument-dependent lookup) for its `StaticVector` itself, so a + `quantity>` is a valid Cartesian source and + target as soon as `mp-units/integrations/blaze.h` is included. That header adds nothing + for it. +- **Eigen** provides none of that for its matrices, so `mp-units/integrations/eigen.h` adds + it for fixed-size Eigen vectors (an N-element row or column known at compile time). + Dynamic-size vectors are excluded on purpose, because `std::tuple_size` needs a + compile-time size. + +`to_cartesian()` constructs `To` from the components with parentheses when it can (an +aggregate or an N-argument constructor, as Eigen and `cartesian_vector` offer) and falls +back to braces for a representation whose only such constructor takes an `initializer_list`, +as Blaze's `StaticVector` does. The parenthesized form is preferred because it permits a +narrowing element conversion (building a `float` vector from `double` components), which +braces reject. + ## Limitations (by design) - **No `+`, `-`, dot, or cross.** Those are not component-wise here, so convert to diff --git a/src/integrations/include/mp-units/integrations/eigen.h b/src/integrations/include/mp-units/integrations/eigen.h index 9f7b832cd..61c47a291 100644 --- a/src/integrations/include/mp-units/integrations/eigen.h +++ b/src/integrations/include/mp-units/integrations/eigen.h @@ -52,6 +52,7 @@ import std; #include #include #include +#include #endif #endif @@ -78,6 +79,43 @@ template requires requires { typename T::PlainObject; } && std::derived_from> constexpr std::size_t tensor_order = (T::RowsAtCompileTime == 1 || T::ColsAtCompileTime == 1) ? 1 : 2; +namespace detail { + +// A fixed-size Eigen vector (a row or column whose length is known at compile time). This is the +// shape the tuple protocol below is defined for: `std::tuple_size` needs a compile-time size, so +// dynamic-size vectors (`SizeAtCompileTime == Eigen::Dynamic`) are deliberately excluded, and a +// two-dimensional matrix is not a vector. The `typename T::PlainObject` probe is checked first and +// short-circuits the rest, because the concept is evaluated for arbitrary representation types +// (`int`, `double`, ...) for which `Eigen::EigenBase` would be ill-formed. +template +concept eigen_fixed_vector = requires { typename T::PlainObject; } && std::derived_from> && + (T::SizeAtCompileTime != Eigen::Dynamic) && (T::SizeAtCompileTime >= 1) && + (T::RowsAtCompileTime == 1 || T::ColsAtCompileTime == 1); + +} // namespace detail + } // namespace mp_units +// Tuple protocol for fixed-size Eigen vectors: makes them structured-bindings friendly +// (`auto [x, y, z] = vec;`) and, lets `mp_units::utility`'s `polar_vector`/`spherical_vector` +// read a vector representation's dimension via `std::tuple_size` and its components via `get`. +namespace Eigen { + +template + requires mp_units::detail::eigen_fixed_vector> +[[nodiscard]] constexpr decltype(auto) get(T&& v) +{ + return std::forward(v)[static_cast(I)]; +} + +} // namespace Eigen + +template +struct std::tuple_size : std::integral_constant(T::SizeAtCompileTime)> {}; + +template +struct std::tuple_element { + using type = std::remove_cv_t; +}; + #endif // __has_include() diff --git a/src/utility/include/mp-units/utility/polar_vector.h b/src/utility/include/mp-units/utility/polar_vector.h index 715dd8328..e4e5b776a 100644 --- a/src/utility/include/mp-units/utility/polar_vector.h +++ b/src/utility/include/mp-units/utility/polar_vector.h @@ -33,16 +33,18 @@ #include #include #include -#include // wrap_to_range - reused to keep angles canonical -#include // opt-in strong angular system (angular::radian/degree/...) -#include // si::radian - the default, and the SI radian to scale through -#include // utility::unspecified / utility::specified +#include // wrap_to_range - reused to keep angles canonical +#include // opt-in strong angular system (angular::radian/degree/...) +#include // si::radian - the default, and the SI radian to scale through +#include // utility::Vector - the exported vector-representation concept +#include // utility::unspecified / utility::specified #ifdef MP_UNITS_IMPORT_STD import std; #else #include #include #include +#include #include #include #if MP_UNITS_HOSTED @@ -108,16 +110,52 @@ template concept RadialReference = Reference && (get_quantity_spec(R{}).character.order == quantity_tensor_order::scalar) && !AngleUnit; +// Whether a vector representation `To` can be initialized from its components. Two spellings are +// possible, and `std::constructible_from` covers only the first: parenthesized init (aggregates since +// C++20, and reps with an N-argument constructor - e.g. Eigen and `cartesian_vector`) and braced init +// (reps whose only N-value constructor is an `initializer_list` one - e.g. Blaze's `StaticVector`). +template +concept InitializableFrom = requires(Cs... cs) { To(cs...); } || requires(Cs... cs) { To{cs...}; }; + +// Build a vector representation `To` from its components, preferring parenthesized init: it is the one +// that permits a narrowing element conversion (e.g. building a `float` vector from `double` +// components), which braced init rejects. Falls back to braced init for the `initializer_list`-only +// reps. `InitializableFrom` (checked by the callers' `requires`) guarantees at least one spelling works. +template +[[nodiscard]] constexpr To make_vector(Cs... cs) +{ + if constexpr (requires { To(cs...); }) + return To(cs...); + else + return To{cs...}; +} + +// A representation usable as a facade's Cartesian source *and* target: an `N`-dimensional vector +// representation that is also structured-bindings-compliant. We use the exported `utility::Vector` +// concept (order-1, magnitude-bearing, addable) rather than `RepresentationOf`: the +// latter is semantically a touch stronger (it also carries `RepresentationBaseline`) but its +// `order_of(V.character)` sub-expression hard-errors under GCC when the character-keyed concept is +// *nested* inside another one like this (fine at top level, not when normalized here). The output +// side still gets its full representation check from the `quantity{...}` construction in the body. +// Dimension reads from `std::tuple_size`, element type from `std::tuple_element`, components via +// `get`; those probes guard the reads below. `cartesian_vector` models it, and so does any such +// vector rep (e.g. an Eigen/Blaze one) - the facades are not tied to `cartesian_vector`. +template +concept VectorRepOf = Vector && requires { + std::tuple_size::value; + typename std::tuple_element<0, V>::type; +} && (std::tuple_size_v == N) && requires(const V& c) { get<0>(c); }; + // The radius reference a facade takes when deduced (CTAD) from a vector quantity. A vector-character // quantity (e.g. `isq::velocity[m/s]`) has a `magnitude()`, so we take that magnitude's reference: a // kind over the unit in V2, upgrading for free to the strong scalar sibling (e.g. `isq::speed`, or a // `magnitude<...>` expression) once V3 names it. A bare-unit vector quantity (`cartesian_vector * m`) // has a scalar-character spec with a vector rep and thus no `magnitude()`; there is no strong scalar // to recover, so we fall back to the plain unit. -template +template constexpr Reference auto magnitude_reference_of = [] { - if constexpr (requires(quantity> v) { v.magnitude(); }) - return decltype(std::declval>>().magnitude())::reference; + if constexpr (requires(quantity v) { v.magnitude(); }) + return decltype(std::declval>().magnitude())::reference; else return get_unit(VR); }(); @@ -201,18 +239,20 @@ public: // turns are equal. constexpr polar_vector(radius_type r, angle_type theta) : _r_(r), _theta_(detail::wrap_azimuth(theta)) {} - // Explicit construction from a 2-D vector quantity: r = |v|, theta = atan2(y, x). The vector's rep - // may differ from `Rep`, constrained exactly as `cartesian_vector` constrains its own conversions - // (`constructible_from`) and converted via `static_cast` so a narrowing target stays warning-clean. - // The constructor is always `explicit` regardless - a Cartesian round-trip is never implicit. - template - requires std::constructible_from && - requires(const quantity>& v) { v.numerical_value_in(get_unit(RadiusRef)); } - constexpr explicit polar_vector(const quantity>& v) : - _r_(static_cast(v.numerical_value_in(get_unit(RadiusRef)).magnitude()), RadiusRef), _theta_([&] { + // Explicit construction from a 2-D vector quantity: r = |v|, theta = atan2(y, x). Works with any + // tuple-like vector representation of dimension 2 that offers the `magnitude` CPO - not only + // `cartesian_vector` - reading its components via structured bindings (see `detail::VectorRepOf`). + // The element type may differ from `Rep`; it is `static_cast`, so a narrowing target stays + // warning-clean. Always `explicit` - a Cartesian round-trip is never implicit. (`VR` is plain `auto` + // to avoid the GCC 15 constrained-`auto`-NTTP ICE.) + template V> + requires requires(const quantity& v) { v.numerical_value_in(get_unit(RadiusRef)); } + constexpr explicit polar_vector(const quantity& v) : + _r_(static_cast(::mp_units::magnitude(v.numerical_value_in(get_unit(RadiusRef)))), RadiusRef), _theta_([&] { using std::atan2; - const cartesian_vector c = v.numerical_value_in(get_unit(RadiusRef)); - return detail::wrap_azimuth(detail::radians_to_angle(static_cast(atan2(c[1], c[0])))); + const V c = v.numerical_value_in(get_unit(RadiusRef)); + const auto& [x, y] = c; + return detail::wrap_azimuth(detail::radians_to_angle(static_cast(atan2(y, x)))); }()) { } @@ -241,14 +281,20 @@ public: [[nodiscard]] constexpr radius_type magnitude() const { return _r_; } [[nodiscard]] constexpr angle_type theta() const { return _theta_; } - // Explicit conversion to a 2-D Cartesian vector quantity: (r cos theta, r sin theta). + // Explicit conversion to a 2-D Cartesian vector quantity: (r cos theta, r sin theta). The destination + // representation `To` defaults to `cartesian_vector`, but may be any 2-D vector representation + // (`detail::VectorRepOf`, the same contract accepted on input) that is initializable from the + // two components. Initialization uses parenthesized init (aggregate or N-argument constructor, e.g. + // Eigen) or braced init (an `initializer_list` constructor, e.g. Blaze); see `detail::make_vector`. + template> + requires detail::VectorRepOf && detail::InitializableFrom [[nodiscard]] constexpr auto to_cartesian() const { using std::cos; using std::sin; const Rep th = detail::angle_in_radians(_theta_); const Rep r = _r_.numerical_value_in(get_unit(RadiusRef)); - return quantity{cartesian_vector{static_cast(r * cos(th)), static_cast(r * sin(th))}, + return quantity{detail::make_vector(static_cast(r * cos(th)), static_cast(r * sin(th))), get_unit(RadiusRef)}; } @@ -302,8 +348,8 @@ public: template polar_vector(quantity, quantity) -> polar_vector; -template -polar_vector(quantity>) - -> polar_vector, si::radian, Rep>; +template V> +polar_vector(quantity) + -> polar_vector, si::radian, std::tuple_element_t<0, V>>; } // namespace mp_units::utility diff --git a/src/utility/include/mp-units/utility/spherical_vector.h b/src/utility/include/mp-units/utility/spherical_vector.h index 48a5268ed..674620980 100644 --- a/src/utility/include/mp-units/utility/spherical_vector.h +++ b/src/utility/include/mp-units/utility/spherical_vector.h @@ -80,25 +80,26 @@ public: } // Explicit construction from a 3-D vector quantity: r = |v|, theta = acos(z / r), phi = atan2(y, x). - // For r == 0 the angles are left at zero (the direction is undefined). The vector's rep may differ - // from `Rep`, constrained exactly as `cartesian_vector` constrains its own conversions - // (`constructible_from`) and converted via `static_cast` so a narrowing target stays warning-clean. - // The constructor is always `explicit` regardless - a Cartesian round-trip is never implicit. - template - requires std::constructible_from && - requires(const quantity>& v) { v.numerical_value_in(get_unit(RadiusRef)); } - constexpr explicit spherical_vector(const quantity>& v) + // For r == 0 the angles are left at zero (the direction is undefined). Works with any tuple-like + // vector representation of dimension 3 that offers the `magnitude` CPO - not only `cartesian_vector` + // - reading its components via structured bindings (see `detail::VectorRepOf`). The trig is done in + // the vector's own element type and `static_cast` to `Rep`, so a narrowing target stays + // warning-clean. Always `explicit` - a Cartesian round-trip is never implicit. + template V> + requires requires(const quantity& v) { v.numerical_value_in(get_unit(RadiusRef)); } + constexpr explicit spherical_vector(const quantity& v) { using std::acos; using std::atan2; - // do the trig in the vector's own rep (VRep), then static_cast to Rep - avoids float<->double mixing - const cartesian_vector c = v.numerical_value_in(get_unit(RadiusRef)); - const VRep rv = c.magnitude(); + using elem = std::tuple_element_t<0, V>; + const V c = v.numerical_value_in(get_unit(RadiusRef)); + const auto& [x, y, z] = c; + const elem rv = static_cast(::mp_units::magnitude(c)); _r_ = quantity{static_cast(rv), RadiusRef}; const auto theta_raw = - rv != VRep{0} ? detail::radians_to_angle(static_cast(acos(c[2] / rv))) : angle_type{}; - const auto [t, p] = detail::canonical_spherical( - theta_raw, detail::radians_to_angle(static_cast(atan2(c[1], c[0])))); + rv != elem{0} ? detail::radians_to_angle(static_cast(acos(z / rv))) : angle_type{}; + const auto [t, p] = + detail::canonical_spherical(theta_raw, detail::radians_to_angle(static_cast(atan2(y, x)))); _theta_ = t; _phi_ = p; } @@ -131,6 +132,13 @@ public: // Explicit conversion to a 3-D Cartesian vector quantity: // (r sin(theta) cos(phi), r sin(theta) sin(phi), r cos(theta)). + // The destination representation `To` defaults to `cartesian_vector`, but may be any 3-D vector + // representation (`detail::VectorRepOf`, the same contract accepted on input) that is + // initializable from the three components. Initialization uses parenthesized init (aggregate or + // N-argument constructor, e.g. Eigen) or braced init (an `initializer_list` constructor, e.g. + // Blaze); see `detail::make_vector`. + template> + requires detail::VectorRepOf && detail::InitializableFrom [[nodiscard]] constexpr auto to_cartesian() const { using std::cos; @@ -138,8 +146,8 @@ public: const Rep th = detail::angle_in_radians(_theta_); const Rep ph = detail::angle_in_radians(_phi_); const Rep r = _r_.numerical_value_in(get_unit(RadiusRef)); - return quantity{cartesian_vector{static_cast(r * sin(th) * cos(ph)), static_cast(r * sin(th) * sin(ph)), - static_cast(r * cos(th))}, + return quantity{detail::make_vector(static_cast(r * sin(th) * cos(ph)), + static_cast(r * sin(th) * sin(ph)), static_cast(r * cos(th))), get_unit(RadiusRef)}; } @@ -196,8 +204,8 @@ public: template spherical_vector(quantity, quantity, quantity) -> spherical_vector; -template -spherical_vector(quantity>) - -> spherical_vector, si::radian, Rep>; +template V> +spherical_vector(quantity) + -> spherical_vector, si::radian, std::tuple_element_t<0, V>>; } // namespace mp_units::utility diff --git a/test/runtime/CMakeLists.txt b/test/runtime/CMakeLists.txt index fc0db5d2b..0550d7ec3 100644 --- a/test/runtime/CMakeLists.txt +++ b/test/runtime/CMakeLists.txt @@ -80,6 +80,27 @@ add_linear_algebra_test(eigen) add_linear_algebra_test(glm) add_linear_algebra_test(blaze) +# The polar/spherical facades are not tied to `cartesian_vector`: they accept any tuple-like vector +# representation that offers the `magnitude` CPO. This test exercises them with a third-party backend +# vector (via the integration adapter's tuple protocol), so it is built once per available backend, +# like the linear algebra test above. The built-in `cartesian_vector` case lives in the +# always-built `polar_spherical_test`. +function(add_polar_spherical_integration_test backend) + if(NOT TARGET mp-units::integrations-${backend}) + message(STATUS "Skipping the '${backend}' polar/spherical integration test (integration not available)") + return() + endif() + add_executable(polar_spherical_integration_test-${backend} polar_spherical_integration_test.cpp) + target_link_libraries( + polar_spherical_integration_test-${backend} PRIVATE mp-units::mp-units mp-units::integrations-${backend} + Catch2::Catch2WithMain + ) + catch_discover_tests(polar_spherical_integration_test-${backend}) +endfunction() + +add_polar_spherical_integration_test(eigen) +add_polar_spherical_integration_test(blaze) + # The built-in `cartesian_vector` backend ships with `mp-units::mp-units` (no third-party dependency # and no integration plugin), so it is always built and forced via `MP_UNITS_LA_USE_CARTESIAN`. It # additionally covers integral representations and `constexpr` evaluation. diff --git a/test/runtime/polar_spherical_integration_test.cpp b/test/runtime/polar_spherical_integration_test.cpp new file mode 100644 index 000000000..a8a15dff3 --- /dev/null +++ b/test/runtime/polar_spherical_integration_test.cpp @@ -0,0 +1,167 @@ +// The MIT License (MIT) +// +// Copyright (c) 2018 Mateusz Pusz +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +// Exercises the `polar_vector`/`spherical_vector` conversion facades with a third-party linear +// algebra vector as the Cartesian representation. The facades are not tied to `cartesian_vector`: +// they read a vector representation's dimension via `std::tuple_size` and its components via `get`, +// both of which the integration adapter adds for the backend's fixed-size vector type. The backend +// is selected from whatever library is available (the build compiles this file once per backend). + +#if __has_include() +#include +#define MP_UNITS_PS_EIGEN +#elif __has_include() +#include +#define MP_UNITS_PS_BLAZE +#endif + +#include +#include + +#ifdef MP_UNITS_IMPORT_STD +import std; +#else +#include +#include +#include +#include +#include +#endif + +#include +#include +#include +#if defined(MP_UNITS_PS_EIGEN) +#include +#elif defined(MP_UNITS_PS_BLAZE) +#include +#endif + +using namespace mp_units; +using namespace mp_units::si::unit_symbols; +using Catch::Matchers::WithinAbs; + +namespace { + +#if defined(MP_UNITS_PS_EIGEN) +inline constexpr const char* backend = "Eigen"; +using vec2 = Eigen::Vector2d; +using vec3 = Eigen::Vector3d; +#elif defined(MP_UNITS_PS_BLAZE) +inline constexpr const char* backend = "Blaze"; +using vec2 = blaze::StaticVector; +using vec3 = blaze::StaticVector; +#endif + +[[nodiscard]] vec2 make_vec2(double x, double y) { return {x, y}; } +[[nodiscard]] vec3 make_vec3(double x, double y, double z) { return {x, y, z}; } + +// The adapter makes the backend's fixed-size vector a valid Cartesian source for the facades: +// dimension 2 feeds `polar_vector`, dimension 3 feeds `spherical_vector`, and never the reverse. +static_assert(std::constructible_from, quantity>); +static_assert( + !std::constructible_from, quantity>); +static_assert( + std::constructible_from, quantity>); +static_assert( + !std::constructible_from, quantity>); + +// The tuple protocol the adapter adds is genuine structured-bindings support, not just enough for +// the facades: dimension is visible through `std::tuple_size`, the element type through +// `std::tuple_element`. +static_assert(std::tuple_size_v == 2); +static_assert(std::tuple_size_v == 3); +static_assert(std::same_as, double>); + +// `to_cartesian()` names the target rep, which must be a vector representation of the matching +// dimension. A detection concept verifies the mismatch is a clean SFINAE rejection (the member is +// simply not callable), not a hard error. +template +concept polar_makes = requires(const utility::polar_vector& p) { p.to_cartesian(); }; +template +concept spherical_makes = + requires(const utility::spherical_vector& s) { s.to_cartesian(); }; +static_assert(polar_makes); +static_assert(!polar_makes); +static_assert(spherical_makes); +static_assert(!spherical_makes); + +} // namespace + +TEST_CASE("polar_vector round-trips through a backend vector", "[polar][integration]") +{ + INFO("backend: " << backend); + + SECTION("construction from a 2-D backend vector quantity") + { + // (3, 4) m -> radius 5 m, heading atan2(4, 3) + const quantity xy{make_vec2(3.0, 4.0), si::metre}; + const utility::polar_vector p{xy}; + CHECK_THAT(p.radius().numerical_value_in(m), WithinAbs(5.0, 1e-12)); + CHECK_THAT(p.theta().numerical_value_in(rad), WithinAbs(std::atan2(4.0, 3.0), 1e-12)); + } + + SECTION("to_cartesian into the backend vector type") + { + const utility::polar_vector p{2.0 * m, 0.0 * rad}; + const quantity xy = p.to_cartesian(); + static_assert(std::same_as); + CHECK_THAT(xy.numerical_value_in(m)[0], WithinAbs(2.0, 1e-12)); + CHECK_THAT(xy.numerical_value_in(m)[1], WithinAbs(0.0, 1e-12)); + } +} + +TEST_CASE("spherical_vector round-trips through a backend vector", "[spherical][integration]") +{ + INFO("backend: " << backend); + + SECTION("construction from a 3-D backend vector quantity") + { + // (3, 4, 0) m -> radius 5 m, inclination pi/2 (in the xy-plane), azimuth atan2(4, 3) + const quantity xyz{make_vec3(3.0, 4.0, 0.0), si::metre}; + const utility::spherical_vector sph{xyz}; + CHECK_THAT(sph.radius().numerical_value_in(m), WithinAbs(5.0, 1e-12)); + CHECK_THAT(sph.theta().numerical_value_in(rad), WithinAbs(std::numbers::pi / 2.0, 1e-12)); + CHECK_THAT(sph.phi().numerical_value_in(rad), WithinAbs(std::atan2(4.0, 3.0), 1e-12)); + } + + SECTION("to_cartesian into the backend vector type") + { + const utility::spherical_vector sph{2.0 * m, std::numbers::pi / 2.0 * rad, + 0.0 * rad}; + const quantity xyz = sph.to_cartesian(); + static_assert(std::same_as); + CHECK_THAT(xyz.numerical_value_in(m)[0], WithinAbs(2.0, 1e-12)); + CHECK_THAT(xyz.numerical_value_in(m)[1], WithinAbs(0.0, 1e-12)); + CHECK_THAT(xyz.numerical_value_in(m)[2], WithinAbs(0.0, 1e-12)); + } +} + +TEST_CASE("the backend vector is structured-bindings friendly", "[integration][structured-bindings]") +{ + INFO("backend: " << backend); + const vec3 v = make_vec3(1.0, 2.0, 3.0); + const auto [x, y, z] = v; + CHECK_THAT(x, WithinAbs(1.0, 1e-12)); + CHECK_THAT(y, WithinAbs(2.0, 1e-12)); + CHECK_THAT(z, WithinAbs(3.0, 1e-12)); +} diff --git a/test/runtime/polar_spherical_test.cpp b/test/runtime/polar_spherical_test.cpp index 68a456f66..79677b45d 100644 --- a/test/runtime/polar_spherical_test.cpp +++ b/test/runtime/polar_spherical_test.cpp @@ -354,6 +354,27 @@ TEST_CASE("converting constructors between compatible facades", "[polar][spheric } } +TEST_CASE("the facades are not tied to cartesian_vector", "[polar][spherical][generic-rep]") +{ + // The input rep only has to be a tuple-like vector of the right dimension with a magnitude() CPO + // (dimension read from std::tuple_size, components via get<>). The dimension gating is observable + // through construction: a 2-D vector builds a polar_vector but not a spherical_vector, and vice versa. + using v2 = quantity>; + using v3 = quantity>; + static_assert(std::constructible_from, v2>); + static_assert(!std::constructible_from, v3>); + static_assert(std::constructible_from, v3>); + static_assert(!std::constructible_from, v2>); + + // to_cartesian's destination representation is a template parameter (here a different element type, + // proving the output is not hard-wired to cartesian_vector) + const polar_vector p{2.0 * m, 0.0 * rad}; + const quantity> xy = + p.to_cartesian>(); + CHECK_THAT(xy.numerical_value_in(m)[0], WithinAbs(2.0f, 1e-6f)); + CHECK_THAT(xy.numerical_value_in(m)[1], WithinAbs(0.0f, 1e-6f)); +} + TEST_CASE("angle unit is an extension point via radian_of", "[polar][spherical][angle_unit]") { using namespace test_angle;