diff --git a/dwave/optimization/include/dwave-optimization/nodes.hpp b/dwave/optimization/include/dwave-optimization/nodes.hpp index 52d86e04..e9e181c0 100644 --- a/dwave/optimization/include/dwave-optimization/nodes.hpp +++ b/dwave/optimization/include/dwave-optimization/nodes.hpp @@ -28,5 +28,6 @@ #include "dwave-optimization/nodes/numbers.hpp" #include "dwave-optimization/nodes/quadratic_model.hpp" #include "dwave-optimization/nodes/reduce.hpp" +#include "dwave-optimization/nodes/set_routines.hpp" #include "dwave-optimization/nodes/testing.hpp" #include "dwave-optimization/nodes/unaryop.hpp" diff --git a/dwave/optimization/include/dwave-optimization/nodes/set_routines.hpp b/dwave/optimization/include/dwave-optimization/nodes/set_routines.hpp index 862f9619..13de318b 100644 --- a/dwave/optimization/include/dwave-optimization/nodes/set_routines.hpp +++ b/dwave/optimization/include/dwave-optimization/nodes/set_routines.hpp @@ -22,6 +22,51 @@ namespace dwave::optimization { +class IsDisjointCoverNode + : public ScalarOutputMixin, false> { + public: + IsDisjointCoverNode(std::span node_ptrs, ssize_t primary_set_size); + + /// @copydoc Array::buff() + double const* buff(const State& state) const override; + + /// @copydoc Node::commit() + void commit(State& state) const override; + + /// @copydoc Array::diff() + std::span diff(const State& state) const override; + + /// @copydoc Node::equal_to() + bool equal_to(const IsDisjointCoverNode& rhs) const override; + + /// @copydoc Node::initialize_state() + void initialize_state(State& state) const override; + + /// @copydoc Array::integral() + bool integral() const override; + + /// @copydoc Array::max() + double max() const override; + + /// @copydoc Array::min() + double min() const override; + + /// The size of the primary set to be covered + ssize_t primary_set_size() const { return primary_set_size_; }; + + /// @copydoc Node::propagate() + void propagate(State& state) const override; + + /// @copydoc Node::revert() + void revert(State& state) const override; + + private: + ssize_t primary_set_size_; + std::vector operands_; + + void replace_predecessor_(ssize_t index, Node* node_ptr) override; +}; + class IsInNode : public ArrayOutputMixin> { public: IsInNode(ArrayNode* element_ptr, ArrayNode* test_elements_ptr); diff --git a/dwave/optimization/libcpp/nodes/set_routines.pxd b/dwave/optimization/libcpp/nodes/set_routines.pxd index 10bd7a1f..64d089da 100644 --- a/dwave/optimization/libcpp/nodes/set_routines.pxd +++ b/dwave/optimization/libcpp/nodes/set_routines.pxd @@ -16,5 +16,8 @@ from dwave.optimization.libcpp.graph cimport ArrayNode cdef extern from "dwave-optimization/nodes/set_routines.hpp" namespace "dwave::optimization" nogil: + cdef cppclass IsDisjointCoverNode(ArrayNode): + Py_ssize_t primary_set_size() const + cdef cppclass IsInNode(ArrayNode): pass diff --git a/dwave/optimization/mathematical.py b/dwave/optimization/mathematical.py index 08a4c59a..3dc26b30 100644 --- a/dwave/optimization/mathematical.py +++ b/dwave/optimization/mathematical.py @@ -37,6 +37,7 @@ Exp, Expit, Extract, + IsDisjointCover, IsIn, LessEqual, LinearProgram, @@ -93,6 +94,7 @@ "expit", "extract", "hstack", + "is_disjoint_cover", "isin", "less_equal", "linprog", @@ -1116,6 +1118,52 @@ def hstack(arrays: collections.abc.Sequence[ArraySymbol]) -> ArraySymbol: return concatenate(arrays, 1) +def is_disjoint_cover(subsets: list[ArraySymbol], *, primary_set_size: int | None = None) -> IsDisjointCover: + """Return whether the symbols are disjoint, set-like, and cover a set of integers. + + Determines whether a collection of array symbols is disjoint and the union equals a fixed set. + + Args: + subsets: List of array symbols to test whether they are disjoint and cover the primary set + primary_set_size: Number of elements in the primary set: {0, 1, ..., primary_set_size - 1}. + Must be non-negative. If `None`, primary set size is inferred from the common, finite, + nonnegative maximum value of the arrays. + + Returns: + A scalar boolean-valued array symbol indicating whether the subsets are a disjoint cover + + Examples: + >>> from dwave.optimization.model import Model + >>> from dwave.optimization.mathematical import is_disjoint_cover + ... + >>> model = Model() + >>> model.states.resize(1) + >>> subsets = [] + >>> subsets.append(model.constant([0, 1])) + >>> subsets.append(model.constant([2, 3, 4])) + >>> subsets.append(model.constant([])) + >>> is_disjoint = is_disjoint_cover(subsets, primary_set_size=5) + >>> with model.lock(): + ... print(is_disjoint.state(0)) + 1.0 + + See Also: + :class:`~dwave.optimization.symbols.IsDisjointCover`: Generated symbol + + :func:`.isin` + + .. versionadded:: 0.7.3 + """ + if primary_set_size is None: + primary_set_size = int(subsets[0].info().max) + 1 + if not np.isfinite(primary_set_size) or primary_set_size <= 0: + raise ValueError("Cannot infer primary set size from subsets") + for subset in subsets[1:]: + if int(subset.info().max) != primary_set_size - 1: + raise ValueError("Cannot infer primary set size from subsets") + return IsDisjointCover(subsets, primary_set_size) + + def isin(element: ArraySymbol, test_elements: ArraySymbol) -> IsIn: """Return which values of one array symbol are in another. diff --git a/dwave/optimization/src/nodes/set_routines.cpp b/dwave/optimization/src/nodes/set_routines.cpp index 8251f76b..c75b2d1e 100644 --- a/dwave/optimization/src/nodes/set_routines.cpp +++ b/dwave/optimization/src/nodes/set_routines.cpp @@ -20,6 +20,199 @@ namespace dwave::optimization { +// IsDisjointCoverNode ******************************************************************* + +struct IsDisjointCoverNodeData : public NodeStateData { + private: + // simple wrapper around ssize_t with default value 1, indicating no violations + struct Count_ { + ssize_t value = 1; + + Count_& operator=(const ssize_t& rhs) { + this->value = rhs; + return *this; + } + + Count_& operator+=(const ssize_t& rhs) { + this->value += rhs; + return *this; + } + + Count_& operator-=(const ssize_t& rhs) { + this->value -= rhs; + return *this; + } + + bool operator==(const ssize_t& rhs) { return this->value == rhs; } + }; + + public: + IsDisjointCoverNodeData(const std::vector& count) : is_disjoint_cover(0, 0.0, 0.0) { + // Record whether the count is not equal to 1 in the count_violations map + for (ssize_t i = 0, stop = count.size(); i < stop; ++i) { + if (count[i] != 1) { + count_violations[i] = count[i]; + } + } + is_disjoint_cover.value = static_cast(count_violations.size() == 0); + is_disjoint_cover.old = is_disjoint_cover.value; + }; + + void propagate() { + // Incorporate changed elements diff into the count_violations map + for (const auto& element : elements_decremented) { + count_violations[element] -= 1; + } + for (const auto& element : elements_incremented) { + count_violations[element] += 1; + } + // Take out any non-violations + for (auto it = count_violations.begin(); it != count_violations.end();) { + if (it->second == 1) { + it = count_violations.erase(it); + } else { + ++it; + } + } + is_disjoint_cover.value = static_cast(count_violations.size() == 0); + }; + + void commit() { + elements_decremented.clear(); + elements_incremented.clear(); + is_disjoint_cover.old = is_disjoint_cover.value; + }; + + void revert() { + for (const auto& element : elements_decremented) { + // Reverse the decrement + count_violations[element] += 1; + } + for (const auto& element : elements_incremented) { + // Reverse the increment + count_violations[element] -= 1; + } + // We'll leave non-violation entries in count_violations - to be cleared in the next + // propagate + elements_decremented.clear(); + elements_incremented.clear(); + is_disjoint_cover.value = is_disjoint_cover.old; + }; + + // The actual logical output; is_disjoint_cover.value is whether the current state is a disjoint + // cover + Update is_disjoint_cover; + // count_violations[i] = the number of times that element `i` appears in the predecessors, if + // not equal to 1 + std::unordered_map count_violations; + // The "diffs" - lists of elements that were removed (respectively, inserted) in the predecessor + // diffs + std::vector elements_decremented; + std::vector elements_incremented; +}; + +IsDisjointCoverNode::IsDisjointCoverNode( + std::span node_ptrs, + ssize_t primary_set_size +) : + ScalarOutputMixin, false>(), + primary_set_size_(primary_set_size) { + for (const auto& node : node_ptrs) { + if (not node->integral()) { + throw std::invalid_argument("Predecessors of DisjointCoverNode must be integral"); + } + if (not (node->max() < primary_set_size)) { + throw std::invalid_argument("Predecessor exceeds primary set size"); + } + if (not (node->min() >= 0)) { + throw std::invalid_argument("Predecessor exceeds primary set size"); + } + add_predecessor_(node); + operands_.push_back(node); + } +} + +void IsDisjointCoverNode::initialize_state(State& state) const { + std::vector count(primary_set_size_, 0); + for (const auto& node : operands_) { + for (const auto& value : node->view(state)) { + ssize_t element = static_cast(value); + // Should not be possible because of checks in constructor + assert(element < primary_set_size_); + assert(element >= 0); + count[element] += 1; + } + } + emplace_data_ptr_(state, std::move(count)); +} + +void IsDisjointCoverNode::commit(State& state) const { + data_ptr_(state)->commit(); +} + +void IsDisjointCoverNode::propagate(State& state) const { + IsDisjointCoverNodeData* data = data_ptr_(state); + + bool no_update = true; + for (const auto& pred : operands_) { + for (const auto& update : pred->diff(state)) { + no_update = false; + if (not update.placed()) { // i.e. removed or changed. + auto element = static_cast(update.old); + data->elements_decremented.push_back(element); + } + if (not update.removed()) { // i.e. placed or changed. + auto element = static_cast(update.value); + data->elements_incremented.push_back(element); + } + } + } + + // If no update, this node is unchanged + if (no_update) { + return; + } + // Else, propagate the populated diffs to rest of the state + data->propagate(); +} + +void IsDisjointCoverNode::revert(State& state) const { + data_ptr_(state)->revert(); +} + +double const* IsDisjointCoverNode::buff(const State& state) const { + return &data_ptr_(state)->is_disjoint_cover.value; +} + +std::span IsDisjointCoverNode::diff(const State& state) const { + auto data = data_ptr_(state); + return std::span( + &data->is_disjoint_cover, data->is_disjoint_cover.old != data->is_disjoint_cover.value + ); +} + +bool IsDisjointCoverNode::equal_to(const IsDisjointCoverNode& rhs) const { + // Check predecessors AND primary set size + return ( + primary_set_size_ == rhs.primary_set_size_ and // + std::ranges::equal(this->predecessors(), rhs.predecessors()) + ); +} + +bool IsDisjointCoverNode::integral() const { return true; } + +double IsDisjointCoverNode::min() const { return 0.0; } + +double IsDisjointCoverNode::max() const { return 1.0; } + +void IsDisjointCoverNode::replace_predecessor_(ssize_t index, Node* node_ptr) { + Node::replace_predecessor_(index, node_ptr); + + assert(0 <= index and static_cast(index) < operands_.size()); + operands_[index] = dynamic_cast(node_ptr); + assert(operands_[index] != nullptr); +} + // IsInNode ******************************************************************* struct IsInNodeSetData { IsInNodeSetData() = default; diff --git a/dwave/optimization/symbols/__init__.py b/dwave/optimization/symbols/__init__.py index 478bf711..3ac14617 100644 --- a/dwave/optimization/symbols/__init__.py +++ b/dwave/optimization/symbols/__init__.py @@ -91,7 +91,7 @@ Prod, Sum, ) -from dwave.optimization.symbols.set_routines import IsIn +from dwave.optimization.symbols.set_routines import IsDisjointCover, IsIn from dwave.optimization.symbols.softmax import SoftMax from dwave.optimization.symbols.sorting import ArgSort from dwave.optimization.symbols.statistics import Mean @@ -142,6 +142,7 @@ "Extract", "Input", "IntegerVariable", + "IsDisjointCover", "IsIn", "LessEqual", "LinearProgram", diff --git a/dwave/optimization/symbols/set_routines.pyi b/dwave/optimization/symbols/set_routines.pyi index 83ff3422..9fed4c72 100644 --- a/dwave/optimization/symbols/set_routines.pyi +++ b/dwave/optimization/symbols/set_routines.pyi @@ -14,4 +14,6 @@ from dwave.optimization.model import ArraySymbol as _ArraySymbol +class IsDisjointCover(_ArraySymbol): ... + class IsIn(_ArraySymbol): ... diff --git a/dwave/optimization/symbols/set_routines.pyx b/dwave/optimization/symbols/set_routines.pyx index bb57618e..2b8ff033 100644 --- a/dwave/optimization/symbols/set_routines.pyx +++ b/dwave/optimization/symbols/set_routines.pyx @@ -14,10 +14,77 @@ # See the License for the specific language governing permissions and # limitations under the License. +import collections.abc +import json + from cython.operator cimport typeid +from libcpp.vector cimport vector + + +from dwave.optimization._model cimport _Graph, _register, ArraySymbol, Symbol +from dwave.optimization.libcpp cimport dynamic_cast_ptr +from dwave.optimization.libcpp.graph cimport ArrayNode +from dwave.optimization.libcpp.nodes.set_routines cimport IsDisjointCoverNode, IsInNode + + +cdef class IsDisjointCover(ArraySymbol): + """Tests whether the symbols are disjoint, set-like, and cover a set of integers + + See Also: + :func:`~dwave.optimization.mathematical.is_disjoint_cover`: Instantiation and usage + of this symbol. + + .. versionadded:: 0.7.3 + """ + def __init__(self, object inputs, Py_ssize_t n): + if (not isinstance(inputs, collections.abc.Sequence) or + not all(isinstance(arr, ArraySymbol) for arr in inputs)): + raise TypeError("disjoint_cover takes a sequence of array symbols") + + if len(inputs) < 1: + raise ValueError("need at least one array symbol to to form a disjoint cover") + + cdef _Graph model = inputs[0].model + cdef vector[ArrayNode*] cppinputs + + for symbol in inputs: + if symbol.model is not model: + raise ValueError("all predecessors must be from the same model") + cppinputs.push_back((symbol).array_ptr) + + self.primary_set_size = n + cdef IsDisjointCoverNode* ptr = model._graph.emplace_node[IsDisjointCoverNode](cppinputs, n) + self.initialize_arraynode(model, ptr) + + @classmethod + def _from_symbol(cls, Symbol symbol): + cdef IsDisjointCoverNode* ptr = dynamic_cast_ptr[IsDisjointCoverNode](symbol.node_ptr) + if not ptr: + raise TypeError(f"given symbol cannot construct a {cls.__name__}") + cdef IsDisjointCover sym = cls.__new__(cls) + sym.primary_set_size = ptr.primary_set_size() + sym.initialize_arraynode(symbol.model, ptr) + return sym + + @classmethod + def _from_zipfile(cls, zf, directory, _Graph model, predecessors): + with zf.open(directory + "args.json", "r") as f: + args = json.load(f) + return cls(list(predecessors), args["primary_set_size"]) + + def _into_zipfile(self, zf, directory): + super()._into_zipfile(zf, directory) + + encoder = json.JSONEncoder(separators=(',', ':')) + + # get the non-array args + args = dict() + args.update(primary_set_size=int(self.primary_set_size)) + zf.writestr(directory + "args.json", encoder.encode(args)) + + cdef Py_ssize_t primary_set_size -from dwave.optimization._model cimport _Graph, _register, ArraySymbol -from dwave.optimization.libcpp.nodes.set_routines cimport IsInNode +_register(IsDisjointCover, typeid(IsDisjointCoverNode)) cdef class IsIn(ArraySymbol): diff --git a/releasenotes/notes/is-disjoint-cover-9f6bd97b637511fc.yaml b/releasenotes/notes/is-disjoint-cover-9f6bd97b637511fc.yaml new file mode 100644 index 00000000..d1d111b2 --- /dev/null +++ b/releasenotes/notes/is-disjoint-cover-9f6bd97b637511fc.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Add C++ ``IsDisjointCoverNode`` node/symbol, which computes whether + a collection of lists are disjoint and cover some primary set. + - Add ``IsDisjointCover`` symbol and `is_disjoint_cover` function. diff --git a/tests/cpp/nodes/test_set_routines.cpp b/tests/cpp/nodes/test_set_routines.cpp index de79611f..cf222e09 100644 --- a/tests/cpp/nodes/test_set_routines.cpp +++ b/tests/cpp/nodes/test_set_routines.cpp @@ -28,6 +28,276 @@ using Catch::Matchers::RangeEquals; namespace dwave::optimization { +TEST_CASE("IsDisjointCoverNode") { + auto graph = Graph(); + + GIVEN("Two constant nodes that are disjoint") { + auto c1_ptr = graph.emplace_node(std::vector{0, 1, 2}); + auto c2_ptr = graph.emplace_node(std::vector{3, 4}); + std::vector sets{c1_ptr, c2_ptr}; + + THEN("We can construct a IsDisjointCover node") { + auto dc_ptr = graph.emplace_node(sets, 5); + graph.emplace_node(dc_ptr); + + CHECK(dc_ptr->min() == 0.0); + CHECK(dc_ptr->max() == 1.0); + CHECK(dc_ptr->integral()); + CHECK(dc_ptr->logical()); + + AND_WHEN("We initialize a state") { + auto state = graph.initialize_state(); + + THEN("The initial IsDisjointCover state is correct") { + CHECK_THAT(dc_ptr->view(state), RangeEquals({1.0})); + } + } + } + THEN("We can construct a IsDisjointCover node with a larger primary set size") { + auto dc_ptr = graph.emplace_node(sets, 6); + graph.emplace_node(dc_ptr); + + CHECK(dc_ptr->min() == 0.0); + CHECK(dc_ptr->max() == 1.0); + CHECK(dc_ptr->integral()); + CHECK(dc_ptr->logical()); + + AND_WHEN("We initialize a state") { + auto state = graph.initialize_state(); + + THEN("The initial IsDisjointCover state is correct") { + CHECK_THAT(dc_ptr->view(state), RangeEquals({0.0})); + } + } + } + THEN("We cannot construct a IsDisjointCover node with a smaller primary set size") { + CHECK_THROWS(graph.emplace_node(sets, 4)); + } + } + + GIVEN("Two constant nodes that are disjoint but not sets") { + auto c1_ptr = graph.emplace_node(std::vector{0, 1, 2, 2}); + auto c2_ptr = graph.emplace_node(std::vector{3, 4}); + std::vector sets{c1_ptr, c2_ptr}; + + THEN("We can construct a IsDisjointCover node") { + auto dc_ptr = graph.emplace_node(sets, 5); + graph.emplace_node(dc_ptr); + + CHECK(dc_ptr->min() == 0.0); + CHECK(dc_ptr->max() == 1.0); + CHECK(dc_ptr->integral()); + CHECK(dc_ptr->logical()); + + AND_WHEN("We initialize a state") { + auto state = graph.initialize_state(); + + THEN("The initial IsDisjointCover state is correct") { + CHECK_THAT(dc_ptr->view(state), RangeEquals({0.0})); + } + } + } + } + + GIVEN("Two set nodes") { + auto set1_ptr = graph.emplace_node(4); + auto set2_ptr = graph.emplace_node(5); + std::vector sets{set1_ptr, set2_ptr}; + + THEN("We can construct a IsDisjointCover node") { + auto dc_ptr = graph.emplace_node(sets, 5); + graph.emplace_node(dc_ptr); + + CHECK(dc_ptr->min() == 0.0); + CHECK(dc_ptr->max() == 1.0); + CHECK(dc_ptr->integral()); + CHECK(dc_ptr->logical()); + + AND_WHEN("We initialize a state with disjoint sets that cover the primary set") { + auto state = graph.initialize_state(); + set1_ptr->assign(state, std::vector{0, 1, 2}); + set2_ptr->assign(state, std::vector{3, 4}); + graph.propagate(state); + + THEN("The initial IsDisjointCover state is correct") { + CHECK_THAT(dc_ptr->view(state), RangeEquals({1.0})); + } + + AND_WHEN("We commit and change the state") { + graph.commit(state); + set1_ptr->assign(state, std::vector{0, 1, 2}); + set2_ptr->assign(state, std::vector{3}); + graph.propagate(state); + + THEN("The IsDisjointCover state is correct") { + CHECK_THAT(dc_ptr->view(state), RangeEquals({0.0})); + } + + AND_WHEN("We revert") { + graph.revert(state); + + THEN("The IsDisjointCover state is correct") { + CHECK_THAT(dc_ptr->view(state), RangeEquals({1.0})); + } + } + } + } + AND_WHEN("We initialize a state with not-disjoint sets") { + auto state = graph.initialize_state(); + set1_ptr->assign(state, std::vector{0, 1, 2}); + set2_ptr->assign(state, std::vector{2, 3, 4}); + graph.propagate(state); + + THEN("The initial IsDisjointCover state is correct") { + CHECK_THAT(dc_ptr->view(state), RangeEquals({0.0})); + } + + AND_WHEN("We commit and change the state") { + graph.commit(state); + set1_ptr->assign(state, std::vector{3, 1, 2}); + set2_ptr->assign(state, std::vector{4, 0}); + graph.propagate(state); + + THEN("The IsDisjointCover state is correct") { + CHECK_THAT(dc_ptr->view(state), RangeEquals({1.0})); + } + + AND_WHEN("We revert") { + graph.revert(state); + + THEN("The IsDisjointCover state is correct") { + CHECK_THAT(dc_ptr->view(state), RangeEquals({0.0})); + } + } + } + } + AND_WHEN("We initialize a state with disjoint sets that do not cover the primary set") { + auto state = graph.initialize_state(); + set1_ptr->assign(state, std::vector{0, 1, 2}); + set2_ptr->assign(state, std::vector{3}); + graph.propagate(state); + + THEN("The initial IsDisjointCover state is correct") { + CHECK_THAT(dc_ptr->view(state), RangeEquals({0.0})); + } + + AND_WHEN("We commit and change the state") { + graph.commit(state); + set1_ptr->assign(state, std::vector{0, 1, 3}); + set2_ptr->assign(state, std::vector{2, 4}); + graph.propagate(state); + + THEN("The IsDisjointCover state is correct") { + CHECK_THAT(dc_ptr->view(state), RangeEquals({1.0})); + } + + AND_WHEN("We revert") { + graph.revert(state); + + THEN("The IsDisjointCover state is correct") { + CHECK_THAT(dc_ptr->view(state), RangeEquals({0.0})); + } + } + } + } + } + } + + GIVEN("Three list nodes") { + auto list1_ptr = graph.emplace_node(5, 0, 5); + auto list2_ptr = graph.emplace_node(5, 0, 5); + auto list3_ptr = graph.emplace_node(5, 0, 5); + std::vector lists{list1_ptr, list2_ptr, list3_ptr}; + + THEN("We can construct a IsDisjointCover node") { + auto dc_ptr = graph.emplace_node(lists, 5); + graph.emplace_node(dc_ptr); + + CHECK(dc_ptr->min() == 0.0); + CHECK(dc_ptr->max() == 1.0); + CHECK(dc_ptr->integral()); + CHECK(dc_ptr->logical()); + + AND_WHEN("We initialize a state with disjoint lists that cover the primary set") { + auto state = graph.initialize_state(); + list1_ptr->assign(state, std::vector{0, 1, 2}); + list2_ptr->assign(state, std::vector{3, 4}); + list3_ptr->assign(state, std::vector{}); + graph.propagate(state); + + THEN("The initial IsDisjointCover state is correct") { + CHECK_THAT(dc_ptr->view(state), RangeEquals({1.0})); + } + + AND_WHEN("We commit and change the state") { + graph.commit(state); + list1_ptr->exchange(state, 0, 2); + graph.propagate(state); + + THEN("The IsDisjointCover state is correct") { + CHECK_THAT(dc_ptr->view(state), RangeEquals({1.0})); + } + } + AND_WHEN("We commit and change the state") { + graph.commit(state); + list1_ptr->shrink(state); + graph.propagate(state); + + THEN("The IsDisjointCover state is correct") { + CHECK_THAT(dc_ptr->view(state), RangeEquals({0.0})); + } + } + } + } + } + + SECTION("equality") { + auto graph = Graph(); + + auto* c0_ptr = graph.emplace_node(std::vector{0, 1, 2, 3}); + auto* c1_ptr = graph.emplace_node(std::vector{4, 5, 6, 7}); + auto* c2_ptr = graph.emplace_node(std::vector{4, 5, 6, 7}); + + std::vector sets01{c0_ptr, c1_ptr}; + std::vector sets02{c0_ptr, c2_ptr}; + + Node* a_ptr = graph.emplace_node(sets01, 8); + Node* b_ptr = graph.emplace_node(sets01, 8); + Node* c_ptr = graph.emplace_node(sets02, 8); + Node* d_ptr = graph.emplace_node(sets01, 9); + + CHECK(a_ptr->equal_to(*a_ptr)); + CHECK(a_ptr->equal_to(*b_ptr)); + CHECK(not a_ptr->equal_to(*c0_ptr)); + CHECK(not a_ptr->equal_to(*c_ptr)); + CHECK(not a_ptr->equal_to(*d_ptr)); + } + + SECTION("predecessor replacement") { + auto graph = Graph(); + + auto* c0_ptr = graph.emplace_node(std::vector{0, 1, 2, 3}); + auto* c1_ptr = graph.emplace_node(std::vector{4, 5, 6, 7}); + std::vector sets01{c0_ptr, c1_ptr}; + auto* dc_ptr = graph.emplace_node(sets01, 8); + + CHECK_THAT(dc_ptr->predecessors(), RangeEquals({c0_ptr, c1_ptr})); + + auto* c2_ptr = graph.emplace_node(std::vector{0, 1, 2, 4}); + auto* c3_ptr = graph.emplace_node(std::vector{3, 5, 6}); + + c2_ptr->take_successors(*c0_ptr); + c3_ptr->take_successors(*c1_ptr); + + CHECK_THAT(dc_ptr->predecessors(), RangeEquals({c2_ptr, c3_ptr})); + + GIVEN("A state") { + auto state = graph.initialize_state(); + CHECK_THAT(dc_ptr->view(state), RangeEquals({0.0})); + } + } +} + TEST_CASE("IsInNode") { auto graph = Graph(); @@ -231,40 +501,40 @@ TEST_CASE("IsInNode") { } GIVEN("Two dynamic set nodes and an isin node") { - auto set1_ptr = graph.emplace_node(6); - auto set2_ptr = graph.emplace_node(6); - auto isin_ptr = graph.emplace_node(set1_ptr, set2_ptr); - graph.emplace_node(isin_ptr); + auto set1_ptr = graph.emplace_node(6); + auto set2_ptr = graph.emplace_node(6); + auto isin_ptr = graph.emplace_node(set1_ptr, set2_ptr); + graph.emplace_node(isin_ptr); - CHECK(isin_ptr->size() == -1); + CHECK(isin_ptr->size() == -1); - WHEN("We initialize a state") { - auto state = graph.initialize_state(); + WHEN("We initialize a state") { + auto state = graph.initialize_state(); - AND_WHEN("We assign the set nodes and propagate") { - set1_ptr->assign(state, std::vector{2}); - set2_ptr->assign(state, std::vector{2}); - graph.propagate(state); + AND_WHEN("We assign the set nodes and propagate") { + set1_ptr->assign(state, std::vector{2}); + set2_ptr->assign(state, std::vector{2}); + graph.propagate(state); - THEN("The isin's state is correct") { - CHECK_THAT(isin_ptr->view(state), RangeEquals({1.0})); - } + THEN("The isin's state is correct") { + CHECK_THAT(isin_ptr->view(state), RangeEquals({1.0})); + } - AND_WHEN("We commit, shrink the state of both set nodes, and propagate") { - graph.commit(state); + AND_WHEN("We commit, shrink the state of both set nodes, and propagate") { + graph.commit(state); - set1_ptr->shrink(state); - set2_ptr->shrink(state); + set1_ptr->shrink(state); + set2_ptr->shrink(state); - graph.propagate(state); + graph.propagate(state); - THEN("The isin's state is correct") { - CHECK(isin_ptr->view(state).size() == 0); - } + THEN("The isin's state is correct") { + CHECK(isin_ptr->view(state).size() == 0); } } } } + } SECTION("predecessor replacement") { auto* e0_ptr = graph.emplace_node(std::vector{0, 1, 6, 7}); @@ -284,4 +554,5 @@ TEST_CASE("IsInNode") { CHECK_THAT(isin_ptr->view(state), RangeEquals({0, 0, 0, 1})); } } + } // namespace dwave::optimization diff --git a/tests/test_model.py b/tests/test_model.py index b265c678..91deacbd 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -395,6 +395,7 @@ def test_remove_unused_symbols(self): self.assertEqual(num_removed, 2) self.assertEqual(model.num_symbols(), 1) + with self.subTest("disjoint lists"): model = Model() diff --git a/tests/test_symbols.py b/tests/test_symbols.py index 8ea3ce41..d20926fa 100644 --- a/tests/test_symbols.py +++ b/tests/test_symbols.py @@ -1532,6 +1532,51 @@ def test_state_serialization_uninitialized(self): np.testing.assert_array_equal(ys[2].state(1), [0, 0, 0, 0, 1]) +class TestDisjointCover(utils.SymbolTests): + def generate_symbols(self): + model = Model() + sets = [ + model.constant([0, 1, 2]), + model.constant([3, 4]) + ] + cover = dwave.optimization.symbols.IsDisjointCover(sets, 5) + + with model.lock(): + yield cover + + def test(self): + from dwave.optimization.symbols import IsDisjointCover + model = Model() + sets = [ + model.constant([0, 1, 2]), + model.constant([3, 4]) + ] + cover = dwave.optimization.symbols.IsDisjointCover(sets, 5) + self.assertIsInstance(cover, IsDisjointCover) + + def test_state(self): + model = Model() + # a disjoint cover + sets = [ + model.constant([0, 1, 2]), + model.constant([3, 4]) + ] + cover = dwave.optimization.symbols.IsDisjointCover(sets, 5) + model.states.resize(1) + with model.lock(): + expected = np.array([1.0]) + np.testing.assert_array_almost_equal(cover.state(0), expected) + + # not disjoint + sets = [ + model.constant([0, 1, 2]), + model.constant([2, 3, 4]) + ] + cover = dwave.optimization.symbols.IsDisjointCover(sets, 5) + with model.lock(): + expected = np.array([0.0]) + np.testing.assert_array_almost_equal(cover.state(0), expected) + class TestDisjointListsVariable(utils.SymbolTests): def test_inequality(self): # TODO re-enable this once equality has been fixed