mirror of
https://github.com/mpusz/mp-units.git
synced 2026-07-08 17:40:58 +02:00
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<To>() 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<To, N> 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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<To>()` 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<To>()` 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 <mp-units/integrations/eigen.h>
|
||||
#include <mp-units/utility/spherical_vector.h>
|
||||
#include <mp-units/systems/si.h>
|
||||
|
||||
using namespace mp_units;
|
||||
using namespace mp_units::si::unit_symbols;
|
||||
|
||||
// an Eigen vector quantity -> spherical and back
|
||||
quantity<si::metre, Eigen::Vector3d> 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<Eigen::Vector3d>().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<si::metre, blaze::StaticVector<double, 3>>` 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<To>()` 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
|
||||
|
||||
@@ -52,6 +52,7 @@ import std;
|
||||
#include <concepts>
|
||||
#include <cstddef>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -78,6 +79,43 @@ template<typename T>
|
||||
requires requires { typename T::PlainObject; } && std::derived_from<T, Eigen::EigenBase<T>>
|
||||
constexpr std::size_t tensor_order<T> = (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<T>` would be ill-formed.
|
||||
template<typename T>
|
||||
concept eigen_fixed_vector = requires { typename T::PlainObject; } && std::derived_from<T, Eigen::EigenBase<T>> &&
|
||||
(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<std::size_t I, typename T>
|
||||
requires mp_units::detail::eigen_fixed_vector<std::remove_cvref_t<T>>
|
||||
[[nodiscard]] constexpr decltype(auto) get(T&& v)
|
||||
{
|
||||
return std::forward<T>(v)[static_cast<Eigen::Index>(I)];
|
||||
}
|
||||
|
||||
} // namespace Eigen
|
||||
|
||||
template<mp_units::detail::eigen_fixed_vector T>
|
||||
struct std::tuple_size<T> : std::integral_constant<std::size_t, static_cast<std::size_t>(T::SizeAtCompileTime)> {};
|
||||
|
||||
template<std::size_t I, mp_units::detail::eigen_fixed_vector T>
|
||||
struct std::tuple_element<I, T> {
|
||||
using type = std::remove_cv_t<typename T::Scalar>;
|
||||
};
|
||||
|
||||
#endif // __has_include(<Eigen/Core>)
|
||||
|
||||
@@ -33,16 +33,18 @@
|
||||
#include <mp-units/framework/quantity_spec_concepts.h>
|
||||
#include <mp-units/framework/reference_concepts.h>
|
||||
#include <mp-units/framework/unit_concepts.h>
|
||||
#include <mp-units/overflow_policies.h> // wrap_to_range - reused to keep angles canonical
|
||||
#include <mp-units/systems/angular.h> // opt-in strong angular system (angular::radian/degree/...)
|
||||
#include <mp-units/systems/si/units.h> // si::radian - the default, and the SI radian to scale through
|
||||
#include <mp-units/utility/unspecified.h> // utility::unspecified / utility::specified
|
||||
#include <mp-units/overflow_policies.h> // wrap_to_range - reused to keep angles canonical
|
||||
#include <mp-units/systems/angular.h> // opt-in strong angular system (angular::radian/degree/...)
|
||||
#include <mp-units/systems/si/units.h> // si::radian - the default, and the SI radian to scale through
|
||||
#include <mp-units/utility/representation.h> // utility::Vector - the exported vector-representation concept
|
||||
#include <mp-units/utility/unspecified.h> // utility::unspecified / utility::specified
|
||||
#ifdef MP_UNITS_IMPORT_STD
|
||||
import std;
|
||||
#else
|
||||
#include <cmath>
|
||||
#include <concepts>
|
||||
#include <numbers>
|
||||
#include <tuple>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#if MP_UNITS_HOSTED
|
||||
@@ -108,16 +110,52 @@ template<typename R>
|
||||
concept RadialReference = Reference<R> && (get_quantity_spec(R{}).character.order == quantity_tensor_order::scalar) &&
|
||||
!AngleUnit<MP_UNITS_NONCONST_TYPE(get_unit(R{}))>;
|
||||
|
||||
// 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<typename To, typename... Cs>
|
||||
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<typename To, typename... Cs>
|
||||
[[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<V, ...vector>`: 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<T, N>` models it, and so does any such
|
||||
// vector rep (e.g. an Eigen/Blaze one) - the facades are not tied to `cartesian_vector`.
|
||||
template<typename V, std::size_t N>
|
||||
concept VectorRepOf = Vector<V> && requires {
|
||||
std::tuple_size<V>::value;
|
||||
typename std::tuple_element<0, V>::type;
|
||||
} && (std::tuple_size_v<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<Reference auto VR, std::floating_point Rep, std::size_t N>
|
||||
template<Reference auto VR, typename V>
|
||||
constexpr Reference auto magnitude_reference_of = [] {
|
||||
if constexpr (requires(quantity<VR, cartesian_vector<Rep, N>> v) { v.magnitude(); })
|
||||
return decltype(std::declval<quantity<VR, cartesian_vector<Rep, N>>>().magnitude())::reference;
|
||||
if constexpr (requires(quantity<VR, V> v) { v.magnitude(); })
|
||||
return decltype(std::declval<quantity<VR, V>>().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<auto VR, typename VRep>
|
||||
requires std::constructible_from<Rep, VRep> &&
|
||||
requires(const quantity<VR, cartesian_vector<VRep, 2>>& v) { v.numerical_value_in(get_unit(RadiusRef)); }
|
||||
constexpr explicit polar_vector(const quantity<VR, cartesian_vector<VRep, 2>>& v) :
|
||||
_r_(static_cast<Rep>(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<auto VR, detail::VectorRepOf<2> V>
|
||||
requires requires(const quantity<VR, V>& v) { v.numerical_value_in(get_unit(RadiusRef)); }
|
||||
constexpr explicit polar_vector(const quantity<VR, V>& v) :
|
||||
_r_(static_cast<Rep>(::mp_units::magnitude(v.numerical_value_in(get_unit(RadiusRef)))), RadiusRef), _theta_([&] {
|
||||
using std::atan2;
|
||||
const cartesian_vector<VRep, 2> c = v.numerical_value_in(get_unit(RadiusRef));
|
||||
return detail::wrap_azimuth(detail::radians_to_angle<AngleUnit, Rep>(static_cast<Rep>(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<AngleUnit, Rep>(static_cast<Rep>(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<To, 2>`, 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<typename To = cartesian_vector<Rep, 2>>
|
||||
requires detail::VectorRepOf<To, 2> && detail::InitializableFrom<To, Rep, Rep>
|
||||
[[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<Rep>(r * cos(th)), static_cast<Rep>(r * sin(th))},
|
||||
return quantity{detail::make_vector<To>(static_cast<Rep>(r * cos(th)), static_cast<Rep>(r * sin(th))),
|
||||
get_unit(RadiusRef)};
|
||||
}
|
||||
|
||||
@@ -302,8 +348,8 @@ public:
|
||||
template<Reference auto RR, Unit auto AU, std::floating_point Rep>
|
||||
polar_vector(quantity<RR, Rep>, quantity<AU, Rep>) -> polar_vector<RR, AU, Rep>;
|
||||
|
||||
template<Reference auto VR, std::floating_point Rep>
|
||||
polar_vector(quantity<VR, cartesian_vector<Rep, 2>>)
|
||||
-> polar_vector<detail::magnitude_reference_of<VR, Rep, 2>, si::radian, Rep>;
|
||||
template<Reference auto VR, detail::VectorRepOf<2> V>
|
||||
polar_vector(quantity<VR, V>)
|
||||
-> polar_vector<detail::magnitude_reference_of<VR, V>, si::radian, std::tuple_element_t<0, V>>;
|
||||
|
||||
} // namespace mp_units::utility
|
||||
|
||||
@@ -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<auto VR, typename VRep>
|
||||
requires std::constructible_from<Rep, VRep> &&
|
||||
requires(const quantity<VR, cartesian_vector<VRep, 3>>& v) { v.numerical_value_in(get_unit(RadiusRef)); }
|
||||
constexpr explicit spherical_vector(const quantity<VR, cartesian_vector<VRep, 3>>& 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<auto VR, detail::VectorRepOf<3> V>
|
||||
requires requires(const quantity<VR, V>& v) { v.numerical_value_in(get_unit(RadiusRef)); }
|
||||
constexpr explicit spherical_vector(const quantity<VR, V>& 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<VRep, 3> 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<elem>(::mp_units::magnitude(c));
|
||||
_r_ = quantity{static_cast<Rep>(rv), RadiusRef};
|
||||
const auto theta_raw =
|
||||
rv != VRep{0} ? detail::radians_to_angle<AngleUnit, Rep>(static_cast<Rep>(acos(c[2] / rv))) : angle_type{};
|
||||
const auto [t, p] = detail::canonical_spherical(
|
||||
theta_raw, detail::radians_to_angle<AngleUnit, Rep>(static_cast<Rep>(atan2(c[1], c[0]))));
|
||||
rv != elem{0} ? detail::radians_to_angle<AngleUnit, Rep>(static_cast<Rep>(acos(z / rv))) : angle_type{};
|
||||
const auto [t, p] =
|
||||
detail::canonical_spherical(theta_raw, detail::radians_to_angle<AngleUnit, Rep>(static_cast<Rep>(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<To, 3>`, 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<typename To = cartesian_vector<Rep, 3>>
|
||||
requires detail::VectorRepOf<To, 3> && detail::InitializableFrom<To, Rep, Rep, Rep>
|
||||
[[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<Rep>(r * sin(th) * cos(ph)), static_cast<Rep>(r * sin(th) * sin(ph)),
|
||||
static_cast<Rep>(r * cos(th))},
|
||||
return quantity{detail::make_vector<To>(static_cast<Rep>(r * sin(th) * cos(ph)),
|
||||
static_cast<Rep>(r * sin(th) * sin(ph)), static_cast<Rep>(r * cos(th))),
|
||||
get_unit(RadiusRef)};
|
||||
}
|
||||
|
||||
@@ -196,8 +204,8 @@ public:
|
||||
template<Reference auto RR, Unit auto AU, std::floating_point Rep>
|
||||
spherical_vector(quantity<RR, Rep>, quantity<AU, Rep>, quantity<AU, Rep>) -> spherical_vector<RR, AU, Rep>;
|
||||
|
||||
template<Reference auto VR, std::floating_point Rep>
|
||||
spherical_vector(quantity<VR, cartesian_vector<Rep, 3>>)
|
||||
-> spherical_vector<detail::magnitude_reference_of<VR, Rep, 3>, si::radian, Rep>;
|
||||
template<Reference auto VR, detail::VectorRepOf<3> V>
|
||||
spherical_vector(quantity<VR, V>)
|
||||
-> spherical_vector<detail::magnitude_reference_of<VR, V>, si::radian, std::tuple_element_t<0, V>>;
|
||||
|
||||
} // namespace mp_units::utility
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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(<Eigen/Core>)
|
||||
#include <Eigen/Core>
|
||||
#define MP_UNITS_PS_EIGEN
|
||||
#elif __has_include(<blaze/math/StaticVector.h>)
|
||||
#include <blaze/math/StaticVector.h>
|
||||
#define MP_UNITS_PS_BLAZE
|
||||
#endif
|
||||
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
#include <catch2/matchers/catch_matchers_floating_point.hpp>
|
||||
|
||||
#ifdef MP_UNITS_IMPORT_STD
|
||||
import std;
|
||||
#else
|
||||
#include <cmath>
|
||||
#include <concepts>
|
||||
#include <numbers>
|
||||
#include <tuple>
|
||||
#include <type_traits>
|
||||
#endif
|
||||
|
||||
#include <mp-units/systems/si.h>
|
||||
#include <mp-units/utility/polar_vector.h>
|
||||
#include <mp-units/utility/spherical_vector.h>
|
||||
#if defined(MP_UNITS_PS_EIGEN)
|
||||
#include <mp-units/integrations/eigen.h>
|
||||
#elif defined(MP_UNITS_PS_BLAZE)
|
||||
#include <mp-units/integrations/blaze.h>
|
||||
#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<double, 2>;
|
||||
using vec3 = blaze::StaticVector<double, 3>;
|
||||
#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<utility::polar_vector<si::metre, si::radian, double>, quantity<si::metre, vec2>>);
|
||||
static_assert(
|
||||
!std::constructible_from<utility::polar_vector<si::metre, si::radian, double>, quantity<si::metre, vec3>>);
|
||||
static_assert(
|
||||
std::constructible_from<utility::spherical_vector<si::metre, si::radian, double>, quantity<si::metre, vec3>>);
|
||||
static_assert(
|
||||
!std::constructible_from<utility::spherical_vector<si::metre, si::radian, double>, quantity<si::metre, vec2>>);
|
||||
|
||||
// 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<vec2> == 2);
|
||||
static_assert(std::tuple_size_v<vec3> == 3);
|
||||
static_assert(std::same_as<std::tuple_element_t<0, vec3>, double>);
|
||||
|
||||
// `to_cartesian<To>()` 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<typename To>
|
||||
concept polar_makes = requires(const utility::polar_vector<si::metre, si::radian, double>& p) { p.to_cartesian<To>(); };
|
||||
template<typename To>
|
||||
concept spherical_makes =
|
||||
requires(const utility::spherical_vector<si::metre, si::radian, double>& s) { s.to_cartesian<To>(); };
|
||||
static_assert(polar_makes<vec2>);
|
||||
static_assert(!polar_makes<vec3>);
|
||||
static_assert(spherical_makes<vec3>);
|
||||
static_assert(!spherical_makes<vec2>);
|
||||
|
||||
} // 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<si::metre, vec2> xy{make_vec2(3.0, 4.0), si::metre};
|
||||
const utility::polar_vector<si::metre, si::radian, double> 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<si::metre, si::radian, double> p{2.0 * m, 0.0 * rad};
|
||||
const quantity xy = p.to_cartesian<vec2>();
|
||||
static_assert(std::same_as<decltype(xy)::rep, vec2>);
|
||||
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<si::metre, vec3> xyz{make_vec3(3.0, 4.0, 0.0), si::metre};
|
||||
const utility::spherical_vector<si::metre, si::radian, double> 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<si::metre, si::radian, double> sph{2.0 * m, std::numbers::pi / 2.0 * rad,
|
||||
0.0 * rad};
|
||||
const quantity xyz = sph.to_cartesian<vec3>();
|
||||
static_assert(std::same_as<decltype(xyz)::rep, vec3>);
|
||||
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));
|
||||
}
|
||||
@@ -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<si::metre, utility::cartesian_vector<double, 2>>;
|
||||
using v3 = quantity<si::metre, utility::cartesian_vector<double, 3>>;
|
||||
static_assert(std::constructible_from<polar_vector<si::metre, si::radian, double>, v2>);
|
||||
static_assert(!std::constructible_from<polar_vector<si::metre, si::radian, double>, v3>);
|
||||
static_assert(std::constructible_from<spherical_vector<si::metre, si::radian, double>, v3>);
|
||||
static_assert(!std::constructible_from<spherical_vector<si::metre, si::radian, double>, 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<Rep, N>)
|
||||
const polar_vector<si::metre, si::radian, double> p{2.0 * m, 0.0 * rad};
|
||||
const quantity<si::metre, utility::cartesian_vector<float, 2>> xy =
|
||||
p.to_cartesian<utility::cartesian_vector<float, 2>>();
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user