diff --git a/dwave/optimization/include/dwave-optimization/graph.hpp b/dwave/optimization/include/dwave-optimization/graph.hpp index e52b56df..e8c95de7 100644 --- a/dwave/optimization/include/dwave-optimization/graph.hpp +++ b/dwave/optimization/include/dwave-optimization/graph.hpp @@ -143,6 +143,9 @@ class Graph { void propagate(State& state, std::span changed) const; void propagate(State& state, std::vector&& changed) const; + // TODO: better name? + std::vector propagate_sparse(State& state) const; + /// Given the source (changing) nodes, update the model incrementally and accept the changes /// according to the accept function. void propose( @@ -337,6 +340,9 @@ class Node { /// updated and may even want to eagerly incorporate changes. virtual void update(State& state, int index) const {} + // TODO: not necessarily the same as having no diff! + virtual bool updated(const State& state) const = 0; + /// Nodes are printable friend std::ostream& operator<<(std::ostream& os, const Node& node); @@ -452,7 +458,10 @@ NodeType* Graph::emplace_node(Args&&... args) { return ptr; // return the observing pointer } -class ArrayNode : public Array, public virtual Node {}; +class ArrayNode : public Array, public virtual Node { + public: + bool updated(const State& state) const override { return not diff(state).empty(); } +}; class DecisionNode : public Decision, public virtual Node { public: /// Decision nodes by definition do not have a deterministic state. diff --git a/dwave/optimization/include/dwave-optimization/nodes/collections.hpp b/dwave/optimization/include/dwave-optimization/nodes/collections.hpp index ed317802..aa1108aa 100644 --- a/dwave/optimization/include/dwave-optimization/nodes/collections.hpp +++ b/dwave/optimization/include/dwave-optimization/nodes/collections.hpp @@ -122,6 +122,8 @@ class DisjointBitSetsNode : public DecisionNode { ssize_t element_i ) const; + bool updated(const State& state) const override; + protected: const ssize_t primary_set_size_; const ssize_t num_disjoint_sets_; @@ -215,6 +217,8 @@ class DisjointListsNode : public DecisionNode { ssize_t element_j ) const; + bool updated(const State& state) const override; + protected: const ssize_t primary_set_size_; const ssize_t num_disjoint_lists_; diff --git a/dwave/optimization/include/dwave-optimization/nodes/lp.hpp b/dwave/optimization/include/dwave-optimization/nodes/lp.hpp index 8c67313f..f1c4830c 100644 --- a/dwave/optimization/include/dwave-optimization/nodes/lp.hpp +++ b/dwave/optimization/include/dwave-optimization/nodes/lp.hpp @@ -149,6 +149,8 @@ class LinearProgramNode : public EqualityMixin solution(const State& state) const override; + bool updated(const State& state) const override; + /// @copydoc LinearProgramNodeBase::variables_minmax() std::pair variables_minmax() const override; diff --git a/dwave/optimization/include/dwave-optimization/nodes/testing.hpp b/dwave/optimization/include/dwave-optimization/nodes/testing.hpp index 51fb0234..1ca3e5c1 100644 --- a/dwave/optimization/include/dwave-optimization/nodes/testing.hpp +++ b/dwave/optimization/include/dwave-optimization/nodes/testing.hpp @@ -33,6 +33,8 @@ class ArrayValidationNode : public EqualityMixin { void propagate(State& state) const override; void revert(State& state) const override; + bool updated(const State& state) const override { return false; } + protected: void replace_predecessor_(ssize_t index, Node* node_ptr) override { assert(false and "ArrayValidationNode cannot have its predecessor replaced"); diff --git a/dwave/optimization/src/graph.cpp b/dwave/optimization/src/graph.cpp index 908acc8a..a8ef8ada 100644 --- a/dwave/optimization/src/graph.cpp +++ b/dwave/optimization/src/graph.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -175,6 +176,57 @@ void Graph::propagate(State& state, std::vector&& changed) const { return propagate(state, std::span(changed)); } +std::vector Graph::propagate_sparse(State& state) const { + // Create a queue of nodes that are (direct) successors of updated nodes. + auto comp = [](const Node* lhs, const Node* rhs) -> bool { + // it's a max queue so we want lhs > rhs + return lhs->topological_index() > rhs->topological_index(); + }; + std::priority_queue, decltype(comp)> queue(comp); + + // Let's start by adding all of the decisions to the queue. We go ahead an skip + // over the ones without any pending changes, though it would work just as well + // to add all of them. + // Once we track which decisions have been mutated we can be more efficient here. + for (Node* ptr : decisions_) { + if (not ptr->updated(state)) continue; + queue.emplace(ptr); + } + + // We'll want to track which nodes will eventually need a commit or revert + std::vector updated; + + while (queue.size()) { + // pull the lowest topological ordered node off the queue + const Node* ptr = queue.top(); + queue.pop(); + + ssize_t tidx = ptr->topological_index(); + state[tidx]->mark = false; + + // incorporate any changes from its predecessor(s) + ptr->propagate(state); + + // If calling propagate did nothing, then we're done + if (not ptr->updated(state)) continue; + + // Otherwise we need to eventually commit/revert this + updated.emplace_back(ptr); + + // Add all of its successors to the queue. + // In C++23 push_range() would be very helpful (and more performant) + // here. + for (const Node* successor_ptr : ptr->successors()) { + bool& seen = state[successor_ptr->topological_index()]->mark; + if (seen) continue; // already seen + seen = true; + queue.emplace(successor_ptr); + } + } + + return updated; +} + // Note: we pass the vector of changed nodes by value as we expect it to be rather small. Revisit if // it becomes too expensive. void Graph::propose( diff --git a/dwave/optimization/src/nodes/collections.cpp b/dwave/optimization/src/nodes/collections.cpp index a673f119..7299182b 100644 --- a/dwave/optimization/src/nodes/collections.cpp +++ b/dwave/optimization/src/nodes/collections.cpp @@ -464,6 +464,14 @@ void DisjointBitSetsNode::swap_between_sets( ); } +bool DisjointBitSetsNode::updated(const State& state) const { + const auto* data_ptr = data_ptr_(state); + for (const auto& diff : data_ptr->diffs) { + if (diff.size()) return true; + } + return false; +} + ssize_t DisjointBitSetsNode::get_containing_set_index(State& state, ssize_t element) const { return data_ptr_(state)->get_containing_set_index(element); } @@ -824,6 +832,14 @@ void DisjointListsNode::pop_to_list( ); } +bool DisjointListsNode::updated(const State& state) const { + const auto* data_ptr = data_ptr_(state); + for (const auto& diff : data_ptr->all_list_updates) { + if (diff.size()) return true; + } + return false; +} + DisjointListNode::DisjointListNode(DisjointListsNode* disjoint_list_node) : ArrayOutputMixin(Array::DYNAMIC_SIZE), disjoint_list_node_ptr_(disjoint_list_node), diff --git a/dwave/optimization/src/nodes/lp.cpp b/dwave/optimization/src/nodes/lp.cpp index ddf281f4..73a5a5f6 100644 --- a/dwave/optimization/src/nodes/lp.cpp +++ b/dwave/optimization/src/nodes/lp.cpp @@ -404,6 +404,10 @@ std::span LinearProgramNode::solution(const State& state) const { return data_ptr_(state)->result.solution(); } +bool LinearProgramNode::updated(const State& state) const { + return true; // TODO: think about this more +} + std::pair LinearProgramNode::variables_minmax() const { return variables_minmax_; } std::span LinearProgramNode::variables_shape() const { return c_ptr_->shape(); } diff --git a/tests/cpp/test_graph.cpp b/tests/cpp/test_graph.cpp index d1975d98..9131f43a 100644 --- a/tests/cpp/test_graph.cpp +++ b/tests/cpp/test_graph.cpp @@ -247,6 +247,24 @@ TEST_CASE("Graph::commit(), Graph::descendants(), Graph::propagate(), and Graph: auto descendants = graph.descendants({x_ptr}); CHECK_THAT(descendants, RangeEquals(std::vector{x_ptr, z_ptr})); } + + SECTION("propagate_sparse()") { + SECTION("no changes") { + auto updated = graph.propagate_sparse(state); + CHECK(updated.empty()); + } + + SECTION("one decision") { + x_ptr->flip(state, 0); + + auto updated = graph.propagate_sparse(state); + + CHECK_THAT(updated, RangeEquals(std::vector{x_ptr, z_ptr})); + } + + } + + SECTION("Propagate all") { CHECK(x_ptr->view(state).front() == 0); CHECK(y_ptr->view(state).front() == 0);