Skip to content
Draft
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
45 changes: 45 additions & 0 deletions dwave/optimization/include/dwave-optimization/cp/core/advisor.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright 2026 D-Wave Systems Inc.
//
// 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 "dwave-optimization/cp/core/index_transform.hpp"
#include "dwave-optimization/cp/core/propagator.hpp"
namespace dwave::optimization::cp {
Copy link
Member

Choose a reason for hiding this comment

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

Minor but I would prefer constraint_propagation or similar as a "top-level" name instead of cp. Abbreviating it on less public interfaces or within methods is fine.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

to be precise you mean

namespace dwave::optimization::constraint_propagation {

?

Copy link
Member

Choose a reason for hiding this comment

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

Yes. I could be convinced otherwise though!


/// Utility class to help schedule propagators when variable change their domains
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
/// Utility class to help schedule propagators when variable change their domains
/// Utility class to help schedule propagators when variables change their domains

/// In our settings variables and constraints are arrays, so we need a way to:
/// - trigger propagator indices when variable indices' domains change
/// - map variable index to propagator
class Advisor {
public:
Advisor(Propagator* p, ssize_t p_input, std::unique_ptr<IndexTransform> index_transform);
Copy link
Member

Choose a reason for hiding this comment

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

p_input is the topological index of a node?

Copy link
Member

Choose a reason for hiding this comment

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

Actually it seems like it is not used?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good catch. I added it in case some propagators need to know which variable triggered the propagator. p_input is which input of that propagator the variable is. I'd wait a bit more to remove it for now, just in case I need it soon.

Copy link
Member

Choose a reason for hiding this comment

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

Re-explaining this to make sure I get it: each propagator is associated with two or more variables (i.e. CPVars). When the propagator is called, it wants to be efficient in terms of which of its "output" indices need to be re-evaluated (e.g. shrinking bounds of an output index, if it's a bound consistency propagator). This is the role of the Advisor: each propagator adds an Advisor to each of its associated variables, and that Advisor owns an IndexTransform which translates indices of the associated variable to change output indices.

However, it also seems reasonable that a propagator may want to know not just which of its output indices needs to be re-evaluated, but also which of the associated variables that change came from. So you are allowing for that info to be passed through here.

Now correct where I went wrong 😆

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I missed this comment but you are correct!


void notify(CPPropagatorsState& p_state, ssize_t i) const;

Propagator* get_propagator() const;

ssize_t input_index() const { return p_input_; }

private:
// The propagator the advisor is watching
Propagator* p_;

// Which input
const ssize_t p_input_;

// Maps of the observed variable indices to the observed propagator indices
std::unique_ptr<IndexTransform> index_transform_;
};

} // namespace dwave::optimization::cp
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Copyright 2026 D-Wave Systems Inc.
//
// 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 "dwave-optimization/array.hpp"
#include "dwave-optimization/cp/core/cpvar.hpp"
#include "dwave-optimization/cp/core/engine.hpp"
#include "dwave-optimization/cp/core/interval_array.hpp"
#include "dwave-optimization/cp/core/propagator.hpp"
#include "dwave-optimization/cp/state/cpvar_state.hpp"
#include "dwave-optimization/graph.hpp"

namespace dwave::optimization::cp {} // namespace dwave::optimization::cp
187 changes: 187 additions & 0 deletions dwave/optimization/include/dwave-optimization/cp/core/cpvar.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
// Copyright 2026 D-Wave Systems Inc.
//
// 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 <memory>

#include "dwave-optimization/array.hpp"
#include "dwave-optimization/cp/core/advisor.hpp"
#include "dwave-optimization/cp/core/engine.hpp"
#include "dwave-optimization/cp/core/propagator.hpp"
#include "dwave-optimization/cp/core/status.hpp"
#include "dwave-optimization/cp/state/cpvar_state.hpp"
#include "dwave-optimization/graph.hpp"

namespace dwave::optimization::cp {

// forward declaration
class CPVar;

class CPModel {
public:
template <class VarType, class... Args>
VarType* emplace_variable(Args&&... args) {
static_assert(std::is_base_of_v<CPVar, VarType>);

if (locked_) {
throw std::logic_error("cannot add a variable to a locked CP model");
}

// Construct via make_unique so we can allow the constructor to throw
auto uptr = std::make_unique<VarType>(std::forward<Args&&>(args)...);
VarType* ptr = uptr.get();

// Pass ownership of the lifespan to nodes_
variables_.emplace_back(std::move(uptr));
return ptr; // return the observing pointer
}

template <class PropagatorType, class... Args>
PropagatorType* emplace_propagator(Args&&... args) {
static_assert(std::is_base_of_v<Propagator, PropagatorType>);

if (locked_) {
throw std::logic_error("cannot add a variable to a locked CP model");
}

// Construct via make_unique so we can allow the constructor to throw
auto uptr = std::make_unique<PropagatorType>(std::forward<Args&&>(args)...);
PropagatorType* ptr = uptr.get();

// Pass ownership of the lifespan to nodes_
propagators_.emplace_back(std::move(uptr));
return ptr; // return the observing pointer
}

template <class SM>
CPState initialize_state() const {
static_assert(std::is_base_of_v<StateManager, SM>);
std::unique_ptr<SM> sm = std::make_unique<SM>();
CPState state = CPState(std::move(sm), num_variables(), num_propagators());
return state;
}

ssize_t num_variables() const { return variables_.size(); }

ssize_t num_propagators() const { return propagators_.size(); }

protected:
bool locked_ = false;
Copy link
Member

Choose a reason for hiding this comment

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

Seems like locked_ is never set by anything? Am I missing a lock() method somewhere?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

good catch again, haven't worked on that yet.
And sorry for my work being somewhat nonlinear, I add stuff that I think I need but I don't complete until later..

Copy link
Member

Choose a reason for hiding this comment

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

No worries. Is the idea here to lock the CP model after parsing the dwopt model, and then we can be sure that the model is not modified during the run?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

yes, that's right!

std::vector<std::unique_ptr<Propagator>> propagators_;
std::vector<std::unique_ptr<CPVar>> variables_;
};

/// Interface for CP variables, allows query and modify their domain. It assumes a flattened array
/// of variables.
class CPVar {
public:
virtual ~CPVar() = default;

// constructor
CPVar(const CPModel& model, const dwave::optimization::ArrayNode* node_ptr, int index);
Copy link
Member

Choose a reason for hiding this comment

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

This index is the topological index of the node_ptr, correct? Why not just derive it from the node_ptr directly in the implementation?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

good point!


CPVar(const CPVar& other) = delete;
CPVar() = delete;

const CPModel& get_model() const { return model_; }

template <std::derived_from<CPVarData> StateData>
StateData* data_ptr(CPVarsState& state) const {
assert(cp_var_index_ >= 0);
return static_cast<StateData*>(state[cp_var_index_].get());
}

template <std::derived_from<CPVarData> StateData>
const StateData* data_ptr(const CPVarsState& state) const {
assert(cp_var_index_ >= 0);
return static_cast<const StateData*>(state[cp_var_index_].get());
}

// query the domains
double min(const CPVarsState& state, int index) const;
double max(const CPVarsState& state, int index) const;
double size(const CPVarsState& state, int index) const;
bool is_bound(const CPVarsState& state, int index) const;
bool contains(const CPVarsState& state, double value, int index) const;
bool is_active(const CPVarsState& state, int index) const;
bool maybe_active(const CPVarsState& state, int index) const;
ssize_t max_size(const CPVarsState& state) const;
ssize_t min_size(const CPVarsState& state) const;

ssize_t max_size() const;
ssize_t min_size() const;

// actions on the domains
CPStatus remove(CPVarsState& state, double value, int index) const;
CPStatus remove_above(CPVarsState& state, double value, int index) const;
CPStatus remove_below(CPVarsState& state, double value, int index) const;
CPStatus assign(CPVarsState& state, double value, int index) const;
CPStatus set_min_size(CPVarsState& state, int index) const;
CPStatus set_max_size(CPVarsState& state, int index) const;

// actions for the propagation engine
void schedule_all(CPState& state, const std::vector<Advisor>& advisors, ssize_t index) const;

// Note: maybe we need the state here.. especially if we want to attach propagators dynamically
// during the search...
// Another note: it is als possible to use a vector or struct where the struct holds the
// propagator and the event type (as an enum) that triggers it. Still considering what to do
// here..
void propagate_on_domain_change(Advisor&& advisor);
void propagate_on_bounds_change(Advisor&& advisor);
void propagate_on_assignment(Advisor&& advisor);

void initialize_state(CPState& state) const;

/// Note: these could be state stacks that can be updated through the search. Keeping them as
/// simple vectors (static in terms of the search for now)
// They represent the propagators to trigger when the variable gets-assigned/changes
// domain/bounds change
std::vector<Advisor> on_bind;
std::vector<Advisor> on_domain;
std::vector<Advisor> on_bounds;
std::vector<Advisor> on_array_size_change;

protected:
const CPModel& model_;

// but should I have this?
const dwave::optimization::ArrayNode* node_;
const ssize_t cp_var_index_;

class Listener : public DomainListener {
public:
Listener(const CPVar* var, CPState& state) : var_(var), state_(state) {}

// domain listener overrides

void bind(ssize_t i) override { var_->schedule_all(state_, var_->on_bind, i); }

void change(ssize_t i) override { var_->schedule_all(state_, var_->on_domain, i); }

void change_min(ssize_t i) override { var_->schedule_all(state_, var_->on_bounds, i); }

void change_max(ssize_t i) override { var_->schedule_all(state_, var_->on_bounds, i); }

void change_array_size(ssize_t i) override {
var_->schedule_all(state_, var_->on_array_size_change, i);
}

private:
const CPVar* var_;
CPState& state_;
};
};
} // namespace dwave::optimization::cp
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Copyright 2026 D-Wave Systems Inc.
//
// 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 <cstddef>

#include "dwave-optimization/common.hpp"
#include "dwave-optimization/cp/core/domain_listener.hpp"
#include "dwave-optimization/cp/core/status.hpp"

namespace dwave::optimization::cp {

/// Interface for the domains of a contiguous array of variables. It is assumed that the variables
/// are flattened and they can be optional (maybe active). This is to mimic the dynamic arrays in
/// dwave-optimization.
class DomainArray {
public:
virtual ~DomainArray() = default;

/// Return the total number of domains
virtual size_t num_domains() const = 0;

/// Return the minimum value of the index-th variable
virtual double min(int index) const = 0;

/// Return the maximum value of the index-th variable
virtual double max(int index) const = 0;

/// Return the size of the domain of the index-th variable
virtual double size(int index) const = 0;
Comment on lines +40 to +41
Copy link
Contributor

Choose a reason for hiding this comment

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

I might prefer to call this "width" (consistent with the width of a real-valued interval) so that the term "size" is not overloaded so much

Copy link
Contributor

Choose a reason for hiding this comment

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

hmm, though if we support sparse set domains, something like "cardinality" might be better, though that would not be accurate for a real-valued interval


/// Return the minimum size of the array
virtual ssize_t min_size() const = 0;

/// Return the minimum size of the array
Copy link
Contributor

Choose a reason for hiding this comment

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

maximum

virtual ssize_t max_size() const = 0;

/// Check whether the index-th variable is fixed or not
virtual bool is_bound(int index) const = 0;

/// Check whether the index-th variable contains the value
virtual bool contains(double value, int index) const = 0;

/// Check whether the index is active
virtual bool is_active(int index) const = 0;

/// Check whether the index-th variable is at least optional
virtual bool maybe_active(int index) const = 0;

/// Remove a value from the domain of the index-th variable
virtual CPStatus remove(double value, int index, DomainListener* l) = 0;

/// Remove the domain of the index-th variable above the given value
virtual CPStatus remove_above(double value, int index, DomainListener* l) = 0;

/// Remove the domain of the index-th variable below the given value
virtual CPStatus remove_below(double value, int index, DomainListener* l) = 0;

/// Remove all the domain of the index-th variable except the given value
virtual CPStatus remove_all_but(double value, int index, DomainListener* l) = 0;

/// Update the minimum size of the array to new_min_size
virtual CPStatus update_min_size(int new_min_size, DomainListener* l) = 0;

/// Update the maximum size of the array to new_max_size
virtual CPStatus update_max_size(int new_max_size, DomainListener* l) = 0;

protected:
// TODO: needed?
static constexpr double PRECISION = 1e-6;
};

} // namespace dwave::optimization::cp
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright 2026 D-Wave Systems Inc.
//
// 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

namespace dwave::optimization::cp {
class DomainListener {
public:
virtual ~DomainListener() = default;
Copy link
Member

Choose a reason for hiding this comment

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

I think if you're defining a virtual destructor like this, you should also declare the copy and move constructors to follow the rule of three, probably as = delete to prevent object slicing.

The plan is to have other types of listeners (other than CPVar::listener) at some point right?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

The point is that if there are other variables types (other than CPVar) then I may need other types of listeners. But to decouple domains from variables, the domains respond to this interface to trigger propagators.

Anyways, yes I need to follow the rule of three! I'll add that!


// TODO: check whether this should be removed or not
// virtual void empty() = 0;
virtual void bind(ssize_t i) = 0;
virtual void change(ssize_t i) = 0;
virtual void change_max(ssize_t i) = 0;
virtual void change_min(ssize_t i) = 0;
virtual void change_array_size(ssize_t i) = 0;
};
} // namespace dwave::optimization::cp
Loading