diff --git a/dwave/optimization/include/dwave-optimization/interval.hpp b/dwave/optimization/include/dwave-optimization/interval.hpp new file mode 100644 index 000000000..bee59eb32 --- /dev/null +++ b/dwave/optimization/include/dwave-optimization/interval.hpp @@ -0,0 +1,195 @@ +// Copyright 2026 D-Wave +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include +#include +#include +#include + +#include "dwave-optimization/typing.hpp" + +namespace dwave::optimization { + +/// An interval encodes a range of possible values. +/// +/// Note that this class does not (yet) implement outward rounding. +template +struct interval { + /// Construct an empty interval. + constexpr interval() = default; + + /// Construct an interval of values between inf and sup (inclusive). + /// When ``sup < inf`` the interval is treated as empty. + constexpr interval(T inf, T sup) noexcept : infimum(inf), supremum(sup) {} + + /// 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. + + /// Move constructor. + interval(interval&&) = default; + + /// Copy assignment operator. + interval& operator=(const interval&) = default; + + /// Move assignment operator. + interval& operator=(interval&&) = default; + + /// Destructor. + ~interval() = default; + + /// An interval evalutes to `true` if it is not empty. + explicit constexpr operator bool() const noexcept { return infimum <= supremum; } + + /// Two intervals are treated as equal if they are the same type and have the same endpoints + /// or if they are both null. + constexpr bool operator==(const interval& rhs) const { + if (not static_cast(*this) and not static_cast(rhs)) return true; // both null + return infimum == rhs.infimum and supremum == rhs.supremum; + } + // dev note: we could support other type combinations in the future + + /// Comparison operators <, <=, >=, > are used for strict subset, subset, superset, and strict + /// superset respectively. + constexpr std::partial_ordering operator<=>(const interval& rhs) const { + // If we're equal then we're equivalent + if (*this == rhs) return std::partial_ordering::equivalent; + + // If lhs != rhs then at most one can be empty + if (not static_cast(*this)) return std::partial_ordering::less; // empty < not-empty + if (not static_cast(rhs)) return std::partial_ordering::greater; // not-empty > empty + + // Ok, neither are empty + + // If lhs <= rhs and lhs != rhs then lhs < rhs + if (rhs.infimum <= infimum and supremum <= rhs.supremum) { + return std::partial_ordering::less; + } + + // If lhs >= rhs and lhs != rhs then lhs > rhs + if (infimum <= rhs.infimum and rhs.supremum <= supremum) { + return std::partial_ordering::greater; + } + + // Otherwise we're not comparable + return std::partial_ordering::unordered; + } + // dev note: we could support other type combinations in the future + + /// Negate and swap the values in the interval. + /// For boolean intervals, negation is treated as logical not. + constexpr interval operator-() const { + // For bool, we overload this to be negation + if constexpr (std::same_as) return interval(not supremum, not infimum); + + // -INT_MIN is undefined. Under the assumption that if the user is using + // INT_MIN/INT_MAX they probably are trying to say "unbounded" we do a + // weird thing and just define -INT_MIN := INT_MAX and -INT_MAX := INT_MIN + // even though that's wrong and leads to some slightly weird outcomes + if constexpr (std::integral) { + using limits = std::numeric_limits; + if (infimum == limits::lowest() and supremum == limits::max()) { + return *this; + } else if (infimum == limits::lowest()) { + return interval(-supremum, limits::max()); + } else if (supremum == limits::max()) { + return interval(limits::lowest(), -infimum); + } + } + + return interval(-supremum, -infimum); + } + + /// Intersection with ``rhs``. + constexpr interval& operator&=(const interval& rhs) { + // If lhs is an empty interval, then the intersection is just lhs + if (not static_cast(*this)) return *this; + + // If rhs is an empty interval, then the intersection is just rhs + if (not static_cast(rhs)) return *this = rhs; + + if (infimum < rhs.infimum) infimum = rhs.infimum; + if (rhs.supremum < supremum) supremum = rhs.supremum; + + return *this; + } + + /// Union with ``rhs``. + constexpr interval& operator|=(const interval& rhs) { + // If rhs is an empty interval, then taking the union with it does nothing + if (not static_cast(rhs)) return *this; + + // If lhs is an empty interval, then the union is just rhs + if (not static_cast(*this)) return *this = rhs; + + if (rhs.infimum < infimum) infimum = rhs.infimum; + if (supremum < rhs.supremum) supremum = rhs.supremum; + + return *this; + } + + /// Interection of two intervals. + friend constexpr interval operator&(interval lhs, const interval& rhs) { + lhs &= rhs; + return lhs; + } + + /// Union of two intervals + friend constexpr interval operator|(interval lhs, const interval& rhs) { + lhs |= rhs; + return lhs; + } + + /// The maximum expressible interval + static consteval interval all() { + using limits = std::numeric_limits; + if constexpr (limits::has_infinity) { + return interval(-limits::infinity(), limits::infinity()); + } else { + return interval(limits::lowest(), limits::max()); + } + } + + /// Test whether `x` is a value in the interval. + constexpr bool contains(const T& x) const { return infimum <= x and x <= supremum; } + // dev note: we could support other type combinations in the future + + /// All expressible non-negative values. + static consteval interval nonnegative() { + using limits = std::numeric_limits; + if constexpr (limits::has_infinity) { + return interval(0, limits::infinity()); + } else { + return interval(0, limits::max()); + } + } + + T infimum = 1; + T supremum = 0; +}; + +// Intervals are printable +template +std::ostream& operator<<(std::ostream& os, const interval& in); + +} // namespace dwave::optimization diff --git a/dwave/optimization/src/interval.cpp b/dwave/optimization/src/interval.cpp new file mode 100644 index 000000000..07c22f191 --- /dev/null +++ b/dwave/optimization/src/interval.cpp @@ -0,0 +1,49 @@ +// Copyright 2026 D-Wave +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "dwave-optimization/interval.hpp" + +#include + +namespace dwave::optimization { + +template +std::ostream& operator<<(std::ostream& os, const interval& in) { + if (not static_cast(in)) return os << "[]"; + + os << "["; + + // Not all compilers print all dtypes. So coerce them into a smaller set of + // possible types + if constexpr (std::integral) { + os << static_cast(in.infimum) << ", " << static_cast(in.supremum); + } else if constexpr (std::floating_point) { + os << static_cast(in.infimum) << ", " << static_cast(in.supremum); + } else { + assert(false and "unexpected dtype"); + } + + os << "]"; + return os; +} + +template std::ostream& operator<<(std::ostream&, const interval&); +template std::ostream& operator<<(std::ostream&, const interval&); +template std::ostream& operator<<(std::ostream&, const interval&); +template std::ostream& operator<<(std::ostream&, const interval&); +template std::ostream& operator<<(std::ostream&, const interval&); +template std::ostream& operator<<(std::ostream&, const interval&); +template std::ostream& operator<<(std::ostream&, const interval&); + +} // namespace dwave::optimization diff --git a/meson.build b/meson.build index 1e64bf33c..f93631a1b 100644 --- a/meson.build +++ b/meson.build @@ -55,6 +55,7 @@ dwave_optimization_src = [ 'dwave/optimization/src/array.cpp', 'dwave/optimization/src/fraction.cpp', 'dwave/optimization/src/graph.cpp', + 'dwave/optimization/src/interval.cpp', 'dwave/optimization/src/simplex.cpp', ] diff --git a/releasenotes/notes/interval-ad3c6f2bd14d3747.yaml b/releasenotes/notes/interval-ad3c6f2bd14d3747.yaml new file mode 100644 index 000000000..84d14e4de --- /dev/null +++ b/releasenotes/notes/interval-ad3c6f2bd14d3747.yaml @@ -0,0 +1,3 @@ +--- +features: + - Add a simple C++ ``dwave::optimization::interval`` class. diff --git a/tests/cpp/meson.build b/tests/cpp/meson.build index 3f1421faa..7b9041fe2 100644 --- a/tests/cpp/meson.build +++ b/tests/cpp/meson.build @@ -36,6 +36,7 @@ tests_all = executable( 'test_functional.cpp', 'test_functional_.cpp', 'test_graph.cpp', + 'test_interval.cpp', 'test_iterators.cpp', 'test_simplex.cpp', 'test_type_list.cpp', diff --git a/tests/cpp/test_interval.cpp b/tests/cpp/test_interval.cpp new file mode 100644 index 000000000..5fb5c5b89 --- /dev/null +++ b/tests/cpp/test_interval.cpp @@ -0,0 +1,146 @@ +// Copyright 2026 D-Wave +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include + +#include "dwave-optimization/interval.hpp" + +namespace dwave::optimization { + +TEMPLATE_LIST_TEST_CASE("interval", "", DTypes) { + SECTION("::operator bool()") { + STATIC_REQUIRE(not interval()); + STATIC_REQUIRE(interval(0, 1)); + STATIC_REQUIRE(interval(0, 0)); + STATIC_REQUIRE(interval(1, 1)); + + // We only allow explicit conversion to bool + STATIC_REQUIRE(not std::convertible_to, bool>); + } + + SECTION("::operator==") { + STATIC_REQUIRE(interval(0, 1) == interval(0, 1)); + + STATIC_REQUIRE(interval(0, 0) != interval(0, 1)); + STATIC_REQUIRE(interval(0, 1) != interval()); + + STATIC_REQUIRE(interval(10, -10) == interval(1, 0)); // null always equals null + } + + SECTION("::operator<= (i.e., subset)") { + STATIC_REQUIRE(interval(0, 0) <= interval(0, 1)); + STATIC_REQUIRE(interval(0, 1) <= interval(0, 1)); // equality allowed + } + + SECTION("::operator-") { + if constexpr (std::same_as) { + STATIC_REQUIRE(interval(0, 1) == -interval(0, 1)); + STATIC_REQUIRE(interval(0, 0) == -interval(1, 1)); + STATIC_REQUIRE(interval(1, 1) == -interval(0, 0)); + } else { + STATIC_REQUIRE(interval(1, 10) == -interval(-10, -1)); + } + + if constexpr (std::integral) { + STATIC_REQUIRE(-interval::all() == interval::all()); + } + } + + SECTION("::operator&=/::operator& (i.e., intersection)") { + STATIC_REQUIRE(not static_cast(interval(0, 1) & interval())); + STATIC_REQUIRE(not static_cast(interval() & interval(0, 1))); + + if constexpr (not std::same_as) { + STATIC_REQUIRE( + (interval(0, 5) & interval(2, 3)) == interval(2, 3) + ); + STATIC_REQUIRE( + (interval(0, 5) & interval(3, 10)) == interval(3, 5) + ); + } + } + + SECTION("::operator|=/::operator| (i.e., union)") { + STATIC_REQUIRE( + (interval(0, 1) | interval()) == interval(0, 1) + ); + STATIC_REQUIRE( + (interval() | interval(0, 1)) == interval(0, 1) + ); + + if constexpr (not std::same_as) { + STATIC_REQUIRE( + (interval(0, 5) | interval(2, 3)) == interval(0, 5) + ); + STATIC_REQUIRE( + (interval(0, 5) | interval(3, 10)) == interval(0, 10) + ); + } + } + + SECTION("::contains") { + STATIC_REQUIRE(not interval().contains(0)); + STATIC_REQUIRE(interval(0, 1).contains(0)); + STATIC_REQUIRE(interval(1, 1).contains(1)); + } + + SECTION("printing") { + SECTION("integral") { + std::stringstream ss; + ss << interval(0, 1); + CHECK(ss.str() == "[0, 1]"); + } + + SECTION("floating") { + if constexpr (std::floating_point) { + std::stringstream ss; + ss << interval(.5, 1.5); + CHECK(ss.str() == "[0.5, 1.5]"); + } + } + } + + SECTION("structured binding") { + SECTION("const reference") { + auto in = interval(0, 1); + const auto& [inf, sup] = in; + STATIC_REQUIRE(std::same_as); + STATIC_REQUIRE(std::same_as); + CHECK(inf == 0); + CHECK(sup == 1); + } + + SECTION("rvalue") { + auto in = interval(0, 1); + auto [inf, sup] = in; + STATIC_REQUIRE(std::same_as); + STATIC_REQUIRE(std::same_as); + CHECK(inf == 0); + CHECK(sup == 1); + } + + SECTION("const rvalue") { + const auto in = interval(0, 1); + + auto&& [inf, sup] = in; + STATIC_REQUIRE(std::same_as); + STATIC_REQUIRE(std::same_as); + CHECK(inf == 0); + CHECK(sup == 1); + } + } +} + +} // namespace dwave::optimization