From 82cc356c370f0ad41c3e7d8b17e97b17eccd365c Mon Sep 17 00:00:00 2001 From: Alexander Condello Date: Tue, 23 Jun 2026 14:31:32 -0700 Subject: [PATCH 01/25] Add methods to remove redundant nodes --- dwave/optimization/_model.pyx | 12 +- .../include/dwave-optimization/graph.hpp | 30 ++++ .../dwave-optimization/nodes/binaryop.hpp | 7 +- .../dwave-optimization/nodes/constants.hpp | 3 + .../include/dwave-optimization/nodes/flow.hpp | 12 ++ .../dwave-optimization/nodes/indexing.hpp | 12 ++ .../dwave-optimization/nodes/manipulation.hpp | 12 ++ .../dwave-optimization/nodes/reduce.hpp | 6 + .../dwave-optimization/nodes/unaryop.hpp | 17 +- dwave/optimization/libcpp/graph.pxd | 4 +- dwave/optimization/src/graph.cpp | 170 ++++++++++++++++++ dwave/optimization/src/nodes/binaryop.cpp | 53 ++++++ dwave/optimization/src/nodes/constants.cpp | 12 ++ dwave/optimization/src/nodes/flow.cpp | 43 +++++ dwave/optimization/src/nodes/indexing.cpp | 34 ++++ dwave/optimization/src/nodes/manipulation.cpp | 33 ++++ dwave/optimization/src/nodes/reduce.cpp | 18 ++ dwave/optimization/src/nodes/unaryop.cpp | 20 +++ tests/cpp/test_graph.cpp | 65 +++++++ 19 files changed, 554 insertions(+), 9 deletions(-) diff --git a/dwave/optimization/_model.pyx b/dwave/optimization/_model.pyx index 2eb45de09..0c70173f2 100644 --- a/dwave/optimization/_model.pyx +++ b/dwave/optimization/_model.pyx @@ -928,6 +928,16 @@ cdef class _Graph: """ return self.num_nodes() + def remove_redundant_symbols(self, time_limit_s=None): + """TODO + """ + if self.is_locked(): + raise ValueError("cannot remove symbols from a locked model") + if time_limit_s is None: + return self._graph.remove_redundant_nodes(False) + else: + return self._graph.remove_redundant_nodes(False, time_limit_s) + def remove_unused_symbols(self): """Remove unused symbols from the model. @@ -981,7 +991,7 @@ cdef class _Graph: """ if self.is_locked(): raise ValueError("cannot remove symbols from a locked model") - return self._graph.remove_unused_nodes() + return self._graph.remove_unused_nodes(False) def state_size(self): r"""Return an estimate of the size, in bytes, of a model's state. diff --git a/dwave/optimization/include/dwave-optimization/graph.hpp b/dwave/optimization/include/dwave-optimization/graph.hpp index f10787cd5..1e2fd226b 100644 --- a/dwave/optimization/include/dwave-optimization/graph.hpp +++ b/dwave/optimization/include/dwave-optimization/graph.hpp @@ -158,6 +158,13 @@ class Graph { /// Reset the state of the given node and all successors recursively. static void recursive_reset(State& state, const Node* ptr); + // todo: document + // - all root nodes (including constants!) are ignored + ssize_t remove_redundant_nodes( + bool ignore_listeners = false, + double time_limit_s = std::numeric_limits::infinity() + ); + /// Remove unused nodes from the graph. /// /// This method will reset the topological sort if there is one. @@ -253,6 +260,16 @@ class Node { Node& operator=(const Node&) = delete; Node& operator=(Node&&) noexcept = delete; + // TODO: document and note that nodes *must* share the same set of + // predecessors (permutations are sometimes allowed) and they *must* be the + // same type. Also that they can have false negatives in some cases. + // Also, we don't check pinned values + virtual bool operator==(const Node& rhs) const { + std::cout << classname() << " missing operator==(...) *********\n"; + assert(false and "not yet implemented"); + return false; + } + /// Methods for interrogating nodes as strings. Useful for error messages /// and debugging. We roughly follow Python's scheme of repr() and str() /// printing different information. @@ -307,6 +324,11 @@ class Node { /// Return the successors of the node. const std::vector& successors() const { return successors_; } + /// Take the successors from `from` and make them successors of the node. + /// Note that this bypasses most checks! Use with extreme caution as it + /// make create undefined models. + void take_successors(Node& from); + /// The current topological index. Will be negative if unsorted. ssize_t topological_index() const { return topological_index_; } @@ -324,6 +346,7 @@ class Node { friend void Graph::reset_topological_sort(); template friend NodeType* Graph::emplace_node(Args&&...); + friend ssize_t Graph::remove_redundant_nodes(bool, double); friend ssize_t Graph::remove_unused_nodes(bool); protected: @@ -382,6 +405,9 @@ class Node { // Remove a successor. *Does not* remove itself from it's successor's predecessors. ssize_t remove_successor_(const Node* ptr); + // todo: document + virtual void replace_predecessor_(ssize_t previous_index, Node* node_ptr); + private: ssize_t topological_index_ = -1; // negative is unset @@ -430,6 +456,10 @@ NodeType* Graph::emplace_node(Args&&... args) { class ArrayNode : public Array, public virtual Node {}; class DecisionNode : public Decision, public virtual Node { public: + /// Decision nodes are never equal to eachother because they are + /// independent variables. + bool operator==(const Node&) const override { return false; } + /// Decision nodes by definition do not have a deterministic state. bool deterministic_state() const final { return false; } diff --git a/dwave/optimization/include/dwave-optimization/nodes/binaryop.hpp b/dwave/optimization/include/dwave-optimization/nodes/binaryop.hpp index fb0a62e46..ed4d31bb2 100644 --- a/dwave/optimization/include/dwave-optimization/nodes/binaryop.hpp +++ b/dwave/optimization/include/dwave-optimization/nodes/binaryop.hpp @@ -34,6 +34,9 @@ class BinaryOpNode : public ArrayOutputMixin { // We need at least two nodes, and they must be the same shape BinaryOpNode(ArrayNode* a_ptr, ArrayNode* b_ptr); + bool operator==(const Node& rhs) const override; + bool operator==(const BinaryOpNode& rhs) const; + double const* buff(const State& state) const override; std::span diff(const State& state) const override; @@ -72,11 +75,13 @@ class BinaryOpNode : public ArrayOutputMixin { } private: + void replace_predecessor_(ssize_t previous_index, Node* node_ptr) override; + BinaryOp op; // There are redundant, because we could dynamic_cast each time from // predecessors(), but this is more performant - std::array operands_; + std::array operands_; const ValuesInfo values_info_; const SizeInfo sizeinfo_; diff --git a/dwave/optimization/include/dwave-optimization/nodes/constants.hpp b/dwave/optimization/include/dwave-optimization/nodes/constants.hpp index 2fc0f194b..96dd0e91c 100644 --- a/dwave/optimization/include/dwave-optimization/nodes/constants.hpp +++ b/dwave/optimization/include/dwave-optimization/nodes/constants.hpp @@ -73,6 +73,9 @@ class ConstantNode : public ArrayOutputMixin { ConstantNode(Range&& values, const std::span shape) : ConstantNode(from_range(std::forward(values), shape), shape) {} + bool operator==(const Node& rhs) const override; + bool operator==(const ConstantNode& rhs) const; + // Stateless access to the underlying data. std::span data() const noexcept { return std::span(buffer_ptr_, this->size()); diff --git a/dwave/optimization/include/dwave-optimization/nodes/flow.hpp b/dwave/optimization/include/dwave-optimization/nodes/flow.hpp index 0487636a7..7e2dbd3d9 100644 --- a/dwave/optimization/include/dwave-optimization/nodes/flow.hpp +++ b/dwave/optimization/include/dwave-optimization/nodes/flow.hpp @@ -30,6 +30,9 @@ class ExtractNode : public ArrayOutputMixin { public: ExtractNode(ArrayNode* condition_ptr, ArrayNode* arr_ptr); + bool operator==(const Node& rhs) const override; + bool operator==(const ExtractNode& rhs) const; + /// @copydoc Array::buff() double const* buff(const State& state) const override; @@ -73,6 +76,9 @@ class ExtractNode : public ArrayOutputMixin { /// @copydoc Array::sizeinfo() SizeInfo sizeinfo() const override; + protected: + void replace_predecessor_(ssize_t previous_index, Node* node_ptr) override; + private: // these are redundant, but convenient const Array* condition_ptr_; @@ -91,6 +97,9 @@ class WhereNode : public ArrayOutputMixin { public: WhereNode(ArrayNode* condition_ptr, ArrayNode* x_ptr, ArrayNode* y_ptr); + bool operator==(const Node& rhs) const override; + bool operator==(const WhereNode& rhs) const; + double const* buff(const State& state) const override; void commit(State& state) const override; std::span diff(const State& state) const override; @@ -116,6 +125,9 @@ class WhereNode : public ArrayOutputMixin { /// @copydoc Array::sizeinfo() SizeInfo sizeinfo() const override; + protected: + void replace_predecessor_(ssize_t previous_index, Node* node_ptr) override; + private: // these are redundant, but convenient const Array* condition_ptr_; diff --git a/dwave/optimization/include/dwave-optimization/nodes/indexing.hpp b/dwave/optimization/include/dwave-optimization/nodes/indexing.hpp index c3968d0d9..a6d51b096 100644 --- a/dwave/optimization/include/dwave-optimization/nodes/indexing.hpp +++ b/dwave/optimization/include/dwave-optimization/nodes/indexing.hpp @@ -56,6 +56,9 @@ class AdvancedIndexingNode : public ArrayNode { explicit AdvancedIndexingNode(ArrayNode* array_ptr, Indices... indices) : AdvancedIndexingNode(array_ptr, make_indices(indices...)) {} + bool operator==(const Node& rhs) const override; + bool operator==(const AdvancedIndexingNode& rhs) const; + // Array overloads ssize_t ndim() const noexcept override { return ndim_; } double const* buff(const State& state) const override; @@ -87,6 +90,9 @@ class AdvancedIndexingNode : public ArrayNode { // AdvancedIndexingHelper methods std::span indices() const { return indices_; } + protected: + void replace_predecessor_(ssize_t previous_index, Node* node_ptr) override; + private: struct IndexParser_; AdvancedIndexingNode(ArrayNode* array_ptr, IndexParser_&& parser); @@ -194,6 +200,9 @@ class BasicIndexingNode : public ArrayNode { explicit BasicIndexingNode(ArrayNode* array_ptr, Indices... indices) : BasicIndexingNode(array_ptr, make_indices(indices...)) {} + bool operator==(const Node& rhs) const override; + bool operator==(const BasicIndexingNode& rhs) const; + // Overloads needed by the Array ABC ************************************** ssize_t ndim() const noexcept override { return ndim_; } @@ -242,6 +251,9 @@ class BasicIndexingNode : public ArrayNode { // Infer the indices used to create the node. std::vector infer_indices() const; + protected: + void replace_predecessor_(ssize_t previous_index, Node* node_ptr) override; + private: // Private constructor using an intermediate object struct IndexParser_; diff --git a/dwave/optimization/include/dwave-optimization/nodes/manipulation.hpp b/dwave/optimization/include/dwave-optimization/nodes/manipulation.hpp index 19ee20943..4d26bfee0 100644 --- a/dwave/optimization/include/dwave-optimization/nodes/manipulation.hpp +++ b/dwave/optimization/include/dwave-optimization/nodes/manipulation.hpp @@ -31,6 +31,9 @@ class BroadcastToNode : public ArrayNode { BroadcastToNode(ArrayNode* array_ptr, std::initializer_list shape); BroadcastToNode(ArrayNode* array_ptr, std::span shape); + bool operator==(const Node& rhs) const override; + bool operator==(const BroadcastToNode& rhs) const; + /// @copydoc Array::buff() double const* buff(const State& state) const override; @@ -78,6 +81,9 @@ class BroadcastToNode : public ArrayNode { /// @copydoc Array::strides() std::span strides() const override; + protected: + void replace_predecessor_(ssize_t previous_index, Node* node_ptr) override; + private: /// Translate a linear index of the predecessor into a linear index of the /// BroadcastToNode. @@ -327,6 +333,9 @@ class ResizeNode : public ArrayOutputMixin { ResizeNode(ArrayNode* node_ptr, Range&& shape, double fill_value = 0) : ResizeNode(node_ptr, std::vector(shape.begin(), shape.end()), fill_value) {} + bool operator==(const Node& rhs) const override; + bool operator==(const ResizeNode& rhs) const; + /// @copydoc Array::buff() double const* buff(const State& state) const override; @@ -357,6 +366,9 @@ class ResizeNode : public ArrayOutputMixin { /// @copydoc Node::revert() void revert(State& state) const override; + protected: + void replace_predecessor_(ssize_t previous_index, Node* node_ptr) override; + private: const Array* array_ptr_; diff --git a/dwave/optimization/include/dwave-optimization/nodes/reduce.hpp b/dwave/optimization/include/dwave-optimization/nodes/reduce.hpp index 9037edc4b..dcaf6afc5 100644 --- a/dwave/optimization/include/dwave-optimization/nodes/reduce.hpp +++ b/dwave/optimization/include/dwave-optimization/nodes/reduce.hpp @@ -44,6 +44,9 @@ class ReduceNode : public ArrayOutputMixin { std::optional initial = std::nullopt ); + bool operator==(const Node& rhs) const override; + bool operator==(const ReduceNode& rhs) const; + /// The axes over which the reduction is performed. std::span axes() const { return axes_; } @@ -102,6 +105,9 @@ class ReduceNode : public ArrayOutputMixin { /// Otherwise uses the first element in the reduction. const std::optional initial; + protected: + void replace_predecessor_(ssize_t previous_index, Node* node_ptr) override; + private: // Perform a reduction over the reduction space associated with the given // index. The return type is determined by the reduction_type, see diff --git a/dwave/optimization/include/dwave-optimization/nodes/unaryop.hpp b/dwave/optimization/include/dwave-optimization/nodes/unaryop.hpp index 6f633ae57..dd99474c8 100644 --- a/dwave/optimization/include/dwave-optimization/nodes/unaryop.hpp +++ b/dwave/optimization/include/dwave-optimization/nodes/unaryop.hpp @@ -32,6 +32,9 @@ class UnaryOpNode : public ArrayOutputMixin { public: explicit UnaryOpNode(ArrayNode* node_ptr); + bool operator==(const Node& rhs) const override; + bool operator==(const UnaryOpNode& rhs) const; + double const* buff(const State& state) const override; std::span diff(const State& state) const override; @@ -57,21 +60,23 @@ class UnaryOpNode : public ArrayOutputMixin { void propagate(State& state) const override; // The predecessor of the operation, as an Array*. - std::span operands() { + std::span operands() { assert(predecessors().size() == 1); - return std::span(&array_ptr_, 1); + return std::span(&array_ptr_, 1); } - std::span operands() const { + std::span operands() const { assert(predecessors().size() == 1); - return std::span(&array_ptr_, 1); + return std::span(&array_ptr_, 1); } private: + void replace_predecessor_(ssize_t previous_index, Node* node_ptr) override; + UnaryOp op; - // There are redundant, because we could dynamic_cast each time from + // This is redundant, because we could dynamic_cast each time from // predecessors(), but this is more performant - Array* const array_ptr_; + ArrayNode* array_ptr_; const ValuesInfo values_info_; const SizeInfo sizeinfo_; diff --git a/dwave/optimization/libcpp/graph.pxd b/dwave/optimization/libcpp/graph.pxd index c4cade7fc..4cf32453c 100644 --- a/dwave/optimization/libcpp/graph.pxd +++ b/dwave/optimization/libcpp/graph.pxd @@ -64,9 +64,11 @@ cdef extern from "dwave-optimization/graph.hpp" namespace "dwave::optimization" void recursive_initialize(State&, Node*) except+ @staticmethod void recursive_reset(State&, Node*) except+ + Py_ssize_t remove_redundant_nodes(bool) except+ + Py_ssize_t remove_redundant_nodes(bool, double) except+ + Py_ssize_t remove_unused_nodes(bool) except+ void reset_topological_sort() void set_objective(ArrayNode*) except+ void add_constraint(ArrayNode*) except+ void topological_sort() bool topologically_sorted() const - Py_ssize_t remove_unused_nodes() diff --git a/dwave/optimization/src/graph.cpp b/dwave/optimization/src/graph.cpp index 1eb25b5f9..a475fb78f 100644 --- a/dwave/optimization/src/graph.cpp +++ b/dwave/optimization/src/graph.cpp @@ -15,6 +15,7 @@ #include "dwave-optimization/graph.hpp" #include +#include #include #include #include @@ -241,6 +242,159 @@ void Graph::recursive_reset(State& state, const Node* ptr) { } } +ssize_t Graph::remove_redundant_nodes(bool ignore_listeners, double time_limit_s) { + if (topologically_sorted_) throw std::logic_error("cannot remove nodes from a locked model"); + + // This function can get quite expensive, so we have a timeout we'll try to respect. + const auto stop_time = + std::chrono::steady_clock::now() + std::chrono::duration(time_limit_s); + auto out_of_time = [&stop_time]() -> bool { + return std::chrono::steady_clock::now() >= stop_time; + }; + + // We need to know how many nodes we started with in order to know how many we removed + const ssize_t num_nodes = this->num_nodes(); + + // We want the nodes in topological order, and we will respect that order + // while we're transferring successors. + // We will, however, abuse the topological_index_ to store information + topological_sort(); + + // We'll set the topological index to one of these values to track what we've + // already done. + // They are arbitrary values, but we steer away from -1 because that's used to + // indicate unsorted + constexpr ssize_t seen = -2; // already checked for duplicates + constexpr ssize_t drop = -3; // marked for deletion + + // The actions that we always want to do before returning, whether it's because + // we hit the time limit or because we have no more work to do. + auto cleanup = [&]() -> ssize_t { + // we should only ever have swapped the objective_ptr + assert(objective_ptr_ == nullptr or objective_ptr_->topological_index() != drop); + + // Whether a node was dropped + auto dropped = [&drop, &ignore_listeners](const auto& ptr) { + if (not ignore_listeners and ptr->num_listeners() > 0) return false; + assert(ptr->topological_index_ != drop or ptr->successors_.empty()); + return ptr->topological_index() == drop; + }; + + // Go through our "special" nodes and drop anything + assert(std::ranges::none_of(decisions_, dropped)); // should never be dropped + assert(std::ranges::none_of(inputs_, dropped)); // should never be dropped + std::erase_if(constants_, dropped); + + std::erase_if(constraints_, dropped); + assert(objective_ptr_ == nullptr or objective_ptr_->topological_index_ != drop); + + // This is the step that actually deallocates the node + for (auto& uptr : nodes_) { + if (not dropped(uptr)) continue; + + // Remove the node from its predecessor's successor vectors. + // This very leaves the node in an invalid state, very briefly + for (auto* pred_ptr : uptr->predecessors_) { + [[maybe_unused]] ssize_t num_removed = pred_ptr->remove_successor_(uptr.get()); + assert(num_removed > 0); + } + + // And then reset the pointer, thereby dellocating the node + uptr.reset(); + + } + // Finally, remove the nullptrs from the nodelist + std::erase_if(nodes_, [](const auto& uptr) { return not uptr; }); + + // Reset the topological index of everything that's left so we clean up + // everything + reset_topological_sort(); + + // Finally, report the number of nodes we dropped + return num_nodes - this->num_nodes(); + }; + + // Ok, all that setup done, now let's start checking for redundancy. + + // Decisions are never redundant, so we skip over them + + // Nor are inputs, so likewise we skip them + + // The first set of nodes we're worried about are the constants - the only + // class of root node that can have redundancy + for (ssize_t i = 0, num_constants = constants_.size(); i < num_constants; ++i) { + if (constants_[i]->topological_index_ == drop) continue; + + for (ssize_t j = i + 1; j < num_constants; ++j) { + if (constants_[j]->topological_index_ == drop) continue; + + // the topological indices of our constants should not yet be touched + // and because they are added in order, they should always be ascending + assert(constants_[i]->topological_index_ >= 0); + assert(constants_[j]->topological_index_ >= 0); + assert(constants_[i]->topological_index_ < constants_[j]->topological_index_); + + // Check against or current time limit before doing the potentially + // expensive (O(buffer_size)) equality check. + if (out_of_time()) return cleanup(); + + // nothing to do if they are not equal + if (*constants_[i] != *constants_[j]) continue; + + constants_[i]->take_successors(*constants_[j]); + + constants_[j]->topological_index_ = drop; + } + + constants_[i]->topological_index_ = seen; + } + + // For each node, we check pairwise among all of its successors. + // This is because for nodes to be equal, they *must* have the same set of + // predecessors. + for (auto& uptr : nodes_) { + auto& successors = uptr->successors(); + + for (ssize_t i = 0, num_successors = successors.size(); i < num_successors; ++i) { + // We've already seen or dropped this node + if (successors[i]->topological_index_ < 0) continue; + + for (ssize_t j = i + 1; j < num_successors; ++j) { + // We've already seen or dropped this node + if (successors[j]->topological_index_ < 0) continue; + + // A node cannot be redundant with itself + if (successors[i] == successors[j]) continue; + + // Check against or current time limit before doing the potentially + // expensive (O(num_nodes^2)) equality check. + if (out_of_time()) return cleanup(); + + // If lhs != rhs there's nothing to do, so keep looking + if (*successors[i] != *successors[j]) continue; + + // We have a redundant node! + + // We want to transfer to the node with the lower topological order + if (successors[i]->topological_index_ < successors[j]->topological_index_) { + successors[i]->take_successors(*successors[j]); + successors[j]->topological_index_ = drop; + } else { + successors[j]->take_successors(*successors[i]); + successors[i]->topological_index_ = drop; + break; // stop comparing i to other nodes because we dropped it + } + } + + // Ok, we've checked i against everything, so if we didn't drop it + // we can mark it as seen + if (successors[i]->topological_index_ >= 0) successors[i]->topological_index_ = seen; + } + } + + return cleanup(); +} + ssize_t Graph::remove_unused_nodes(bool ignore_listeners) { if (topologically_sorted_) throw std::logic_error("cannot remove nodes from a locked model"); @@ -523,6 +677,22 @@ std::string Node::repr() const { std::string Node::str() const { return classname(); } +void Node::take_successors(Node& from) { + assert(this != &from and "a node cannot take successors from itself"); + + for (const auto& sv : from.successors_) { + sv.ptr->replace_predecessor_(sv.index, this); + this->successors_.emplace_back(sv); + } + from.successors_.clear(); +} + +void Node::replace_predecessor_(ssize_t previous_index, Node* node_ptr) { + assert(0 <= previous_index); + assert(static_cast(previous_index) < predecessors_.size()); + predecessors_[previous_index] = node_ptr; +} + std::ostream& operator<<(std::ostream& os, const Node& node) { return os << node.repr(); } [[noreturn]] void DecisionNode::update(State& state, int index) const { diff --git a/dwave/optimization/src/nodes/binaryop.cpp b/dwave/optimization/src/nodes/binaryop.cpp index 2241cc923..0b92296d2 100644 --- a/dwave/optimization/src/nodes/binaryop.cpp +++ b/dwave/optimization/src/nodes/binaryop.cpp @@ -198,6 +198,50 @@ BinaryOpNode::BinaryOpNode(ArrayNode* a_ptr, ArrayNode* b_ptr) : this->add_predecessor_(b_ptr); } +template +bool BinaryOpNode::operator==(const Node& rhs) const { + const auto* rhs_ptr = dynamic_cast(&rhs); + if (rhs_ptr == nullptr) return false; // not same type so not equal + return *this == *rhs_ptr; +} + +template +bool BinaryOpNode::operator==(const BinaryOpNode& rhs) const { + // if we're the same type, then we just need to make sure we're operating + // on the same operands (subject to whether we're commutative or not. + // Once we switch to ufuncs, this gets even easier + if (std::ranges::equal(operands_, rhs.operands_)) return true; + + // Once we switch to unfuncs + // https://github.com/dwavesystems/dwave-optimization/pull/412 + // we can use ::communcative. For now we hardcode it. + + if constexpr ( + std::same_as or // + std::same_as or // + std::same_as or // + std::same_as or // + std::same_as or // + std::same_as or // + std::same_as or // + std::same_as + ) { + // commutative + return std::ranges::equal(operands_, rhs.operands_ | std::views::reverse); + } else { + // not commutative + static_assert( + std::same_as or // + std::same_as or // + std::same_as or // + std::same_as or // + std::same_as + ); + } + + return false; +} + template double const* BinaryOpNode::buff(const State& state) const { return data_ptr_(state)->buff(); @@ -387,6 +431,15 @@ void BinaryOpNode::propagate(State& state) const { if (ptr->diff().size()) Node::propagate(state); } +template +void BinaryOpNode::replace_predecessor_(ssize_t previous_index, Node* node_ptr) { + Node::replace_predecessor_(previous_index, node_ptr); + + assert(0 <= previous_index and previous_index < 2); + operands_[previous_index] = dynamic_cast(node_ptr); + assert(operands_[previous_index] != nullptr); +} + template void BinaryOpNode::revert(State& state) const { data_ptr_(state)->revert(); diff --git a/dwave/optimization/src/nodes/constants.cpp b/dwave/optimization/src/nodes/constants.cpp index 98f795799..93da0ede5 100644 --- a/dwave/optimization/src/nodes/constants.cpp +++ b/dwave/optimization/src/nodes/constants.cpp @@ -65,6 +65,18 @@ ConstantNode::ConstantNode(OwningDataSource&& data_source, const std::span(buffer_ptr_, this->size()))), data_source_(std::make_unique(std::move(data_source))) {} +bool ConstantNode::operator==(const Node& rhs) const { + const auto* rhs_ptr = dynamic_cast(&rhs); + if (rhs_ptr == nullptr) return false; // not same type so not equal + return *this == *rhs_ptr; +} + +bool ConstantNode::operator==(const ConstantNode& rhs) const { + return this->ndim() == rhs.ndim() and // same ndim + std::ranges::equal(this->shape(), rhs.shape()) and // same shape + std::ranges::equal(this->data(), rhs.data()); // same content +} + bool ConstantNode::integral() const { return this->values_info_.integral; } double ConstantNode::min() const { return this->values_info_.min; } diff --git a/dwave/optimization/src/nodes/flow.cpp b/dwave/optimization/src/nodes/flow.cpp index ebd464b28..000432915 100644 --- a/dwave/optimization/src/nodes/flow.cpp +++ b/dwave/optimization/src/nodes/flow.cpp @@ -63,6 +63,16 @@ ExtractNode::ExtractNode(ArrayNode* condition_ptr, ArrayNode* arr_ptr) : add_predecessor_(arr_ptr); } +bool ExtractNode::operator==(const Node& rhs) const { + const auto* rhs_ptr = dynamic_cast(&rhs); + if (rhs_ptr == nullptr) return false; // not same type so not equal + return *this == *rhs_ptr; +} + +bool ExtractNode::operator==(const ExtractNode& rhs) const { + return condition_ptr_ == rhs.condition_ptr_ and arr_ptr_ == rhs.arr_ptr_; +} + double const* ExtractNode::buff(const State& state) const { return data_ptr_(state)->buff(); } @@ -143,6 +153,10 @@ void ExtractNode::propagate(State& state) const { node_data->assign(std::move(new_values), count); } +void ExtractNode::replace_predecessor_(ssize_t previous_index, Node* node_ptr) { + assert(false and "not yet implemented"); +} + void ExtractNode::revert(State& state) const { data_ptr_(state)->revert(); } std::span ExtractNode::shape(const State& state) const { @@ -266,6 +280,16 @@ WhereNode::WhereNode(ArrayNode* condition_ptr, ArrayNode* x_ptr, ArrayNode* y_pt add_predecessor_(y_ptr); } +bool WhereNode::operator==(const Node& rhs) const { + const auto* rhs_ptr = dynamic_cast(&rhs); + if (rhs_ptr == nullptr) return false; // not same type so not equal + return *this == *rhs_ptr; +} + +bool WhereNode::operator==(const WhereNode& rhs) const { + return condition_ptr_ == rhs.condition_ptr_ and x_ptr_ == rhs.x_ptr_ and y_ptr_ == rhs.y_ptr_; +} + double const* WhereNode::buff(const State& state) const { return data_ptr_(state)->buff(); } @@ -364,6 +388,25 @@ void WhereNode::propagate(State& state) const { } } +void WhereNode::replace_predecessor_(ssize_t previous_index, Node* node_ptr) { + Node::replace_predecessor_(previous_index, node_ptr); + + assert(0 <= previous_index and previous_index < 3); + if (previous_index == 0) { + condition_ptr_ = dynamic_cast(node_ptr); + assert(condition_ptr_ != nullptr); + } else if (previous_index == 1) { + x_ptr_ = dynamic_cast(node_ptr); + assert(x_ptr_ != nullptr); + } else if (previous_index == 2) { + y_ptr_ = dynamic_cast(node_ptr); + assert(y_ptr_ != nullptr); + } else { + assert(false); + unreachable(); + } +} + void WhereNode::revert(State& state) const { data_ptr_(state)->revert(); } std::span WhereNode::shape(const State& state) const { diff --git a/dwave/optimization/src/nodes/indexing.cpp b/dwave/optimization/src/nodes/indexing.cpp index cb753b573..464763d57 100644 --- a/dwave/optimization/src/nodes/indexing.cpp +++ b/dwave/optimization/src/nodes/indexing.cpp @@ -520,6 +520,17 @@ AdvancedIndexingNode::AdvancedIndexingNode(ArrayNode* array_ptr, IndexParser_&& } } +bool AdvancedIndexingNode::operator==(const Node& rhs) const { + const auto* rhs_ptr = dynamic_cast(&rhs); + if (rhs_ptr == nullptr) return false; // not same type so not equal + return *this == *rhs_ptr; +} + +bool AdvancedIndexingNode::operator==(const AdvancedIndexingNode& rhs) const { + assert(false and "not yet implemented"); + return false; +} + double const* AdvancedIndexingNode::buff(const State& state) const { return data_ptr_(state)->data.data(); } @@ -743,6 +754,10 @@ void AdvancedIndexingNode::commit(State& state) const { data_ptr_(state)->commit(); } +void AdvancedIndexingNode::replace_predecessor_(ssize_t previous_index, Node* node_ptr) { + assert(false and "not yet implemented"); +} + void AdvancedIndexingNode::revert(State& state) const { data_ptr_(state)->revert(); @@ -1387,6 +1402,21 @@ BasicIndexingNode::BasicIndexingNode(ArrayNode* array_ptr, IndexParser_&& parser add_predecessor_(array_ptr); } +bool BasicIndexingNode::operator==(const Node& rhs) const { + const auto* rhs_ptr = dynamic_cast(&rhs); + if (rhs_ptr == nullptr) return false; // not same type so not equal + return *this == *rhs_ptr; +} + +bool BasicIndexingNode::operator==(const BasicIndexingNode& rhs) const { + return (array_ptr_ == rhs.array_ptr_ and + ndim_ == rhs.ndim_ and + start_ == rhs.start_ and + std::ranges::equal(shape(), rhs.shape()) and + std::ranges::equal(strides(), rhs.strides()) + ); +} + struct BasicIndexingNodeData : NodeStateData { BasicIndexingNodeData(const BasicIndexingNode* node) { if (node->dynamic()) { @@ -1889,6 +1919,10 @@ void BasicIndexingNode::propagate(State& state) const { if (diff.size()) Node::propagate(state); } +void BasicIndexingNode::replace_predecessor_(ssize_t previous_index, Node* node_ptr) { + assert(false and "not yet implemented"); +} + void BasicIndexingNode::revert(State& state) const { auto node_data = data_ptr_(state); node_data->diff.clear(); diff --git a/dwave/optimization/src/nodes/manipulation.cpp b/dwave/optimization/src/nodes/manipulation.cpp index 7260c123d..c9ae1bc76 100644 --- a/dwave/optimization/src/nodes/manipulation.cpp +++ b/dwave/optimization/src/nodes/manipulation.cpp @@ -194,6 +194,16 @@ BroadcastToNode::BroadcastToNode(ArrayNode* array_ptr, std::span add_predecessor_(array_ptr); } +bool BroadcastToNode::operator==(const Node& rhs) const { + const auto* rhs_ptr = dynamic_cast(&rhs); + if (rhs_ptr == nullptr) return false; // not same type so not equal + return *this == *rhs_ptr; +} + +bool BroadcastToNode::operator==(const BroadcastToNode& rhs) const { + return array_ptr_ == rhs.array_ptr_ and std::ranges::equal(shape(), rhs.shape()); +} + double const* BroadcastToNode::buff(const State& state) const { return array_ptr_->buff(state); } void BroadcastToNode::commit(State& state) const { @@ -355,6 +365,14 @@ ssize_t BroadcastToNode::convert_predecessor_index_(ssize_t index) const { return flat_index; } +void BroadcastToNode::replace_predecessor_(ssize_t previous_index, Node* node_ptr) { + Node::replace_predecessor_(previous_index, node_ptr); + + assert(previous_index == 0); // we should only ever have one predecessor + array_ptr_ = dynamic_cast(node_ptr); + assert(array_ptr_ != nullptr); +} + void BroadcastToNode::revert(State& state) const { data_ptr_(state)->revert(); } @@ -1082,6 +1100,17 @@ ResizeNode::ResizeNode(ArrayNode* array_ptr, std::vector&& shape, doubl add_predecessor_(array_ptr); } +bool ResizeNode::operator==(const Node& rhs) const { + const auto* rhs_ptr = dynamic_cast(&rhs); + if (rhs_ptr == nullptr) return false; // not same type so not equal + return *this == *rhs_ptr; +} + +bool ResizeNode::operator==(const ResizeNode& rhs) const { + assert(false and "not yet implemented"); + return false; +} + double const* ResizeNode::buff(const State& state) const { return data_ptr_(state)->buff(); } @@ -1147,6 +1176,10 @@ void ResizeNode::propagate(State& state) const { if (data_ptr->diff().size()) Node::propagate(state); } +void ResizeNode::replace_predecessor_(ssize_t previous_index, Node* node_ptr) { + assert(false and "not yet implemented"); +} + void ResizeNode::revert(State& state) const { return data_ptr_(state)->revert(); } diff --git a/dwave/optimization/src/nodes/reduce.cpp b/dwave/optimization/src/nodes/reduce.cpp index 487c5ee74..be54b8761 100644 --- a/dwave/optimization/src/nodes/reduce.cpp +++ b/dwave/optimization/src/nodes/reduce.cpp @@ -818,6 +818,19 @@ ReduceNode::ReduceNode( ) : ReduceNode(array_ptr, std::span(axes), initial) {} +template +bool ReduceNode::operator==(const Node& rhs) const { + const auto* rhs_ptr = dynamic_cast(&rhs); + if (rhs_ptr == nullptr) return false; // not same type so not equal + return *this == *rhs_ptr; +} + +template +bool ReduceNode::operator==(const ReduceNode& rhs) const { + assert(false and "not yet implemented"); + return false; +} + template double const* ReduceNode::buff(const State& state) const { return data_ptr_>(state)->buff(); @@ -1066,6 +1079,11 @@ auto ReduceNode::reduce_(const State& state, const ssize_t index) cons return ufunc.reduce(std::ranges::subrange(it, std::default_sentinel), initial); } +template +void ReduceNode::replace_predecessor_(ssize_t previous_index, Node* node_ptr) { + assert(false and "not yet implemented"); +} + template void ReduceNode::revert(State& state) const { return data_ptr_>(state)->revert(); diff --git a/dwave/optimization/src/nodes/unaryop.cpp b/dwave/optimization/src/nodes/unaryop.cpp index 5aaae206c..6c36d9a7c 100644 --- a/dwave/optimization/src/nodes/unaryop.cpp +++ b/dwave/optimization/src/nodes/unaryop.cpp @@ -170,6 +170,19 @@ UnaryOpNode::UnaryOpNode(ArrayNode* node_ptr) : add_predecessor_(node_ptr); } +template +bool UnaryOpNode::operator==(const Node& rhs) const { + const auto* rhs_ptr = dynamic_cast(&rhs); + if (rhs_ptr == nullptr) return false; // not same type so not equal + return *this == *rhs_ptr; +} + +template +bool UnaryOpNode::operator==(const UnaryOpNode& rhs) const { + // If we're the same type, then we just need to check we have the same predecessor + return this->array_ptr_ == rhs.array_ptr_; +} + template void UnaryOpNode::commit(State& state) const { data_ptr_(state)->commit(); @@ -232,6 +245,13 @@ void UnaryOpNode::propagate(State& state) const { if (node_data->diff().size()) Node::propagate(state); } +template +void UnaryOpNode::replace_predecessor_(ssize_t previous_index, Node* node_ptr) { + Node::replace_predecessor_(previous_index, node_ptr); + array_ptr_ = dynamic_cast(node_ptr); + assert(array_ptr_ != nullptr); +} + template void UnaryOpNode::revert(State& state) const { data_ptr_(state)->revert(); diff --git a/tests/cpp/test_graph.cpp b/tests/cpp/test_graph.cpp index 0cccfe1de..fc8d419a9 100644 --- a/tests/cpp/test_graph.cpp +++ b/tests/cpp/test_graph.cpp @@ -316,6 +316,71 @@ TEST_CASE("Graph::objective()") { } } +TEST_CASE("Graph::remove_redundant_nodes()") { + GIVEN("A model with two redundant nodes") { + auto graph = Graph(); + + auto* x_ptr = graph.emplace_node(); + auto* y_ptr = graph.emplace_node(); + + auto* left_x_plus_y = graph.emplace_node(x_ptr, y_ptr); + auto* right_x_plus_y = graph.emplace_node(x_ptr, y_ptr); + + CHECK(graph.num_nodes() == 4); + + THEN("remove_redundant_nodes() removes one") { + CHECK(graph.remove_redundant_nodes() == 1); + + CHECK(graph.num_nodes() == 3); + + // we kept the one that's earlier in the topological order + CHECK(graph.nodes()[2].get() == left_x_plus_y); + } + + AND_GIVEN("Two more redundant nodes decended from them") { + auto* negative_left_x_plus_y = graph.emplace_node(left_x_plus_y); + auto* negative_right_x_plus_y = graph.emplace_node(right_x_plus_y); + + THEN("remove_redundant_nodes() removes 2") { + CHECK(graph.remove_redundant_nodes() == 2); + + CHECK(graph.num_nodes() == 4); + + // we kept the ones earlier in the topological order + CHECK(graph.nodes()[2].get() == left_x_plus_y); + CHECK(graph.nodes()[3].get() == negative_left_x_plus_y); + } + + WHEN("we have a listener on one of the redundant nodes") { + auto listener_ptr = negative_right_x_plus_y->expired_ptr(); + + THEN("by default we don't remove the listened to node") { + CHECK(graph.remove_redundant_nodes() == 1); + CHECK(graph.num_nodes() == 5); + + // we kept the ones earlier in the topological order + CHECK(graph.nodes()[2].get() == left_x_plus_y); + CHECK(graph.nodes()[3].get() == negative_left_x_plus_y); + CHECK(graph.nodes()[4].get() == negative_right_x_plus_y); + } + + THEN("we can force the removal of the listened-to node") { + CHECK(graph.remove_redundant_nodes(true) == 2); + + CHECK(graph.num_nodes() == 4); + + // we kept the ones earlier in the topological order + CHECK(graph.nodes()[2].get() == left_x_plus_y); + CHECK(graph.nodes()[3].get() == negative_left_x_plus_y); + } + } + } + } + + // todo: test redundant constants + // todo: test objective/constraitns +} + TEST_CASE("Graph::remove_unused_nodes()") { GIVEN("A single integer variable") { auto graph = Graph(); From 57638a880b7cee514de98d39694d813d8757236c Mon Sep 17 00:00:00 2001 From: Alexander Condello Date: Wed, 24 Jun 2026 11:18:05 -0700 Subject: [PATCH 02/25] Add BinaryOpNode equality and predecessor replacement --- .../dwave-optimization/nodes/binaryop.hpp | 3 + dwave/optimization/src/nodes/binaryop.cpp | 6 +- tests/cpp/nodes/test_binaryop.cpp | 64 +++++++++++++++++++ 3 files changed, 72 insertions(+), 1 deletion(-) diff --git a/dwave/optimization/include/dwave-optimization/nodes/binaryop.hpp b/dwave/optimization/include/dwave-optimization/nodes/binaryop.hpp index ed4d31bb2..ae0b1cdf3 100644 --- a/dwave/optimization/include/dwave-optimization/nodes/binaryop.hpp +++ b/dwave/optimization/include/dwave-optimization/nodes/binaryop.hpp @@ -34,6 +34,9 @@ class BinaryOpNode : public ArrayOutputMixin { // We need at least two nodes, and they must be the same shape BinaryOpNode(ArrayNode* a_ptr, ArrayNode* b_ptr); + /// Two BinaryOpNodes are equal if they have the same operation and the + /// same predecessors. If the BinaryOp is commutative, then permutations + /// of predecessors are allowed. bool operator==(const Node& rhs) const override; bool operator==(const BinaryOpNode& rhs) const; diff --git a/dwave/optimization/src/nodes/binaryop.cpp b/dwave/optimization/src/nodes/binaryop.cpp index 0b92296d2..bab41246a 100644 --- a/dwave/optimization/src/nodes/binaryop.cpp +++ b/dwave/optimization/src/nodes/binaryop.cpp @@ -212,10 +212,14 @@ bool BinaryOpNode::operator==(const BinaryOpNode& rhs) const { // Once we switch to ufuncs, this gets even easier if (std::ranges::equal(operands_, rhs.operands_)) return true; - // Once we switch to unfuncs + // Once we switch to ufuncs // https://github.com/dwavesystems/dwave-optimization/pull/412 // we can use ::communcative. For now we hardcode it. + // Dev note: This implementation doesn't check the pathalogical case op(x, x). + // While it would be mathematically correct, IMO it would be more confusing + // than helpful. + if constexpr ( std::same_as or // std::same_as or // diff --git a/tests/cpp/nodes/test_binaryop.cpp b/tests/cpp/nodes/test_binaryop.cpp index d0e59c922..6c09db4cc 100644 --- a/tests/cpp/nodes/test_binaryop.cpp +++ b/tests/cpp/nodes/test_binaryop.cpp @@ -383,6 +383,70 @@ TEMPLATE_TEST_CASE( } } } + + SECTION("equality") { + auto* x_ptr = graph.emplace_node(); + auto* y_ptr = graph.emplace_node(); + auto* z_ptr = graph.emplace_node(); + + Node* a_ptr = graph.emplace_node>(x_ptr, y_ptr); + Node* b_ptr = graph.emplace_node>(x_ptr, y_ptr); + Node* c_ptr = graph.emplace_node>(y_ptr, x_ptr); // commutative + Node* d_ptr = graph.emplace_node>(x_ptr, z_ptr); // not equal + + CHECK(*a_ptr == *b_ptr); + CHECK(*b_ptr == *a_ptr); + + CHECK(*a_ptr != *d_ptr); + CHECK(*d_ptr != *a_ptr); + + // Once we switch to ufuncs + // https://github.com/dwavesystems/dwave-optimization/pull/412 + // we can use ::communcative. For now we hardcode it. + + if constexpr ( + std::same_as, AddNode> or + std::same_as, AndNode> or + std::same_as, EqualNode> or + std::same_as, MaximumNode> or + std::same_as, MinimumNode> or + std::same_as, MultiplyNode> or + std::same_as, OrNode> or + std::same_as, XorNode> + ) { + // commutative + CHECK(*a_ptr == *c_ptr); + CHECK(*c_ptr == *a_ptr); + } else { + // not commutative + static_assert( + std::same_as, DivideNode> or + std::same_as, LessEqualNode> or + std::same_as, ModulusNode> or + std::same_as, SafeDivideNode> or + std::same_as, SubtractNode> + ); + + CHECK(*a_ptr != *c_ptr); + CHECK(*c_ptr != *a_ptr); + } + } + + SECTION("predecessor replacement") { + auto* x_ptr = graph.emplace_node(); + auto* y_ptr = graph.emplace_node(); + auto* z_ptr = graph.emplace_node(); + + auto* a_ptr = graph.emplace_node>(x_ptr, y_ptr); + + CHECK_THAT(a_ptr->predecessors(), RangeEquals({x_ptr, y_ptr})); + CHECK_THAT(a_ptr->operands(), RangeEquals({x_ptr, y_ptr})); + + z_ptr->take_successors(*y_ptr); + + CHECK_THAT(a_ptr->predecessors(), RangeEquals({x_ptr, z_ptr})); + CHECK_THAT(a_ptr->operands(), RangeEquals({x_ptr, z_ptr})); + } } TEST_CASE("BinaryOpNode - LessEqualNode") { From 16a29382e782f92d2422b70dc0ef1f8324cb442e Mon Sep 17 00:00:00 2001 From: Alexander Condello Date: Wed, 24 Jun 2026 13:32:00 -0700 Subject: [PATCH 03/25] Switch from Node::operator== to Node::equal_to In C++20 Node::operator==(const Node&) const is amibiguous because either order is valid. --- .../include/dwave-optimization/graph.hpp | 30 +++++------ .../dwave-optimization/nodes/binaryop.hpp | 10 ++-- .../dwave-optimization/nodes/collections.hpp | 8 +++ .../dwave-optimization/nodes/constants.hpp | 6 +-- .../include/dwave-optimization/nodes/flow.hpp | 13 ++--- .../dwave-optimization/nodes/indexing.hpp | 11 ++-- .../dwave-optimization/nodes/manipulation.hpp | 12 ++--- .../dwave-optimization/nodes/reduce.hpp | 6 +-- .../dwave-optimization/nodes/unaryop.hpp | 6 +-- dwave/optimization/src/graph.cpp | 4 +- dwave/optimization/src/nodes/binaryop.cpp | 36 ++++++------- dwave/optimization/src/nodes/collections.cpp | 12 +++++ dwave/optimization/src/nodes/constants.cpp | 6 +-- dwave/optimization/src/nodes/flow.cpp | 40 +++++++-------- dwave/optimization/src/nodes/indexing.cpp | 50 +++++++++---------- dwave/optimization/src/nodes/manipulation.cpp | 42 ++++++++-------- dwave/optimization/src/nodes/reduce.cpp | 26 +++++----- dwave/optimization/src/nodes/unaryop.cpp | 26 +++++----- tests/cpp/nodes/test_binaryop.cpp | 16 +++--- tests/cpp/nodes/test_collections.cpp | 11 ++++ 20 files changed, 202 insertions(+), 169 deletions(-) diff --git a/dwave/optimization/include/dwave-optimization/graph.hpp b/dwave/optimization/include/dwave-optimization/graph.hpp index 1e2fd226b..8b3b3bd23 100644 --- a/dwave/optimization/include/dwave-optimization/graph.hpp +++ b/dwave/optimization/include/dwave-optimization/graph.hpp @@ -260,16 +260,6 @@ class Node { Node& operator=(const Node&) = delete; Node& operator=(Node&&) noexcept = delete; - // TODO: document and note that nodes *must* share the same set of - // predecessors (permutations are sometimes allowed) and they *must* be the - // same type. Also that they can have false negatives in some cases. - // Also, we don't check pinned values - virtual bool operator==(const Node& rhs) const { - std::cout << classname() << " missing operator==(...) *********\n"; - assert(false and "not yet implemented"); - return false; - } - /// Methods for interrogating nodes as strings. Useful for error messages /// and debugging. We roughly follow Python's scheme of repr() and str() /// printing different information. @@ -282,6 +272,16 @@ class Node { /// derived from its predecessors. Defaults to `true`, except for decisions. virtual bool deterministic_state() const { return true; } + // TODO: document and note that nodes *must* share the same set of + // predecessors (permutations are sometimes allowed) and they *must* be the + // same type. Also that they can have false negatives in some cases. + // Also, we don't check pinned values + virtual bool equal_to(const Node& rhs) const { + std::cout << classname() << " missing operator==(...) *********\n"; + assert(false and "not yet implemented"); + return false; + } + /// Return a shared pointer to a bool value. When the node is destructed /// the bool will be set to True std::shared_ptr expired_ptr() const { return expired_ptr_; } @@ -456,13 +456,15 @@ NodeType* Graph::emplace_node(Args&&... args) { class ArrayNode : public Array, public virtual Node {}; class DecisionNode : public Decision, public virtual Node { public: - /// Decision nodes are never equal to eachother because they are - /// independent variables. - bool operator==(const Node&) const override { return false; } - /// Decision nodes by definition do not have a deterministic state. bool deterministic_state() const final { return false; } + /// Decision can only ever be equal to themselves because they are + /// independent variables. + bool equal_to(const Node& rhs) const final { + return static_cast(this) == &rhs; + } + /// Decisions don't have predecessors so no one should be calling update(). /// Always throws a logic_error. [[noreturn]] void update(State& state, int index) const override; diff --git a/dwave/optimization/include/dwave-optimization/nodes/binaryop.hpp b/dwave/optimization/include/dwave-optimization/nodes/binaryop.hpp index ae0b1cdf3..8ddf4622e 100644 --- a/dwave/optimization/include/dwave-optimization/nodes/binaryop.hpp +++ b/dwave/optimization/include/dwave-optimization/nodes/binaryop.hpp @@ -34,14 +34,14 @@ class BinaryOpNode : public ArrayOutputMixin { // We need at least two nodes, and they must be the same shape BinaryOpNode(ArrayNode* a_ptr, ArrayNode* b_ptr); + double const* buff(const State& state) const override; + std::span diff(const State& state) const override; + /// Two BinaryOpNodes are equal if they have the same operation and the /// same predecessors. If the BinaryOp is commutative, then permutations /// of predecessors are allowed. - bool operator==(const Node& rhs) const override; - bool operator==(const BinaryOpNode& rhs) const; - - double const* buff(const State& state) const override; - std::span diff(const State& state) const override; + bool equal_to(const Node& rhs) const override; + bool equal_to(const BinaryOpNode& rhs) const; /// @copydoc Array::integral() bool integral() const override; diff --git a/dwave/optimization/include/dwave-optimization/nodes/collections.hpp b/dwave/optimization/include/dwave-optimization/nodes/collections.hpp index 48b20119c..a2bdbf6ac 100644 --- a/dwave/optimization/include/dwave-optimization/nodes/collections.hpp +++ b/dwave/optimization/include/dwave-optimization/nodes/collections.hpp @@ -138,6 +138,10 @@ class DisjointBitSetNode : public ArrayOutputMixin { std::span diff(const State& state) const override; + // A DisjointBitSetNode can only ever be equal to itself. + bool equal_to(const Node& rhs) const override; + bool equal_to(const DisjointBitSetNode& rhs) const; + /// @copydoc Array::integral() bool integral() const override; @@ -228,6 +232,10 @@ class DisjointListNode : public ArrayOutputMixin { std::span diff(const State& state) const override; + // A DisjointListNode can only ever be equal to itself. + bool equal_to(const Node& rhs) const override; + bool equal_to(const DisjointListNode& rhs) const; + /// @copydoc Array::integral() bool integral() const override; diff --git a/dwave/optimization/include/dwave-optimization/nodes/constants.hpp b/dwave/optimization/include/dwave-optimization/nodes/constants.hpp index 96dd0e91c..b786dfa3d 100644 --- a/dwave/optimization/include/dwave-optimization/nodes/constants.hpp +++ b/dwave/optimization/include/dwave-optimization/nodes/constants.hpp @@ -73,9 +73,6 @@ class ConstantNode : public ArrayOutputMixin { ConstantNode(Range&& values, const std::span shape) : ConstantNode(from_range(std::forward(values), shape), shape) {} - bool operator==(const Node& rhs) const override; - bool operator==(const ConstantNode& rhs) const; - // Stateless access to the underlying data. std::span data() const noexcept { return std::span(buffer_ptr_, this->size()); @@ -103,6 +100,9 @@ class ConstantNode : public ArrayOutputMixin { // Overloads required by the Node ABC ************************************* + bool equal_to(const Node& rhs) const override; + bool equal_to(const ConstantNode& rhs) const; + // This node never needs to update its successors void propagate(State&) const noexcept override {} diff --git a/dwave/optimization/include/dwave-optimization/nodes/flow.hpp b/dwave/optimization/include/dwave-optimization/nodes/flow.hpp index 7e2dbd3d9..6bf2c92f2 100644 --- a/dwave/optimization/include/dwave-optimization/nodes/flow.hpp +++ b/dwave/optimization/include/dwave-optimization/nodes/flow.hpp @@ -30,9 +30,6 @@ class ExtractNode : public ArrayOutputMixin { public: ExtractNode(ArrayNode* condition_ptr, ArrayNode* arr_ptr); - bool operator==(const Node& rhs) const override; - bool operator==(const ExtractNode& rhs) const; - /// @copydoc Array::buff() double const* buff(const State& state) const override; @@ -42,6 +39,9 @@ class ExtractNode : public ArrayOutputMixin { /// @copydoc Array::diff() std::span diff(const State& state) const override; + bool equal_to(const Node& rhs) const override; + bool equal_to(const ExtractNode& rhs) const; + /// @copydoc Node::initialize_state() void initialize_state(State& state) const override; @@ -97,12 +97,13 @@ class WhereNode : public ArrayOutputMixin { public: WhereNode(ArrayNode* condition_ptr, ArrayNode* x_ptr, ArrayNode* y_ptr); - bool operator==(const Node& rhs) const override; - bool operator==(const WhereNode& rhs) const; - double const* buff(const State& state) const override; void commit(State& state) const override; std::span diff(const State& state) const override; + + bool equal_to(const Node& rhs) const override; + bool equal_to(const WhereNode& rhs) const; + void initialize_state(State& state) const override; /// @copydoc Array::integral() diff --git a/dwave/optimization/include/dwave-optimization/nodes/indexing.hpp b/dwave/optimization/include/dwave-optimization/nodes/indexing.hpp index a6d51b096..bdf4b2715 100644 --- a/dwave/optimization/include/dwave-optimization/nodes/indexing.hpp +++ b/dwave/optimization/include/dwave-optimization/nodes/indexing.hpp @@ -56,9 +56,6 @@ class AdvancedIndexingNode : public ArrayNode { explicit AdvancedIndexingNode(ArrayNode* array_ptr, Indices... indices) : AdvancedIndexingNode(array_ptr, make_indices(indices...)) {} - bool operator==(const Node& rhs) const override; - bool operator==(const AdvancedIndexingNode& rhs) const; - // Array overloads ssize_t ndim() const noexcept override { return ndim_; } double const* buff(const State& state) const override; @@ -83,6 +80,8 @@ class AdvancedIndexingNode : public ArrayNode { // Node overloads void commit(State& state) const override; + bool equal_to(const Node& rhs) const override; + bool equal_to(const AdvancedIndexingNode& rhs) const; void initialize_state(State& state) const override; void propagate(State& state) const override; void revert(State& state) const override; @@ -200,9 +199,6 @@ class BasicIndexingNode : public ArrayNode { explicit BasicIndexingNode(ArrayNode* array_ptr, Indices... indices) : BasicIndexingNode(array_ptr, make_indices(indices...)) {} - bool operator==(const Node& rhs) const override; - bool operator==(const BasicIndexingNode& rhs) const; - // Overloads needed by the Array ABC ************************************** ssize_t ndim() const noexcept override { return ndim_; } @@ -238,6 +234,9 @@ class BasicIndexingNode : public ArrayNode { // don't need to overload update() + bool equal_to(const Node& rhs) const override; + bool equal_to(const BasicIndexingNode& rhs) const; + void propagate(State& state) const override; void initialize_state(State& state) const override; diff --git a/dwave/optimization/include/dwave-optimization/nodes/manipulation.hpp b/dwave/optimization/include/dwave-optimization/nodes/manipulation.hpp index 4d26bfee0..550f4c053 100644 --- a/dwave/optimization/include/dwave-optimization/nodes/manipulation.hpp +++ b/dwave/optimization/include/dwave-optimization/nodes/manipulation.hpp @@ -31,9 +31,6 @@ class BroadcastToNode : public ArrayNode { BroadcastToNode(ArrayNode* array_ptr, std::initializer_list shape); BroadcastToNode(ArrayNode* array_ptr, std::span shape); - bool operator==(const Node& rhs) const override; - bool operator==(const BroadcastToNode& rhs) const; - /// @copydoc Array::buff() double const* buff(const State& state) const override; @@ -46,6 +43,9 @@ class BroadcastToNode : public ArrayNode { /// @copydoc Array::diff() std::span diff(const State& state) const override; + bool equal_to(const Node& rhs) const override; + bool equal_to(const BroadcastToNode& rhs) const; + /// @copydoc Node::initialize_state() void initialize_state(State& state) const override; @@ -333,9 +333,6 @@ class ResizeNode : public ArrayOutputMixin { ResizeNode(ArrayNode* node_ptr, Range&& shape, double fill_value = 0) : ResizeNode(node_ptr, std::vector(shape.begin(), shape.end()), fill_value) {} - bool operator==(const Node& rhs) const override; - bool operator==(const ResizeNode& rhs) const; - /// @copydoc Array::buff() double const* buff(const State& state) const override; @@ -345,6 +342,9 @@ class ResizeNode : public ArrayOutputMixin { /// @copydoc Array::diff() std::span diff(const State& state) const override; + bool equal_to(const Node& rhs) const override; + bool equal_to(const ResizeNode& rhs) const; + /// The fill value. double fill_value() const { return fill_value_; } diff --git a/dwave/optimization/include/dwave-optimization/nodes/reduce.hpp b/dwave/optimization/include/dwave-optimization/nodes/reduce.hpp index dcaf6afc5..81fbb7d85 100644 --- a/dwave/optimization/include/dwave-optimization/nodes/reduce.hpp +++ b/dwave/optimization/include/dwave-optimization/nodes/reduce.hpp @@ -44,9 +44,6 @@ class ReduceNode : public ArrayOutputMixin { std::optional initial = std::nullopt ); - bool operator==(const Node& rhs) const override; - bool operator==(const ReduceNode& rhs) const; - /// The axes over which the reduction is performed. std::span axes() const { return axes_; } @@ -59,6 +56,9 @@ class ReduceNode : public ArrayOutputMixin { /// @copydoc Array::diff() std::span diff(const State& state) const override; + bool equal_to(const Node& rhs) const override; + bool equal_to(const ReduceNode& rhs) const; + /// @copydoc Node::initialize_state() void initialize_state(State& state) const override; diff --git a/dwave/optimization/include/dwave-optimization/nodes/unaryop.hpp b/dwave/optimization/include/dwave-optimization/nodes/unaryop.hpp index dd99474c8..9aa8b81ff 100644 --- a/dwave/optimization/include/dwave-optimization/nodes/unaryop.hpp +++ b/dwave/optimization/include/dwave-optimization/nodes/unaryop.hpp @@ -32,12 +32,12 @@ class UnaryOpNode : public ArrayOutputMixin { public: explicit UnaryOpNode(ArrayNode* node_ptr); - bool operator==(const Node& rhs) const override; - bool operator==(const UnaryOpNode& rhs) const; - double const* buff(const State& state) const override; std::span diff(const State& state) const override; + bool equal_to(const Node& rhs) const override; + bool equal_to(const UnaryOpNode& rhs) const; + /// @copydoc Array::integral() bool integral() const override; diff --git a/dwave/optimization/src/graph.cpp b/dwave/optimization/src/graph.cpp index a475fb78f..9bef7260a 100644 --- a/dwave/optimization/src/graph.cpp +++ b/dwave/optimization/src/graph.cpp @@ -339,7 +339,7 @@ ssize_t Graph::remove_redundant_nodes(bool ignore_listeners, double time_limit_s if (out_of_time()) return cleanup(); // nothing to do if they are not equal - if (*constants_[i] != *constants_[j]) continue; + if (not constants_[i]->equal_to(*constants_[j])) continue; constants_[i]->take_successors(*constants_[j]); @@ -371,7 +371,7 @@ ssize_t Graph::remove_redundant_nodes(bool ignore_listeners, double time_limit_s if (out_of_time()) return cleanup(); // If lhs != rhs there's nothing to do, so keep looking - if (*successors[i] != *successors[j]) continue; + if (not successors[i]->equal_to(*successors[j])) continue; // We have a redundant node! diff --git a/dwave/optimization/src/nodes/binaryop.cpp b/dwave/optimization/src/nodes/binaryop.cpp index bab41246a..7beaa8de0 100644 --- a/dwave/optimization/src/nodes/binaryop.cpp +++ b/dwave/optimization/src/nodes/binaryop.cpp @@ -199,14 +199,29 @@ BinaryOpNode::BinaryOpNode(ArrayNode* a_ptr, ArrayNode* b_ptr) : } template -bool BinaryOpNode::operator==(const Node& rhs) const { +double const* BinaryOpNode::buff(const State& state) const { + return data_ptr_(state)->buff(); +} + +template +std::span BinaryOpNode::diff(const State& state) const { + return data_ptr_(state)->diff(); +} + +template +void BinaryOpNode::commit(State& state) const { + data_ptr_(state)->commit(); +} + +template +bool BinaryOpNode::equal_to(const Node& rhs) const { const auto* rhs_ptr = dynamic_cast(&rhs); if (rhs_ptr == nullptr) return false; // not same type so not equal - return *this == *rhs_ptr; + return this->equal_to(*rhs_ptr); } template -bool BinaryOpNode::operator==(const BinaryOpNode& rhs) const { +bool BinaryOpNode::equal_to(const BinaryOpNode& rhs) const { // if we're the same type, then we just need to make sure we're operating // on the same operands (subject to whether we're commutative or not. // Once we switch to ufuncs, this gets even easier @@ -246,21 +261,6 @@ bool BinaryOpNode::operator==(const BinaryOpNode& rhs) const { return false; } -template -double const* BinaryOpNode::buff(const State& state) const { - return data_ptr_(state)->buff(); -} - -template -std::span BinaryOpNode::diff(const State& state) const { - return data_ptr_(state)->diff(); -} - -template -void BinaryOpNode::commit(State& state) const { - data_ptr_(state)->commit(); -} - template void BinaryOpNode::initialize_state(State& state) const { auto lhs_ptr = operands_[0]; diff --git a/dwave/optimization/src/nodes/collections.cpp b/dwave/optimization/src/nodes/collections.cpp index 0ea4f805b..d44d3e115 100644 --- a/dwave/optimization/src/nodes/collections.cpp +++ b/dwave/optimization/src/nodes/collections.cpp @@ -493,6 +493,12 @@ std::span DisjointBitSetNode::diff(const State& state) const { return pred_data->diffs[set_index_]; } +bool DisjointBitSetNode::equal_to(const Node& rhs) const { + return static_cast(this) == &rhs; +} + +bool DisjointBitSetNode::equal_to(const DisjointBitSetNode& rhs) const { return this == &rhs; } + bool DisjointBitSetNode::integral() const { return true; } double DisjointBitSetNode::min() const { return 0; } @@ -844,6 +850,12 @@ std::span DisjointListNode::diff(const State& state) const { return data->all_list_updates[list_index_]; } +bool DisjointListNode::equal_to(const Node& rhs) const { + return static_cast(this) == &rhs; +} + +bool DisjointListNode::equal_to(const DisjointListNode& rhs) const { return this == &rhs; } + bool DisjointListNode::integral() const { return true; } double DisjointListNode::min() const { return 0; } diff --git a/dwave/optimization/src/nodes/constants.cpp b/dwave/optimization/src/nodes/constants.cpp index 93da0ede5..e8215548b 100644 --- a/dwave/optimization/src/nodes/constants.cpp +++ b/dwave/optimization/src/nodes/constants.cpp @@ -65,13 +65,13 @@ ConstantNode::ConstantNode(OwningDataSource&& data_source, const std::span(buffer_ptr_, this->size()))), data_source_(std::make_unique(std::move(data_source))) {} -bool ConstantNode::operator==(const Node& rhs) const { +bool ConstantNode::equal_to(const Node& rhs) const { const auto* rhs_ptr = dynamic_cast(&rhs); if (rhs_ptr == nullptr) return false; // not same type so not equal - return *this == *rhs_ptr; + return this->equal_to(*rhs_ptr); } -bool ConstantNode::operator==(const ConstantNode& rhs) const { +bool ConstantNode::equal_to(const ConstantNode& rhs) const { return this->ndim() == rhs.ndim() and // same ndim std::ranges::equal(this->shape(), rhs.shape()) and // same shape std::ranges::equal(this->data(), rhs.data()); // same content diff --git a/dwave/optimization/src/nodes/flow.cpp b/dwave/optimization/src/nodes/flow.cpp index 000432915..cdaaacf55 100644 --- a/dwave/optimization/src/nodes/flow.cpp +++ b/dwave/optimization/src/nodes/flow.cpp @@ -63,16 +63,6 @@ ExtractNode::ExtractNode(ArrayNode* condition_ptr, ArrayNode* arr_ptr) : add_predecessor_(arr_ptr); } -bool ExtractNode::operator==(const Node& rhs) const { - const auto* rhs_ptr = dynamic_cast(&rhs); - if (rhs_ptr == nullptr) return false; // not same type so not equal - return *this == *rhs_ptr; -} - -bool ExtractNode::operator==(const ExtractNode& rhs) const { - return condition_ptr_ == rhs.condition_ptr_ and arr_ptr_ == rhs.arr_ptr_; -} - double const* ExtractNode::buff(const State& state) const { return data_ptr_(state)->buff(); } @@ -83,6 +73,16 @@ std::span ExtractNode::diff(const State& state) const { return data_ptr_(state)->diff(); } +bool ExtractNode::equal_to(const Node& rhs) const { + const auto* rhs_ptr = dynamic_cast(&rhs); + if (rhs_ptr == nullptr) return false; // not same type so not equal + return this->equal_to(*rhs_ptr); +} + +bool ExtractNode::equal_to(const ExtractNode& rhs) const { + return condition_ptr_ == rhs.condition_ptr_ and arr_ptr_ == rhs.arr_ptr_; +} + void ExtractNode::initialize_state(State& state) const { const std::ranges::view auto condition = condition_ptr_->view(state); const std::ranges::view auto arr = arr_ptr_->view(state); @@ -280,16 +280,6 @@ WhereNode::WhereNode(ArrayNode* condition_ptr, ArrayNode* x_ptr, ArrayNode* y_pt add_predecessor_(y_ptr); } -bool WhereNode::operator==(const Node& rhs) const { - const auto* rhs_ptr = dynamic_cast(&rhs); - if (rhs_ptr == nullptr) return false; // not same type so not equal - return *this == *rhs_ptr; -} - -bool WhereNode::operator==(const WhereNode& rhs) const { - return condition_ptr_ == rhs.condition_ptr_ and x_ptr_ == rhs.x_ptr_ and y_ptr_ == rhs.y_ptr_; -} - double const* WhereNode::buff(const State& state) const { return data_ptr_(state)->buff(); } @@ -300,6 +290,16 @@ std::span WhereNode::diff(const State& state) const { return data_ptr_(state)->diff(); } +bool WhereNode::equal_to(const Node& rhs) const { + const auto* rhs_ptr = dynamic_cast(&rhs); + if (rhs_ptr == nullptr) return false; // not same type so not equal + return this->equal_to(*rhs_ptr); +} + +bool WhereNode::equal_to(const WhereNode& rhs) const { + return condition_ptr_ == rhs.condition_ptr_ and x_ptr_ == rhs.x_ptr_ and y_ptr_ == rhs.y_ptr_; +} + void WhereNode::initialize_state(State& state) const { if (condition_ptr_->size() != 1) { // `condition` has the same shape as x/y and isn't a single value diff --git a/dwave/optimization/src/nodes/indexing.cpp b/dwave/optimization/src/nodes/indexing.cpp index 464763d57..2a312f6c8 100644 --- a/dwave/optimization/src/nodes/indexing.cpp +++ b/dwave/optimization/src/nodes/indexing.cpp @@ -520,24 +520,24 @@ AdvancedIndexingNode::AdvancedIndexingNode(ArrayNode* array_ptr, IndexParser_&& } } -bool AdvancedIndexingNode::operator==(const Node& rhs) const { +double const* AdvancedIndexingNode::buff(const State& state) const { + return data_ptr_(state)->data.data(); +} +std::span AdvancedIndexingNode::diff(const State& state) const { + return data_ptr_(state)->diff; +} + +bool AdvancedIndexingNode::equal_to(const Node& rhs) const { const auto* rhs_ptr = dynamic_cast(&rhs); if (rhs_ptr == nullptr) return false; // not same type so not equal - return *this == *rhs_ptr; + return this->equal_to(*rhs_ptr); } -bool AdvancedIndexingNode::operator==(const AdvancedIndexingNode& rhs) const { +bool AdvancedIndexingNode::equal_to(const AdvancedIndexingNode& rhs) const { assert(false and "not yet implemented"); return false; } -double const* AdvancedIndexingNode::buff(const State& state) const { - return data_ptr_(state)->data.data(); -} -std::span AdvancedIndexingNode::diff(const State& state) const { - return data_ptr_(state)->diff; -} - void AdvancedIndexingNode::fill_subspace( State& state, ssize_t array_offset, @@ -1402,21 +1402,6 @@ BasicIndexingNode::BasicIndexingNode(ArrayNode* array_ptr, IndexParser_&& parser add_predecessor_(array_ptr); } -bool BasicIndexingNode::operator==(const Node& rhs) const { - const auto* rhs_ptr = dynamic_cast(&rhs); - if (rhs_ptr == nullptr) return false; // not same type so not equal - return *this == *rhs_ptr; -} - -bool BasicIndexingNode::operator==(const BasicIndexingNode& rhs) const { - return (array_ptr_ == rhs.array_ptr_ and - ndim_ == rhs.ndim_ and - start_ == rhs.start_ and - std::ranges::equal(shape(), rhs.shape()) and - std::ranges::equal(strides(), rhs.strides()) - ); -} - struct BasicIndexingNodeData : NodeStateData { BasicIndexingNodeData(const BasicIndexingNode* node) { if (node->dynamic()) { @@ -1491,6 +1476,21 @@ std::span BasicIndexingNode::diff(const State& state) const { return data_ptr_(state)->diff; } +bool BasicIndexingNode::equal_to(const Node& rhs) const { + const auto* rhs_ptr = dynamic_cast(&rhs); + if (rhs_ptr == nullptr) return false; // not same type so not equal + return this->equal_to(*rhs_ptr); +} + +bool BasicIndexingNode::equal_to(const BasicIndexingNode& rhs) const { + return (array_ptr_ == rhs.array_ptr_ and + ndim_ == rhs.ndim_ and + start_ == rhs.start_ and + std::ranges::equal(shape(), rhs.shape()) and + std::ranges::equal(strides(), rhs.strides()) + ); +} + std::vector BasicIndexingNode::infer_indices() const { std::vector indices; diff --git a/dwave/optimization/src/nodes/manipulation.cpp b/dwave/optimization/src/nodes/manipulation.cpp index c9ae1bc76..6ec13825c 100644 --- a/dwave/optimization/src/nodes/manipulation.cpp +++ b/dwave/optimization/src/nodes/manipulation.cpp @@ -194,16 +194,6 @@ BroadcastToNode::BroadcastToNode(ArrayNode* array_ptr, std::span add_predecessor_(array_ptr); } -bool BroadcastToNode::operator==(const Node& rhs) const { - const auto* rhs_ptr = dynamic_cast(&rhs); - if (rhs_ptr == nullptr) return false; // not same type so not equal - return *this == *rhs_ptr; -} - -bool BroadcastToNode::operator==(const BroadcastToNode& rhs) const { - return array_ptr_ == rhs.array_ptr_ and std::ranges::equal(shape(), rhs.shape()); -} - double const* BroadcastToNode::buff(const State& state) const { return array_ptr_->buff(state); } void BroadcastToNode::commit(State& state) const { @@ -216,6 +206,16 @@ std::span BroadcastToNode::diff(const State& state) const { return data_ptr_(state)->diff; } +bool BroadcastToNode::equal_to(const Node& rhs) const { + const auto* rhs_ptr = dynamic_cast(&rhs); + if (rhs_ptr == nullptr) return false; // not same type so not equal + return this->equal_to(*rhs_ptr); +} + +bool BroadcastToNode::equal_to(const BroadcastToNode& rhs) const { + return array_ptr_ == rhs.array_ptr_ and std::ranges::equal(shape(), rhs.shape()); +} + void BroadcastToNode::initialize_state(State& state) const { std::vector offsets = diff_offsets(array_ptr_->shape(), this->shape()); @@ -1100,17 +1100,6 @@ ResizeNode::ResizeNode(ArrayNode* array_ptr, std::vector&& shape, doubl add_predecessor_(array_ptr); } -bool ResizeNode::operator==(const Node& rhs) const { - const auto* rhs_ptr = dynamic_cast(&rhs); - if (rhs_ptr == nullptr) return false; // not same type so not equal - return *this == *rhs_ptr; -} - -bool ResizeNode::operator==(const ResizeNode& rhs) const { - assert(false and "not yet implemented"); - return false; -} - double const* ResizeNode::buff(const State& state) const { return data_ptr_(state)->buff(); } @@ -1123,6 +1112,17 @@ std::span ResizeNode::diff(const State& state) const { return data_ptr_(state)->diff(); } +bool ResizeNode::equal_to(const Node& rhs) const { + const auto* rhs_ptr = dynamic_cast(&rhs); + if (rhs_ptr == nullptr) return false; // not same type so not equal + return this->equal_to(*rhs_ptr); +} + +bool ResizeNode::equal_to(const ResizeNode& rhs) const { + assert(false and "not yet implemented"); + return false; +} + void ResizeNode::initialize_state(State& state) const { const ssize_t size = this->size(); // the desired size of our state assert(size >= 0); // we're never dynamic diff --git a/dwave/optimization/src/nodes/reduce.cpp b/dwave/optimization/src/nodes/reduce.cpp index be54b8761..f01e85124 100644 --- a/dwave/optimization/src/nodes/reduce.cpp +++ b/dwave/optimization/src/nodes/reduce.cpp @@ -818,19 +818,6 @@ ReduceNode::ReduceNode( ) : ReduceNode(array_ptr, std::span(axes), initial) {} -template -bool ReduceNode::operator==(const Node& rhs) const { - const auto* rhs_ptr = dynamic_cast(&rhs); - if (rhs_ptr == nullptr) return false; // not same type so not equal - return *this == *rhs_ptr; -} - -template -bool ReduceNode::operator==(const ReduceNode& rhs) const { - assert(false and "not yet implemented"); - return false; -} - template double const* ReduceNode::buff(const State& state) const { return data_ptr_>(state)->buff(); @@ -846,6 +833,19 @@ std::span ReduceNode::diff(const State& state) const { return data_ptr_>(state)->diff(); } +template +bool ReduceNode::equal_to(const Node& rhs) const { + const auto* rhs_ptr = dynamic_cast(&rhs); + if (rhs_ptr == nullptr) return false; // not same type so not equal + return this->equal_to(*rhs_ptr); +} + +template +bool ReduceNode::equal_to(const ReduceNode& rhs) const { + assert(false and "not yet implemented"); + return false; +} + template void ReduceNode::initialize_state(State& state) const { std::vector::reduction_type> reductions; diff --git a/dwave/optimization/src/nodes/unaryop.cpp b/dwave/optimization/src/nodes/unaryop.cpp index 6c36d9a7c..6cfd39806 100644 --- a/dwave/optimization/src/nodes/unaryop.cpp +++ b/dwave/optimization/src/nodes/unaryop.cpp @@ -170,19 +170,6 @@ UnaryOpNode::UnaryOpNode(ArrayNode* node_ptr) : add_predecessor_(node_ptr); } -template -bool UnaryOpNode::operator==(const Node& rhs) const { - const auto* rhs_ptr = dynamic_cast(&rhs); - if (rhs_ptr == nullptr) return false; // not same type so not equal - return *this == *rhs_ptr; -} - -template -bool UnaryOpNode::operator==(const UnaryOpNode& rhs) const { - // If we're the same type, then we just need to check we have the same predecessor - return this->array_ptr_ == rhs.array_ptr_; -} - template void UnaryOpNode::commit(State& state) const { data_ptr_(state)->commit(); @@ -198,6 +185,19 @@ std::span UnaryOpNode::diff(const State& state) const { return data_ptr_(state)->diff(); } +template +bool UnaryOpNode::equal_to(const Node& rhs) const { + const auto* rhs_ptr = dynamic_cast(&rhs); + if (rhs_ptr == nullptr) return false; // not same type so not equal + return this->equal_to(*rhs_ptr); +} + +template +bool UnaryOpNode::equal_to(const UnaryOpNode& rhs) const { + // If we're the same type, then we just need to check we have the same predecessor + return this->array_ptr_ == rhs.array_ptr_; +} + template void UnaryOpNode::initialize_state(State& state) const { std::vector values; diff --git a/tests/cpp/nodes/test_binaryop.cpp b/tests/cpp/nodes/test_binaryop.cpp index 6c09db4cc..fcd9f98c9 100644 --- a/tests/cpp/nodes/test_binaryop.cpp +++ b/tests/cpp/nodes/test_binaryop.cpp @@ -394,11 +394,11 @@ TEMPLATE_TEST_CASE( Node* c_ptr = graph.emplace_node>(y_ptr, x_ptr); // commutative Node* d_ptr = graph.emplace_node>(x_ptr, z_ptr); // not equal - CHECK(*a_ptr == *b_ptr); - CHECK(*b_ptr == *a_ptr); + CHECK(a_ptr->equal_to(*b_ptr)); + CHECK(b_ptr->equal_to(*a_ptr)); - CHECK(*a_ptr != *d_ptr); - CHECK(*d_ptr != *a_ptr); + CHECK(not a_ptr->equal_to(*d_ptr)); + CHECK(not d_ptr->equal_to(*a_ptr)); // Once we switch to ufuncs // https://github.com/dwavesystems/dwave-optimization/pull/412 @@ -415,8 +415,8 @@ TEMPLATE_TEST_CASE( std::same_as, XorNode> ) { // commutative - CHECK(*a_ptr == *c_ptr); - CHECK(*c_ptr == *a_ptr); + CHECK(a_ptr->equal_to(*c_ptr)); + CHECK(c_ptr->equal_to(*a_ptr)); } else { // not commutative static_assert( @@ -427,8 +427,8 @@ TEMPLATE_TEST_CASE( std::same_as, SubtractNode> ); - CHECK(*a_ptr != *c_ptr); - CHECK(*c_ptr != *a_ptr); + CHECK(not a_ptr->equal_to(*c_ptr)); + CHECK(not c_ptr->equal_to(*a_ptr)); } } diff --git a/tests/cpp/nodes/test_collections.cpp b/tests/cpp/nodes/test_collections.cpp index 4090de043..b6e45bafe 100644 --- a/tests/cpp/nodes/test_collections.cpp +++ b/tests/cpp/nodes/test_collections.cpp @@ -49,6 +49,17 @@ TEST_CASE("DisjointBitSetsNode") { graph.emplace_node(sets.at(i)); } + THEN("node equality works as expected") { + CHECK(ptr->equal_to(*ptr)); + + CHECK(sets[0]->equal_to(*sets[0])); + CHECK(not sets[0]->equal_to(*sets[1])); + CHECK(static_cast(sets[0])->equal_to(*sets[0])); + CHECK(not static_cast(sets[0])->equal_to(*sets[1])); + CHECK(sets[0]->equal_to(*static_cast(sets[0]))); + CHECK(not sets[0]->equal_to(*static_cast(sets[1]))); + } + THEN("We shouldn't be able to add any more successors") { CHECK_THROWS(graph.emplace_node(ptr)); } From c0e52bc22fa2ecb2345a61f923d5e3efbd90aef4 Mon Sep 17 00:00:00 2001 From: Alexander Condello Date: Thu, 25 Jun 2026 13:39:12 -0700 Subject: [PATCH 04/25] Add ARangeNode equality and predecessor replacement --- .../dwave-optimization/nodes/creation.hpp | 13 +++-- dwave/optimization/src/nodes/creation.cpp | 49 ++++++++++++++++++ tests/cpp/nodes/test_creation.cpp | 50 +++++++++++++++++++ 3 files changed, 109 insertions(+), 3 deletions(-) diff --git a/dwave/optimization/include/dwave-optimization/nodes/creation.hpp b/dwave/optimization/include/dwave-optimization/nodes/creation.hpp index 876c1a106..18052ef0c 100644 --- a/dwave/optimization/include/dwave-optimization/nodes/creation.hpp +++ b/dwave/optimization/include/dwave-optimization/nodes/creation.hpp @@ -83,6 +83,10 @@ class ARangeNode : public ArrayOutputMixin { /// @copydoc Array::diff() std::span diff(const State& state) const override; + /// ARangeNode is equal to another ARangeNode encoding the same range. + bool equal_to(const Node& rhs) const override; + bool equal_to(const ARangeNode& rhs) const; + /// @copydoc Node::initialize_state() void initialize_state(State& state) const override; @@ -126,10 +130,13 @@ class ARangeNode : public ArrayOutputMixin { /// The value or array defining the spacing between values. array_or_int step() const { return step_; } + protected: + void replace_predecessor_(ssize_t index, Node* node_ptr) override; + private: - const array_or_int start_; - const array_or_int stop_; - const array_or_int step_; + array_or_int start_; + array_or_int stop_; + array_or_int step_; const std::pair values_minmax_; const SizeInfo sizeinfo_; diff --git a/dwave/optimization/src/nodes/creation.cpp b/dwave/optimization/src/nodes/creation.cpp index 318607223..a6da0f285 100644 --- a/dwave/optimization/src/nodes/creation.cpp +++ b/dwave/optimization/src/nodes/creation.cpp @@ -354,6 +354,16 @@ std::span ARangeNode::diff(const State& state) const { return data_ptr_(state)->diff(); } +bool ARangeNode::equal_to(const Node& rhs) const { + const auto* rhs_ptr = dynamic_cast(&rhs); + if (rhs_ptr == nullptr) return false; + return this->equal_to(*rhs_ptr); +} + +bool ARangeNode::equal_to(const ARangeNode& rhs) const { + return start_ == rhs.start_ and stop_ == rhs.stop_ and step_ == rhs.step_; +} + bool ARangeNode::integral() const { return true; } void ARangeNode::initialize_state(State& state) const { @@ -422,6 +432,45 @@ void ARangeNode::propagate(State& state) const { if (ptr->diff().size()) Node::propagate(state); } +void ARangeNode::replace_predecessor_(ssize_t index, Node* node_ptr) { + Node::replace_predecessor_(index, node_ptr); + + assert(0 <= index and static_cast(index) < predecessors().size()); + + const Array* array_ptr = dynamic_cast(node_ptr); + assert(array_ptr != nullptr); + + if (std::holds_alternative(start_)) { + if (index == 0) { + // found it! + start_ = array_ptr; + return; + } + + // If we haven't found the Array* we want to replace, then "normalize" + // the index so that it looks like it would if start wasn't an array. + index -= 1; + } + + if (std::holds_alternative(stop_)) { + if (index == 0) { + // found it! + stop_ = array_ptr; + return; + } + + // If we haven't found the Array* we want to replace, then "normalize" + // the index so that it looks like it would if stop wasn't an array. + index -= 1; + } + + // If we're here then step must be an array and index must be 0 (whether + // because that's what was passed or because we've changed it). + assert(index == 0); + assert(std::holds_alternative(step_)); + step_ = array_ptr; +} + void ARangeNode::revert(State& state) const { data_ptr_(state)->revert(); } std::span ARangeNode::shape(const State& state) const { diff --git a/tests/cpp/nodes/test_creation.cpp b/tests/cpp/nodes/test_creation.cpp index e0db6ae9d..754fb0966 100644 --- a/tests/cpp/nodes/test_creation.cpp +++ b/tests/cpp/nodes/test_creation.cpp @@ -16,6 +16,7 @@ #include #include "dwave-optimization/nodes/collections.hpp" +#include "dwave-optimization/nodes/constants.hpp" #include "dwave-optimization/nodes/creation.hpp" #include "dwave-optimization/nodes/manipulation.hpp" #include "dwave-optimization/nodes/numbers.hpp" @@ -566,6 +567,55 @@ TEST_CASE("ARangeNode") { CHECK_THAT(arange_ptr->view(state), RangeEquals({17, 14, 11})); } } + + SECTION("equality") { + auto graph = Graph(); + + auto* x_ptr = graph.emplace_node(); + auto* y_ptr = graph.emplace_node(); + + Node* a_ptr = graph.emplace_node(0, 10, 1); + Node* b_ptr = graph.emplace_node(0, 10, 1); + Node* c_ptr = graph.emplace_node(1, 10, 2); + + CHECK(a_ptr->equal_to(*b_ptr)); + CHECK(not a_ptr->equal_to(*c_ptr)); + + Node* d_ptr = graph.emplace_node(0, x_ptr); + Node* e_ptr = graph.emplace_node(0, x_ptr); + Node* f_ptr = graph.emplace_node(0, y_ptr); + + CHECK(d_ptr->equal_to(*e_ptr)); + CHECK(not d_ptr->equal_to(*b_ptr)); + CHECK(not d_ptr->equal_to(*f_ptr)); + } + + SECTION("predecessor replacement") { + auto graph = Graph(); + + auto* x_ptr = graph.emplace_node(1); + auto* y_ptr = graph.emplace_node(1); + + SECTION("range(x, x, x); y.take_sucessors(x)") { + auto* arange_ptr = graph.emplace_node(x_ptr, x_ptr, x_ptr); + y_ptr->take_successors(*x_ptr); + + CHECK_THAT(arange_ptr->predecessors(), RangeEquals({y_ptr, y_ptr, y_ptr})); + CHECK(std::get(arange_ptr->start()) == y_ptr); + CHECK(std::get(arange_ptr->stop()) == y_ptr); + CHECK(std::get(arange_ptr->step()) == y_ptr); + } + + SECTION("range(x, 1, x); y.take_sucessors(x)") { + auto* arange_ptr = graph.emplace_node(x_ptr, 1, x_ptr); + y_ptr->take_successors(*x_ptr); + + CHECK_THAT(arange_ptr->predecessors(), RangeEquals({y_ptr, y_ptr})); + CHECK(std::get(arange_ptr->start()) == y_ptr); + CHECK(std::get(arange_ptr->stop()) == 1); + CHECK(std::get(arange_ptr->step()) == y_ptr); + } + } } } // namespace dwave::optimization From 948c199dd99968d6824a67b481bae48557bebe60 Mon Sep 17 00:00:00 2001 From: Alexander Condello Date: Thu, 25 Jun 2026 15:40:08 -0700 Subject: [PATCH 05/25] Add flow nodes equality and predecessor replacement --- .../include/dwave-optimization/nodes/flow.hpp | 4 +- dwave/optimization/src/nodes/flow.cpp | 28 +++--- tests/cpp/nodes/test_flow.cpp | 95 +++++++++++++++++++ 3 files changed, 114 insertions(+), 13 deletions(-) diff --git a/dwave/optimization/include/dwave-optimization/nodes/flow.hpp b/dwave/optimization/include/dwave-optimization/nodes/flow.hpp index 6bf2c92f2..465c3db76 100644 --- a/dwave/optimization/include/dwave-optimization/nodes/flow.hpp +++ b/dwave/optimization/include/dwave-optimization/nodes/flow.hpp @@ -77,7 +77,7 @@ class ExtractNode : public ArrayOutputMixin { SizeInfo sizeinfo() const override; protected: - void replace_predecessor_(ssize_t previous_index, Node* node_ptr) override; + void replace_predecessor_(ssize_t index, Node* node_ptr) override; private: // these are redundant, but convenient @@ -127,7 +127,7 @@ class WhereNode : public ArrayOutputMixin { SizeInfo sizeinfo() const override; protected: - void replace_predecessor_(ssize_t previous_index, Node* node_ptr) override; + void replace_predecessor_(ssize_t index, Node* node_ptr) override; private: // these are redundant, but convenient diff --git a/dwave/optimization/src/nodes/flow.cpp b/dwave/optimization/src/nodes/flow.cpp index cdaaacf55..1301a014c 100644 --- a/dwave/optimization/src/nodes/flow.cpp +++ b/dwave/optimization/src/nodes/flow.cpp @@ -153,8 +153,17 @@ void ExtractNode::propagate(State& state) const { node_data->assign(std::move(new_values), count); } -void ExtractNode::replace_predecessor_(ssize_t previous_index, Node* node_ptr) { - assert(false and "not yet implemented"); +void ExtractNode::replace_predecessor_(ssize_t index, Node* node_ptr) { + Node::replace_predecessor_(index, node_ptr); + + if (index == 0) { + condition_ptr_ = dynamic_cast(node_ptr); + assert(condition_ptr_ != nullptr); + } else { + assert(index == 1); + arr_ptr_ = dynamic_cast(node_ptr); + assert(condition_ptr_ != nullptr); + } } void ExtractNode::revert(State& state) const { data_ptr_(state)->revert(); } @@ -388,22 +397,19 @@ void WhereNode::propagate(State& state) const { } } -void WhereNode::replace_predecessor_(ssize_t previous_index, Node* node_ptr) { - Node::replace_predecessor_(previous_index, node_ptr); +void WhereNode::replace_predecessor_(ssize_t index, Node* node_ptr) { + Node::replace_predecessor_(index, node_ptr); - assert(0 <= previous_index and previous_index < 3); - if (previous_index == 0) { + if (index == 0) { condition_ptr_ = dynamic_cast(node_ptr); assert(condition_ptr_ != nullptr); - } else if (previous_index == 1) { + } else if (index == 1) { x_ptr_ = dynamic_cast(node_ptr); assert(x_ptr_ != nullptr); - } else if (previous_index == 2) { + } else { + assert(index == 2); y_ptr_ = dynamic_cast(node_ptr); assert(y_ptr_ != nullptr); - } else { - assert(false); - unreachable(); } } diff --git a/tests/cpp/nodes/test_flow.cpp b/tests/cpp/nodes/test_flow.cpp index 37a983956..bebe3d751 100644 --- a/tests/cpp/nodes/test_flow.cpp +++ b/tests/cpp/nodes/test_flow.cpp @@ -201,6 +201,49 @@ TEST_CASE("ExtractNode") { } } } + + SECTION("equality") { + auto* c0_ptr = graph.emplace_node(std::vector{}, 0, 5); + auto* x0_ptr = graph.emplace_node(std::vector{}, -10, 10); + + auto* c1_ptr = graph.emplace_node(std::vector{}, 0, 5); + auto* x1_ptr = graph.emplace_node(std::vector{}, -10, 10); + + Node* a_ptr = graph.emplace_node(c0_ptr, x0_ptr); + Node* b_ptr = graph.emplace_node(c0_ptr, x0_ptr); + Node* c_ptr = graph.emplace_node(c1_ptr, x0_ptr); + Node* d_ptr = graph.emplace_node(c0_ptr, x1_ptr); + + CHECK(a_ptr->equal_to(*a_ptr)); + CHECK(a_ptr->equal_to(*b_ptr)); + CHECK(not a_ptr->equal_to(*x0_ptr)); + CHECK(not a_ptr->equal_to(*c_ptr)); + CHECK(not a_ptr->equal_to(*d_ptr)); + } + + SECTION("predecessor replacement") { + auto* c0_ptr = graph.emplace_node(std::vector{}, 0, 5); + auto* x0_ptr = graph.emplace_node(std::vector{}, -10, 10); + + auto* c1_ptr = graph.emplace_node(std::vector{}, 0, 5); + auto* x1_ptr = graph.emplace_node(std::vector{}, -10, 10); + + auto* extract_ptr = graph.emplace_node(c0_ptr, x0_ptr); + + c1_ptr->take_successors(*c0_ptr); + x1_ptr->take_successors(*x0_ptr); + + CHECK_THAT(extract_ptr->predecessors(), RangeEquals({c1_ptr, x1_ptr})); + + auto state = graph.empty_state(); + c0_ptr->initialize_state(state, {0}); + x0_ptr->initialize_state(state, {1}); + c1_ptr->initialize_state(state, {1}); + x1_ptr->initialize_state(state, {5}); + graph.initialize_state(state); + + CHECK_THAT(extract_ptr->view(state), RangeEquals({5})); + } } TEST_CASE("WhereNode") { @@ -555,6 +598,58 @@ TEST_CASE("WhereNode") { CHECK_THROWS_AS(WhereNode(condition_ptr, x_ptr, y_ptr), std::invalid_argument); } } + + SECTION("equality") { + auto* c0_ptr = graph.emplace_node(std::vector{}, 0, 5); + auto* x0_ptr = graph.emplace_node(std::vector{}, -10, 10); + auto* y0_ptr = graph.emplace_node(std::vector{}, 0, 10); + + auto* c1_ptr = graph.emplace_node(std::vector{}, 0, 5); + auto* x1_ptr = graph.emplace_node(std::vector{}, -10, 10); + auto* y1_ptr = graph.emplace_node(std::vector{}, 0, 10); + + Node* a_ptr = graph.emplace_node(c0_ptr, x0_ptr, y0_ptr); + Node* b_ptr = graph.emplace_node(c0_ptr, x0_ptr, y0_ptr); + Node* c_ptr = graph.emplace_node(c1_ptr, x0_ptr, y0_ptr); + Node* d_ptr = graph.emplace_node(c0_ptr, x1_ptr, y0_ptr); + Node* e_ptr = graph.emplace_node(c0_ptr, x0_ptr, y1_ptr); + + CHECK(a_ptr->equal_to(*a_ptr)); + CHECK(a_ptr->equal_to(*b_ptr)); + CHECK(not a_ptr->equal_to(*x0_ptr)); + CHECK(not a_ptr->equal_to(*c_ptr)); + CHECK(not a_ptr->equal_to(*d_ptr)); + CHECK(not a_ptr->equal_to(*e_ptr)); + } + + SECTION("predecessor replacement") { + auto* c0_ptr = graph.emplace_node(std::vector{}, 0, 5); + auto* x0_ptr = graph.emplace_node(std::vector{}, -10, 10); + auto* y0_ptr = graph.emplace_node(std::vector{}, 0, 10); + + auto* c1_ptr = graph.emplace_node(std::vector{}, 0, 5); + auto* x1_ptr = graph.emplace_node(std::vector{}, -10, 10); + auto* y1_ptr = graph.emplace_node(std::vector{}, 0, 10); + + auto* where_ptr = graph.emplace_node(c0_ptr, x0_ptr, y0_ptr); + + c1_ptr->take_successors(*c0_ptr); + x1_ptr->take_successors(*x0_ptr); + y1_ptr->take_successors(*y0_ptr); + + CHECK_THAT(where_ptr->predecessors(), RangeEquals({c1_ptr, x1_ptr, y1_ptr})); + + auto state = graph.empty_state(); + c0_ptr->initialize_state(state, {0}); + x0_ptr->initialize_state(state, {1}); + y0_ptr->initialize_state(state, {2}); + c1_ptr->initialize_state(state, {1}); + x1_ptr->initialize_state(state, {3}); + y1_ptr->initialize_state(state, {4}); + graph.initialize_state(state); + + CHECK_THAT(where_ptr->view(state), RangeEquals({3})); + } } } // namespace dwave::optimization From 2985dcf0cea2d0ea785f2c3a8ebc5135d0561d09 Mon Sep 17 00:00:00 2001 From: Alexander Condello Date: Tue, 30 Jun 2026 10:36:22 -0700 Subject: [PATCH 06/25] Add InputNode equality and predecessor replacement --- .../include/dwave-optimization/nodes/inputs.hpp | 6 ++++++ dwave/optimization/src/nodes/inputs.cpp | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/dwave/optimization/include/dwave-optimization/nodes/inputs.hpp b/dwave/optimization/include/dwave-optimization/nodes/inputs.hpp index 6b13f35da..66011280b 100644 --- a/dwave/optimization/include/dwave-optimization/nodes/inputs.hpp +++ b/dwave/optimization/include/dwave-optimization/nodes/inputs.hpp @@ -76,6 +76,9 @@ class InputNode : public ArrayOutputMixin { /// @copydoc Array::diff() std::span diff(const State& state) const noexcept override; + /// InputNodes are never equal to other nodes + bool equal_to(const Node& rhs) const override { return false; } + /// @copydoc Array::integral() bool integral() const override { return values_info_.integral; }; @@ -102,6 +105,9 @@ class InputNode : public ArrayOutputMixin { /// @copydoc Node::revert() void revert(State& state) const noexcept override; + protected: + void replace_predecessor_(ssize_t index, Node* node_ptr) override; + private: void check_values(std::span new_values) const; diff --git a/dwave/optimization/src/nodes/inputs.cpp b/dwave/optimization/src/nodes/inputs.cpp index ba0967bec..63f0b0d95 100644 --- a/dwave/optimization/src/nodes/inputs.cpp +++ b/dwave/optimization/src/nodes/inputs.cpp @@ -99,6 +99,10 @@ void InputNode::initialize_state(State& state, std::span data) con emplace_data_ptr_(state, std::move(copy)); } +void InputNode::replace_predecessor_(ssize_t index, Node* node_ptr) { + assert(false and "InputNode never has any predecessors"); +} + void InputNode::revert(State& state) const noexcept { data_ptr_(state)->revert(); } From 1cece38f7691ad137c3f9511d47c9a337dd79378 Mon Sep 17 00:00:00 2001 From: Alexander Condello Date: Tue, 30 Jun 2026 11:08:42 -0700 Subject: [PATCH 07/25] Add interpolation node equality and predecessor replacement --- .../nodes/interpolation.hpp | 8 ++++ .../optimization/src/nodes/interpolation.cpp | 19 ++++++++ tests/cpp/nodes/test_interpolation.cpp | 48 +++++++++++++++++++ 3 files changed, 75 insertions(+) diff --git a/dwave/optimization/include/dwave-optimization/nodes/interpolation.hpp b/dwave/optimization/include/dwave-optimization/nodes/interpolation.hpp index 928ce9e59..8f6ac383b 100644 --- a/dwave/optimization/include/dwave-optimization/nodes/interpolation.hpp +++ b/dwave/optimization/include/dwave-optimization/nodes/interpolation.hpp @@ -36,6 +36,11 @@ class BSplineNode : public ArrayOutputMixin { double const* buff(const State& state) const override; void commit(State& state) const override; std::span diff(const State&) const override; + + /// @copydoc Node::equal_to() + bool equal_to(const Node& rhs) const override; + bool equal_to(const BSplineNode& rhs) const; + void initialize_state(State& state) const override; /// @copydoc Array::integral() @@ -58,6 +63,9 @@ class BSplineNode : public ArrayOutputMixin { using Array::size; ssize_t size(const State& state) const override; + protected: + void replace_predecessor_(ssize_t index, Node* node_ptr) override; + private: const Array* array_ptr_; diff --git a/dwave/optimization/src/nodes/interpolation.cpp b/dwave/optimization/src/nodes/interpolation.cpp index 45bc9f0f5..c8833d92b 100644 --- a/dwave/optimization/src/nodes/interpolation.cpp +++ b/dwave/optimization/src/nodes/interpolation.cpp @@ -127,6 +127,17 @@ void BSplineNode::commit(State& state) const { std::span BSplineNode::diff(const State& state) const { return data_ptr_(state)->diff(); } + +bool BSplineNode::equal_to(const Node& rhs) const { + const auto* rhs_ptr = dynamic_cast(&rhs); + if (rhs_ptr == nullptr) return false; // not same type so not equal + return this->equal_to(*rhs_ptr); +} + +bool BSplineNode::equal_to(const BSplineNode& rhs) const { + return array_ptr_ == rhs.array_ptr_ and k_ == rhs.k_ and t_ == rhs.t_ and c_ == rhs.c_; +} + bool BSplineNode::integral() const { return false; } void BSplineNode::revert(State& state) const { @@ -157,4 +168,12 @@ void BSplineNode::propagate(State& state) const { } } +void BSplineNode::replace_predecessor_(ssize_t index, Node* node_ptr) { + Node::replace_predecessor_(index, node_ptr); + + assert(index == 0); + array_ptr_ = dynamic_cast(node_ptr); + assert(array_ptr_ != nullptr); +} + } // namespace dwave::optimization diff --git a/tests/cpp/nodes/test_interpolation.cpp b/tests/cpp/nodes/test_interpolation.cpp index 51768d4ba..f07da2329 100644 --- a/tests/cpp/nodes/test_interpolation.cpp +++ b/tests/cpp/nodes/test_interpolation.cpp @@ -13,12 +13,16 @@ // limitations under the License. #include +#include +#include "catch2/matchers/catch_matchers_range_equals.hpp" #include "dwave-optimization/graph.hpp" #include "dwave-optimization/nodes/constants.hpp" #include "dwave-optimization/nodes/interpolation.hpp" #include "dwave-optimization/nodes/numbers.hpp" +using Catch::Matchers::RangeEquals; + namespace dwave::optimization { TEST_CASE("BSpline") { @@ -114,6 +118,50 @@ TEST_CASE("BSpline") { } } } + + SECTION("equality") { + auto graph = Graph(); + + auto* x0_ptr = graph.emplace_node(std::vector{2.5}); + auto* x1_ptr = graph.emplace_node(std::vector{3.5}); + + int k = 2; + std::vector t0 = {0, 1, 2, 3, 4, 5, 6}; + std::vector t1 = {1, 1, 2, 3, 4, 5, 6}; + std::vector c0 = {-1, 2, 0, -1}; + std::vector c1 = {1, 2, 0, -1}; + + Node* a_ptr = graph.emplace_node(x0_ptr, k, t0, c0); + Node* b_ptr = graph.emplace_node(x0_ptr, k, t0, c0); + Node* c_ptr = graph.emplace_node(x1_ptr, k, t1, c0); + Node* d_ptr = graph.emplace_node(x0_ptr, k, t0, c1); + + CHECK(a_ptr->equal_to(*a_ptr)); + CHECK(a_ptr->equal_to(*b_ptr)); + CHECK(not a_ptr->equal_to(*x0_ptr)); + CHECK(not a_ptr->equal_to(*c_ptr)); + CHECK(not a_ptr->equal_to(*d_ptr)); + } + + SECTION("predecessor replacement") { + auto graph = Graph(); + + auto* x0_ptr = graph.emplace_node(std::vector{2.5}); + auto* x1_ptr = graph.emplace_node(std::vector{3.5}); + + int k = 2; + std::vector t = {0, 1, 2, 3, 4, 5, 6}; + std::vector c = {-1, 2, 0, -1}; + + auto* bspline_ptr = graph.emplace_node(x0_ptr, k, t, c); + + x1_ptr->take_successors(*x0_ptr); + + CHECK_THAT(bspline_ptr->predecessors(), RangeEquals({x1_ptr})); + + auto state = graph.initialize_state(); + CHECK_THAT(bspline_ptr->view(state), RangeEquals({0.125})); + } } } // namespace dwave::optimization From 055fe6200beae9d60faec23398f7cbef371a174e Mon Sep 17 00:00:00 2001 From: Alexander Condello Date: Thu, 2 Jul 2026 09:13:35 -0700 Subject: [PATCH 08/25] Add lambda node equality and predecessor replacement --- .../dwave-optimization/nodes/lambda.hpp | 12 ++- dwave/optimization/libcpp/nodes/lambda_.pxd | 2 +- dwave/optimization/src/nodes/lambda.cpp | 49 +++++++-- dwave/optimization/symbols/accumulate_zip.pyx | 12 +-- ...dundant-node-removal-9e6335836d0f5603.yaml | 3 + tests/cpp/nodes/test_lambda.cpp | 102 ++++++++++++++++++ 6 files changed, 164 insertions(+), 16 deletions(-) create mode 100644 releasenotes/notes/feature-redundant-node-removal-9e6335836d0f5603.yaml diff --git a/dwave/optimization/include/dwave-optimization/nodes/lambda.hpp b/dwave/optimization/include/dwave-optimization/nodes/lambda.hpp index 4c85618ec..1e9c50fc0 100644 --- a/dwave/optimization/include/dwave-optimization/nodes/lambda.hpp +++ b/dwave/optimization/include/dwave-optimization/nodes/lambda.hpp @@ -97,10 +97,16 @@ class AccumulateZipNode : public ArrayOutputMixin { /// @copydoc Array::diff() std::span diff(const State& state) const override; + /// @copydoc Node::equal_to() + bool equal_to(const Node& rhs) const override; + bool equal_to(const AccumulateZipNode& rhs) const; + /// Access the underlying shared_ptr holding the Graph. /// Modifying the Graph leads to undefined behavior. std::shared_ptr& expression_ptr() { return expression_ptr_; } + const array_or_double& initial() const { return initial_; } + /// @copydoc Node::initialize_state() void initialize_state(State& state) const override; @@ -125,7 +131,8 @@ class AccumulateZipNode : public ArrayOutputMixin { ssize_t size_diff(const State& state) const override; SizeInfo sizeinfo() const override; - const array_or_double initial; + protected: + void replace_predecessor_(ssize_t index, Node* node_ptr) override; private: double evaluate_expression(State& register_) const; @@ -136,7 +143,8 @@ class AccumulateZipNode : public ArrayOutputMixin { const InputNode* const accumulate_input() const; std::shared_ptr expression_ptr_; - const std::vector operands_; + array_or_double initial_; + std::vector operands_; const SizeInfo sizeinfo_; }; diff --git a/dwave/optimization/libcpp/nodes/lambda_.pxd b/dwave/optimization/libcpp/nodes/lambda_.pxd index 0cf2d53df..08d4ec08c 100644 --- a/dwave/optimization/libcpp/nodes/lambda_.pxd +++ b/dwave/optimization/libcpp/nodes/lambda_.pxd @@ -22,4 +22,4 @@ cdef extern from "dwave-optimization/nodes/lambda.hpp" namespace "dwave::optimiz cdef cppclass AccumulateZipNode(ArrayNode): ctypedef variant["ArrayNode*", double] array_or_double shared_ptr[Graph] expression_ptr() - const array_or_double initial + const array_or_double& initial() const diff --git a/dwave/optimization/src/nodes/lambda.cpp b/dwave/optimization/src/nodes/lambda.cpp index bdb0bd4de..0154b7c76 100644 --- a/dwave/optimization/src/nodes/lambda.cpp +++ b/dwave/optimization/src/nodes/lambda.cpp @@ -16,6 +16,7 @@ #include "_state.hpp" #include "dwave-optimization/array.hpp" +#include "dwave-optimization/graph.hpp" #include "dwave-optimization/nodes/inputs.hpp" #include "dwave-optimization/state.hpp" @@ -44,15 +45,15 @@ AccumulateZipNode::AccumulateZipNode( array_or_double initial ) : ArrayOutputMixin(operands.empty() ? std::span() : operands[0]->shape()), - initial(initial), expression_ptr_(std::move(expression_ptr)), + initial_(initial), operands_(operands), sizeinfo_(operands.empty() ? SizeInfo(0) : operands_[0]->sizeinfo()) { - check(*expression_ptr_, operands, initial); + check(*expression_ptr_, operands, initial_); - if (std::holds_alternative(initial)) { + if (std::holds_alternative(initial_)) { // was checked in check() method - add_predecessor_(std::get(initial)); + add_predecessor_(std::get(initial_)); } for (const auto& op : operands_) { add_predecessor_(op); @@ -242,10 +243,10 @@ double AccumulateZipNode::evaluate_expression(State& register_) const { } double AccumulateZipNode::get_initial_value(const State& state) const { - if (std::holds_alternative(initial)) { - return std::get(initial); + if (std::holds_alternative(initial_)) { + return std::get(initial_); } else { - return std::get(initial)->view(state)[0]; + return std::get(initial_)->view(state)[0]; } } @@ -301,6 +302,22 @@ void AccumulateZipNode::initialize_state(State& state) const { ); } +bool AccumulateZipNode::equal_to(const Node& rhs) const { + const auto* rhs_ptr = dynamic_cast(&rhs); + if (rhs_ptr == nullptr) return false; // not same type so not equal + return this->equal_to(*rhs_ptr); +} + +bool AccumulateZipNode::equal_to(const AccumulateZipNode& rhs) const { + // note that we don't have a notion of Graph equality, so we instead + // just check whether we have the same underlying shared ptr + return ( + expression_ptr_ == rhs.expression_ptr_ and // + operands_ == rhs.operands_ and // + initial_ == rhs.initial_ + ); +} + bool AccumulateZipNode::integral() const { return expression_ptr_->objective()->integral(); } double AccumulateZipNode::max() const { return expression_ptr_->objective()->max(); } @@ -354,6 +371,24 @@ const InputNode* const AccumulateZipNode::accumulate_input() const { return expression_ptr_->inputs()[0]; } +void AccumulateZipNode::replace_predecessor_(ssize_t index, Node* node_ptr) { + Node::replace_predecessor_(index, node_ptr); + + if (std::holds_alternative(initial_)) { + if (index == 0) { + // we're replacing the initial array + initial_ = dynamic_cast(node_ptr); + assert(std::get(initial_) != nullptr); + return; + } + + index -= 1; + } + + operands_[index] = dynamic_cast(node_ptr); + assert(operands_[index] != nullptr); +} + void AccumulateZipNode::revert(State& state) const { data_ptr_(state)->revert(); } diff --git a/dwave/optimization/symbols/accumulate_zip.pyx b/dwave/optimization/symbols/accumulate_zip.pyx index f9d801397..0319a8a3f 100644 --- a/dwave/optimization/symbols/accumulate_zip.pyx +++ b/dwave/optimization/symbols/accumulate_zip.pyx @@ -191,10 +191,10 @@ cdef class AccumulateZip(ArraySymbol): return AccumulateZip(expression, operands, initial=initial) def initial(self): - if holds_alternative["ArrayNode*"](self.ptr.initial): - return symbol_from_ptr(self.model, get["ArrayNode*"](self.ptr.initial)) + if holds_alternative["ArrayNode*"](self.ptr.initial()): + return symbol_from_ptr(self.model, get["ArrayNode*"](self.ptr.initial())) else: - return get[double](self.ptr.initial) + return get[double](self.ptr.initial()) def _into_zipfile(self, zf, directory): """Store a AccumulateZip symbol as a compressed file. @@ -224,13 +224,13 @@ cdef class AccumulateZip(ArraySymbol): # Now save information about the initial state encoder = json.JSONEncoder(separators=(',', ':')) - if holds_alternative["ArrayNode*"](self.ptr.initial): + if holds_alternative["ArrayNode*"](self.ptr.initial()): initial_info = dict( type="node", - value=get["ArrayNode*"](self.ptr.initial).topological_index() + value=get["ArrayNode*"](self.ptr.initial()).topological_index() ) else: - initial_info = dict(type="double", value=get[double](self.ptr.initial)) + initial_info = dict(type="double", value=get[double](self.ptr.initial())) zf.writestr(directory + "initial.json", encoder.encode(initial_info)) cdef AccumulateZipNode* ptr diff --git a/releasenotes/notes/feature-redundant-node-removal-9e6335836d0f5603.yaml b/releasenotes/notes/feature-redundant-node-removal-9e6335836d0f5603.yaml new file mode 100644 index 000000000..6f4ae7496 --- /dev/null +++ b/releasenotes/notes/feature-redundant-node-removal-9e6335836d0f5603.yaml @@ -0,0 +1,3 @@ +--- +upgrade: + - Remove ``AccumulateZipNode::initial`` attribute and replace it with ``AccumulateZipNode::initial()`` method. diff --git a/tests/cpp/nodes/test_lambda.cpp b/tests/cpp/nodes/test_lambda.cpp index 0382bdb11..83641e9ce 100644 --- a/tests/cpp/nodes/test_lambda.cpp +++ b/tests/cpp/nodes/test_lambda.cpp @@ -12,6 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. +#include +#include + #include #include @@ -454,6 +457,105 @@ TEST_CASE("AccumulateZipNode") { } } } + + SECTION("equality") { + auto* i0_ptr = graph.emplace_node(std::vector{0, 1, 2, 2}); + auto* i1_ptr = graph.emplace_node(std::vector{0, 1, 2, 2}); + + auto* j0_ptr = graph.emplace_node(std::vector{1, 2, 4, 3}); + auto* j1_ptr = graph.emplace_node(std::vector{1, 2, 4, 3}); + + // x0 * x1 + x2 + auto make_expression = []() -> std::shared_ptr { + auto expression = Graph(); + + std::vector inputs = { + expression.emplace_node(InputNode::unbounded_scalar()), + expression.emplace_node(InputNode::unbounded_scalar()), + expression.emplace_node(InputNode::unbounded_scalar()) + }; + auto output_ptr = expression.emplace_node( + expression.emplace_node(inputs[1], inputs[2]), inputs[0] + ); + expression.set_objective(output_ptr); + expression.topological_sort(); + + return std::make_shared(std::move(expression)); + }; + + auto expr0_ptr = make_expression(); + auto expr1_ptr = make_expression(); + + Node* a_ptr = graph.emplace_node( + expr0_ptr, std::vector{i0_ptr, j0_ptr}, 5.0 + ); + Node* b_ptr = graph.emplace_node( + expr0_ptr, std::vector{i0_ptr, j0_ptr}, 5.0 + ); + Node* c_ptr = graph.emplace_node( + expr1_ptr, std::vector{i0_ptr, j0_ptr}, 5.0 + ); + Node* d_ptr = graph.emplace_node( + expr0_ptr, std::vector{i1_ptr, j0_ptr}, 5.0 + ); + Node* e_ptr = graph.emplace_node( + expr0_ptr, std::vector{i0_ptr, j1_ptr}, 5.0 + ); + Node* f_ptr = graph.emplace_node( + expr0_ptr, std::vector{i0_ptr, j0_ptr}, 4.0 + ); + + CHECK(a_ptr->equal_to(*a_ptr)); + CHECK(a_ptr->equal_to(*b_ptr)); + CHECK(not a_ptr->equal_to(*i0_ptr)); + CHECK(not a_ptr->equal_to(*c_ptr)); + CHECK(not a_ptr->equal_to(*d_ptr)); + CHECK(not a_ptr->equal_to(*e_ptr)); + CHECK(not a_ptr->equal_to(*f_ptr)); + } + + SECTION("predecessor replacement") { + auto* i0_ptr = graph.emplace_node(std::vector{0, 1, 2, 2}); + auto* i1_ptr = graph.emplace_node(std::vector{1, 2, 2, 0}); + + auto* j0_ptr = graph.emplace_node(std::vector{1, 2, 4, 3}); + auto* j1_ptr = graph.emplace_node(std::vector{2, 4, 3, 1}); + + auto* init0_ptr = graph.emplace_node(5); + auto* init1_ptr = graph.emplace_node(6); + + // x0 * x1 + x2 + auto make_expression = []() -> std::shared_ptr { + auto expression = Graph(); + + std::vector inputs = { + expression.emplace_node(InputNode::unbounded_scalar()), + expression.emplace_node(InputNode::unbounded_scalar()), + expression.emplace_node(InputNode::unbounded_scalar()) + }; + auto output_ptr = expression.emplace_node( + expression.emplace_node(inputs[1], inputs[2]), inputs[0] + ); + expression.set_objective(output_ptr); + expression.topological_sort(); + + return std::make_shared(std::move(expression)); + }; + + auto* acc_ptr = graph.emplace_node( + make_expression(), std::vector{i0_ptr, j0_ptr}, init0_ptr + ); + + i1_ptr->take_successors(*i0_ptr); + j1_ptr->take_successors(*j0_ptr); + init1_ptr->take_successors(*init0_ptr); + + CHECK_THAT(acc_ptr->predecessors(), RangeEquals({init1_ptr, i1_ptr, j1_ptr})); + + auto state = graph.initialize_state(); + + CHECK_THAT(acc_ptr->view(state), RangeEquals({8, 16, 22, 22})); + } } } // namespace dwave::optimization From 770a9cb374e4f3ca371f8ac7d99c1c2383d305e0 Mon Sep 17 00:00:00 2001 From: Alexander Condello Date: Thu, 2 Jul 2026 10:39:58 -0700 Subject: [PATCH 09/25] Add EqualityMixin to reduce code duplication --- .../include/dwave-optimization/graph.hpp | 45 +++++++++++++++++-- .../dwave-optimization/nodes/constants.hpp | 5 +-- .../dwave-optimization/nodes/creation.hpp | 5 +-- .../include/dwave-optimization/nodes/flow.hpp | 10 +---- .../dwave-optimization/nodes/indexing.hpp | 15 +++---- .../dwave-optimization/nodes/inputs.hpp | 2 +- .../nodes/interpolation.hpp | 5 +-- .../dwave-optimization/nodes/lambda.hpp | 5 +-- dwave/optimization/src/nodes/constants.cpp | 6 --- dwave/optimization/src/nodes/creation.cpp | 6 --- dwave/optimization/src/nodes/flow.cpp | 20 --------- dwave/optimization/src/nodes/indexing.cpp | 18 -------- .../optimization/src/nodes/interpolation.cpp | 6 --- dwave/optimization/src/nodes/lambda.cpp | 6 --- 14 files changed, 59 insertions(+), 95 deletions(-) diff --git a/dwave/optimization/include/dwave-optimization/graph.hpp b/dwave/optimization/include/dwave-optimization/graph.hpp index 8b3b3bd23..f0c3c2bd9 100644 --- a/dwave/optimization/include/dwave-optimization/graph.hpp +++ b/dwave/optimization/include/dwave-optimization/graph.hpp @@ -16,6 +16,7 @@ #include #include +#include #include #include #include @@ -461,9 +462,7 @@ class DecisionNode : public Decision, public virtual Node { /// Decision can only ever be equal to themselves because they are /// independent variables. - bool equal_to(const Node& rhs) const final { - return static_cast(this) == &rhs; - } + bool equal_to(const Node& rhs) const final { return static_cast(this) == &rhs; } /// Decisions don't have predecessors so no one should be calling update(). /// Always throws a logic_error. @@ -474,4 +473,44 @@ class DecisionNode : public Decision, public virtual Node { bool removable_() const override { return false; } }; +/// Provide an implementation of equal_to() that defers to a type-specific +/// implementation. +template Base, typename Derived = void> +struct EqualityMixin : Base { + /// @copydoc Node::equal_to() + bool equal_to(const Node& rhs) const final { + // Every node is equal with itself + if (&rhs == static_cast(this)) return true; + + // A node cannot be equal if they are not of the same type. + const auto* rhs_ptr = dynamic_cast(&rhs); + if (rhs_ptr == nullptr) return false; + + // lhs and rhs are the same type, so let's go to our type-specific + // method. + return equal_to(*rhs_ptr); + } + + // Nodes must implement `equal_to()` for their own type. + virtual bool equal_to(const Derived& rhs) const = 0; +}; + +/// Overload for EqualityMixin that does not require a class-specific equal_to() +/// method overload. This simply checks that they have the same type and same +/// predecessors. +template Base> +struct EqualityMixin : Base { + /// @copydoc Node::equal_to() + bool equal_to(const Node& rhs) const final { + // Every node is equal with itself + if (&rhs == static_cast(this)) return true; + + // Nodes that are not the same type cannot be equal + if (typeid(*this) != typeid(rhs)) return false; + + // Nodes that are of the same type must have the same predecessors + return std::ranges::equal(this->predecessors(), rhs.predecessors()); + } +}; + } // namespace dwave::optimization diff --git a/dwave/optimization/include/dwave-optimization/nodes/constants.hpp b/dwave/optimization/include/dwave-optimization/nodes/constants.hpp index b786dfa3d..b01291479 100644 --- a/dwave/optimization/include/dwave-optimization/nodes/constants.hpp +++ b/dwave/optimization/include/dwave-optimization/nodes/constants.hpp @@ -26,7 +26,7 @@ namespace dwave::optimization { /// A contiguous block of numbers. -class ConstantNode : public ArrayOutputMixin { +class ConstantNode : public ArrayOutputMixin> { public: struct DataSource { DataSource() = default; @@ -100,8 +100,7 @@ class ConstantNode : public ArrayOutputMixin { // Overloads required by the Node ABC ************************************* - bool equal_to(const Node& rhs) const override; - bool equal_to(const ConstantNode& rhs) const; + bool equal_to(const ConstantNode& rhs) const override; // This node never needs to update its successors void propagate(State&) const noexcept override {} diff --git a/dwave/optimization/include/dwave-optimization/nodes/creation.hpp b/dwave/optimization/include/dwave-optimization/nodes/creation.hpp index 18052ef0c..5e77e6ff4 100644 --- a/dwave/optimization/include/dwave-optimization/nodes/creation.hpp +++ b/dwave/optimization/include/dwave-optimization/nodes/creation.hpp @@ -24,7 +24,7 @@ namespace dwave::optimization { /// A node encoding evenly spaced values within a given interval. -class ARangeNode : public ArrayOutputMixin { +class ARangeNode : public ArrayOutputMixin> { public: using array_or_int = std::variant; @@ -84,8 +84,7 @@ class ARangeNode : public ArrayOutputMixin { std::span diff(const State& state) const override; /// ARangeNode is equal to another ARangeNode encoding the same range. - bool equal_to(const Node& rhs) const override; - bool equal_to(const ARangeNode& rhs) const; + bool equal_to(const ARangeNode& rhs) const override; /// @copydoc Node::initialize_state() void initialize_state(State& state) const override; diff --git a/dwave/optimization/include/dwave-optimization/nodes/flow.hpp b/dwave/optimization/include/dwave-optimization/nodes/flow.hpp index 465c3db76..a8e01cc97 100644 --- a/dwave/optimization/include/dwave-optimization/nodes/flow.hpp +++ b/dwave/optimization/include/dwave-optimization/nodes/flow.hpp @@ -26,7 +26,7 @@ namespace dwave::optimization { /// /// `condition` and `arr` must be the same size. This always outputs a /// 1d array. -class ExtractNode : public ArrayOutputMixin { +class ExtractNode : public ArrayOutputMixin> { public: ExtractNode(ArrayNode* condition_ptr, ArrayNode* arr_ptr); @@ -39,9 +39,6 @@ class ExtractNode : public ArrayOutputMixin { /// @copydoc Array::diff() std::span diff(const State& state) const override; - bool equal_to(const Node& rhs) const override; - bool equal_to(const ExtractNode& rhs) const; - /// @copydoc Node::initialize_state() void initialize_state(State& state) const override; @@ -93,7 +90,7 @@ class ExtractNode : public ArrayOutputMixin { /// `condition` must be either a scalar array or the same shape as `x` and `y`. /// `x` and `y` must have the same shape, including dynamic. /// dynamically sized `condition`s are not allowed. -class WhereNode : public ArrayOutputMixin { +class WhereNode : public ArrayOutputMixin> { public: WhereNode(ArrayNode* condition_ptr, ArrayNode* x_ptr, ArrayNode* y_ptr); @@ -101,9 +98,6 @@ class WhereNode : public ArrayOutputMixin { void commit(State& state) const override; std::span diff(const State& state) const override; - bool equal_to(const Node& rhs) const override; - bool equal_to(const WhereNode& rhs) const; - void initialize_state(State& state) const override; /// @copydoc Array::integral() diff --git a/dwave/optimization/include/dwave-optimization/nodes/indexing.hpp b/dwave/optimization/include/dwave-optimization/nodes/indexing.hpp index 9a456762b..e61cce7e3 100644 --- a/dwave/optimization/include/dwave-optimization/nodes/indexing.hpp +++ b/dwave/optimization/include/dwave-optimization/nodes/indexing.hpp @@ -41,7 +41,7 @@ namespace dwave::optimization { // https://numpy.org/doc/stable/user/basics.indexing.html#combining-advanced-and-basic-indexing // A combination of int/slice/array indices. The rules are... more complicated. -class AdvancedIndexingNode : public ArrayNode { +class AdvancedIndexingNode : public EqualityMixin { public: // Indices are some combination of arrays and slices using array_or_slice = std::variant; @@ -80,8 +80,7 @@ class AdvancedIndexingNode : public ArrayNode { // Node overloads void commit(State& state) const override; - bool equal_to(const Node& rhs) const override; - bool equal_to(const AdvancedIndexingNode& rhs) const; + bool equal_to(const AdvancedIndexingNode& rhs) const override; void initialize_state(State& state) const override; void propagate(State& state) const override; void revert(State& state) const override; @@ -184,7 +183,7 @@ class AdvancedIndexingNode : public ArrayNode { // See https://numpy.org/doc/stable/user/basics.indexing.html#basic-indexing // BasicIndex nodes always have exactly one predecessor representing the array. // to be indexed. -class BasicIndexingNode : public ArrayNode { +class BasicIndexingNode : public EqualityMixin { public: // Indices are some combination of slices and integers. using slice_or_int = std::variant; @@ -234,8 +233,7 @@ class BasicIndexingNode : public ArrayNode { // don't need to overload update() - bool equal_to(const Node& rhs) const override; - bool equal_to(const BasicIndexingNode& rhs) const; + bool equal_to(const BasicIndexingNode& rhs) const override; void propagate(State& state) const override; @@ -304,7 +302,7 @@ class BasicIndexingNode : public ArrayNode { const SizeInfo sizeinfo_; }; -class PermutationNode : public ArrayOutputMixin { +class PermutationNode : public ArrayOutputMixin> { public: // We use this style rather than a template to support Cython later PermutationNode(ArrayNode* array_ptr, ArrayNode* order_ptr); @@ -313,8 +311,7 @@ class PermutationNode : public ArrayOutputMixin { std::span diff(const State& state) const override; /// @copydoc Node::equal_to() - bool equal_to(const Node& rhs) const override; - bool equal_to(const PermutationNode& rhs) const; + bool equal_to(const PermutationNode& rhs) const override; /// @copydoc Array::integral() bool integral() const override; diff --git a/dwave/optimization/include/dwave-optimization/nodes/inputs.hpp b/dwave/optimization/include/dwave-optimization/nodes/inputs.hpp index 66011280b..578799f69 100644 --- a/dwave/optimization/include/dwave-optimization/nodes/inputs.hpp +++ b/dwave/optimization/include/dwave-optimization/nodes/inputs.hpp @@ -77,7 +77,7 @@ class InputNode : public ArrayOutputMixin { std::span diff(const State& state) const noexcept override; /// InputNodes are never equal to other nodes - bool equal_to(const Node& rhs) const override { return false; } + bool equal_to(const Node& rhs) const override { return static_cast(this) == &rhs; } /// @copydoc Array::integral() bool integral() const override { return values_info_.integral; }; diff --git a/dwave/optimization/include/dwave-optimization/nodes/interpolation.hpp b/dwave/optimization/include/dwave-optimization/nodes/interpolation.hpp index 8f6ac383b..d77073be7 100644 --- a/dwave/optimization/include/dwave-optimization/nodes/interpolation.hpp +++ b/dwave/optimization/include/dwave-optimization/nodes/interpolation.hpp @@ -24,7 +24,7 @@ #include "dwave-optimization/state.hpp" namespace dwave::optimization { -class BSplineNode : public ArrayOutputMixin { +class BSplineNode : public ArrayOutputMixin> { public: explicit BSplineNode( ArrayNode* array_ptr, @@ -38,8 +38,7 @@ class BSplineNode : public ArrayOutputMixin { std::span diff(const State&) const override; /// @copydoc Node::equal_to() - bool equal_to(const Node& rhs) const override; - bool equal_to(const BSplineNode& rhs) const; + bool equal_to(const BSplineNode& rhs) const override; void initialize_state(State& state) const override; diff --git a/dwave/optimization/include/dwave-optimization/nodes/lambda.hpp b/dwave/optimization/include/dwave-optimization/nodes/lambda.hpp index 1e9c50fc0..bb1b4dc5e 100644 --- a/dwave/optimization/include/dwave-optimization/nodes/lambda.hpp +++ b/dwave/optimization/include/dwave-optimization/nodes/lambda.hpp @@ -36,7 +36,7 @@ namespace dwave::optimization { // `std::accumulate()`, the special input should be the first `InputNode` // on the given `Graph`, with the remaining inputs used for the values of // the operands. -class AccumulateZipNode : public ArrayOutputMixin { +class AccumulateZipNode : public ArrayOutputMixin> { public: // Initial value can either be a double or another node using array_or_double = std::variant; @@ -98,8 +98,7 @@ class AccumulateZipNode : public ArrayOutputMixin { std::span diff(const State& state) const override; /// @copydoc Node::equal_to() - bool equal_to(const Node& rhs) const override; - bool equal_to(const AccumulateZipNode& rhs) const; + bool equal_to(const AccumulateZipNode& rhs) const override; /// Access the underlying shared_ptr holding the Graph. /// Modifying the Graph leads to undefined behavior. diff --git a/dwave/optimization/src/nodes/constants.cpp b/dwave/optimization/src/nodes/constants.cpp index e8215548b..e20de6f0f 100644 --- a/dwave/optimization/src/nodes/constants.cpp +++ b/dwave/optimization/src/nodes/constants.cpp @@ -65,12 +65,6 @@ ConstantNode::ConstantNode(OwningDataSource&& data_source, const std::span(buffer_ptr_, this->size()))), data_source_(std::make_unique(std::move(data_source))) {} -bool ConstantNode::equal_to(const Node& rhs) const { - const auto* rhs_ptr = dynamic_cast(&rhs); - if (rhs_ptr == nullptr) return false; // not same type so not equal - return this->equal_to(*rhs_ptr); -} - bool ConstantNode::equal_to(const ConstantNode& rhs) const { return this->ndim() == rhs.ndim() and // same ndim std::ranges::equal(this->shape(), rhs.shape()) and // same shape diff --git a/dwave/optimization/src/nodes/creation.cpp b/dwave/optimization/src/nodes/creation.cpp index a6da0f285..d68c32bea 100644 --- a/dwave/optimization/src/nodes/creation.cpp +++ b/dwave/optimization/src/nodes/creation.cpp @@ -354,12 +354,6 @@ std::span ARangeNode::diff(const State& state) const { return data_ptr_(state)->diff(); } -bool ARangeNode::equal_to(const Node& rhs) const { - const auto* rhs_ptr = dynamic_cast(&rhs); - if (rhs_ptr == nullptr) return false; - return this->equal_to(*rhs_ptr); -} - bool ARangeNode::equal_to(const ARangeNode& rhs) const { return start_ == rhs.start_ and stop_ == rhs.stop_ and step_ == rhs.step_; } diff --git a/dwave/optimization/src/nodes/flow.cpp b/dwave/optimization/src/nodes/flow.cpp index 1301a014c..f4b52d748 100644 --- a/dwave/optimization/src/nodes/flow.cpp +++ b/dwave/optimization/src/nodes/flow.cpp @@ -73,16 +73,6 @@ std::span ExtractNode::diff(const State& state) const { return data_ptr_(state)->diff(); } -bool ExtractNode::equal_to(const Node& rhs) const { - const auto* rhs_ptr = dynamic_cast(&rhs); - if (rhs_ptr == nullptr) return false; // not same type so not equal - return this->equal_to(*rhs_ptr); -} - -bool ExtractNode::equal_to(const ExtractNode& rhs) const { - return condition_ptr_ == rhs.condition_ptr_ and arr_ptr_ == rhs.arr_ptr_; -} - void ExtractNode::initialize_state(State& state) const { const std::ranges::view auto condition = condition_ptr_->view(state); const std::ranges::view auto arr = arr_ptr_->view(state); @@ -299,16 +289,6 @@ std::span WhereNode::diff(const State& state) const { return data_ptr_(state)->diff(); } -bool WhereNode::equal_to(const Node& rhs) const { - const auto* rhs_ptr = dynamic_cast(&rhs); - if (rhs_ptr == nullptr) return false; // not same type so not equal - return this->equal_to(*rhs_ptr); -} - -bool WhereNode::equal_to(const WhereNode& rhs) const { - return condition_ptr_ == rhs.condition_ptr_ and x_ptr_ == rhs.x_ptr_ and y_ptr_ == rhs.y_ptr_; -} - void WhereNode::initialize_state(State& state) const { if (condition_ptr_->size() != 1) { // `condition` has the same shape as x/y and isn't a single value diff --git a/dwave/optimization/src/nodes/indexing.cpp b/dwave/optimization/src/nodes/indexing.cpp index 8b9217556..776a0eff3 100644 --- a/dwave/optimization/src/nodes/indexing.cpp +++ b/dwave/optimization/src/nodes/indexing.cpp @@ -530,12 +530,6 @@ std::span AdvancedIndexingNode::diff(const State& state) const { return data_ptr_(state)->diff; } -bool AdvancedIndexingNode::equal_to(const Node& rhs) const { - const auto* rhs_ptr = dynamic_cast(&rhs); - if (rhs_ptr == nullptr) return false; // not same type so not equal - return this->equal_to(*rhs_ptr); -} - bool AdvancedIndexingNode::equal_to(const AdvancedIndexingNode& rhs) const { return array_ptr_ == rhs.array_ptr_ and std::ranges::equal(indices_, rhs.indices_); } @@ -1507,12 +1501,6 @@ std::span BasicIndexingNode::diff(const State& state) const { return data_ptr_(state)->diff; } -bool BasicIndexingNode::equal_to(const Node& rhs) const { - const auto* rhs_ptr = dynamic_cast(&rhs); - if (rhs_ptr == nullptr) return false; // not same type so not equal - return this->equal_to(*rhs_ptr); -} - bool BasicIndexingNode::equal_to(const BasicIndexingNode& rhs) const { if (array_ptr_ == rhs.array_ptr_ and // ndim_ == rhs.ndim_ and // @@ -2066,12 +2054,6 @@ std::span PermutationNode::diff(const State& state) const { return data_ptr_(state)->diff; } -bool PermutationNode::equal_to(const Node& rhs) const { - const auto* rhs_ptr = dynamic_cast(&rhs); - if (rhs_ptr == nullptr) return false; // not same type so not equal - return this->equal_to(*rhs_ptr); -} - bool PermutationNode::equal_to(const PermutationNode& rhs) const { return array_ptr_ == rhs.array_ptr_ and order_ptr_ == rhs.order_ptr_; } diff --git a/dwave/optimization/src/nodes/interpolation.cpp b/dwave/optimization/src/nodes/interpolation.cpp index c8833d92b..0f3cca567 100644 --- a/dwave/optimization/src/nodes/interpolation.cpp +++ b/dwave/optimization/src/nodes/interpolation.cpp @@ -128,12 +128,6 @@ std::span BSplineNode::diff(const State& state) const { return data_ptr_(state)->diff(); } -bool BSplineNode::equal_to(const Node& rhs) const { - const auto* rhs_ptr = dynamic_cast(&rhs); - if (rhs_ptr == nullptr) return false; // not same type so not equal - return this->equal_to(*rhs_ptr); -} - bool BSplineNode::equal_to(const BSplineNode& rhs) const { return array_ptr_ == rhs.array_ptr_ and k_ == rhs.k_ and t_ == rhs.t_ and c_ == rhs.c_; } diff --git a/dwave/optimization/src/nodes/lambda.cpp b/dwave/optimization/src/nodes/lambda.cpp index 0154b7c76..84976b51f 100644 --- a/dwave/optimization/src/nodes/lambda.cpp +++ b/dwave/optimization/src/nodes/lambda.cpp @@ -302,12 +302,6 @@ void AccumulateZipNode::initialize_state(State& state) const { ); } -bool AccumulateZipNode::equal_to(const Node& rhs) const { - const auto* rhs_ptr = dynamic_cast(&rhs); - if (rhs_ptr == nullptr) return false; // not same type so not equal - return this->equal_to(*rhs_ptr); -} - bool AccumulateZipNode::equal_to(const AccumulateZipNode& rhs) const { // note that we don't have a notion of Graph equality, so we instead // just check whether we have the same underlying shared ptr From 5b213bca8a005de7f705f5578c66117a597852ed Mon Sep 17 00:00:00 2001 From: Alexander Condello Date: Fri, 3 Jul 2026 10:28:12 -0700 Subject: [PATCH 10/25] Use EqualityMixin with BinaryOpNode This requires some fiddling, see https://isocpp.org/wiki/faq/templates#nondependent-name-lookup-members for the why. --- .../dwave-optimization/nodes/binaryop.hpp | 16 ++++++------- .../dwave-optimization/nodes/constants.hpp | 1 + dwave/optimization/src/nodes/binaryop.cpp | 23 +++++++------------ 3 files changed, 17 insertions(+), 23 deletions(-) diff --git a/dwave/optimization/include/dwave-optimization/nodes/binaryop.hpp b/dwave/optimization/include/dwave-optimization/nodes/binaryop.hpp index 8ddf4622e..724a248e9 100644 --- a/dwave/optimization/include/dwave-optimization/nodes/binaryop.hpp +++ b/dwave/optimization/include/dwave-optimization/nodes/binaryop.hpp @@ -29,7 +29,7 @@ namespace dwave::optimization { template -class BinaryOpNode : public ArrayOutputMixin { +class BinaryOpNode : public ArrayOutputMixin>> { public: // We need at least two nodes, and they must be the same shape BinaryOpNode(ArrayNode* a_ptr, ArrayNode* b_ptr); @@ -39,9 +39,9 @@ class BinaryOpNode : public ArrayOutputMixin { /// Two BinaryOpNodes are equal if they have the same operation and the /// same predecessors. If the BinaryOp is commutative, then permutations - /// of predecessors are allowed. - bool equal_to(const Node& rhs) const override; - bool equal_to(const BinaryOpNode& rhs) const; + /// of predecessors are allowed. + // bool equal_to(const Node& rhs) const override; + bool equal_to(const BinaryOpNode& rhs) const override; /// @copydoc Array::integral() bool integral() const override; @@ -52,10 +52,10 @@ class BinaryOpNode : public ArrayOutputMixin { /// @copydoc Array::max() double max() const override; - using ArrayOutputMixin::shape; + using ArrayOutputMixin>>::shape; std::span shape(const State& state) const override; - using ArrayOutputMixin::size; + using ArrayOutputMixin>>::size; ssize_t size(const State& state) const override; ssize_t size_diff(const State& state) const override; @@ -69,11 +69,11 @@ class BinaryOpNode : public ArrayOutputMixin { // The predecessors of the operation, as Array*. std::span operands() { - assert(predecessors().size() == operands_.size()); + assert(this->predecessors().size() == operands_.size()); return operands_; } std::span operands() const { - assert(predecessors().size() == operands_.size()); + assert(this->predecessors().size() == operands_.size()); return operands_; } diff --git a/dwave/optimization/include/dwave-optimization/nodes/constants.hpp b/dwave/optimization/include/dwave-optimization/nodes/constants.hpp index b01291479..57efe7a96 100644 --- a/dwave/optimization/include/dwave-optimization/nodes/constants.hpp +++ b/dwave/optimization/include/dwave-optimization/nodes/constants.hpp @@ -100,6 +100,7 @@ class ConstantNode : public ArrayOutputMixin BinaryOpNode::BinaryOpNode(ArrayNode* a_ptr, ArrayNode* b_ptr) : - ArrayOutputMixin(broadcast_shapes(a_ptr->shape(), b_ptr->shape())), + ArrayOutputMixin>>(broadcast_shapes(a_ptr->shape(), b_ptr->shape())), operands_({a_ptr, b_ptr}), values_info_( calculate_values_minmax(operands_[0], operands_[1]), @@ -200,24 +200,17 @@ BinaryOpNode::BinaryOpNode(ArrayNode* a_ptr, ArrayNode* b_ptr) : template double const* BinaryOpNode::buff(const State& state) const { - return data_ptr_(state)->buff(); + return this->template data_ptr_(state)->buff(); } template std::span BinaryOpNode::diff(const State& state) const { - return data_ptr_(state)->diff(); + return this->template data_ptr_(state)->diff(); } template void BinaryOpNode::commit(State& state) const { - data_ptr_(state)->commit(); -} - -template -bool BinaryOpNode::equal_to(const Node& rhs) const { - const auto* rhs_ptr = dynamic_cast(&rhs); - if (rhs_ptr == nullptr) return false; // not same type so not equal - return this->equal_to(*rhs_ptr); + this->template data_ptr_(state)->commit(); } template @@ -302,7 +295,7 @@ void BinaryOpNode::initialize_state(State& state) const { unreachable(); } - emplace_data_ptr_(state, std::move(values)); + this->template emplace_data_ptr_(state, std::move(values)); } template @@ -322,7 +315,7 @@ double BinaryOpNode::min() const { template void BinaryOpNode::propagate(State& state) const { - auto ptr = data_ptr_(state); + auto ptr = this->template data_ptr_(state); const Array* lhs_ptr = operands_[0]; const Array* rhs_ptr = operands_[1]; @@ -446,7 +439,7 @@ void BinaryOpNode::replace_predecessor_(ssize_t previous_index, Node* template void BinaryOpNode::revert(State& state) const { - data_ptr_(state)->revert(); + this->template data_ptr_(state)->revert(); } template @@ -491,7 +484,7 @@ ssize_t BinaryOpNode::size(const State& state) const { template ssize_t BinaryOpNode::size_diff(const State& state) const { - return data_ptr_(state)->size_diff(); + return this->template data_ptr_(state)->size_diff(); } SizeInfo binaryop_calculate_sizeinfo( From 47e1be76a66868ca0f6bb131509b674e60a55e7a Mon Sep 17 00:00:00 2001 From: Alexander Condello Date: Fri, 3 Jul 2026 11:01:44 -0700 Subject: [PATCH 11/25] Add node equality and predecessor replacement to MatrixMultiplyNode --- .../nodes/linear_algebra.hpp | 13 +++-- .../optimization/src/nodes/linear_algebra.cpp | 58 ++++++++++++------- ...dundant-node-removal-9e6335836d0f5603.yaml | 2 + tests/cpp/nodes/test_linear_algebra.cpp | 54 +++++++++++++++++ 4 files changed, 103 insertions(+), 24 deletions(-) diff --git a/dwave/optimization/include/dwave-optimization/nodes/linear_algebra.hpp b/dwave/optimization/include/dwave-optimization/nodes/linear_algebra.hpp index 1f6a0c381..ec6260f60 100644 --- a/dwave/optimization/include/dwave-optimization/nodes/linear_algebra.hpp +++ b/dwave/optimization/include/dwave-optimization/nodes/linear_algebra.hpp @@ -14,15 +14,15 @@ #pragma once +#include #include -#include #include "dwave-optimization/array.hpp" #include "dwave-optimization/graph.hpp" namespace dwave::optimization { -class MatrixMultiplyNode : public ArrayOutputMixin { +class MatrixMultiplyNode : public ArrayOutputMixin> { public: MatrixMultiplyNode(ArrayNode* x_ptr, ArrayNode* y_ptr); @@ -47,6 +47,9 @@ class MatrixMultiplyNode : public ArrayOutputMixin { /// @copydoc Array::min() double min() const override; + /// The predecessors, as ArrayNode* + std::span operands() const { return operands_; } + /// @copydoc Node::propagate() void propagate(State& state) const override; @@ -71,12 +74,14 @@ class MatrixMultiplyNode : public ArrayOutputMixin { /// Either `"blas"` or `"fallback"`. static std::string implementation; + protected: + void replace_predecessor_(ssize_t index, Node* node_ptr) override; + private: void matmul_(State& state, std::span out, std::span out_shape) const; void update_shape_(State& state) const; - const ArrayNode* x_ptr_; - const ArrayNode* y_ptr_; + std::array operands_; const SizeInfo sizeinfo_; const ValuesInfo values_info_; diff --git a/dwave/optimization/src/nodes/linear_algebra.cpp b/dwave/optimization/src/nodes/linear_algebra.cpp index 267b07937..94f85b43f 100644 --- a/dwave/optimization/src/nodes/linear_algebra.cpp +++ b/dwave/optimization/src/nodes/linear_algebra.cpp @@ -15,15 +15,16 @@ #include "dwave-optimization/nodes/linear_algebra.hpp" #include -#include #include +#include + +#include "dwave-optimization/graph.hpp" #if __has_include() and __has_include() #include #define HAS_BLAS_ #endif -#include "../functional_.hpp" #include "_state.hpp" #include "dwave-optimization/array.hpp" #include "dwave-optimization/state.hpp" @@ -381,8 +382,7 @@ class MatrixMultiplyNodeData : public ArrayNodeStateData { MatrixMultiplyNode::MatrixMultiplyNode(ArrayNode* x_ptr, ArrayNode* y_ptr) : ArrayOutputMixin(output_shape(x_ptr, y_ptr)), - x_ptr_(x_ptr), - y_ptr_(y_ptr), + operands_{x_ptr, y_ptr}, sizeinfo_(get_sizeinfo(x_ptr, y_ptr)), values_info_(get_values_info(x_ptr, y_ptr)) { add_predecessor_(x_ptr); @@ -421,15 +421,18 @@ void MatrixMultiplyNode::matmul_( // to handle x/y with more than 2 dimensions. We do all that handling in terms of // "leaps", i.e. the number of pointer increments to apply in each dimension. - // const ssize_t x_penultimate_axis_size = get_axis_size(x_ptr_->shape(state), -2, true); + const ArrayNode* const x_ptr = operands_[0]; + const ArrayNode* const y_ptr = operands_[1]; + + // const ssize_t x_penultimate_axis_size = get_axis_size(x_ptr->shape(state), -2, true); const ssize_t leading_subspace_size = - get_leading_subspace_size(x_ptr_->shape(state), y_ptr_->shape(state)); + get_leading_subspace_size(x_ptr->shape(state), y_ptr->shape(state)); - const ssize_t x_leading_leap = get_leading_leap(x_ptr_->shape(state)); - const ssize_t y_leading_leap = get_leading_leap(y_ptr_->shape(state)); + const ssize_t x_leading_leap = get_leading_leap(x_ptr->shape(state)); + const ssize_t y_leading_leap = get_leading_leap(y_ptr->shape(state)); const ssize_t out_leading_leap = [&]() -> ssize_t { - if (x_ptr_->ndim() >= 2 and y_ptr_->ndim() >= 2) return get_leading_leap(out_shape); - if (x_ptr_->ndim() == 1 and y_ptr_->ndim() == 1) return 0; + if (x_ptr->ndim() >= 2 and y_ptr->ndim() >= 2) return get_leading_leap(out_shape); + if (x_ptr->ndim() == 1 and y_ptr->ndim() == 1) return 0; return out_shape.back(); }(); @@ -443,9 +446,9 @@ void MatrixMultiplyNode::matmul_( return vals[static_cast(vals.size()) + index]; }; - const ssize_t m = (x_ptr_->ndim() > 1) ? get(x_ptr_->shape(state), -2) : 1; - const ssize_t n = (y_ptr_->ndim() > 1) ? get(y_ptr_->shape(state), -1) : 1; - const ssize_t k = get(x_ptr_->shape(state), -1); + const ssize_t m = (x_ptr->ndim() > 1) ? get(x_ptr->shape(state), -2) : 1; + const ssize_t n = (y_ptr->ndim() > 1) ? get(y_ptr->shape(state), -1) : 1; + const ssize_t k = get(x_ptr->shape(state), -1); // We also need the stride information about x/y/out @@ -454,14 +457,14 @@ void MatrixMultiplyNode::matmul_( return std::array{k * static_cast(sizeof(double)), strides[0]}; } return std::array{get(strides, -2), get(strides, -1)}; - }(x_ptr_->strides()); + }(x_ptr->strides()); std::array y_matmul_strides = [&get](std::span strides) { if (strides.size() == 1) { return std::array{strides[0], sizeof(double)}; } return std::array{get(strides, -2), get(strides, -1)}; - }(y_ptr_->strides()); + }(y_ptr->strides()); std::array out_matmul_strides{ n * static_cast(sizeof(double)), sizeof(double) @@ -472,8 +475,8 @@ void MatrixMultiplyNode::matmul_( // and checking the strides of both x/y, we use ArrayIterators to // get us to the correct subspace, and then use the strides of the // predecessors to iterate through the last one or two dimensions. - const double* const x_data = &x_ptr_->view(state).begin()[w * x_leading_leap]; - const double* const y_data = &y_ptr_->view(state).begin()[w * y_leading_leap]; + const double* const x_data = &x_ptr->view(state).begin()[w * x_leading_leap]; + const double* const y_data = &y_ptr->view(state).begin()[w * y_leading_leap]; double* const out_data = out.data() + w * out_leading_leap; gemm( @@ -494,7 +497,7 @@ void MatrixMultiplyNode::initialize_state(State& state) const { ssize_t start_size = this->size(); std::vector shape(this->shape().begin(), this->shape().end()); if (this->dynamic()) { - shape[0] = x_ptr_->shape(state)[0]; + shape[0] = operands_[0]->shape(state)[0]; start_size = Array::shape_to_size(shape); } @@ -523,12 +526,12 @@ double MatrixMultiplyNode::min() const { return values_info_.min; } void MatrixMultiplyNode::update_shape_(State& state) const { if (this->dynamic()) { - data_ptr_(state)->shape[0] = x_ptr_->shape(state)[0]; + data_ptr_(state)->shape[0] = operands_[0]->shape(state)[0]; } } void MatrixMultiplyNode::propagate(State& state) const { - if (x_ptr_->diff(state).size() == 0 and y_ptr_->diff(state).size() == 0) return; + if (operands_[0]->diff(state).size() == 0 and operands_[1]->diff(state).size() == 0) return; auto data = data_ptr_(state); @@ -541,6 +544,21 @@ void MatrixMultiplyNode::propagate(State& state) const { data->assign(data->output); } +void MatrixMultiplyNode::replace_predecessor_(ssize_t index, Node* node_ptr) { + Node::replace_predecessor_(index, node_ptr); + + const ArrayNode* array_ptr = dynamic_cast(node_ptr); + assert(array_ptr != nullptr); + + if (index == 0) { + assert(std::ranges::equal(operands_[0]->shape(), array_ptr->shape())); + operands_[0] = array_ptr; + } else { + assert(std::ranges::equal(operands_[1]->shape(), array_ptr->shape())); + operands_[1] = array_ptr; + } +} + void MatrixMultiplyNode::revert(State& state) const { auto data = data_ptr_(state); data->revert(); diff --git a/releasenotes/notes/feature-redundant-node-removal-9e6335836d0f5603.yaml b/releasenotes/notes/feature-redundant-node-removal-9e6335836d0f5603.yaml index 6f4ae7496..a50418e61 100644 --- a/releasenotes/notes/feature-redundant-node-removal-9e6335836d0f5603.yaml +++ b/releasenotes/notes/feature-redundant-node-removal-9e6335836d0f5603.yaml @@ -1,3 +1,5 @@ --- +features: + - Add ``MatrixMultiplyNode::operands()`` method. upgrade: - Remove ``AccumulateZipNode::initial`` attribute and replace it with ``AccumulateZipNode::initial()`` method. diff --git a/tests/cpp/nodes/test_linear_algebra.cpp b/tests/cpp/nodes/test_linear_algebra.cpp index 9eef38948..2c9ea065c 100644 --- a/tests/cpp/nodes/test_linear_algebra.cpp +++ b/tests/cpp/nodes/test_linear_algebra.cpp @@ -547,6 +547,60 @@ TEST_CASE("MatrixMultiplyNode") { } } } + + SECTION("equality") { + auto* x0_ptr = graph.emplace_node( + std::vector{1, 2, 3, 4}, std::vector{2, 2} + ); + auto* x1_ptr = graph.emplace_node( + std::vector{6, 5, 4, 3}, std::vector{2, 2} + ); + + auto* y0_ptr = graph.emplace_node( + std::vector{7, 8, 9, 10}, std::vector{2, 2} + ); + auto* y1_ptr = graph.emplace_node( + std::vector{12, 11, 10, 9}, std::vector{2, 2} + ); + + Node* a_ptr = graph.emplace_node(x0_ptr, y0_ptr); + Node* b_ptr = graph.emplace_node(x0_ptr, y0_ptr); + Node* c_ptr = graph.emplace_node(x1_ptr, y0_ptr); + Node* d_ptr = graph.emplace_node(x0_ptr, y1_ptr); + Node* e_ptr = graph.emplace_node(y0_ptr, x0_ptr); + + CHECK(a_ptr->equal_to(*a_ptr)); + CHECK(a_ptr->equal_to(*b_ptr)); + + CHECK(not a_ptr->equal_to(*x0_ptr)); + CHECK(not a_ptr->equal_to(*c_ptr)); + CHECK(not a_ptr->equal_to(*d_ptr)); + CHECK(not a_ptr->equal_to(*e_ptr)); + } + + SECTION("predecessor replacement") { + auto* x0_ptr = graph.emplace_node( + std::vector{1, 2, 3, 4, 5, 6}, std::vector{2, 3} + ); + auto* x1_ptr = graph.emplace_node( + std::vector{6, 5, 4, 3, 2, 1}, std::vector{2, 3} + ); + + auto* y0_ptr = graph.emplace_node( + std::vector{7, 8, 9, 10, 11, 12}, std::vector{3, 2} + ); + auto* y1_ptr = graph.emplace_node( + std::vector{12, 11, 10, 9, 8, 7}, std::vector{3, 2} + ); + + auto* matmul_ptr = graph.emplace_node(x0_ptr, y0_ptr); + + x1_ptr->take_successors(*x0_ptr); + y1_ptr->take_successors(*y0_ptr); + + CHECK_THAT(matmul_ptr->predecessors(), RangeEquals({x1_ptr, y1_ptr})); + CHECK_THAT(matmul_ptr->operands(), RangeEquals({x1_ptr, y1_ptr})); + } } } // namespace dwave::optimization From cd107a07c300cdec727ea3506d4b7c8e051e0a4c Mon Sep 17 00:00:00 2001 From: Alexander Condello Date: Fri, 3 Jul 2026 15:19:28 -0700 Subject: [PATCH 12/25] Add node equality and predecessor replacement for LP nodes --- .../include/dwave-optimization/nodes/lp.hpp | 26 ++- dwave/optimization/src/nodes/lp.cpp | 70 ++++++++ tests/cpp/nodes/test_lp.cpp | 164 +++++++++++++++++- 3 files changed, 255 insertions(+), 5 deletions(-) diff --git a/dwave/optimization/include/dwave-optimization/nodes/lp.hpp b/dwave/optimization/include/dwave-optimization/nodes/lp.hpp index 9143140b9..8c67313f8 100644 --- a/dwave/optimization/include/dwave-optimization/nodes/lp.hpp +++ b/dwave/optimization/include/dwave-optimization/nodes/lp.hpp @@ -25,7 +25,7 @@ namespace dwave::optimization { class LinearProgramNodeBase; /// A logical node that propagates whether or not its predecessor LinearProgram is feasible. -class LinearProgramFeasibleNode : public ScalarOutputMixin { +class LinearProgramFeasibleNode : public ScalarOutputMixin, true> { public: explicit LinearProgramFeasibleNode(LinearProgramNodeBase* lp_ptr); @@ -44,6 +44,9 @@ class LinearProgramFeasibleNode : public ScalarOutputMixin { /// @copydoc Node::propagate() void propagate(State& state) const override; + protected: + void replace_predecessor_(ssize_t index, Node* node_ptr) override; + private: const LinearProgramNodeBase* lp_ptr_; }; @@ -97,7 +100,7 @@ class LinearProgramNodeBase : public Node { /// Following https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.linprog.html /// linprog(c, A_ub=None, b_ub=None, A_eq=None, b_eq=None, bounds=(0, None), /// callback=None, options=None, x0=None, integrality=None) -class LinearProgramNode : public LinearProgramNodeBase { +class LinearProgramNode : public EqualityMixin { public: /// Construct a LinearProgramNode /// @@ -119,6 +122,9 @@ class LinearProgramNode : public LinearProgramNodeBase { /// The LP node's state is potentially degenerate, and therefore not deterministic. bool deterministic_state() const override; + /// @copydoc Node::equal_to() + bool equal_to(const LinearProgramNode& rhs) const override; + /// @copydoc LinearProgramNodeBase::feasible() bool feasible(const State& state) const override; @@ -149,6 +155,9 @@ class LinearProgramNode : public LinearProgramNodeBase { /// @copydoc LinearProgramNodeBase::variables_shape() std::span variables_shape() const override; + protected: + void replace_predecessor_(ssize_t index, Node* node_ptr) override; + private: /// Read out each of the predecessor arrays and copy the data to `lp`, where /// it can be easily passed in to linprog (which expects (contiguous) vectors). @@ -157,16 +166,19 @@ class LinearProgramNode : public LinearProgramNodeBase { // The coefficients of the linear objective function to be minimized. // minimize c @ x + // Should never be nullptr const ArrayNode* c_ptr_; // The inequality constraint matrix and vector. // b_lb <= A @ x <= b_ub + // Either all are nullptr or A and at least one of b_lb/b_ub exist const ArrayNode* b_lb_ptr_; const ArrayNode* A_ptr_; const ArrayNode* b_ub_ptr_; // The equality constaint matrix and vector. // A_eq @ x == b_eq + // Either both are nullptr or neither const ArrayNode* A_eq_ptr_; const ArrayNode* b_eq_ptr_; @@ -180,7 +192,7 @@ class LinearProgramNode : public LinearProgramNodeBase { /// A scalar node that propagates the objective value of the solution found by the /// LinearProgramNode. Note that the output is undefined if the solution is not feasible. -class LinearProgramObjectiveValueNode : public ScalarOutputMixin { +class LinearProgramObjectiveValueNode : public ScalarOutputMixin, true> { public: explicit LinearProgramObjectiveValueNode(LinearProgramNodeBase* lp_ptr); @@ -199,13 +211,16 @@ class LinearProgramObjectiveValueNode : public ScalarOutputMixin { +class LinearProgramSolutionNode : public ArrayOutputMixin> { public: explicit LinearProgramSolutionNode(LinearProgramNodeBase* lp_ptr); @@ -240,6 +255,9 @@ class LinearProgramSolutionNode : public ArrayOutputMixin { using ArrayOutputMixin::size; + protected: + void replace_predecessor_(ssize_t index, Node* node_ptr) override; + private: const LinearProgramNodeBase* lp_ptr_; }; diff --git a/dwave/optimization/src/nodes/lp.cpp b/dwave/optimization/src/nodes/lp.cpp index b7ea17144..9a6352236 100644 --- a/dwave/optimization/src/nodes/lp.cpp +++ b/dwave/optimization/src/nodes/lp.cpp @@ -16,8 +16,12 @@ #include +#include + #include "../simplex.hpp" #include "_state.hpp" +#include "dwave-optimization/common.hpp" +#include "dwave-optimization/graph.hpp" namespace dwave::optimization { @@ -60,6 +64,12 @@ void LinearProgramFeasibleNode::propagate(State& state) const { set_state(state, lp_ptr_->feasible(state)); } +void LinearProgramFeasibleNode::replace_predecessor_(ssize_t index, Node* node_ptr) { + Node::replace_predecessor_(index, node_ptr); + lp_ptr_ = dynamic_cast(node_ptr); + assert(lp_ptr_ != nullptr); +} + void LinearProgramNodeBase::check_input_arguments( const ArrayNode* c_ptr, const ArrayNode* b_lb_ptr, @@ -207,6 +217,19 @@ void LinearProgramNode::commit(State& state) const {}; bool LinearProgramNode::deterministic_state() const { return false; } +bool LinearProgramNode::equal_to(const LinearProgramNode& rhs) const { + return ( + c_ptr_ == rhs.c_ptr_ and // + b_lb_ptr_ == rhs.b_lb_ptr_ and // + A_ptr_ == rhs.A_ptr_ and // + b_ub_ptr_ == rhs.b_ub_ptr_ and // + A_eq_ptr_ == rhs.A_eq_ptr_ and // + b_eq_ptr_ == rhs.b_eq_ptr_ and // + lb_ptr_ == rhs.lb_ptr_ and // + ub_ptr_ == rhs.ub_ptr_ // + ); +} + bool LinearProgramNode::feasible(const State& state) const { return data_ptr_(state)->result.feasible(); } @@ -340,6 +363,41 @@ void LinearProgramNode::propagate(State& state) const { Node::propagate(state); } +void LinearProgramNode::replace_predecessor_(ssize_t index, Node* node_ptr) { + Node::replace_predecessor_(index, node_ptr); + + const ArrayNode* array_ptr = dynamic_cast(node_ptr); + assert(array_ptr != nullptr); + + auto check_and_replace = [&array_ptr, &index](const ArrayNode*& to_replace) -> bool { + if (to_replace == nullptr) return false; // nothing to replace and no need to re-index + if (index-- > 0) return false; // found a node, but it's not the one we're replacing + + // sanity check the size. There are other things we could check, but this is easy + // and simple + assert(std::ranges::equal(to_replace->shape(), array_ptr->shape())); + to_replace = array_ptr; + + return true; // we found the one we wanted to replace. + }; + + assert(c_ptr_ != nullptr); // never null + if (check_and_replace(c_ptr_)) return; + + if (check_and_replace(b_lb_ptr_)) return; + if (check_and_replace(A_ptr_)) return; + if (check_and_replace(b_ub_ptr_)) return; + + if (check_and_replace(A_eq_ptr_)) return; + if (check_and_replace(b_eq_ptr_)) return; + + if (check_and_replace(lb_ptr_)) return; + if (check_and_replace(ub_ptr_)) return; + + unreachable(); + assert(false and "should never get here"); +} + void LinearProgramNode::revert(State& state) const { // Nothing to do on revert as all changes are tracked by successor nodes } @@ -373,6 +431,12 @@ void LinearProgramObjectiveValueNode::propagate(State& state) const { // We could consider setting ourselves to max() } +void LinearProgramObjectiveValueNode::replace_predecessor_(ssize_t index, Node* node_ptr) { + Node::replace_predecessor_(index, node_ptr); + lp_ptr_ = dynamic_cast(node_ptr); + assert(lp_ptr_ != nullptr); +} + LinearProgramSolutionNode::LinearProgramSolutionNode(LinearProgramNodeBase* lp_ptr) : ArrayOutputMixin(lp_ptr->variables_shape()), lp_ptr_(lp_ptr) { add_predecessor_(lp_ptr); @@ -439,6 +503,12 @@ void LinearProgramSolutionNode::propagate(State& state) const { } } +void LinearProgramSolutionNode::replace_predecessor_(ssize_t index, Node* node_ptr) { + Node::replace_predecessor_(index, node_ptr); + lp_ptr_ = dynamic_cast(node_ptr); + assert(lp_ptr_ != nullptr); +} + void LinearProgramSolutionNode::revert(State& state) const { data_ptr_(state)->revert(); } diff --git a/tests/cpp/nodes/test_lp.cpp b/tests/cpp/nodes/test_lp.cpp index 08e4020b9..f28850c8d 100644 --- a/tests/cpp/nodes/test_lp.cpp +++ b/tests/cpp/nodes/test_lp.cpp @@ -16,8 +16,12 @@ #include #include +#include -#include "dwave-optimization/nodes.hpp" +#include "dwave-optimization/nodes/constants.hpp" +#include "dwave-optimization/nodes/lp.hpp" +#include "dwave-optimization/nodes/numbers.hpp" +#include "dwave-optimization/nodes/testing.hpp" using namespace Catch::Matchers; @@ -738,6 +742,164 @@ TEST_CASE("LinearProgramNode") { } } } + + SECTION("equality") { + auto graph = Graph(); + + const ssize_t num_variables = 3; + const ssize_t num_eq = 2; + const ssize_t num_ineq = 2; + + const ssize_t min = IntegerNode::minimum_lower_bound; + const ssize_t max = IntegerNode::maximum_upper_bound; + + auto* c0 = graph.emplace_node(std::vector{num_variables}, min, max); + auto* c1 = graph.emplace_node(std::vector{num_variables}, min, max); + + auto* b_lb0 = graph.emplace_node(std::vector{num_ineq}, min, max); + auto* b_lb1 = graph.emplace_node(std::vector{num_ineq}, min, max); + auto* A0 = graph.emplace_node(std::vector{num_ineq, num_variables}, min, max); + auto* A1 = graph.emplace_node(std::vector{num_ineq, num_variables}, min, max); + auto* b_ub0 = graph.emplace_node(std::vector{num_ineq}, min, max); + auto* b_ub1 = graph.emplace_node(std::vector{num_ineq}, min, max); + + auto* A_eq0 = graph.emplace_node(std::vector{num_eq, num_variables}, min, max); + auto* A_eq1 = graph.emplace_node(std::vector{num_eq, num_variables}, min, max); + auto* b_eq0 = graph.emplace_node(std::vector{num_eq}, min, max); + auto* b_eq1 = graph.emplace_node(std::vector{num_eq}, min, max); + + auto* lb0 = graph.emplace_node(std::vector{num_variables}, min, max); + auto* lb1 = graph.emplace_node(std::vector{num_variables}, min, max); + auto* ub0 = graph.emplace_node(std::vector{num_variables}, min, max); + auto* ub1 = graph.emplace_node(std::vector{num_variables}, min, max); + + Node* lp0 = + graph.emplace_node(c0, b_lb0, A0, b_ub0, A_eq0, b_eq0, lb0, ub0); + Node* lp1 = + graph.emplace_node(c0, b_lb0, A0, b_ub0, A_eq0, b_eq0, lb0, ub0); + Node* lp2 = + graph.emplace_node(c1, b_lb0, A0, b_ub0, A_eq0, b_eq0, lb0, ub0); + Node* lp3 = + graph.emplace_node(c0, b_lb1, A0, b_ub0, A_eq0, b_eq0, lb0, ub0); + Node* lp4 = + graph.emplace_node(c0, b_lb0, A1, b_ub0, A_eq0, b_eq0, lb0, ub0); + Node* lp5 = + graph.emplace_node(c0, b_lb0, A0, b_ub1, A_eq0, b_eq0, lb0, ub0); + Node* lp6 = + graph.emplace_node(c0, b_lb0, A0, b_ub0, A_eq1, b_eq0, lb0, ub0); + Node* lp7 = + graph.emplace_node(c0, b_lb0, A0, b_ub0, A_eq0, b_eq1, lb0, ub0); + Node* lp8 = + graph.emplace_node(c0, b_lb0, A0, b_ub0, A_eq0, b_eq0, lb1, ub0); + Node* lp9 = + graph.emplace_node(c0, b_lb0, A0, b_ub0, A_eq0, b_eq0, lb0, ub1); + + CHECK(lp0->equal_to(*lp0)); + CHECK(lp0->equal_to(*lp1)); + + CHECK(not lp0->equal_to(*c0)); + CHECK(not lp0->equal_to(*lp2)); + CHECK(not lp0->equal_to(*lp3)); + CHECK(not lp0->equal_to(*lp4)); + CHECK(not lp0->equal_to(*lp5)); + CHECK(not lp0->equal_to(*lp6)); + CHECK(not lp0->equal_to(*lp7)); + CHECK(not lp0->equal_to(*lp8)); + CHECK(not lp0->equal_to(*lp9)); + + Node* feas0 = + graph.emplace_node(dynamic_cast(lp0)); + Node* feas1 = + graph.emplace_node(dynamic_cast(lp0)); + Node* feas2 = + graph.emplace_node(dynamic_cast(lp1)); + + CHECK(feas0->equal_to(*feas0)); + CHECK(feas0->equal_to(*feas1)); + CHECK(not feas0->equal_to(*feas2)); + CHECK(not feas0->equal_to(*c0)); + + Node* obj0 = graph.emplace_node( + dynamic_cast(lp0) + ); + Node* obj1 = graph.emplace_node( + dynamic_cast(lp0) + ); + Node* obj2 = graph.emplace_node( + dynamic_cast(lp1) + ); + + CHECK(obj0->equal_to(*obj0)); + CHECK(obj0->equal_to(*obj1)); + CHECK(not obj0->equal_to(*obj2)); + CHECK(not obj0->equal_to(*c0)); + + Node* sol0 = + graph.emplace_node(dynamic_cast(lp0)); + Node* sol1 = + graph.emplace_node(dynamic_cast(lp0)); + Node* sol2 = + graph.emplace_node(dynamic_cast(lp1)); + + CHECK(sol0->equal_to(*sol0)); + CHECK(sol0->equal_to(*sol1)); + CHECK(not sol0->equal_to(*sol2)); + CHECK(not sol0->equal_to(*c0)); + } + + SECTION("predecessor replacement") { + auto graph = Graph(); + + const ssize_t num_variables = 3; + const ssize_t num_eq = 2; + const ssize_t num_ineq = 2; + + const ssize_t min = IntegerNode::minimum_lower_bound; + const ssize_t max = IntegerNode::maximum_upper_bound; + + auto* c0 = graph.emplace_node(std::vector{num_variables}, min, max); + auto* c1 = graph.emplace_node(std::vector{num_variables}, min, max); + + auto* b_lb0 = graph.emplace_node(std::vector{num_ineq}, min, max); + auto* b_lb1 = graph.emplace_node(std::vector{num_ineq}, min, max); + auto* A0 = graph.emplace_node(std::vector{num_ineq, num_variables}, min, max); + auto* A1 = graph.emplace_node(std::vector{num_ineq, num_variables}, min, max); + + auto* A_eq0 = graph.emplace_node(std::vector{num_eq, num_variables}, min, max); + auto* A_eq1 = graph.emplace_node(std::vector{num_eq, num_variables}, min, max); + auto* b_eq0 = graph.emplace_node(std::vector{num_eq}, min, max); + auto* b_eq1 = graph.emplace_node(std::vector{num_eq}, min, max); + + auto* ub0 = graph.emplace_node(std::vector{num_variables}, min, max); + auto* ub1 = graph.emplace_node(std::vector{num_variables}, min, max); + + auto* lp0 = graph.emplace_node( + c0, b_lb0, A0, nullptr, A_eq0, b_eq0, nullptr, ub0 + ); + auto* lp1 = graph.emplace_node( + c0, b_lb0, A0, nullptr, A_eq0, b_eq0, nullptr, ub0 + ); + + auto* feas = graph.emplace_node(lp0); + auto* obj = graph.emplace_node(lp0); + auto* sol = graph.emplace_node(lp0); + + lp1->take_successors(*lp0); + + CHECK_THAT(feas->predecessors(), RangeEquals({lp1})); + CHECK_THAT(obj->predecessors(), RangeEquals({lp1})); + CHECK_THAT(sol->predecessors(), RangeEquals({lp1})); + + c1->take_successors(*c0); + b_lb1->take_successors(*b_lb0); + A1->take_successors(*A0); + A_eq1->take_successors(*A_eq0); + b_eq1->take_successors(*b_eq0); + ub1->take_successors(*ub0); + + CHECK_THAT(lp0->predecessors(), RangeEquals({c1, b_lb1, A1, A_eq1, b_eq1, ub1})); + CHECK_THAT(lp1->predecessors(), RangeEquals({c1, b_lb1, A1, A_eq1, b_eq1, ub1})); + } } } // namespace dwave::optimization From 6915ba4a035f655659c1cf5c2fb41bc15f6e4d3c Mon Sep 17 00:00:00 2001 From: Alexander Condello Date: Wed, 8 Jul 2026 10:06:52 -0700 Subject: [PATCH 13/25] Add equality and predecessor replacement for manipulation nodes --- .../dwave-optimization/nodes/manipulation.hpp | 58 +++- dwave/optimization/src/nodes/manipulation.cpp | 147 ++++++-- tests/cpp/nodes/test_manipulation.cpp | 327 ++++++++++++++++++ 3 files changed, 497 insertions(+), 35 deletions(-) diff --git a/dwave/optimization/include/dwave-optimization/nodes/manipulation.hpp b/dwave/optimization/include/dwave-optimization/nodes/manipulation.hpp index 550f4c053..b99d93664 100644 --- a/dwave/optimization/include/dwave-optimization/nodes/manipulation.hpp +++ b/dwave/optimization/include/dwave-optimization/nodes/manipulation.hpp @@ -26,7 +26,7 @@ namespace dwave::optimization { -class BroadcastToNode : public ArrayNode { +class BroadcastToNode : public EqualityMixin { public: BroadcastToNode(ArrayNode* array_ptr, std::initializer_list shape); BroadcastToNode(ArrayNode* array_ptr, std::span shape); @@ -43,8 +43,8 @@ class BroadcastToNode : public ArrayNode { /// @copydoc Array::diff() std::span diff(const State& state) const override; - bool equal_to(const Node& rhs) const override; - bool equal_to(const BroadcastToNode& rhs) const; + /// @copydoc Node::equal_to() + bool equal_to(const BroadcastToNode& rhs) const override; /// @copydoc Node::initialize_state() void initialize_state(State& state) const override; @@ -82,7 +82,7 @@ class BroadcastToNode : public ArrayNode { std::span strides() const override; protected: - void replace_predecessor_(ssize_t previous_index, Node* node_ptr) override; + void replace_predecessor_(ssize_t index, Node* node_ptr) override; private: /// Translate a linear index of the predecessor into a linear index of the @@ -100,7 +100,7 @@ class BroadcastToNode : public ArrayNode { const ValuesInfo values_info_; }; -class ConcatenateNode : public ArrayOutputMixin { +class ConcatenateNode : public ArrayOutputMixin> { public: explicit ConcatenateNode(std::span array_ptrs, ssize_t axis); explicit ConcatenateNode(std::ranges::contiguous_range auto&& array_ptrs, ssize_t axis) : @@ -109,6 +109,10 @@ class ConcatenateNode : public ArrayOutputMixin { double const* buff(const State& statfe) const override; void commit(State& state) const override; std::span diff(const State& state) const override; + + /// @copydoc Node::equal_to() + bool equal_to(const ConcatenateNode& rhs) const override; + void initialize_state(State& state) const override; /// @copydoc Array::integral() @@ -125,6 +129,9 @@ class ConcatenateNode : public ArrayOutputMixin { ssize_t axis() const { return axis_; } + protected: + void replace_predecessor_(ssize_t index, Node* node_ptr) override; + private: ssize_t axis_; std::vector array_ptrs_; @@ -134,7 +141,7 @@ class ConcatenateNode : public ArrayOutputMixin { }; /// An array node that is a contiguous copy of its predecessor. -class CopyNode : public ArrayOutputMixin { +class CopyNode : public ArrayOutputMixin> { public: explicit CopyNode(ArrayNode* array_ptr); @@ -178,6 +185,9 @@ class CopyNode : public ArrayOutputMixin { /// @copydoc Array::size_diff() ssize_t size_diff(const State& state) const override; + protected: + void replace_predecessor_(ssize_t index, Node* node_ptr) override; + private: const Array* array_ptr_; @@ -196,7 +206,7 @@ class CopyNode : public ArrayOutputMixin { /// @endcode /// /// In the case of duplicate indices, the most recent update will be propagated. -class PutNode : public ArrayOutputMixin { +class PutNode : public ArrayOutputMixin> { public: /// Constructor for PutNode. /// @@ -236,6 +246,9 @@ class PutNode : public ArrayOutputMixin { /// @copydoc Node::revert() void revert(State& state) const override; + protected: + void replace_predecessor_(ssize_t index, Node* node_ptr) override; + private: const Array* array_ptr_; const Array* indices_ptr_; @@ -245,7 +258,7 @@ class PutNode : public ArrayOutputMixin { }; /// Propagates the values of its predecessor, interpreted into a different shape. -class ReshapeNode : public ArrayOutputMixin { +class ReshapeNode : public ArrayOutputMixin> { public: /// Constructor for ReshapeNode. /// @@ -270,6 +283,9 @@ class ReshapeNode : public ArrayOutputMixin { /// @copydoc Array::diff() std::span diff(const State& state) const override; + /// @copydoc Node::equal_to() + bool equal_to(const ReshapeNode& rhs) const override; + /// @copydoc Node::initialize_state() void initialize_state(State& state) const override; @@ -304,6 +320,9 @@ class ReshapeNode : public ArrayOutputMixin { /// @copydoc Array::size_diff() ssize_t size_diff(const State& state) const override; + protected: + void replace_predecessor_(ssize_t index, Node* node_ptr) override; + private: // we could dynamically cast each time, but it's easier to just keep separate // pointer to the "array" part of the predecessor @@ -315,7 +334,7 @@ class ReshapeNode : public ArrayOutputMixin { /// Reshape a node to a specific non-dynamic shape. Use fill_value for any missing /// values. -class ResizeNode : public ArrayOutputMixin { +class ResizeNode : public ArrayOutputMixin> { public: /// Constructor for ResizeNode. /// @@ -342,8 +361,7 @@ class ResizeNode : public ArrayOutputMixin { /// @copydoc Array::diff() std::span diff(const State& state) const override; - bool equal_to(const Node& rhs) const override; - bool equal_to(const ResizeNode& rhs) const; + bool equal_to(const ResizeNode& rhs) const override; /// The fill value. double fill_value() const { return fill_value_; } @@ -367,7 +385,7 @@ class ResizeNode : public ArrayOutputMixin { void revert(State& state) const override; protected: - void replace_predecessor_(ssize_t previous_index, Node* node_ptr) override; + void replace_predecessor_(ssize_t index, Node* node_ptr) override; private: const Array* array_ptr_; @@ -377,7 +395,7 @@ class ResizeNode : public ArrayOutputMixin { const ValuesInfo values_info_; }; -class RollNode : public ArrayOutputMixin { +class RollNode : public ArrayOutputMixin> { public: /// Construct a RollNode. /// @@ -403,6 +421,9 @@ class RollNode : public ArrayOutputMixin { /// @copydoc Array::diff() std::span diff(const State& state) const override; + /// @copydoc Node::equal_to() + bool equal_to(const RollNode& rhs) const override; + /// @copydoc Node::initialize_state() void initialize_state(State& state) const override; @@ -440,6 +461,9 @@ class RollNode : public ArrayOutputMixin { /// @copydoc Array::size_diff() ssize_t size_diff(const State& state) const override; + protected: + void replace_predecessor_(ssize_t index, Node* node_ptr) override; + private: // Rotate the given array by shift in-place. static void rotate_(std::span array, ssize_t shift); @@ -469,7 +493,7 @@ class RollNode : public ArrayOutputMixin { const SizeInfo sizeinfo_; }; -class SizeNode : public ScalarOutputMixin { +class SizeNode : public ScalarOutputMixin, true> { public: explicit SizeNode(ArrayNode* node_ptr); @@ -486,6 +510,9 @@ class SizeNode : public ScalarOutputMixin { void propagate(State& state) const override; + protected: + void replace_predecessor_(ssize_t index, Node* node_ptr) override; + private: // we could dynamically cast each time, but it's easier to just keep separate // pointer to the "array" part of the predecessor @@ -549,6 +576,9 @@ class TransposeNode : public ArrayNode { void revert(State&) const override; + protected: + void replace_predecessor_(ssize_t index, Node* node_ptr) override; + private: const Array* array_ptr_; diff --git a/dwave/optimization/src/nodes/manipulation.cpp b/dwave/optimization/src/nodes/manipulation.cpp index 6ec13825c..57223f874 100644 --- a/dwave/optimization/src/nodes/manipulation.cpp +++ b/dwave/optimization/src/nodes/manipulation.cpp @@ -206,12 +206,6 @@ std::span BroadcastToNode::diff(const State& state) const { return data_ptr_(state)->diff; } -bool BroadcastToNode::equal_to(const Node& rhs) const { - const auto* rhs_ptr = dynamic_cast(&rhs); - if (rhs_ptr == nullptr) return false; // not same type so not equal - return this->equal_to(*rhs_ptr); -} - bool BroadcastToNode::equal_to(const BroadcastToNode& rhs) const { return array_ptr_ == rhs.array_ptr_ and std::ranges::equal(shape(), rhs.shape()); } @@ -365,12 +359,14 @@ ssize_t BroadcastToNode::convert_predecessor_index_(ssize_t index) const { return flat_index; } -void BroadcastToNode::replace_predecessor_(ssize_t previous_index, Node* node_ptr) { - Node::replace_predecessor_(previous_index, node_ptr); +void BroadcastToNode::replace_predecessor_(ssize_t index, Node* node_ptr) { + Node::replace_predecessor_(index, node_ptr); - assert(previous_index == 0); // we should only ever have one predecessor - array_ptr_ = dynamic_cast(node_ptr); - assert(array_ptr_ != nullptr); + assert(index == 0); // we should only ever have one predecessor + ArrayNode* array_ptr = dynamic_cast(node_ptr); + assert(std::ranges::equal(array_ptr_->shape(), array_ptr->shape())); + assert(array_ptr != nullptr); + array_ptr_ = array_ptr; } void BroadcastToNode::revert(State& state) const { @@ -448,6 +444,10 @@ std::span ConcatenateNode::diff(const State& state) const { return data_ptr_(state)->diff(); } +bool ConcatenateNode::equal_to(const ConcatenateNode& rhs) const { + return axis_ == rhs.axis_ and std::ranges::equal(array_ptrs_, rhs.array_ptrs_); +} + void ConcatenateNode::initialize_state(State& state) const { std::vector values; values.resize(size()); @@ -557,6 +557,17 @@ void ConcatenateNode::propagate(State& state) const { } } +void ConcatenateNode::replace_predecessor_(ssize_t index, Node* node_ptr) { + Node::replace_predecessor_(index, node_ptr); + + ArrayNode* array_ptr = dynamic_cast(node_ptr); + assert(array_ptr != nullptr); + assert(std::ranges::equal(array_ptrs_[index]->shape(), array_ptr->shape())); + + assert(0 <= index and static_cast(index) < array_ptrs_.size()); + array_ptrs_[index] = array_ptr; +} + void ConcatenateNode::revert(State& state) const { data_ptr_(state)->revert(); } CopyNode::CopyNode(ArrayNode* array_ptr) : @@ -588,6 +599,17 @@ void CopyNode::propagate(State& state) const { data_ptr_(state)->update(array_ptr_->diff(state)); } +void CopyNode::replace_predecessor_(ssize_t index, Node* node_ptr) { + Node::replace_predecessor_(index, node_ptr); + + ArrayNode* array_ptr = dynamic_cast(node_ptr); + assert(array_ptr != nullptr); + assert(std::ranges::equal(array_ptr_->shape(), array_ptr->shape())); + + assert(index == 0); + array_ptr_ = array_ptr; +} + void CopyNode::revert(State& state) const { data_ptr_(state)->revert(); } std::span CopyNode::shape(const State& state) const { @@ -878,6 +900,25 @@ void PutNode::propagate(State& state) const { if (ptr->diff().size()) Node::propagate(state); } +void PutNode::replace_predecessor_(ssize_t index, Node* node_ptr) { + Node::replace_predecessor_(index, node_ptr); + + ArrayNode* array_ptr = dynamic_cast(node_ptr); + assert(array_ptr != nullptr); + + if (index == 0) { + assert(std::ranges::equal(array_ptr_->shape(), array_ptr->shape())); + array_ptr_ = array_ptr; + } else if (index == 1) { + assert(std::ranges::equal(indices_ptr_->shape(), array_ptr->shape())); + indices_ptr_ = array_ptr; + } else { + assert(index == 2); + assert(std::ranges::equal(values_ptr_->shape(), array_ptr->shape())); + values_ptr_ = array_ptr; + } +} + void PutNode::revert(State& state) const { return data_ptr_(state)->revert(); } // Reshape allows one shape dimension to be -1. In that case the size is inferred. @@ -1020,6 +1061,10 @@ std::span ReshapeNode::diff(const State& state) const { return array_ptr_->diff(state); } +bool ReshapeNode::equal_to(const ReshapeNode& rhs) const { + return array_ptr_ == rhs.array_ptr_ and std::ranges::equal(shape(), rhs.shape()); +} + void ReshapeNode::initialize_state(State& state) const { if (!this->dynamic()) return Node::initialize_state(state); // stateless emplace_data_ptr_(state, this->shape(), array_ptr_->size(state)); @@ -1036,6 +1081,17 @@ void ReshapeNode::propagate(State& state) const { data_ptr_(state)->set_size(array_ptr_->size(state)); } +void ReshapeNode::replace_predecessor_(ssize_t index, Node* node_ptr) { + Node::replace_predecessor_(index, node_ptr); + + ArrayNode* array_ptr = dynamic_cast(node_ptr); + assert(array_ptr != nullptr); + assert(std::ranges::equal(array_ptr_->shape(), array_ptr->shape())); + + assert(index == 0); + array_ptr_ = array_ptr; +} + void ReshapeNode::revert(State& state) const { if (!this->dynamic()) return; // stateless return data_ptr_(state)->revert(); @@ -1112,15 +1168,12 @@ std::span ResizeNode::diff(const State& state) const { return data_ptr_(state)->diff(); } -bool ResizeNode::equal_to(const Node& rhs) const { - const auto* rhs_ptr = dynamic_cast(&rhs); - if (rhs_ptr == nullptr) return false; // not same type so not equal - return this->equal_to(*rhs_ptr); -} - bool ResizeNode::equal_to(const ResizeNode& rhs) const { - assert(false and "not yet implemented"); - return false; + return ( + array_ptr_ == rhs.array_ptr_ and // + std::ranges::equal(shape(), rhs.shape()) and // + fill_value_ == rhs.fill_value_ + ); } void ResizeNode::initialize_state(State& state) const { @@ -1176,8 +1229,15 @@ void ResizeNode::propagate(State& state) const { if (data_ptr->diff().size()) Node::propagate(state); } -void ResizeNode::replace_predecessor_(ssize_t previous_index, Node* node_ptr) { - assert(false and "not yet implemented"); +void ResizeNode::replace_predecessor_(ssize_t index, Node* node_ptr) { + Node::replace_predecessor_(index, node_ptr); + + ArrayNode* array_ptr = dynamic_cast(node_ptr); + assert(array_ptr != nullptr); + assert(std::ranges::equal(array_ptr_->shape(), array_ptr->shape())); + + assert(index == 0); + array_ptr_ = array_ptr; } void ResizeNode::revert(State& state) const { @@ -1275,6 +1335,14 @@ std::span RollNode::diff(const State& state) const { return data_ptr_(state)->diff(); } +bool RollNode::equal_to(const RollNode& rhs) const { + return ( + array_ptr_ == rhs.array_ptr_ and // + shift_ == rhs.shift_ and // + std::ranges::equal(axis_, rhs.axis_) + ); +} + void RollNode::initialize_state(State& state) const { // Get the predecessor's state as a vector that will become our state. std::vector buffer(array_ptr_->begin(state), array_ptr_->end(state)); @@ -1391,6 +1459,23 @@ void RollNode::propagate(State& state) const { } } +void RollNode::replace_predecessor_(ssize_t index, Node* node_ptr) { + Node::replace_predecessor_(index, node_ptr); + + ArrayNode* array_ptr = dynamic_cast(node_ptr); + assert(array_ptr != nullptr); + + if (index == 0) { + assert(std::ranges::equal(array_ptr_->shape(), array_ptr->shape())); + array_ptr_ = array_ptr; + } else { + assert(index == 1); + assert(std::holds_alternative(shift_)); + assert(std::ranges::equal(std::get(shift_)->shape(), array_ptr->shape())); + shift_ = array_ptr; + } +} + void RollNode::revert(State& state) const { data_ptr_(state)->revert(); } // Act on the given array *in place*. @@ -1533,6 +1618,14 @@ double SizeNode::max() const { return minmax_.second; } void SizeNode::propagate(State& state) const { set_state(state, array_ptr_->size(state)); } +void SizeNode::replace_predecessor_(ssize_t index, Node* node_ptr) { + Node::replace_predecessor_(index, node_ptr); + + assert(index == 0); + array_ptr_ = dynamic_cast(node_ptr); + assert(array_ptr_ != nullptr); +} + // TransposeNode ************************************************************** ArrayNode* TransposeNode::predeccesor_check_(ArrayNode* array_ptr) const { @@ -1725,4 +1818,16 @@ void TransposeNode::revert(State& state) const { } // otherwise, stateless } +void TransposeNode::replace_predecessor_(ssize_t index, Node* node_ptr) { + Node::replace_predecessor_(index, node_ptr); + + ArrayNode* array_ptr = dynamic_cast(node_ptr); + assert(array_ptr != nullptr); + assert(std::ranges::equal(array_ptr_->shape(), array_ptr->shape())); + assert(std::ranges::equal(array_ptr_->strides(), array_ptr->strides())); + + assert(index == 0); + array_ptr_ = array_ptr; +} + } // namespace dwave::optimization diff --git a/tests/cpp/nodes/test_manipulation.cpp b/tests/cpp/nodes/test_manipulation.cpp index 71d2bc1b0..6071763c8 100644 --- a/tests/cpp/nodes/test_manipulation.cpp +++ b/tests/cpp/nodes/test_manipulation.cpp @@ -337,6 +337,37 @@ TEST_CASE("BroadcastToNode") { } } } + + SECTION("equality") { + auto* arr0_ptr = graph.emplace_node(std::vector{0, 1, 2}); + auto* arr1_ptr = graph.emplace_node(std::vector{0, 1, 2}); + + Node* a_ptr = graph.emplace_node(arr0_ptr, std::vector{2, 3}); + Node* b_ptr = graph.emplace_node(arr0_ptr, std::vector{2, 3}); + Node* c_ptr = graph.emplace_node(arr1_ptr, std::vector{2, 3}); + Node* d_ptr = graph.emplace_node(arr0_ptr, std::vector{5, 3}); + + CHECK(a_ptr->equal_to(*a_ptr)); + CHECK(a_ptr->equal_to(*b_ptr)); + + CHECK(not a_ptr->equal_to(*arr0_ptr)); + CHECK(not a_ptr->equal_to(*c_ptr)); + CHECK(not a_ptr->equal_to(*d_ptr)); + } + + SECTION("predecessor replacement") { + auto* arr0_ptr = graph.emplace_node(std::vector{0, 1, 2}); + auto* arr1_ptr = graph.emplace_node(std::vector{0, 1, 4}); + + auto* b_ptr = graph.emplace_node(arr0_ptr, std::vector{2, 3}); + + arr1_ptr->take_successors(*arr0_ptr); + + CHECK_THAT(b_ptr->predecessors(), RangeEquals({arr1_ptr})); + + auto state = graph.initialize_state(); + CHECK_THAT(b_ptr->view(state), RangeEquals({0, 1, 4, 0, 1, 4})); + } } TEST_CASE("ConcatenateNode") { @@ -664,6 +695,61 @@ TEST_CASE("ConcatenateNode") { THEN("The concatenated node is not integral") { CHECK(c.integral() == false); } } } + + SECTION("equality") { + auto graph = Graph(); + + auto* arr0_ptr = graph.emplace_node( + std::vector{1, 2.2, 3}, std::vector{1, 3} + ); + auto* arr1_ptr = graph.emplace_node( + std::vector{1, 2, 3}, std::vector{1, 3} + ); + auto* arr2_ptr = graph.emplace_node( + std::vector{1, 2, 3}, std::vector{1, 3} + ); + + Node* a_ptr = + graph.emplace_node(std::vector{arr0_ptr, arr1_ptr}, 0); + Node* b_ptr = + graph.emplace_node(std::vector{arr0_ptr, arr1_ptr}, 0); + Node* c_ptr = + graph.emplace_node(std::vector{arr0_ptr, arr2_ptr}, 0); + Node* d_ptr = + graph.emplace_node(std::vector{arr0_ptr, arr1_ptr}, 1); + + CHECK(a_ptr->equal_to(*a_ptr)); + CHECK(a_ptr->equal_to(*b_ptr)); + + CHECK(not a_ptr->equal_to(*arr0_ptr)); + CHECK(not a_ptr->equal_to(*c_ptr)); + CHECK(not a_ptr->equal_to(*d_ptr)); + } + + SECTION("predecessor replacement") { + auto graph = Graph(); + + auto* arr0_ptr = graph.emplace_node( + std::vector{1, 2.2, 3}, std::vector{1, 3} + ); + auto* arr1_ptr = graph.emplace_node( + std::vector{1, 2, 3}, std::vector{1, 3} + ); + auto* arr2_ptr = graph.emplace_node( + std::vector{4, 5, 6}, std::vector{1, 3} + ); + + auto* concat_ptr = + graph.emplace_node(std::vector{arr0_ptr, arr1_ptr}, 0); + + arr2_ptr->take_successors(*arr1_ptr); + arr1_ptr->take_successors(*arr0_ptr); + + CHECK_THAT(concat_ptr->predecessors(), RangeEquals({arr1_ptr, arr2_ptr})); + + auto state = graph.initialize_state(); + CHECK_THAT(concat_ptr->view(state), RangeEquals({1, 2, 3, 4, 5, 6})); + } } TEST_CASE("CopyNode") { @@ -768,6 +854,39 @@ TEST_CASE("CopyNode") { } } } + + SECTION("equality") { + auto graph = Graph(); + + auto* x0_ptr = graph.emplace_node(6); + auto* x1_ptr = graph.emplace_node(6); + + Node* a_ptr = graph.emplace_node(x0_ptr); + Node* b_ptr = graph.emplace_node(x0_ptr); + Node* c_ptr = graph.emplace_node(x1_ptr); + + CHECK(a_ptr->equal_to(*a_ptr)); + CHECK(a_ptr->equal_to(*b_ptr)); + + CHECK(not a_ptr->equal_to(*x0_ptr)); + CHECK(not a_ptr->equal_to(*c_ptr)); + } + + SECTION("predecessor replacement") { + auto graph = Graph(); + + auto* x0_ptr = graph.emplace_node(std::vector{0, 1, 2}); + auto* x1_ptr = graph.emplace_node(std::vector{3, 4, 5}); + + auto* copy_ptr = graph.emplace_node(x0_ptr); + + x1_ptr->take_successors(*x0_ptr); + + CHECK_THAT(copy_ptr->predecessors(), RangeEquals({x1_ptr})); + + auto state = graph.initialize_state(); + CHECK_THAT(copy_ptr->view(state), RangeEquals({3, 4, 5})); + } } TEST_CASE("PutNode") { @@ -1057,6 +1176,57 @@ TEST_CASE("PutNode") { CHECK_THAT(put_ptr->mask(state), RangeEquals({2, 0, 0, 0})); } } + + SECTION("equality") { + auto graph = Graph(); + + auto* a0_ptr = graph.emplace_node(std::vector{0, 1, 2, 3, 4}); + auto* a1_ptr = graph.emplace_node(std::vector{0, 1, 2, 3, 4}); + + auto* i0_ptr = graph.emplace_node(std::vector{0, 2}); + auto* i1_ptr = graph.emplace_node(std::vector{0, 1}); + + auto* v0_ptr = graph.emplace_node(std::vector{-44, -55}); + auto* v1_ptr = graph.emplace_node(std::vector{44, 55}); + + Node* a_ptr = graph.emplace_node(a0_ptr, i0_ptr, v0_ptr); + Node* b_ptr = graph.emplace_node(a0_ptr, i0_ptr, v0_ptr); + Node* c_ptr = graph.emplace_node(a1_ptr, i0_ptr, v0_ptr); + Node* d_ptr = graph.emplace_node(a0_ptr, i1_ptr, v0_ptr); + Node* e_ptr = graph.emplace_node(a0_ptr, i0_ptr, v1_ptr); + + CHECK(a_ptr->equal_to(*a_ptr)); + CHECK(a_ptr->equal_to(*b_ptr)); + + CHECK(not a_ptr->equal_to(*a0_ptr)); + CHECK(not a_ptr->equal_to(*c_ptr)); + CHECK(not a_ptr->equal_to(*d_ptr)); + CHECK(not a_ptr->equal_to(*e_ptr)); + } + + SECTION("predecessor replacement") { + auto graph = Graph(); + + auto* a0_ptr = graph.emplace_node(std::vector{0, 1, 2, 3, 4}); + auto* a1_ptr = graph.emplace_node(std::vector{5, 6, 7, 8, 9}); + + auto* i0_ptr = graph.emplace_node(std::vector{0, 2}); + auto* i1_ptr = graph.emplace_node(std::vector{0, 1}); + + auto* v0_ptr = graph.emplace_node(std::vector{-44, -55}); + auto* v1_ptr = graph.emplace_node(std::vector{44, 55}); + + auto* put_ptr = graph.emplace_node(a0_ptr, i0_ptr, v0_ptr); + + a1_ptr->take_successors(*a0_ptr); + i1_ptr->take_successors(*i0_ptr); + v1_ptr->take_successors(*v0_ptr); + + CHECK_THAT(put_ptr->predecessors(), RangeEquals({a1_ptr, i1_ptr, v1_ptr})); + + auto state = graph.initialize_state(); + CHECK_THAT(put_ptr->view(state), RangeEquals({44, 55, 7, 8, 9})); + } } TEST_CASE("ReshapeNode") { @@ -1208,6 +1378,41 @@ TEST_CASE("ReshapeNode") { CHECK(graph.num_nodes() == 1); // no side effects } + + SECTION("equality") { + auto graph = Graph(); + + auto* arr0_ptr = graph.emplace_node(std::vector{0, 1, 2, 3}); + auto* arr1_ptr = graph.emplace_node(std::vector{0, 1, 2, 3}); + + Node* a_ptr = graph.emplace_node(arr0_ptr, std::array{2, 2}); + Node* b_ptr = graph.emplace_node(arr0_ptr, std::array{2, 2}); + Node* c_ptr = graph.emplace_node(arr1_ptr, std::array{2, 2}); + Node* d_ptr = graph.emplace_node(arr0_ptr, std::array{4, 1}); + + CHECK(a_ptr->equal_to(*a_ptr)); + CHECK(a_ptr->equal_to(*b_ptr)); + + CHECK(not a_ptr->equal_to(*arr0_ptr)); + CHECK(not a_ptr->equal_to(*c_ptr)); + CHECK(not a_ptr->equal_to(*d_ptr)); + } + + SECTION("predecessor replacement") { + auto graph = Graph(); + + auto* arr0_ptr = graph.emplace_node(std::vector{0, 1, 2, 3}); + auto* arr1_ptr = graph.emplace_node(std::vector{2, 3, 4, 5}); + + auto* reshape_ptr = graph.emplace_node(arr0_ptr, std::array{2, 2}); + + arr1_ptr->take_successors(*arr0_ptr); + + CHECK_THAT(reshape_ptr->predecessors(), RangeEquals({arr1_ptr})); + + auto state = graph.initialize_state(); + CHECK_THAT(reshape_ptr->view(state), RangeEquals({2, 3, 4, 5})); + } } TEST_CASE("ResizeNode") { @@ -1297,6 +1502,45 @@ TEST_CASE("ResizeNode") { auto state = graph.initialize_state(); CHECK_THAT(resize_ptr->view(state), RangeEquals({0, 1, 2, 3, 4, -1})); } + + SECTION("equality") { + auto graph = Graph(); + + auto* set0_ptr = graph.emplace_node(10); + auto* set1_ptr = graph.emplace_node(10); + + Node* a_ptr = graph.emplace_node(set0_ptr, std::array{5}, 15.1); + Node* b_ptr = graph.emplace_node(set0_ptr, std::array{5}, 15.1); + Node* c_ptr = graph.emplace_node(set1_ptr, std::array{5}, 15.1); + Node* d_ptr = graph.emplace_node(set0_ptr, std::array{6}, 15.1); + Node* e_ptr = graph.emplace_node(set0_ptr, std::array{5}, -15.1); + + CHECK(a_ptr->equal_to(*a_ptr)); + CHECK(a_ptr->equal_to(*b_ptr)); + + CHECK(not a_ptr->equal_to(*set0_ptr)); + CHECK(not a_ptr->equal_to(*c_ptr)); + CHECK(not a_ptr->equal_to(*d_ptr)); + CHECK(not a_ptr->equal_to(*e_ptr)); + } + + SECTION("predecessor replacement") { + auto graph = Graph(); + + auto* set0_ptr = graph.emplace_node(10); + auto* set1_ptr = graph.emplace_node(10); + + auto* resize_ptr = graph.emplace_node(set0_ptr, std::array{5}, 15.1); + + set1_ptr->take_successors(*set0_ptr); + + CHECK_THAT(resize_ptr->predecessors(), RangeEquals({set1_ptr})); + + auto state = graph.empty_state(); + set1_ptr->initialize_state(state, {0, 1, 2}); + graph.initialize_state(state); + CHECK_THAT(resize_ptr->view(state), RangeEquals(std::vector{0, 1, 2, 15.1, 15.1})); + } } TEST_CASE("RollNode") { @@ -1495,6 +1739,52 @@ TEST_CASE("RollNode") { } } } + + SECTION("equality") { + auto graph = Graph(); + + auto* arr0_ptr = graph.emplace_node(std::vector{0, 1, 2, 3}, std::array{2, 2}); + auto* arr1_ptr = graph.emplace_node(std::vector{4, 5, 6, 7}, std::array{2, 2}); + + auto* shift0_ptr = graph.emplace_node(2); + auto* shift1_ptr = graph.emplace_node(3); + + Node* a_ptr = graph.emplace_node(arr0_ptr, shift0_ptr, std::vector{1}); + Node* b_ptr = graph.emplace_node(arr0_ptr, shift0_ptr, std::vector{1}); + Node* c_ptr = graph.emplace_node(arr1_ptr, shift0_ptr, std::vector{1}); + Node* d_ptr = graph.emplace_node(arr0_ptr, shift1_ptr, std::vector{1}); + Node* e_ptr = graph.emplace_node(arr0_ptr, shift0_ptr, std::vector{0}); + + CHECK(a_ptr->equal_to(*a_ptr)); + CHECK(a_ptr->equal_to(*b_ptr)); + + CHECK(not a_ptr->equal_to(*arr0_ptr)); + CHECK(not a_ptr->equal_to(*c_ptr)); + CHECK(not a_ptr->equal_to(*d_ptr)); + CHECK(not a_ptr->equal_to(*e_ptr)); + } + + SECTION("predecessor replacement") { + auto graph = Graph(); + + auto* arr0_ptr = + graph.emplace_node(std::vector{0, 1, 2, 3}, std::array{2, 2}); + auto* arr1_ptr = + graph.emplace_node(std::vector{4, 5, 6, 7}, std::array{2, 2}); + + auto* shift0_ptr = graph.emplace_node(2); + auto* shift1_ptr = graph.emplace_node(3); + + auto* roll_ptr = graph.emplace_node(arr0_ptr, shift0_ptr); + + arr1_ptr->take_successors(*arr0_ptr); + shift1_ptr->take_successors(*shift0_ptr); + + CHECK_THAT(roll_ptr->predecessors(), RangeEquals({arr1_ptr, shift1_ptr})); + + auto state = graph.initialize_state(); + CHECK_THAT(roll_ptr->view(state), RangeEquals(std::vector{5, 6, 7, 4})); + } } TEST_CASE("SizeNode") { @@ -1623,6 +1913,25 @@ TEST_CASE("SizeNode") { } } } + + SECTION("predecessor replacement") { + auto graph = Graph(); + + auto* set0_ptr = graph.emplace_node(5); + auto* set1_ptr = graph.emplace_node(5); + + auto* size_ptr = graph.emplace_node(set0_ptr); + + set1_ptr->take_successors(*set0_ptr); + + CHECK_THAT(size_ptr->predecessors(), RangeEquals({set1_ptr})); + + auto state = graph.empty_state(); + set0_ptr->initialize_state(state, {0}); + set1_ptr->initialize_state(state, {0, 1, 2, 3}); + graph.initialize_state(state); + CHECK_THAT(size_ptr->view(state), RangeEquals({4})); + } } TEST_CASE("TransposeNode") { @@ -1993,6 +2302,24 @@ TEST_CASE("TransposeNode") { } } } + + SECTION("predecessor replacement") { + auto graph = Graph(); + + auto* arr0_ptr = + graph.emplace_node(std::vector{0, 1, 2, 3}, std::array{2, 2}); + auto* arr1_ptr = + graph.emplace_node(std::vector{4, 5, 6, 7}, std::array{2, 2}); + + auto* transpose_ptr = graph.emplace_node(arr0_ptr); + + arr1_ptr->take_successors(*arr0_ptr); + + CHECK_THAT(transpose_ptr->predecessors(), RangeEquals({arr1_ptr})); + + auto state = graph.initialize_state(); + CHECK_THAT(transpose_ptr->view(state), RangeEquals({4, 6, 5, 7})); + } } } // namespace dwave::optimization From edf9a68867867b8f0e2baca390272ca176bbfb4e Mon Sep 17 00:00:00 2001 From: Alexander Condello Date: Wed, 8 Jul 2026 10:20:45 -0700 Subject: [PATCH 14/25] Add equality and predecessor replacement for nary op nodes --- .../include/dwave-optimization/nodes/naryop.hpp | 5 ++++- dwave/optimization/src/nodes/naryop.cpp | 12 ++++++++++++ tests/cpp/nodes/test_naryop.cpp | 16 ++++++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/dwave/optimization/include/dwave-optimization/nodes/naryop.hpp b/dwave/optimization/include/dwave-optimization/nodes/naryop.hpp index 22ebded2a..ff9d50f63 100644 --- a/dwave/optimization/include/dwave-optimization/nodes/naryop.hpp +++ b/dwave/optimization/include/dwave-optimization/nodes/naryop.hpp @@ -30,7 +30,7 @@ namespace dwave::optimization { template -class NaryOpNode : public ArrayOutputMixin { +class NaryOpNode : public ArrayOutputMixin> { public: // Need at least one node to start with to determine the shape explicit NaryOpNode(ArrayNode* node_ptr); @@ -69,6 +69,9 @@ class NaryOpNode : public ArrayOutputMixin { return operands_; } + protected: + void replace_predecessor_(ssize_t index, Node* node_ptr) override; + private: BinaryOp op; diff --git a/dwave/optimization/src/nodes/naryop.cpp b/dwave/optimization/src/nodes/naryop.cpp index 420d9ebbc..76377d499 100644 --- a/dwave/optimization/src/nodes/naryop.cpp +++ b/dwave/optimization/src/nodes/naryop.cpp @@ -315,6 +315,18 @@ void NaryOpNode::propagate(State& state) const { } } +template +void NaryOpNode::replace_predecessor_(ssize_t index, Node* node_ptr) { + Node::replace_predecessor_(index, node_ptr); + + assert(0 <= index and static_cast(index) < operands_.size()); + ArrayNode* array_ptr = dynamic_cast(node_ptr); + assert(array_ptr != nullptr); + + assert(std::ranges::equal(operands_[index]->shape(), array_ptr->shape())); + operands_[index] = array_ptr; +} + template class NaryOpNode>; template class NaryOpNode>; template class NaryOpNode>; diff --git a/tests/cpp/nodes/test_naryop.cpp b/tests/cpp/nodes/test_naryop.cpp index 9d3d2fc25..1aee97535 100644 --- a/tests/cpp/nodes/test_naryop.cpp +++ b/tests/cpp/nodes/test_naryop.cpp @@ -14,6 +14,7 @@ #include #include +#include #include "dwave-optimization/graph.hpp" #include "dwave-optimization/nodes/binaryop.hpp" @@ -24,6 +25,8 @@ #include "dwave-optimization/nodes/numbers.hpp" #include "dwave-optimization/nodes/testing.hpp" +using Catch::Matchers::RangeEquals; + namespace dwave::optimization { TEMPLATE_TEST_CASE( @@ -186,6 +189,19 @@ TEMPLATE_TEST_CASE( } } } + + THEN("We can replace predecessors") { + auto* a0_ptr = graph.emplace_node(4); + auto* b0_ptr = graph.emplace_node(4); + auto* c0_ptr = graph.emplace_node(4); + + a0_ptr->take_successors(*a_ptr); + b0_ptr->take_successors(*b_ptr); + c0_ptr->take_successors(*c_ptr); + + CHECK_THAT(p_ptr->predecessors(), RangeEquals({a0_ptr, b0_ptr, c0_ptr})); + CHECK_THAT(p_ptr->operands(), RangeEquals({a0_ptr, b0_ptr, c0_ptr})); + } } GIVEN("A vector of constants") { From cf7f812e4e5f4e3795cad7eecf83d3099741dd8b Mon Sep 17 00:00:00 2001 From: Alexander Condello Date: Thu, 9 Jul 2026 12:55:48 -0700 Subject: [PATCH 15/25] Add equality and predecessor replacement for ReduceNode --- .../dwave-optimization/nodes/reduce.hpp | 18 +++---- dwave/optimization/src/nodes/reduce.cpp | 51 ++++++++++--------- tests/cpp/nodes/test_reduce.cpp | 38 ++++++++++++++ 3 files changed, 75 insertions(+), 32 deletions(-) diff --git a/dwave/optimization/include/dwave-optimization/nodes/reduce.hpp b/dwave/optimization/include/dwave-optimization/nodes/reduce.hpp index 81fbb7d85..6e379e924 100644 --- a/dwave/optimization/include/dwave-optimization/nodes/reduce.hpp +++ b/dwave/optimization/include/dwave-optimization/nodes/reduce.hpp @@ -29,7 +29,7 @@ namespace dwave::optimization { template -class ReduceNode : public ArrayOutputMixin { +class ReduceNode : public ArrayOutputMixin>> { public: ReduceNode(ArrayNode* array_ptr); @@ -56,8 +56,8 @@ class ReduceNode : public ArrayOutputMixin { /// @copydoc Array::diff() std::span diff(const State& state) const override; - bool equal_to(const Node& rhs) const override; - bool equal_to(const ReduceNode& rhs) const; + /// @copydoc Node::equal_to() + bool equal_to(const ReduceNode& rhs) const override; /// @copydoc Node::initialize_state() void initialize_state(State& state) const override; @@ -73,11 +73,11 @@ class ReduceNode : public ArrayOutputMixin { // The predecessor of the reduction, as an Array*. std::span operands() { - assert(predecessors().size() == 1); + assert(this->predecessors().size() == 1); return std::span(&array_ptr_, 1); } std::span operands() const { - assert(predecessors().size() == 1); + assert(this->predecessors().size() == 1); return std::span(&array_ptr_, 1); } @@ -88,11 +88,11 @@ class ReduceNode : public ArrayOutputMixin { void revert(State& state) const override; /// @copydoc Array::shape() - using ArrayOutputMixin::shape; + using ArrayOutputMixin>>::shape; std::span shape(const State& state) const override; /// @copydoc Array::size() - using ArrayOutputMixin::size; + using ArrayOutputMixin>>::size; ssize_t size(const State& state) const override; /// @copydoc Array::sizeinfo() @@ -106,7 +106,7 @@ class ReduceNode : public ArrayOutputMixin { const std::optional initial; protected: - void replace_predecessor_(ssize_t previous_index, Node* node_ptr) override; + void replace_predecessor_(ssize_t index, Node* node_ptr) override; private: // Perform a reduction over the reduction space associated with the given @@ -116,7 +116,7 @@ class ReduceNode : public ArrayOutputMixin { BinaryOp op; - Array* const array_ptr_; + Array* array_ptr_; // The axes we're reducing in a sorted, unique vector. // An empty vector means we're reducing over everything diff --git a/dwave/optimization/src/nodes/reduce.cpp b/dwave/optimization/src/nodes/reduce.cpp index f01e85124..8b1794aa2 100644 --- a/dwave/optimization/src/nodes/reduce.cpp +++ b/dwave/optimization/src/nodes/reduce.cpp @@ -801,13 +801,13 @@ ReduceNode::ReduceNode( std::span axes, std::optional initial ) : - ArrayOutputMixin(reduce_shape(array_ptr, axes)), + ArrayOutputMixin>>(reduce_shape(array_ptr, axes)), initial(initial), array_ptr_(array_ptr), axes_(normalize_axes(array_ptr, axes)), values_info_(values_info(array_ptr_, axes_, initial)), sizeinfo_(reducenode_calculate_sizeinfo(this, array_ptr_, axes_)) { - add_predecessor_(array_ptr); + this->add_predecessor_(array_ptr); } template @@ -820,30 +820,26 @@ ReduceNode::ReduceNode( template double const* ReduceNode::buff(const State& state) const { - return data_ptr_>(state)->buff(); + return this->template data_ptr_>(state)->buff(); } template void ReduceNode::commit(State& state) const { - return data_ptr_>(state)->commit(); + return this->template data_ptr_>(state)->commit(); } template std::span ReduceNode::diff(const State& state) const { - return data_ptr_>(state)->diff(); -} - -template -bool ReduceNode::equal_to(const Node& rhs) const { - const auto* rhs_ptr = dynamic_cast(&rhs); - if (rhs_ptr == nullptr) return false; // not same type so not equal - return this->equal_to(*rhs_ptr); + return this->template data_ptr_>(state)->diff(); } template bool ReduceNode::equal_to(const ReduceNode& rhs) const { - assert(false and "not yet implemented"); - return false; + return ( + array_ptr_ == rhs.array_ptr_ and // + initial == rhs.initial and // + std::ranges::equal(axes_, rhs.axes_) + ); } template @@ -859,12 +855,14 @@ void ReduceNode::initialize_state(State& state) const { reductions.emplace_back(reduce_(state, index)); } - emplace_data_ptr_>(state, std::move(reductions), this->shape()); + this->template emplace_data_ptr_>( + state, std::move(reductions), this->shape() + ); } else { for (ssize_t index = 0; index < this->size(); ++index) { reductions.emplace_back(reduce_(state, index)); } - emplace_data_ptr_>(state, std::move(reductions)); + this->template emplace_data_ptr_>(state, std::move(reductions)); } } @@ -957,7 +955,7 @@ ssize_t ReduceNode::convert_predecessor_index_(ssize_t index) const { template void ReduceNode::propagate(State& state) const { - auto* const state_ptr = data_ptr_>(state); + auto* const state_ptr = this->template data_ptr_>(state); // We are reducing over all axes, so this is nice and simple if (axes_.empty() or axes_.size() == static_cast(array_ptr_->ndim())) { @@ -1080,31 +1078,38 @@ auto ReduceNode::reduce_(const State& state, const ssize_t index) cons } template -void ReduceNode::replace_predecessor_(ssize_t previous_index, Node* node_ptr) { - assert(false and "not yet implemented"); +void ReduceNode::replace_predecessor_(ssize_t index, Node* node_ptr) { + Node::replace_predecessor_(index, node_ptr); + + ArrayNode* array_ptr = dynamic_cast(node_ptr); + assert(array_ptr != nullptr); + assert(std::ranges::equal(array_ptr_->shape(), array_ptr->shape())); + + assert(index == 0); + array_ptr_ = array_ptr; } template void ReduceNode::revert(State& state) const { - return data_ptr_>(state)->revert(); + return this->template data_ptr_>(state)->revert(); } template std::span ReduceNode::shape(const State& state) const { if (ssize_t size = this->size(); size >= 0) return this->shape(); // if we're not dynamic - return data_ptr_>(state)->shape(); + return this->template data_ptr_>(state)->shape(); } template ssize_t ReduceNode::size(const State& state) const { if (ssize_t size = this->size(); size >= 0) return size; // if we're not dynamic - return data_ptr_>(state)->size(); + return this->template data_ptr_>(state)->size(); } template ssize_t ReduceNode::size_diff(const State& state) const { if (ssize_t size = this->size(); size >= 0) return 0; // if we're not dynamic - return data_ptr_>(state)->size_diff(); + return this->template data_ptr_>(state)->size_diff(); } template class ReduceNode>; diff --git a/tests/cpp/nodes/test_reduce.cpp b/tests/cpp/nodes/test_reduce.cpp index 794e687e9..31c28d5e0 100644 --- a/tests/cpp/nodes/test_reduce.cpp +++ b/tests/cpp/nodes/test_reduce.cpp @@ -261,6 +261,44 @@ TEMPLATE_TEST_CASE( } } } + + SECTION("equality") { + auto graph = Graph(); + + auto* c0_ptr = + graph.emplace_node(std::vector{0, 1, 2, 3}, std::vector{2, 2}); + auto* c1_ptr = + graph.emplace_node(std::vector{4, 5, 6, 7}, std::vector{2, 2}); + + Node* a_ptr = graph.emplace_node>(c0_ptr, std::vector{}, 1.5); + Node* b_ptr = graph.emplace_node>(c0_ptr, std::vector{}, 1.5); + Node* c_ptr = graph.emplace_node>(c1_ptr, std::vector{}, 1.5); + Node* d_ptr = graph.emplace_node>(c0_ptr, std::vector{0}, 1.5); + Node* e_ptr = graph.emplace_node>(c0_ptr, std::vector{}, 2.5); + + 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)); + CHECK(not a_ptr->equal_to(*e_ptr)); + } + + SECTION("predecessor replacement") { + auto graph = Graph(); + + auto* c0_ptr = + graph.emplace_node(std::vector{0, 1, 2, 3}, std::vector{2, 2}); + auto* c1_ptr = + graph.emplace_node(std::vector{4, 5, 6, 7}, std::vector{2, 2}); + + auto* reduce_ptr = graph.emplace_node>(c0_ptr, std::vector{0}); + + c1_ptr->take_successors(*c0_ptr); + + CHECK_THAT(reduce_ptr->predecessors(), RangeEquals({c1_ptr})); + CHECK_THAT(reduce_ptr->operands(), RangeEquals({c1_ptr})); + } } TEST_CASE("AllNode/AnyNode") { From b1df040a167b25543a1ef1600009070e3c34a24b Mon Sep 17 00:00:00 2001 From: Alexander Condello Date: Thu, 9 Jul 2026 13:25:17 -0700 Subject: [PATCH 16/25] Add equality and predecessor replacement for IsInNode --- .../dwave-optimization/nodes/set_routines.hpp | 5 ++++- dwave/optimization/src/nodes/set_routines.cpp | 14 ++++++++++++++ tests/cpp/nodes/test_set_routines.cpp | 18 ++++++++++++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/dwave/optimization/include/dwave-optimization/nodes/set_routines.hpp b/dwave/optimization/include/dwave-optimization/nodes/set_routines.hpp index 0ce7def66..862f96194 100644 --- a/dwave/optimization/include/dwave-optimization/nodes/set_routines.hpp +++ b/dwave/optimization/include/dwave-optimization/nodes/set_routines.hpp @@ -22,7 +22,7 @@ namespace dwave::optimization { -class IsInNode : public ArrayOutputMixin { +class IsInNode : public ArrayOutputMixin> { public: IsInNode(ArrayNode* element_ptr, ArrayNode* test_elements_ptr); @@ -69,6 +69,9 @@ class IsInNode : public ArrayOutputMixin { /// @copydoc Array::sizeinfo() SizeInfo sizeinfo() const override; + protected: + void replace_predecessor_(ssize_t index, Node* node_ptr) override; + private: // these are redundant, but convenient const Array* element_ptr_; diff --git a/dwave/optimization/src/nodes/set_routines.cpp b/dwave/optimization/src/nodes/set_routines.cpp index a15301a4b..8251f76b3 100644 --- a/dwave/optimization/src/nodes/set_routines.cpp +++ b/dwave/optimization/src/nodes/set_routines.cpp @@ -230,6 +230,20 @@ void IsInNode::propagate(State& state) const { assert(set_data_is_correct(state, element_ptr_, *node_data)); } +void IsInNode::replace_predecessor_(ssize_t index, Node* node_ptr) { + Node::replace_predecessor_(index, node_ptr); + + ArrayNode* array_ptr = dynamic_cast(node_ptr); + assert(array_ptr != nullptr); + + if (index == 0) { + element_ptr_ = array_ptr; + } else { + assert(index == 1); + test_elements_ptr_ = array_ptr; + } +} + void IsInNode::revert(State& state) const { IsInNodeData* node_data = data_ptr_(state); diff --git a/tests/cpp/nodes/test_set_routines.cpp b/tests/cpp/nodes/test_set_routines.cpp index 6770605f5..de79611f5 100644 --- a/tests/cpp/nodes/test_set_routines.cpp +++ b/tests/cpp/nodes/test_set_routines.cpp @@ -265,5 +265,23 @@ TEST_CASE("IsInNode") { } } } + + SECTION("predecessor replacement") { + auto* e0_ptr = graph.emplace_node(std::vector{0, 1, 6, 7}); + auto* e1_ptr = graph.emplace_node(std::vector{2, 3, 4, 5}); + + auto* t0_ptr = graph.emplace_node(std::vector{1, 2}); + auto* t1_ptr = graph.emplace_node(std::vector{6, 5}); + + auto* isin_ptr = graph.emplace_node(e0_ptr, t0_ptr); + + e1_ptr->take_successors(*e0_ptr); + t1_ptr->take_successors(*t0_ptr); + + CHECK_THAT(isin_ptr->predecessors(), RangeEquals({e1_ptr, t1_ptr})); + + auto state = graph.initialize_state(); + CHECK_THAT(isin_ptr->view(state), RangeEquals({0, 0, 0, 1})); } +} } // namespace dwave::optimization From f643bcafce3b705e985cf4ea37d37904394bc102 Mon Sep 17 00:00:00 2001 From: Alexander Condello Date: Thu, 9 Jul 2026 15:39:40 -0700 Subject: [PATCH 17/25] Add equality and predecessor replacement to SoftMaxNode --- .../dwave-optimization/nodes/softmax.hpp | 5 ++- dwave/optimization/src/nodes/softmax.cpp | 11 ++++++ tests/cpp/nodes/test_softmax.cpp | 34 +++++++++++++++++++ 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/dwave/optimization/include/dwave-optimization/nodes/softmax.hpp b/dwave/optimization/include/dwave-optimization/nodes/softmax.hpp index 26b66dd5a..c2560d277 100644 --- a/dwave/optimization/include/dwave-optimization/nodes/softmax.hpp +++ b/dwave/optimization/include/dwave-optimization/nodes/softmax.hpp @@ -24,7 +24,7 @@ namespace dwave::optimization { -class SoftMaxNode : public ArrayOutputMixin { +class SoftMaxNode : public ArrayOutputMixin> { public: SoftMaxNode(ArrayNode* arr_ptr); @@ -71,6 +71,9 @@ class SoftMaxNode : public ArrayOutputMixin { /// @copydoc Array::sizeinfo() SizeInfo sizeinfo() const override; + protected: + void replace_predecessor_(ssize_t index, Node* node_ptr) override; + private: // these are redundant, but convenient const Array* arr_ptr_; diff --git a/dwave/optimization/src/nodes/softmax.cpp b/dwave/optimization/src/nodes/softmax.cpp index dce5136aa..467a413a0 100644 --- a/dwave/optimization/src/nodes/softmax.cpp +++ b/dwave/optimization/src/nodes/softmax.cpp @@ -139,6 +139,17 @@ void SoftMaxNode::propagate(State& state) const { node_data->prior_denominator = prior_denominator; } +void SoftMaxNode::replace_predecessor_(ssize_t index, Node* node_ptr) { + Node::replace_predecessor_(index, node_ptr); + + ArrayNode* array_ptr = dynamic_cast(node_ptr); + assert(array_ptr != nullptr); + assert(std::ranges::equal(arr_ptr_->shape(), array_ptr->shape())); + + assert(index == 0); + arr_ptr_ = array_ptr; +} + void SoftMaxNode::revert(State& state) const { auto node_data = data_ptr_(state); node_data->revert(); diff --git a/tests/cpp/nodes/test_softmax.cpp b/tests/cpp/nodes/test_softmax.cpp index 848065f92..92595f0dd 100644 --- a/tests/cpp/nodes/test_softmax.cpp +++ b/tests/cpp/nodes/test_softmax.cpp @@ -14,6 +14,7 @@ #include #include +#include #include "dwave-optimization/nodes/constants.hpp" #include "dwave-optimization/nodes/numbers.hpp" @@ -22,6 +23,7 @@ namespace dwave::optimization { +using Catch::Matchers::RangeEquals; using Catch::Matchers::WithinRel; TEST_CASE("SoftMaxNode") { @@ -236,5 +238,37 @@ TEST_CASE("SoftMaxNode") { } } } + + 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}); + + Node* a_ptr = graph.emplace_node(c0_ptr); + Node* b_ptr = graph.emplace_node(c0_ptr); + Node* c_ptr = graph.emplace_node(c1_ptr); + + 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)); + } + + 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}); + + auto* softmax_ptr = graph.emplace_node(c0_ptr); + + c1_ptr->take_successors(*c0_ptr); + + CHECK_THAT(softmax_ptr->predecessors(), RangeEquals({c1_ptr})); + + auto state = graph.initialize_state(); + CHECK_THAT(softmax_ptr->view(state)[0], WithinRel(0.03205860328008499, 1e-9)); + } } } // namespace dwave::optimization From 08728d9bb4f043e996077cd62a023990a8646199 Mon Sep 17 00:00:00 2001 From: Alexander Condello Date: Thu, 9 Jul 2026 15:45:30 -0700 Subject: [PATCH 18/25] Add equality and predecessor replacement to ArgSortNode --- .../dwave-optimization/nodes/sorting.hpp | 5 ++- dwave/optimization/src/nodes/sorting.cpp | 11 +++++++ tests/cpp/nodes/test_sorting.cpp | 32 +++++++++++++++++++ 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/dwave/optimization/include/dwave-optimization/nodes/sorting.hpp b/dwave/optimization/include/dwave-optimization/nodes/sorting.hpp index 55ea5abdc..6effdae4f 100644 --- a/dwave/optimization/include/dwave-optimization/nodes/sorting.hpp +++ b/dwave/optimization/include/dwave-optimization/nodes/sorting.hpp @@ -23,7 +23,7 @@ namespace dwave::optimization { -class ArgSortNode : public ArrayOutputMixin { +class ArgSortNode : public ArrayOutputMixin> { public: ArgSortNode(ArrayNode* arr_ptr); @@ -70,6 +70,9 @@ class ArgSortNode : public ArrayOutputMixin { /// @copydoc Array::sizeinfo() SizeInfo sizeinfo() const override; + protected: + void replace_predecessor_(ssize_t index, Node* node_ptr) override; + private: // these are redundant, but convenient const Array* arr_ptr_; diff --git a/dwave/optimization/src/nodes/sorting.cpp b/dwave/optimization/src/nodes/sorting.cpp index 281315621..c194a708e 100644 --- a/dwave/optimization/src/nodes/sorting.cpp +++ b/dwave/optimization/src/nodes/sorting.cpp @@ -112,6 +112,17 @@ void ArgSortNode::propagate(State& state) const { ); } +void ArgSortNode::replace_predecessor_(ssize_t index, Node* node_ptr) { + Node::replace_predecessor_(index, node_ptr); + + ArrayNode* array_ptr = dynamic_cast(node_ptr); + assert(array_ptr != nullptr); + assert(std::ranges::equal(arr_ptr_->shape(), array_ptr->shape())); + + assert(index == 0); + arr_ptr_ = array_ptr; +} + void ArgSortNode::revert(State& state) const { auto node_data = data_ptr_(state); diff --git a/tests/cpp/nodes/test_sorting.cpp b/tests/cpp/nodes/test_sorting.cpp index df2310773..b519b4478 100644 --- a/tests/cpp/nodes/test_sorting.cpp +++ b/tests/cpp/nodes/test_sorting.cpp @@ -151,6 +151,38 @@ TEST_CASE("ArgSortNode") { } } } + + 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}); + + Node* a_ptr = graph.emplace_node(c0_ptr); + Node* b_ptr = graph.emplace_node(c0_ptr); + Node* c_ptr = graph.emplace_node(c1_ptr); + + 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)); + } + + 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{7, 6, 5, 4}); + + auto* argsort_ptr = graph.emplace_node(c0_ptr); + + c1_ptr->take_successors(*c0_ptr); + + CHECK_THAT(argsort_ptr->predecessors(), RangeEquals({c1_ptr})); + + auto state = graph.initialize_state(); + CHECK_THAT(argsort_ptr->view(state), RangeEquals({3, 2, 1, 0})); + } } } // namespace dwave::optimization From 7e3ccae91601767f3df4c4f648d952723b993912 Mon Sep 17 00:00:00 2001 From: Alexander Condello Date: Thu, 9 Jul 2026 15:50:43 -0700 Subject: [PATCH 19/25] Add equality and predecessor replacement to MeanNode --- .../dwave-optimization/nodes/statistics.hpp | 5 ++- dwave/optimization/src/nodes/statistics.cpp | 14 +++++++- tests/cpp/nodes/test_sorting.cpp | 2 +- tests/cpp/nodes/test_statistics.cpp | 35 +++++++++++++++++++ 4 files changed, 53 insertions(+), 3 deletions(-) diff --git a/dwave/optimization/include/dwave-optimization/nodes/statistics.hpp b/dwave/optimization/include/dwave-optimization/nodes/statistics.hpp index a6c3b4071..0283c6e10 100644 --- a/dwave/optimization/include/dwave-optimization/nodes/statistics.hpp +++ b/dwave/optimization/include/dwave-optimization/nodes/statistics.hpp @@ -20,7 +20,7 @@ namespace dwave::optimization { -class MeanNode : public ScalarOutputMixin { +class MeanNode : public ScalarOutputMixin, true> { public: MeanNode(ArrayNode* arr_ptr); @@ -39,6 +39,9 @@ class MeanNode : public ScalarOutputMixin { /// @copydoc Node::propagate() void propagate(State& state) const override; + protected: + void replace_predecessor_(ssize_t index, Node* node_ptr) override; + private: // these are redundant, but convenient const Array* arr_ptr_; diff --git a/dwave/optimization/src/nodes/statistics.cpp b/dwave/optimization/src/nodes/statistics.cpp index 341fdc219..7157447ad 100644 --- a/dwave/optimization/src/nodes/statistics.cpp +++ b/dwave/optimization/src/nodes/statistics.cpp @@ -38,7 +38,7 @@ std::pair calculate_values_minmax_(const Array* arr_ptr) { } MeanNode::MeanNode(ArrayNode* arr_ptr) : - ScalarOutputMixin(), + ScalarOutputMixin, true>(), arr_ptr_(arr_ptr), minmax_(calculate_values_minmax_(arr_ptr_)) { add_predecessor_(arr_ptr); @@ -95,4 +95,16 @@ void MeanNode::propagate(State& state) const { } set_state(state, sum / static_cast(state_size)); } + +void MeanNode::replace_predecessor_(ssize_t index, Node* node_ptr) { + Node::replace_predecessor_(index, node_ptr); + + ArrayNode* array_ptr = dynamic_cast(node_ptr); + assert(array_ptr != nullptr); + assert(std::ranges::equal(arr_ptr_->shape(), array_ptr->shape())); + + assert(index == 0); + arr_ptr_ = array_ptr; +} + } // namespace dwave::optimization diff --git a/tests/cpp/nodes/test_sorting.cpp b/tests/cpp/nodes/test_sorting.cpp index b519b4478..393b75837 100644 --- a/tests/cpp/nodes/test_sorting.cpp +++ b/tests/cpp/nodes/test_sorting.cpp @@ -14,7 +14,7 @@ #include #include -#include +#include #include "dwave-optimization/nodes/collections.hpp" #include "dwave-optimization/nodes/constants.hpp" diff --git a/tests/cpp/nodes/test_statistics.cpp b/tests/cpp/nodes/test_statistics.cpp index 2efbe896c..faeffc60b 100644 --- a/tests/cpp/nodes/test_statistics.cpp +++ b/tests/cpp/nodes/test_statistics.cpp @@ -21,6 +21,8 @@ #include "dwave-optimization/nodes/statistics.hpp" #include "dwave-optimization/nodes/testing.hpp" +using Catch::Matchers::RangeEquals; + namespace dwave::optimization { TEST_CASE("MeanNode") { @@ -155,5 +157,38 @@ TEST_CASE("MeanNode") { } } } + + 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}); + + Node* a_ptr = graph.emplace_node(c0_ptr); + Node* b_ptr = graph.emplace_node(c0_ptr); + Node* c_ptr = graph.emplace_node(c1_ptr); + + 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)); + } + + 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{7, 6, 5, 4}); + + auto* argsort_ptr = graph.emplace_node(c0_ptr); + + c1_ptr->take_successors(*c0_ptr); + + CHECK_THAT(argsort_ptr->predecessors(), RangeEquals({c1_ptr})); + + auto state = graph.initialize_state(); + CHECK_THAT(argsort_ptr->view(state), RangeEquals({5.5})); + } + } } // namespace dwave::optimization From 918d197e56940524c73f0289a9a9a77793657572 Mon Sep 17 00:00:00 2001 From: Alexander Condello Date: Thu, 9 Jul 2026 15:54:13 -0700 Subject: [PATCH 20/25] Add equality and predecessor replacement to unary op nodes --- .../dwave-optimization/nodes/testing.hpp | 7 ++++- .../dwave-optimization/nodes/unaryop.hpp | 2 +- tests/cpp/nodes/test_unaryop.cpp | 31 +++++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/dwave/optimization/include/dwave-optimization/nodes/testing.hpp b/dwave/optimization/include/dwave-optimization/nodes/testing.hpp index 53e08a614..51fb02347 100644 --- a/dwave/optimization/include/dwave-optimization/nodes/testing.hpp +++ b/dwave/optimization/include/dwave-optimization/nodes/testing.hpp @@ -23,7 +23,7 @@ namespace dwave::optimization { -class ArrayValidationNode : public Node { +class ArrayValidationNode : public EqualityMixin { public: explicit ArrayValidationNode(ArrayNode* node_ptr); @@ -33,6 +33,11 @@ class ArrayValidationNode : public Node { void propagate(State& state) const override; void revert(State& state) const override; + protected: + void replace_predecessor_(ssize_t index, Node* node_ptr) override { + assert(false and "ArrayValidationNode cannot have its predecessor replaced"); + } + private: const ArrayNode* array_ptr; diff --git a/dwave/optimization/include/dwave-optimization/nodes/unaryop.hpp b/dwave/optimization/include/dwave-optimization/nodes/unaryop.hpp index 9aa8b81ff..7e2795339 100644 --- a/dwave/optimization/include/dwave-optimization/nodes/unaryop.hpp +++ b/dwave/optimization/include/dwave-optimization/nodes/unaryop.hpp @@ -70,7 +70,7 @@ class UnaryOpNode : public ArrayOutputMixin { } private: - void replace_predecessor_(ssize_t previous_index, Node* node_ptr) override; + void replace_predecessor_(ssize_t index, Node* node_ptr) override; UnaryOp op; diff --git a/tests/cpp/nodes/test_unaryop.cpp b/tests/cpp/nodes/test_unaryop.cpp index e4f853049..6788c149e 100644 --- a/tests/cpp/nodes/test_unaryop.cpp +++ b/tests/cpp/nodes/test_unaryop.cpp @@ -244,6 +244,37 @@ TEMPLATE_TEST_CASE( } } } + + 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}); + + Node* a_ptr = graph.emplace_node>(c0_ptr); + Node* b_ptr = graph.emplace_node>(c0_ptr); + Node* c_ptr = graph.emplace_node>(c1_ptr); + + 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)); + } + + 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{7, 6, 5, 4}); + + auto* argsort_ptr = graph.emplace_node>(c0_ptr); + + c1_ptr->take_successors(*c0_ptr); + + CHECK_THAT(argsort_ptr->predecessors(), RangeEquals({c1_ptr})); + CHECK_THAT(argsort_ptr->operands(), RangeEquals({c1_ptr})); + } + } TEST_CASE("UnaryOpNode - AbsoluteNode") { From b43d9d3d1066803801ab8159658316aa6e8d64c8 Mon Sep 17 00:00:00 2001 From: Alexander Condello Date: Thu, 9 Jul 2026 15:57:03 -0700 Subject: [PATCH 21/25] Make Node::equal_to() and abstract method --- .../include/dwave-optimization/graph.hpp | 14 +++++--------- .../dwave-optimization/nodes/manipulation.hpp | 2 +- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/dwave/optimization/include/dwave-optimization/graph.hpp b/dwave/optimization/include/dwave-optimization/graph.hpp index 8e85f7b3d..1585e2be8 100644 --- a/dwave/optimization/include/dwave-optimization/graph.hpp +++ b/dwave/optimization/include/dwave-optimization/graph.hpp @@ -270,15 +270,11 @@ class Node { /// derived from its predecessors. Defaults to `true`, except for decisions. virtual bool deterministic_state() const { return true; } - // TODO: document and note that nodes *must* share the same set of - // predecessors (permutations are sometimes allowed) and they *must* be the - // same type. Also that they can have false negatives in some cases. - // Also, we don't check pinned values - virtual bool equal_to(const Node& rhs) const { - std::cout << classname() << " missing operator==(...) *********\n"; - assert(false and "not yet implemented"); - return false; - } + /// Test whether two nodes are equal. Each node class defines equality for + /// itself but nodes *must* share the same set of + /// predecessors (permutations are sometimes allowed) and they *must* be the + /// same type. Also that they can have false negatives in some cases. + virtual bool equal_to(const Node& rhs) const = 0; /// Return a shared pointer to a bool value. When the node is destructed /// the bool will be set to True diff --git a/dwave/optimization/include/dwave-optimization/nodes/manipulation.hpp b/dwave/optimization/include/dwave-optimization/nodes/manipulation.hpp index b99d93664..21cfc549e 100644 --- a/dwave/optimization/include/dwave-optimization/nodes/manipulation.hpp +++ b/dwave/optimization/include/dwave-optimization/nodes/manipulation.hpp @@ -522,7 +522,7 @@ class SizeNode : public ScalarOutputMixin, true> { }; // Compute the transpose of predecessor -class TransposeNode : public ArrayNode { +class TransposeNode : public EqualityMixin { public: TransposeNode(ArrayNode* array_ptr); From c60bcb73f8c7184a482d499e1b71a7be397258f0 Mon Sep 17 00:00:00 2001 From: Alexander Condello Date: Fri, 10 Jul 2026 10:45:18 -0700 Subject: [PATCH 22/25] Fix misc issues with redundant_node PR --- dwave/optimization/_model.pyi | 1 + dwave/optimization/_model.pyx | 32 ++++++++++++++++++- .../include/dwave-optimization/graph.hpp | 13 +++++--- .../dwave-optimization/nodes/binaryop.hpp | 1 - .../dwave-optimization/nodes/collections.hpp | 2 -- dwave/optimization/src/nodes/collections.cpp | 4 --- dwave/optimization/src/nodes/lp.cpp | 2 -- ...dundant-node-removal-9e6335836d0f5603.yaml | 3 ++ 8 files changed, 44 insertions(+), 14 deletions(-) diff --git a/dwave/optimization/_model.pyi b/dwave/optimization/_model.pyi index 53adc213c..f1c79fd06 100644 --- a/dwave/optimization/_model.pyi +++ b/dwave/optimization/_model.pyi @@ -71,6 +71,7 @@ class _Graph: def num_inputs(self) -> int: ... def num_nodes(self) -> int: ... def num_symbols(self) -> int: ... + def remove_redundant_symbols(self, time_limit_s: None | float) -> int: ... def remove_unused_symbols(self) -> int: ... def state_size(self) -> int: ... def unlock(self): ... diff --git a/dwave/optimization/_model.pyx b/dwave/optimization/_model.pyx index 0c70173f2..43f20e5ab 100644 --- a/dwave/optimization/_model.pyx +++ b/dwave/optimization/_model.pyx @@ -929,7 +929,37 @@ cdef class _Graph: return self.num_nodes() def remove_redundant_symbols(self, time_limit_s=None): - """TODO + """Remove redundant symbols from the model. + + Symbols are redundant if they are the same type, they share the + same predecessors, and they encode the same operation. + + Args: + time_limit_s (float): The maximum amount of time to spend trying + to remove redundant symbols. If not provided, the method will + run until there are no redundant symbols to remove. + + Returns: + int: Number of symbols removed. + + Examples: + + >>> from dwave.optimization import Model + ... + >>> model = Model() + ... + >>> x = model.integer(5) + ... + >>> -(x + 1).sum() # doctest: +ELLIPSIS + + >>> -(x + 1).sum() # doctest: +ELLIPSIS + + >>> model.num_symbols() + 8 + >>> model.remove_redundant_symbols() + 3 + >>> model.num_symbols() + 5 """ if self.is_locked(): raise ValueError("cannot remove symbols from a locked model") diff --git a/dwave/optimization/include/dwave-optimization/graph.hpp b/dwave/optimization/include/dwave-optimization/graph.hpp index 1585e2be8..e52b56dfe 100644 --- a/dwave/optimization/include/dwave-optimization/graph.hpp +++ b/dwave/optimization/include/dwave-optimization/graph.hpp @@ -156,8 +156,12 @@ class Graph { /// Reset the state of the given node and all successors recursively. static void recursive_reset(State& state, const Node* ptr); - // todo: document - // - all root nodes (including constants!) are ignored + /// Remove redundant nodes from the graph. + /// + /// Redundant nodes are ones that are Node::equal_to() another node in the + /// graph. + /// + /// Returns the number of nodes removed from the graph. ssize_t remove_redundant_nodes( bool ignore_listeners = false, double time_limit_s = std::numeric_limits::infinity() @@ -399,8 +403,9 @@ class Node { // Remove a successor. *Does not* remove itself from it's successor's predecessors. ssize_t remove_successor_(const Node* ptr); - // todo: document - virtual void replace_predecessor_(ssize_t previous_index, Node* node_ptr); + // Replace the predecessor at `index` with the node specified by `node_ptr`. + // Does not update `node_ptr`. + virtual void replace_predecessor_(ssize_t index, Node* node_ptr); private: ssize_t topological_index_ = -1; // negative is unset diff --git a/dwave/optimization/include/dwave-optimization/nodes/binaryop.hpp b/dwave/optimization/include/dwave-optimization/nodes/binaryop.hpp index 724a248e9..3e7785613 100644 --- a/dwave/optimization/include/dwave-optimization/nodes/binaryop.hpp +++ b/dwave/optimization/include/dwave-optimization/nodes/binaryop.hpp @@ -40,7 +40,6 @@ class BinaryOpNode : public ArrayOutputMixin { // A DisjointBitSetNode can only ever be equal to itself. bool equal_to(const Node& rhs) const override; - bool equal_to(const DisjointBitSetNode& rhs) const; /// @copydoc Array::integral() bool integral() const override; @@ -234,7 +233,6 @@ class DisjointListNode : public ArrayOutputMixin { // A DisjointListNode can only ever be equal to itself. bool equal_to(const Node& rhs) const override; - bool equal_to(const DisjointListNode& rhs) const; /// @copydoc Array::integral() bool integral() const override; diff --git a/dwave/optimization/src/nodes/collections.cpp b/dwave/optimization/src/nodes/collections.cpp index d44d3e115..a673f1191 100644 --- a/dwave/optimization/src/nodes/collections.cpp +++ b/dwave/optimization/src/nodes/collections.cpp @@ -497,8 +497,6 @@ bool DisjointBitSetNode::equal_to(const Node& rhs) const { return static_cast(this) == &rhs; } -bool DisjointBitSetNode::equal_to(const DisjointBitSetNode& rhs) const { return this == &rhs; } - bool DisjointBitSetNode::integral() const { return true; } double DisjointBitSetNode::min() const { return 0; } @@ -854,8 +852,6 @@ bool DisjointListNode::equal_to(const Node& rhs) const { return static_cast(this) == &rhs; } -bool DisjointListNode::equal_to(const DisjointListNode& rhs) const { return this == &rhs; } - bool DisjointListNode::integral() const { return true; } double DisjointListNode::min() const { return 0; } diff --git a/dwave/optimization/src/nodes/lp.cpp b/dwave/optimization/src/nodes/lp.cpp index 9a6352236..ddf281f41 100644 --- a/dwave/optimization/src/nodes/lp.cpp +++ b/dwave/optimization/src/nodes/lp.cpp @@ -16,8 +16,6 @@ #include -#include - #include "../simplex.hpp" #include "_state.hpp" #include "dwave-optimization/common.hpp" diff --git a/releasenotes/notes/feature-redundant-node-removal-9e6335836d0f5603.yaml b/releasenotes/notes/feature-redundant-node-removal-9e6335836d0f5603.yaml index a50418e61..2582a1ea7 100644 --- a/releasenotes/notes/feature-redundant-node-removal-9e6335836d0f5603.yaml +++ b/releasenotes/notes/feature-redundant-node-removal-9e6335836d0f5603.yaml @@ -1,5 +1,8 @@ --- features: - Add ``MatrixMultiplyNode::operands()`` method. + - Add ``Graph::remove_redundant_nodes()`` method. See `#563 `_. + - Add ``Model.remove_redundant_symbols()`` method. See `#563 `_. + - Add ``Node::take_successors()`` method. upgrade: - Remove ``AccumulateZipNode::initial`` attribute and replace it with ``AccumulateZipNode::initial()`` method. From d0dd3d3b431609c89a9469a1a571e1fa60801271 Mon Sep 17 00:00:00 2001 From: Alexander Condello Date: Fri, 10 Jul 2026 12:04:43 -0700 Subject: [PATCH 23/25] Fix objective handling in Graph::remove_redundant_nodes() --- dwave/optimization/src/graph.cpp | 30 +++++++++++++++++++++++------- tests/cpp/test_graph.cpp | 32 +++++++++++++++++++++++++++----- 2 files changed, 50 insertions(+), 12 deletions(-) diff --git a/dwave/optimization/src/graph.cpp b/dwave/optimization/src/graph.cpp index f49859de7..8b53fdad3 100644 --- a/dwave/optimization/src/graph.cpp +++ b/dwave/optimization/src/graph.cpp @@ -314,6 +314,26 @@ ssize_t Graph::remove_redundant_nodes(bool ignore_listeners, double time_limit_s return num_nodes - this->num_nodes(); }; + // Given two equal nodes, transfer successors and the objective marker + // from `from_ptr` to `to_ptr`. This does not fix the constraints, we handle + // that as part of `cleanup()`. + auto transfer = [&](Node* from_ptr, Node* to_ptr) -> void { + assert(from_ptr->topological_index_ > to_ptr->topological_index_); + + // Transfer the successors + to_ptr->take_successors(*from_ptr); + + // Mark from for dropping + from_ptr->topological_index_ = drop; + + // We also want to fix the objective if relevant + if (objective_ptr_ != nullptr and static_cast(objective_ptr_) == from_ptr) { + objective_ptr_ = dynamic_cast(to_ptr); + assert(objective_ptr_ != nullptr); + assert(objective_ptr_->size() == 1); + } + }; + // Ok, all that setup done, now let's start checking for redundancy. // Decisions are never redundant, so we skip over them @@ -341,9 +361,7 @@ ssize_t Graph::remove_redundant_nodes(bool ignore_listeners, double time_limit_s // nothing to do if they are not equal if (not constants_[i]->equal_to(*constants_[j])) continue; - constants_[i]->take_successors(*constants_[j]); - - constants_[j]->topological_index_ = drop; + transfer(constants_[j], constants_[i]); } constants_[i]->topological_index_ = seen; @@ -377,11 +395,9 @@ ssize_t Graph::remove_redundant_nodes(bool ignore_listeners, double time_limit_s // We want to transfer to the node with the lower topological order if (successors[i]->topological_index_ < successors[j]->topological_index_) { - successors[i]->take_successors(*successors[j]); - successors[j]->topological_index_ = drop; + transfer(successors[j], successors[i]); } else { - successors[j]->take_successors(*successors[i]); - successors[i]->topological_index_ = drop; + transfer(successors[i], successors[j]); break; // stop comparing i to other nodes because we dropped it } } diff --git a/tests/cpp/test_graph.cpp b/tests/cpp/test_graph.cpp index 368cdb6d6..3a966666d 100644 --- a/tests/cpp/test_graph.cpp +++ b/tests/cpp/test_graph.cpp @@ -13,11 +13,13 @@ // limitations under the License. #include -#include +#include #include "dwave-optimization/graph.hpp" #include "dwave-optimization/nodes.hpp" +using Catch::Matchers::RangeEquals; + namespace dwave::optimization { TEST_CASE("Topological Sort", "[topological_sort]") { @@ -243,7 +245,7 @@ TEST_CASE("Graph::commit(), Graph::descendants(), Graph::propagate(), and Graph: SECTION("Find descendants") { auto descendants = graph.descendants({x_ptr}); - CHECK_THAT(descendants, Catch::Matchers::RangeEquals(std::vector{x_ptr, z_ptr})); + CHECK_THAT(descendants, RangeEquals(std::vector{x_ptr, z_ptr})); } SECTION("Propagate all") { CHECK(x_ptr->view(state).front() == 0); @@ -375,10 +377,30 @@ TEST_CASE("Graph::remove_redundant_nodes()") { } } } - } - // todo: test redundant constants - // todo: test objective/constraitns + AND_GIVEN("that the later redundant node is used in the objective") { + graph.set_objective(right_x_plus_y); + + CHECK(graph.remove_redundant_nodes() == 1); + + CHECK(graph.nodes()[2].get() == left_x_plus_y); + CHECK(graph.objective() == left_x_plus_y); // objective was updated + } + + AND_GIVEN("two more redundant logical nodes") { + auto* logical_left_x_plus_y = graph.emplace_node(left_x_plus_y); + auto* logical_right_x_plus_y = graph.emplace_node(right_x_plus_y); + + WHEN("both are registered as constraints") { + graph.add_constraint(logical_left_x_plus_y); + graph.add_constraint(logical_right_x_plus_y); + + CHECK(graph.remove_redundant_nodes() == 2); + CHECK(graph.constraints().size() == 1); + CHECK_THAT(graph.constraints(), RangeEquals({logical_left_x_plus_y})); + } + } + } } TEST_CASE("Graph::remove_unused_nodes()") { From a7912ddfc27316ff71a6e06ca7808bec4c4acdbf Mon Sep 17 00:00:00 2001 From: Alexander Condello Date: Fri, 10 Jul 2026 12:11:10 -0700 Subject: [PATCH 24/25] Add a test for removing redundant constants --- tests/cpp/test_graph.cpp | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/cpp/test_graph.cpp b/tests/cpp/test_graph.cpp index 3a966666d..9ae628751 100644 --- a/tests/cpp/test_graph.cpp +++ b/tests/cpp/test_graph.cpp @@ -401,6 +401,21 @@ TEST_CASE("Graph::remove_redundant_nodes()") { } } } + + GIVEN("A model with two redundant constants used in the same unary op") { + auto graph = Graph(); + + auto* a = graph.emplace_node(5); + auto* negative_a = graph.emplace_node(a); + auto* b = graph.emplace_node(5); + graph.emplace_node(b); + + THEN("remove_redundant_nodes() removes the redundant path") { + CHECK(graph.remove_redundant_nodes() == 2); + CHECK_THAT(graph.constants(), RangeEquals({a})); + CHECK_THAT(negative_a->predecessors(), RangeEquals({a})); + } + } } TEST_CASE("Graph::remove_unused_nodes()") { From 3583a42b059600e6781d864d762837266fe0a361 Mon Sep 17 00:00:00 2001 From: Alexander Condello Date: Fri, 10 Jul 2026 12:16:05 -0700 Subject: [PATCH 25/25] Add a smoke test for Model.remove_redundant_symbols() --- dwave/optimization/_model.pyi | 2 +- tests/test_model.py | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/dwave/optimization/_model.pyi b/dwave/optimization/_model.pyi index f1c79fd06..2afa1d235 100644 --- a/dwave/optimization/_model.pyi +++ b/dwave/optimization/_model.pyi @@ -71,7 +71,7 @@ class _Graph: def num_inputs(self) -> int: ... def num_nodes(self) -> int: ... def num_symbols(self) -> int: ... - def remove_redundant_symbols(self, time_limit_s: None | float) -> int: ... + def remove_redundant_symbols(self, time_limit_s: None | float = None) -> int: ... def remove_unused_symbols(self) -> int: ... def state_size(self) -> int: ... def unlock(self): ... diff --git a/tests/test_model.py b/tests/test_model.py index 41de0f34b..f798781f3 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -326,6 +326,22 @@ def test_objective(self): with self.assertRaises(TypeError): model.objective = "hello" + def test_remove_redundant_symbols(self): + model = Model() + + x = model.binary() + y = model.binary() + + first = x + y + second = y + x + + self.assertEqual(model.remove_redundant_symbols(), 0) # both are named symbols + + del second + self.assertEqual(model.remove_redundant_symbols(), 1) + + self.assertEqual(list(model.iter_symbols())[2].id(), first.id()) + def test_remove_unused_symbols(self): with self.subTest("all unused"): model = Model()