Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ environment: &global-environment
jobs:
python-linux:
docker:
- image: cimg/python:3.13 # just need a version that can install cibuildwheel
- image: cimg/python:3.13 # need a version that can install cibuildwheel and that has docker

environment:
<<: *global-environment
Expand All @@ -57,7 +57,7 @@ jobs:

python-linux-debug:
docker:
- image: cimg/python:3.10
- image: python:3.10

steps:
- checkout
Expand Down Expand Up @@ -127,7 +127,7 @@ jobs:

python-sdist:
docker:
- image: cimg/python:3.10
- image: python:3.10

steps:
- checkout
Expand Down
3 changes: 3 additions & 0 deletions dwave/optimization/include/dwave-optimization/array.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,9 @@ struct Update {
// Return true if the update does nothing - that is old and value are the same.
bool identity() const { return null() || old == value; }

// Return the update that would undo the current update
Update inverse() const { return Update(index, value, old); }

// Use NaN to represent the "nothing" value used in placements/removals
static constexpr double nothing = std::numeric_limits<double>::signaling_NaN();

Expand Down
12 changes: 12 additions & 0 deletions dwave/optimization/include/dwave-optimization/graph.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,11 @@ class Graph {
std::function<bool(const Graph&, State&)> accept = [](const Graph&, State&) { return true; }
) const;

/// Propagate any pending changes to all nodes in the graph and commit them.
void propose(State& state) const;
// dev note: the name is a bit funny in this case, but we essentially want
// an overload for a "default" `sources` and `accept`.

/// Initialize the state of the given node and all predecessors recursively.
static void recursive_initialize(State& state, const Node* ptr);
/// Reset the state of the given node and all successors recursively.
Expand Down Expand Up @@ -431,6 +436,13 @@ class DecisionNode : public Decision, public virtual Node {
/// Decision nodes by definition do not have a deterministic state.
bool deterministic_state() const final { return false; }

/// Set the current state to match the one at the time the given checkpoint was created.
virtual void assign_from_checkpoint(State& state, checkpoint_type& checkpoint) const = 0;
virtual void assign_from_checkpoint(State& state, checkpoint_type&& checkpoint) const = 0;

/// Get a checkpoint, an IOU that can be used to return the node to its current state.
virtual checkpoint_type checkpoint(State& state) const = 0;

/// 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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,15 @@ class CollectionNode : public ArrayOutputMixin<ArrayNode>, public DecisionNode {
// Set the node's state, tracking the diff.
void assign(State& state, std::vector<double> values) const;

/// @copydoc DecisionNode::assign_from_checkpoint()
void assign_from_checkpoint(State& state, checkpoint_type& checkpoint) const override;
void assign_from_checkpoint(State& state, checkpoint_type&& checkpoint) const override;

const double* buff(const State& state) const override;

/// @copydoc DecisionNode::checkpoint()
checkpoint_type checkpoint(State& state) const override;

void commit(State&) const override;

std::span<const Update> diff(const State& state) const override;
Expand Down Expand Up @@ -100,6 +107,13 @@ class DisjointBitSetsNode : public DecisionNode {
// i.e. the set `range(primary_set_size)`.
DisjointBitSetsNode(ssize_t primary_set_size, ssize_t num_disjoint_sets);

/// @copydoc DecisionNode::assign_from_checkpoint()
void assign_from_checkpoint(State& state, checkpoint_type& checkpoint) const override;
void assign_from_checkpoint(State& state, checkpoint_type&& checkpoint) const override;

/// @copydoc DecisionNode::checkpoint()
checkpoint_type checkpoint(State& state) const override;

void commit(State&) const override;

ssize_t get_containing_set_index(State& state, ssize_t element_i) const;
Expand Down Expand Up @@ -172,6 +186,13 @@ class DisjointListsNode : public DecisionNode {
// i.e. the set `range(primary_set_size)`.
DisjointListsNode(ssize_t primary_set_size, ssize_t num_disjoint_lists);

/// @copydoc DecisionNode::assign_from_checkpoint()
void assign_from_checkpoint(State& state, checkpoint_type& checkpoint) const override;
void assign_from_checkpoint(State& state, checkpoint_type&& checkpoint) const override;

/// @copydoc DecisionNode::checkpoint()
checkpoint_type checkpoint(State& state) const override;

void commit(State&) const override;

ssize_t get_disjoint_list_size(State& state, ssize_t list_index) const;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
#include <optional>
#include <ranges>
#include <span>
#include <utility>
#include <vector>

#include "dwave-optimization/array.hpp"
Expand Down Expand Up @@ -122,6 +121,9 @@ class NumberNode : public ArrayOutputMixin<ArrayNode>, public DecisionNode {

// NumberNode methods *****************************************************

/// @copydoc DecisionNode::checkpoint()
checkpoint_type checkpoint(State& state) const override;

// In the given state, swap the value of index i with the value of index j.
// Users may pass the slices (per sum constraint) that each index lies on.
void exchange(
Expand Down Expand Up @@ -290,6 +292,10 @@ class IntegerNode : public NumberNode {

// IntegerNode methods ****************************************************

/// @copydoc DecisionNode::assign_from_checkpoint()
void assign_from_checkpoint(State& state, checkpoint_type& checkpoint) const override;
void assign_from_checkpoint(State& state, checkpoint_type&& checkpoint) const override;

// Set the value at the given index in the given state.
// Users may pass the slices (per sum constraint) that each index lies on.
void set_value(
Expand Down
22 changes: 22 additions & 0 deletions dwave/optimization/include/dwave-optimization/nodes/testing.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,28 @@ class DynamicArrayTestingNode : public ArrayOutputMixin<ArrayNode>, public Decis
void revert(State&) const override;
void update(State&, int) const override;

// Overloads required by the DecisionNode ABC *****************************

// DynamicArrayTestingNode does not impement checkpointing

@arcondello arcondello Jul 27, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See #593

Also I see the typo here.

[[noreturn]] void assign_from_checkpoint(
State& state,
checkpoint_type& checkpoint
) const override {
assert(false and "not implemented");
unreachable();
}
[[noreturn]] void assign_from_checkpoint(
State& state,
checkpoint_type&& checkpoint
) const override {
assert(false and "not implemented");
unreachable();
}
[[noreturn]] virtual checkpoint_type checkpoint(State& state) const override {
assert(false and "not implemented");
unreachable();
}

// State mutation methods *************************************************

// Grow the array by a single row of the given values.
Expand Down
13 changes: 13 additions & 0 deletions dwave/optimization/include/dwave-optimization/state.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,17 @@ struct NodeStateData {

using State = typename std::vector<std::unique_ptr<NodeStateData>>;

/// A generic base class for node checkpoints.
struct NodeStateCheckpoint {
NodeStateCheckpoint() = default;
NodeStateCheckpoint(const NodeStateCheckpoint&) = delete;
NodeStateCheckpoint(NodeStateCheckpoint&&) = delete;
NodeStateCheckpoint& operator=(const NodeStateCheckpoint&) = delete;
NodeStateCheckpoint& operator=(NodeStateCheckpoint&&) = delete;

virtual ~NodeStateCheckpoint() = default;
};

using checkpoint_type = std::unique_ptr<NodeStateCheckpoint>;

} // namespace dwave::optimization
5 changes: 5 additions & 0 deletions dwave/optimization/src/graph.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,11 @@ void Graph::propose(
}
}

void Graph::propose(State& state) const {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be smarter with #589 or #592, but that's for another PR.

propagate(state);
commit(state);
}

void Graph::recursive_initialize(State& state, const Node* ptr) {
ssize_t index = ptr->topological_index();

Expand Down
93 changes: 93 additions & 0 deletions dwave/optimization/src/nodes/_checkpoints.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// Copyright 2026 D-Wave
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include "_checkpoints.hpp"

namespace dwave::optimization {

CheckpointableState::~CheckpointableState() {
if (prev_ptr_ == nullptr) return; // nothing to clean up
prev_ptr_->next_ptr_ = static_cast<CheckpointableState*>(nullptr);
Comment thread
fastbodin marked this conversation as resolved.
}

// Place self between the state and any checkpoint it's currently holding
LinkedListCheckpoint::LinkedListCheckpoint(CheckpointableState& state) :
prev_ptr_(state.prev_ptr_), next_ptr_(&state) {
if (prev_ptr_ != nullptr) prev_ptr_->next_ptr_ = this;
state.prev_ptr_ = this;
}

LinkedListCheckpoint::~LinkedListCheckpoint() {
Comment thread
fastbodin marked this conversation as resolved.
if (prev_ptr_ != nullptr) prev_ptr_->next_ptr_ = next_ptr_;

// Now make sure next_ptr is pointing to prev_ptr (which can be null)
std::visit(
[&](auto* next_ptr) -> void {
if (next_ptr == nullptr) return; // state was destructed first
next_ptr->prev_ptr_ = prev_ptr_;
},
next_ptr_
);
}

DiffCheckpoint::DiffCheckpoint(CheckpointableState& state, ssize_t drop) :
LinkedListCheckpoint(state), updates_(), drop_(drop) {}

DiffCheckpoint::DiffCheckpoint(CheckpointableState& state, std::span<const Update> diff) :
LinkedListCheckpoint(state), updates_(), drop_(diff.size()) {
if (auto* prev_ptr = static_cast<DiffCheckpoint*>(prev_ptr_)) {
prev_ptr->commit_updates(std::vector<Update>(diff.begin(), diff.end()));
assert(prev_ptr->drop_ == 0);
}
}

DiffCheckpoint::~DiffCheckpoint() {
// if we're the oldest checkpoint, just let whatever information we're
// holding get destructed with us
if (prev_ptr_ == nullptr) return;

// otherwise we need to transfer our info over
auto* prev_ptr = static_cast<DiffCheckpoint*>(prev_ptr_);
assert(prev_ptr->drop_ == 0);
for (auto& updates : updates_) prev_ptr->commit_updates(std::move(updates));
prev_ptr->drop_ = drop_;
}

void DiffCheckpoint::commit_updates(std::vector<Update> updates) {
assert(0 <= drop_ and static_cast<size_t>(drop_) <= updates.size());

if (drop_) {
updates.erase(updates.begin(), updates.begin() + drop_);
drop_ = 0;
}

updates_.emplace_back(std::move(updates));
}

void DiffCheckpoint::revert_updates(std::vector<Update> updates) {
assert(0 <= drop_ and static_cast<size_t>(drop_) <= updates.size());

if (not drop_) return; // nothing to do

// We want to track the updates that would revert the changes from the
// current state.
// In C++23 we could use assign_range() which would be nicer
auto relevant = std::move(updates) | std::views::take(drop_) | std::views::reverse |
std::views::transform([](const Update& up) { return up.inverse(); });
updates_.emplace_back(relevant.begin(), relevant.end());

drop_ = 0;
}

} // namespace dwave::optimization
98 changes: 98 additions & 0 deletions dwave/optimization/src/nodes/_checkpoints.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// Copyright 2026 D-Wave
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#pragma once

#include <span>
#include <variant>
#include <vector>

#include "dwave-optimization/array.hpp"
#include "dwave-optimization/state.hpp"

namespace dwave::optimization {

class LinkedListCheckpoint;

class CheckpointableState {
public:
CheckpointableState() = default;

CheckpointableState(const CheckpointableState&) {} // the checkpoint pointer is not copied
CheckpointableState(CheckpointableState&&) = default;

CheckpointableState& operator=(const CheckpointableState&) = delete;
CheckpointableState& operator=(CheckpointableState&&) = default;

~CheckpointableState();

protected:
template <std::derived_from<LinkedListCheckpoint> T>
T* checkpoint_ptr() {
return static_cast<T*>(prev_ptr_);
}

private: // todo: private?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hanging comment?

friend LinkedListCheckpoint;

// The name is a bit confusing, but by making it match LinkedListCheckpoint::prev_ptr_
// it makes the implementations of the various visit methods clearer.
LinkedListCheckpoint* prev_ptr_ = nullptr; // Will be nullptr if there are no checkpoints
};

class LinkedListCheckpoint : public NodeStateCheckpoint {
public:
LinkedListCheckpoint() = delete;
// We're not moveable or copyable because NodeStateCheckpoint is not.

LinkedListCheckpoint(CheckpointableState& state);

~LinkedListCheckpoint() override;

protected: // todo: private?
friend CheckpointableState;

LinkedListCheckpoint* prev_ptr_;

// Is usually not nullptr unless the state has been destructed
std::variant<LinkedListCheckpoint*, CheckpointableState*> next_ptr_;
};

class DiffCheckpoint : public LinkedListCheckpoint {
public:
DiffCheckpoint(CheckpointableState& state, std::span<const Update> diff);

~DiffCheckpoint() override;

void commit_updates(std::vector<Update> updates);

auto detach_updates() {
auto updates = std::move(updates_) | std::views::join;
assert(updates_.empty());
return updates;
}

ssize_t& drop() { return drop_; }

void revert_updates(std::vector<Update> updates);

protected:
DiffCheckpoint(CheckpointableState& state, ssize_t drop);

private:
std::vector<std::vector<Update>> updates_;
ssize_t drop_;
};

} // namespace dwave::optimization
Loading