From 04472887e8ba19d63d701429a51940901c0c3d40 Mon Sep 17 00:00:00 2001 From: Alexander Condello Date: Wed, 9 Sep 2026 00:36:02 -0700 Subject: [PATCH] Add concepts/classes for NumPy-style type promotions --- .../include/dwave-optimization/interval.hpp | 10 +- .../include/dwave-optimization/typing.hpp | 46 ++++++ .../notes/promote-types-eb20e379783baaf6.yaml | 5 + tests/cpp/test_interval.cpp | 4 + tests/cpp/test_typing.cpp | 148 +++++++++++++++++- 5 files changed, 206 insertions(+), 7 deletions(-) create mode 100644 releasenotes/notes/promote-types-eb20e379783baaf6.yaml diff --git a/dwave/optimization/include/dwave-optimization/interval.hpp b/dwave/optimization/include/dwave-optimization/interval.hpp index bee59eb32..ea4c3dc1f 100644 --- a/dwave/optimization/include/dwave-optimization/interval.hpp +++ b/dwave/optimization/include/dwave-optimization/interval.hpp @@ -39,12 +39,10 @@ struct interval { /// Copy constructor. interval(const interval&) = default; - /// Create an ``interval`` from another interval. - template - requires(std::same_as) - interval(const interval& other) noexcept : interval(other.infimum, other.supremum) {} - // dev note: we could expand this. E.g., we could support all promotions - // allowed by NumPy promotion. + /// Create an one interval from another, allowing for safe type promotions + template U> + constexpr interval(const interval& other) noexcept : + interval(other.infimum, other.supremum) {} /// Move constructor. interval(interval&&) = default; diff --git a/dwave/optimization/include/dwave-optimization/typing.hpp b/dwave/optimization/include/dwave-optimization/typing.hpp index d042cb81e..a9052df02 100644 --- a/dwave/optimization/include/dwave-optimization/typing.hpp +++ b/dwave/optimization/include/dwave-optimization/typing.hpp @@ -95,4 +95,50 @@ constexpr FormatCharacter format_of() { if constexpr (std::same_as) return FormatCharacter::signedlonglong_; } +/// NumPy's type promotion doesn't match the behavior of C++'s numeric promotions +/// This class is meant to mimic the behavior of the `numpy.promote_types()` +/// function. +template +class promote_types { + private: + consteval static auto promote_types_() { + using T_ = std::remove_cvref_t; + using U_ = std::remove_cvref_t; + + if constexpr (std::same_as) { + // bool doesn't promote any other types + return U_(); + } else if constexpr (std::same_as) { + // bool doesn't promote any other types + return T_(); + } else if constexpr (std::integral and std::integral) { + // If both are integers, promote to the larger type + return std::conditional_t= sizeof(U_), T_, U_>(); + } else if constexpr (std::floating_point and std::floating_point) { + // If both are floating point, promote to the larger type + return std::conditional_t= sizeof(U_), T_, U_>(); + } else if constexpr (std::integral) { + // T_ is an integer and U_ is a floating point, so make sure we return + // a large enough type + return std::conditional_t(); + } else { + // T_ is a floating point and U_ is an integer, so make sure we return + // a large enough type + return std::conditional_t(); + } + } + + public: + using type = decltype(promote_types_()); +}; + +template +using promote_types_t = typename promote_types::type; + +/// Test whether `From` can be safely cast to `To` according to NumPy's promotion +/// rules. +/// Note that `np.can_cast(int64, float64, "safe")` is `True` according to NumPy. +template +concept can_cast = DType and DType and std::same_as, To>; + } // namespace dwave::optimization diff --git a/releasenotes/notes/promote-types-eb20e379783baaf6.yaml b/releasenotes/notes/promote-types-eb20e379783baaf6.yaml new file mode 100644 index 000000000..87d5de1e4 --- /dev/null +++ b/releasenotes/notes/promote-types-eb20e379783baaf6.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add ``promote_types`` and ``can_cast`` to ``dwave-optimization/typing.hpp`` + for NumPy-style type promotions. diff --git a/tests/cpp/test_interval.cpp b/tests/cpp/test_interval.cpp index 5fb5c5b89..c1bd60c40 100644 --- a/tests/cpp/test_interval.cpp +++ b/tests/cpp/test_interval.cpp @@ -143,4 +143,8 @@ TEMPLATE_LIST_TEST_CASE("interval", "", DTypes) { } } +TEST_CASE("interval") { + STATIC_REQUIRE(interval(interval(0, 5)) == interval(0, 5)); +} + } // namespace dwave::optimization diff --git a/tests/cpp/test_typing.cpp b/tests/cpp/test_typing.cpp index a8e37da5b..e4dd4ab18 100644 --- a/tests/cpp/test_typing.cpp +++ b/tests/cpp/test_typing.cpp @@ -20,7 +20,7 @@ namespace dwave::optimization { -void forwarding_reference_func(DTypeLike auto&&) { } +void forwarding_reference_func(DTypeLike auto&&) {} TEST_CASE("DTypeLike") { int a = 1; @@ -30,4 +30,150 @@ TEST_CASE("DTypeLike") { forwarding_reference_func(std::move(a)); } +TEST_CASE("can_cast") { + // import itertools + + // import numpy as np + + // dtypes = { + // np.float32: "float", + // np.float64: "double", + // np.bool: "bool", + // np.int8: "std::int8_t", + // np.int16: "std::int16_t", + // np.int32: "std::int32_t", + // np.int64: "std::int64_t", + // } + + // for dt0, dt1 in itertools.product(dtypes, repeat=2): + // From = dtypes[dt0] + // To = dtypes[dt1] + // if np.can_cast(dt0, dt1): + // print(f"STATIC_REQUIRE(can_cast<{From}, {To}>);") + // else: + // print(f"STATIC_REQUIRE(not can_cast<{From}, {To}>);") + + STATIC_REQUIRE(can_cast); + STATIC_REQUIRE(can_cast); + STATIC_REQUIRE(not can_cast); + STATIC_REQUIRE(not can_cast); + STATIC_REQUIRE(not can_cast); + STATIC_REQUIRE(not can_cast); + STATIC_REQUIRE(not can_cast); + STATIC_REQUIRE(not can_cast); + STATIC_REQUIRE(can_cast); + STATIC_REQUIRE(not can_cast); + STATIC_REQUIRE(not can_cast); + STATIC_REQUIRE(not can_cast); + STATIC_REQUIRE(not can_cast); + STATIC_REQUIRE(not can_cast); + STATIC_REQUIRE(can_cast); + STATIC_REQUIRE(can_cast); + STATIC_REQUIRE(can_cast); + STATIC_REQUIRE(can_cast); + STATIC_REQUIRE(can_cast); + STATIC_REQUIRE(can_cast); + STATIC_REQUIRE(can_cast); + STATIC_REQUIRE(can_cast); + STATIC_REQUIRE(can_cast); + STATIC_REQUIRE(not can_cast); + STATIC_REQUIRE(can_cast); + STATIC_REQUIRE(can_cast); + STATIC_REQUIRE(can_cast); + STATIC_REQUIRE(can_cast); + STATIC_REQUIRE(can_cast); + STATIC_REQUIRE(can_cast); + STATIC_REQUIRE(not can_cast); + STATIC_REQUIRE(not can_cast); + STATIC_REQUIRE(can_cast); + STATIC_REQUIRE(can_cast); + STATIC_REQUIRE(can_cast); + STATIC_REQUIRE(not can_cast); + STATIC_REQUIRE(can_cast); + STATIC_REQUIRE(not can_cast); + STATIC_REQUIRE(not can_cast); + STATIC_REQUIRE(not can_cast); + STATIC_REQUIRE(can_cast); + STATIC_REQUIRE(can_cast); + STATIC_REQUIRE(not can_cast); + STATIC_REQUIRE(can_cast); + STATIC_REQUIRE(not can_cast); + STATIC_REQUIRE(not can_cast); + STATIC_REQUIRE(not can_cast); + STATIC_REQUIRE(not can_cast); + STATIC_REQUIRE(can_cast); +} + +TEST_CASE("promote_types") { + // import itertools + + // import numpy as np + + // dtypes = { + // np.float32: "float", + // np.float64: "double", + // np.bool: "bool", + // np.int8: "std::int8_t", + // np.int16: "std::int16_t", + // np.int32: "std::int32_t", + // np.int64: "std::int64_t", + // } + + // for dt0, dt1 in itertools.product(dtypes, repeat=2): + // lhs = dtypes[dt0] + // rhs = dtypes[dt1] + // result = dtypes[np.promote_types(dt0, dt1).type] + // print(f"STATIC_REQUIRE(std::same_as::type, {result}>);") + + STATIC_REQUIRE(std::same_as::type, float>); + STATIC_REQUIRE(std::same_as::type, double>); + STATIC_REQUIRE(std::same_as::type, float>); + STATIC_REQUIRE(std::same_as::type, float>); + STATIC_REQUIRE(std::same_as::type, float>); + STATIC_REQUIRE(std::same_as::type, double>); + STATIC_REQUIRE(std::same_as::type, double>); + STATIC_REQUIRE(std::same_as::type, double>); + STATIC_REQUIRE(std::same_as::type, double>); + STATIC_REQUIRE(std::same_as::type, double>); + STATIC_REQUIRE(std::same_as::type, double>); + STATIC_REQUIRE(std::same_as::type, double>); + STATIC_REQUIRE(std::same_as::type, double>); + STATIC_REQUIRE(std::same_as::type, double>); + STATIC_REQUIRE(std::same_as::type, float>); + STATIC_REQUIRE(std::same_as::type, double>); + STATIC_REQUIRE(std::same_as::type, bool>); + STATIC_REQUIRE(std::same_as::type, std::int8_t>); + STATIC_REQUIRE(std::same_as::type, std::int16_t>); + STATIC_REQUIRE(std::same_as::type, std::int32_t>); + STATIC_REQUIRE(std::same_as::type, std::int64_t>); + STATIC_REQUIRE(std::same_as::type, float>); + STATIC_REQUIRE(std::same_as::type, double>); + STATIC_REQUIRE(std::same_as::type, std::int8_t>); + STATIC_REQUIRE(std::same_as::type, std::int8_t>); + STATIC_REQUIRE(std::same_as::type, std::int16_t>); + STATIC_REQUIRE(std::same_as::type, std::int32_t>); + STATIC_REQUIRE(std::same_as::type, std::int64_t>); + STATIC_REQUIRE(std::same_as::type, float>); + STATIC_REQUIRE(std::same_as::type, double>); + STATIC_REQUIRE(std::same_as::type, std::int16_t>); + STATIC_REQUIRE(std::same_as::type, std::int16_t>); + STATIC_REQUIRE(std::same_as::type, std::int16_t>); + STATIC_REQUIRE(std::same_as::type, std::int32_t>); + STATIC_REQUIRE(std::same_as::type, std::int64_t>); + STATIC_REQUIRE(std::same_as::type, double>); + STATIC_REQUIRE(std::same_as::type, double>); + STATIC_REQUIRE(std::same_as::type, std::int32_t>); + STATIC_REQUIRE(std::same_as::type, std::int32_t>); + STATIC_REQUIRE(std::same_as::type, std::int32_t>); + STATIC_REQUIRE(std::same_as::type, std::int32_t>); + STATIC_REQUIRE(std::same_as::type, std::int64_t>); + STATIC_REQUIRE(std::same_as::type, double>); + STATIC_REQUIRE(std::same_as::type, double>); + STATIC_REQUIRE(std::same_as::type, std::int64_t>); + STATIC_REQUIRE(std::same_as::type, std::int64_t>); + STATIC_REQUIRE(std::same_as::type, std::int64_t>); + STATIC_REQUIRE(std::same_as::type, std::int64_t>); + STATIC_REQUIRE(std::same_as::type, std::int64_t>); +} + } // namespace dwave::optimization