From dc9ff2693cdf825bb73d0b993e9dcb1a959103fa Mon Sep 17 00:00:00 2001 From: azucca Date: Fri, 27 Feb 2026 15:47:50 -0800 Subject: [PATCH 01/19] Add CP code base --- .../dwave-optimization/cp/core/base_node.hpp | 11 + .../dwave-optimization/cp/core/cpvar.hpp | 154 +++++++++++ .../cp/core/domain_array.hpp | 29 ++ .../cp/core/domain_listener.hpp | 15 ++ .../dwave-optimization/cp/core/engine.hpp | 65 +++++ .../cp/core/interval_array.hpp | 39 +++ .../dwave-optimization/cp/core/propagator.hpp | 57 ++++ .../dwave-optimization/cp/core/status.hpp | 5 + .../dwave-optimization/cp/state/copier.hpp | 79 ++++++ .../dwave-optimization/cp/state/copy.hpp | 55 ++++ .../cp/state/cpvar_state.hpp | 37 +++ .../dwave-optimization/cp/state/state.hpp | 58 ++++ .../cp/state/state_entry.hpp | 10 + .../cp/state/state_manager.hpp | 20 ++ .../cp/state/state_stack.hpp | 25 ++ .../dwave-optimization/cp/state/storage.hpp | 17 ++ dwave/optimization/src/cp/core/cpvar.cpp | 80 ++++++ dwave/optimization/src/cp/core/engine.cpp | 40 +++ .../src/cp/core/interval_array.cpp | 255 ++++++++++++++++++ dwave/optimization/src/cp/core/propagator.cpp | 42 +++ dwave/optimization/src/cp/state/copier.cpp | 48 ++++ dwave/optimization/src/cp/state/copy.cpp | 14 + .../optimization/src/cp/state/cpvar_state.cpp | 49 ++++ .../optimization/src/cp/state/state_stack.cpp | 36 +++ meson.build | 11 + tests/cpp/cp/test_copier.cpp | 97 +++++++ tests/cpp/cp/test_cp_var.cpp | 51 ++++ tests/cpp/cp/test_interval.cpp | 119 ++++++++ tests/cpp/cp/utils.hpp | 15 ++ tests/cpp/meson.build | 5 + 30 files changed, 1538 insertions(+) create mode 100644 dwave/optimization/include/dwave-optimization/cp/core/base_node.hpp create mode 100644 dwave/optimization/include/dwave-optimization/cp/core/cpvar.hpp create mode 100644 dwave/optimization/include/dwave-optimization/cp/core/domain_array.hpp create mode 100644 dwave/optimization/include/dwave-optimization/cp/core/domain_listener.hpp create mode 100644 dwave/optimization/include/dwave-optimization/cp/core/engine.hpp create mode 100644 dwave/optimization/include/dwave-optimization/cp/core/interval_array.hpp create mode 100644 dwave/optimization/include/dwave-optimization/cp/core/propagator.hpp create mode 100644 dwave/optimization/include/dwave-optimization/cp/core/status.hpp create mode 100644 dwave/optimization/include/dwave-optimization/cp/state/copier.hpp create mode 100644 dwave/optimization/include/dwave-optimization/cp/state/copy.hpp create mode 100644 dwave/optimization/include/dwave-optimization/cp/state/cpvar_state.hpp create mode 100644 dwave/optimization/include/dwave-optimization/cp/state/state.hpp create mode 100644 dwave/optimization/include/dwave-optimization/cp/state/state_entry.hpp create mode 100644 dwave/optimization/include/dwave-optimization/cp/state/state_manager.hpp create mode 100644 dwave/optimization/include/dwave-optimization/cp/state/state_stack.hpp create mode 100644 dwave/optimization/include/dwave-optimization/cp/state/storage.hpp create mode 100644 dwave/optimization/src/cp/core/cpvar.cpp create mode 100644 dwave/optimization/src/cp/core/engine.cpp create mode 100644 dwave/optimization/src/cp/core/interval_array.cpp create mode 100644 dwave/optimization/src/cp/core/propagator.cpp create mode 100644 dwave/optimization/src/cp/state/copier.cpp create mode 100644 dwave/optimization/src/cp/state/copy.cpp create mode 100644 dwave/optimization/src/cp/state/cpvar_state.cpp create mode 100644 dwave/optimization/src/cp/state/state_stack.cpp create mode 100644 tests/cpp/cp/test_copier.cpp create mode 100644 tests/cpp/cp/test_cp_var.cpp create mode 100644 tests/cpp/cp/test_interval.cpp create mode 100644 tests/cpp/cp/utils.hpp diff --git a/dwave/optimization/include/dwave-optimization/cp/core/base_node.hpp b/dwave/optimization/include/dwave-optimization/cp/core/base_node.hpp new file mode 100644 index 00000000..52b42fe6 --- /dev/null +++ b/dwave/optimization/include/dwave-optimization/cp/core/base_node.hpp @@ -0,0 +1,11 @@ +#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 diff --git a/dwave/optimization/include/dwave-optimization/cp/core/cpvar.hpp b/dwave/optimization/include/dwave-optimization/cp/core/cpvar.hpp new file mode 100644 index 00000000..6b3f895a --- /dev/null +++ b/dwave/optimization/include/dwave-optimization/cp/core/cpvar.hpp @@ -0,0 +1,154 @@ + +#pragma once + +#include + +#include "dwave-optimization/array.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 + VarType* emplace_variable(Args&&... args) { + static_assert(std::is_base_of_v); + + 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(std::forward(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 + PropagatorType* emplace_propagator(Args&&... args) { + static_assert(std::is_base_of_v); + + 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(std::forward(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 + CPState initialize_state() const { + static_assert(std::is_base_of_v); + std::unique_ptr sm = std::make_unique(); + 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; + std::vector> propagators_; + std::vector> 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); + + const CPModel& get_model() const { return model_; } + + template StateData> + StateData* data_ptr(CPVarsState& state) const { + assert(cp_var_index_ >= 0); + return static_cast(state[cp_var_index_].get()); + } + + template StateData> + const StateData* data_ptr(const CPVarsState& state) const { + assert(cp_var_index_ >= 0); + return static_cast(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; + + // 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; + + // actions for the propagation engine + void schedule_all(CPState& state, const std::vector& propagators) const; + + // Note: maybe we need the state here.. especially if we want to attach propagators dynamically + // during the search... + void propagate_on_domain_change(Propagator* p); + void propagate_on_bounds_change(Propagator* p); + void propagate_on_assignment(Propagator* p); + + 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 on_bind; + std::vector on_domain; + std::vector on_bounds; + + 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() override { var_->schedule_all(state_, var_->on_bind); } + + void change() override { var_->schedule_all(state_, var_->on_domain); } + + void change_min() override { var_->schedule_all(state_, var_->on_bounds); } + + void change_max() override { var_->schedule_all(state_, var_->on_bounds); } + + // void empty() override { return CPStatus::Inconsistency; } + + private: + const CPVar* var_; + CPState& state_; + }; +}; +} // namespace dwave::optimization::cp diff --git a/dwave/optimization/include/dwave-optimization/cp/core/domain_array.hpp b/dwave/optimization/include/dwave-optimization/cp/core/domain_array.hpp new file mode 100644 index 00000000..1d8753c4 --- /dev/null +++ b/dwave/optimization/include/dwave-optimization/cp/core/domain_array.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include "dwave-optimization/cp/core/domain_listener.hpp" +#include "dwave-optimization/cp/core/status.hpp" + +namespace dwave::optimization::cp { +class DomainArray { + public: + virtual ~DomainArray() = default; + + virtual size_t num_domains() const = 0; + + virtual double min(int index) const = 0; + virtual double max(int index) const = 0; + virtual double size(int index) const = 0; + virtual bool is_bound(int index) const = 0; + virtual bool contains(double value, int index) const = 0; + + virtual CPStatus remove(double value, int index, DomainListener* l) = 0; + virtual CPStatus remove_above(double value, int index, DomainListener* l) = 0; + virtual CPStatus remove_below(double value, int index, DomainListener* l) = 0; + virtual CPStatus remove_all_but(double value, int index, DomainListener* l) = 0; + + protected: + // TODO: needed? + static constexpr double PRECISION = 1e-6; +}; + +} // namespace dwave::optimization::cp diff --git a/dwave/optimization/include/dwave-optimization/cp/core/domain_listener.hpp b/dwave/optimization/include/dwave-optimization/cp/core/domain_listener.hpp new file mode 100644 index 00000000..7731325f --- /dev/null +++ b/dwave/optimization/include/dwave-optimization/cp/core/domain_listener.hpp @@ -0,0 +1,15 @@ +#pragma once + +namespace dwave::optimization::cp { +class DomainListener { + public: + virtual ~DomainListener() = default; + + // TODO: check whether this should be removed or not + // virtual void empty() = 0; + virtual void bind() = 0; + virtual void change() = 0; + virtual void change_max() = 0; + virtual void change_min() = 0; +}; +} // namespace dwave::optimization::cp diff --git a/dwave/optimization/include/dwave-optimization/cp/core/engine.hpp b/dwave/optimization/include/dwave-optimization/cp/core/engine.hpp new file mode 100644 index 00000000..487eff34 --- /dev/null +++ b/dwave/optimization/include/dwave-optimization/cp/core/engine.hpp @@ -0,0 +1,65 @@ +#pragma once + +#include +#include + +#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/cp/state/state_manager.hpp" + +namespace dwave::optimization::cp { + +// forward declaration +class CPVar; +class CPState; + +class CPEngine { + public: + CPStatus fix_point(CPState& state) const; + CPStatus propagate(CPState& state, Propagator* p) const; + StateManager* get_state_manager(const CPState& state) const; +}; + +// Class that holds all the data used during the CP search +class CPState { + friend CPEngine; + friend CPVar; + + public: + CPState(std::unique_ptr sm, ssize_t num_variables, ssize_t num_propagators) + : sm_(std::move(sm)) { + var_state_.resize(num_variables); + propagator_state_.resize(num_propagators); + } + + void schedule(Propagator* p) { + if (p->active(propagator_state_) and not p->scheduled(propagator_state_)) { + p->set_scheduled(propagator_state_, true); + propagation_queue_.push_back(p); + } + } + + StateManager* get_state_manager() const { return sm_.get(); } + + const CPVarsState& get_variables_state() const { return var_state_; } + CPVarsState& get_variables_state() { return var_state_; } + + const CPPropagatorsState& get_propagators_state() const { return propagator_state_; } + CPPropagatorsState& get_propagators_state() { return propagator_state_; } + + protected: + // Manager that handles the backtracking stack + std::unique_ptr sm_; + + // Internal state for the CP variables + CPVarsState var_state_; + + // Internal state for the Propagators + CPPropagatorsState propagator_state_; + + // Propagation queue + std::deque propagation_queue_; +}; + +} // namespace dwave::optimization::cp diff --git a/dwave/optimization/include/dwave-optimization/cp/core/interval_array.hpp b/dwave/optimization/include/dwave-optimization/cp/core/interval_array.hpp new file mode 100644 index 00000000..6d1a5f64 --- /dev/null +++ b/dwave/optimization/include/dwave-optimization/cp/core/interval_array.hpp @@ -0,0 +1,39 @@ +#pragma once +#include + +#include "dwave-optimization/cp/core/domain_array.hpp" +#include "dwave-optimization/cp/state/state.hpp" +#include "dwave-optimization/cp/state/state_manager.hpp" + +namespace dwave::optimization::cp { + +template +class IntervalArray : public DomainArray { + public: + IntervalArray(StateManager* sm, ssize_t size); + IntervalArray(StateManager* sm, ssize_t size, double lb, double up); + IntervalArray(StateManager* sm, std::vector lb, std::vector ub); + + size_t num_domains() const override { return min_.size(); } + double min(int index) const override; + double max(int index) const override; + double size(int index) const override; + bool is_bound(int index) const override; + bool contains(double value, int index) const override; + + CPStatus remove(double value, int index, DomainListener* l) override; + CPStatus remove_above(double value, int index, DomainListener* l) override; + CPStatus remove_below(double value, int index, DomainListener* l) override; + CPStatus remove_all_but(double value, int index, DomainListener* l) override; + + private: + // Change double do an object that can be backtracked. + // And maybe get + std::vector*> min_; + std::vector*> max_; +}; + +using IntIntervalArray = IntervalArray; +using RealIntervalArray = IntervalArray; + +} // namespace dwave::optimization::cp diff --git a/dwave/optimization/include/dwave-optimization/cp/core/propagator.hpp b/dwave/optimization/include/dwave-optimization/cp/core/propagator.hpp new file mode 100644 index 00000000..b5bd55bb --- /dev/null +++ b/dwave/optimization/include/dwave-optimization/cp/core/propagator.hpp @@ -0,0 +1,57 @@ +#pragma once + +#include +#include + +//#include "dwave-optimization/cp/core/status.hpp" +#include "dwave-optimization/cp/state/cpvar_state.hpp" +#include "dwave-optimization/cp/state/state.hpp" + +namespace dwave::optimization::cp { + +// internal structure of propagators +class PropagatorData { + public: + virtual ~PropagatorData() = default; + bool scheduled() const { return scheduled_; } + void set_scheduled(bool scheduled) { scheduled_ = scheduled; } + bool active() const { return active_->get_value(); } + void set_active(bool active) { active_->set_value(active); } + + // indices of the constraint (corresponding to this propagator) to propagate. + std::vector indices; + + protected: + bool scheduled_; + StateBool* active_; +}; + +using CPPropagatorsState = std::vector>; + +class Propagator { + public: + virtual ~Propagator() = default; + + Propagator(int index) : propagator_index_(index) {} + + template PData> + PData* data_ptr(CPPropagatorsState& state) const; + + template PData> + const PData* data_ptr(const CPPropagatorsState& state) const; + + bool scheduled(const CPPropagatorsState& state) const; + + void set_scheduled(CPPropagatorsState& state, bool scheduled) const; + + bool active(const CPPropagatorsState& state) const; + + bool set_active(CPPropagatorsState& state, bool active) const; + + virtual CPStatus propagate(CPPropagatorsState& p_state, CPVarsState& v_state) = 0; + + private: + const ssize_t propagator_index_; +}; + +} // namespace dwave::optimization::cp diff --git a/dwave/optimization/include/dwave-optimization/cp/core/status.hpp b/dwave/optimization/include/dwave-optimization/cp/core/status.hpp new file mode 100644 index 00000000..3b494458 --- /dev/null +++ b/dwave/optimization/include/dwave-optimization/cp/core/status.hpp @@ -0,0 +1,5 @@ +#pragma once + +namespace dwave::optimization::cp { +enum CPStatus { OK, Inconsistency, Complete }; +} // namespace dwave::optimization::cp diff --git a/dwave/optimization/include/dwave-optimization/cp/state/copier.hpp b/dwave/optimization/include/dwave-optimization/cp/state/copier.hpp new file mode 100644 index 00000000..1f587b74 --- /dev/null +++ b/dwave/optimization/include/dwave-optimization/cp/state/copier.hpp @@ -0,0 +1,79 @@ +#pragma once + +#include +#include +#include + +#include "dwave-optimization/cp/state/state.hpp" +#include "dwave-optimization/cp/state/state_entry.hpp" +#include "dwave-optimization/cp/state/state_manager.hpp" +#include "dwave-optimization/cp/state/storage.hpp" + +namespace dwave::optimization::cp { +class Copier : public StateManager { + private: + class Backup { + private: + Copier& copier_; + ssize_t size_; + std::vector> backup_store_; + + public: + Backup(Copier& outer) : copier_(outer) { + size_ = copier_.store.size(); + for (const auto& st_ptr : copier_.store) { + backup_store_.emplace_back(std::move(st_ptr->save())); + } + } + + void restore() { + // Safety assertion + assert(static_cast(copier_.store.size()) >= size_); + + copier_.store.resize(size_); + for (const auto& cse_ptr : backup_store_) { + cse_ptr->restore(); + } + } + }; + + public: + // We store pointers of storage because we inherit from that class. + // also I give the state manager the ownership of the state.. + // might need to make sure that it is the correct way + std::vector> store; + std::vector prior; + std::vector> on_restore_listeners; + + Copier() = default; + + /// state manager overrides + + int get_level() override; + + void restore_state() override; + + void save_state() override; + + void restore_state_until(int level) override; + + void with_new_state(std::function body) override; + + // TODO: the next two methods have a dynamic cast that doesn't make me really comfortable + StateInt* make_state_int(int init_value) override; + + StateBool* make_state_bool(bool init_value) override; + + StateReal* make_state_real(double init_value) override; + + /// other methods + + int store_size() { return store.size(); } + + void on_restore(std::function listener) { on_restore_listeners.push_back(listener); } + + private: + void notify_restore(); +}; + +} // namespace dwave::optimization::cp diff --git a/dwave/optimization/include/dwave-optimization/cp/state/copy.hpp b/dwave/optimization/include/dwave-optimization/cp/state/copy.hpp new file mode 100644 index 00000000..81a53503 --- /dev/null +++ b/dwave/optimization/include/dwave-optimization/cp/state/copy.hpp @@ -0,0 +1,55 @@ +#pragma once +#include + +#include "dwave-optimization/cp/state/state.hpp" +#include "dwave-optimization/cp/state/state_entry.hpp" +#include "dwave-optimization/cp/state/storage.hpp" + +namespace dwave::optimization::cp { +template +class Copy : virtual public State, public Storage { + protected: + class CopyStateEntry : public StateEntry { + private: + T v_; + Copy* copy_; + + public: + CopyStateEntry(T v, Copy* copy) : v_(v), copy_(copy) {} + void restore() override { copy_->set_value(v_); } + }; + + public: + ~Copy() = default; + + // override of storage + + virtual std::unique_ptr save() override; +}; + +class CopyInt : virtual public StateInt, virtual public Copy { + public: + // constructor + CopyInt(int initial) : StateInt(initial) {} + using StateInt::decrement; + using StateInt::get_value; + using StateInt::increment; + using StateInt::set_value; +}; + +class CopyBool : virtual public StateBool, virtual public Copy { + public: + CopyBool(bool initial) : StateBool(initial) {} + using StateBool::get_value; + using StateBool::set_value; +}; + +class CopyReal : virtual public StateReal, virtual public Copy { + public: + CopyReal(double initial) : StateReal(initial) {} + + using StateReal::get_value; + using StateReal::set_value; +}; + +} // namespace dwave::optimization::cp diff --git a/dwave/optimization/include/dwave-optimization/cp/state/cpvar_state.hpp b/dwave/optimization/include/dwave-optimization/cp/state/cpvar_state.hpp new file mode 100644 index 00000000..869a3ac6 --- /dev/null +++ b/dwave/optimization/include/dwave-optimization/cp/state/cpvar_state.hpp @@ -0,0 +1,37 @@ +#pragma once +#include +#include + +#include "dwave-optimization/cp/core/domain_array.hpp" +#include "dwave-optimization/cp/state/state_manager.hpp" + +namespace dwave::optimization::cp { + +class CPVarData { + public: + CPVarData(StateManager* sm, ssize_t size, double lb, double ub, + std::unique_ptr listener, bool integral); + + // actions on the underlying domain + size_t num_domains() const; + + double min(int index) const; + double max(int index) const; + double size(int index) const; + bool is_bound(int index) const; + bool contains(double value, int index) const; + + CPStatus remove(double value, int index); + CPStatus remove_above(double value, int index); + CPStatus remove_below(double value, int index); + CPStatus remove_all_but(double value, int index); + + protected: + // keeping it as a unique pointer in case we wanna have different types of domains.. + std::unique_ptr domains_; + std::unique_ptr listen_; +}; + +using CPVarsState = std::vector>; + +} // namespace dwave::optimization::cp diff --git a/dwave/optimization/include/dwave-optimization/cp/state/state.hpp b/dwave/optimization/include/dwave-optimization/cp/state/state.hpp new file mode 100644 index 00000000..fc1eaf6c --- /dev/null +++ b/dwave/optimization/include/dwave-optimization/cp/state/state.hpp @@ -0,0 +1,58 @@ +#pragma once + +#include +#include +#include +#include + +namespace dwave::optimization::cp { +template +class State { + public: + virtual ~State() = default; + + virtual T get_value() = 0; + virtual void set_value(T v) = 0; +}; + +class StateInt : virtual public State { + public: + StateInt() : v_(0) {} + StateInt(int value) : v_(value) {} + + int64_t get_value() override { return v_; } + void set_value(int64_t value) override { v_ = value; } + + virtual void increment() { v_++; } + virtual void decrement() { v_--; } + + protected: + int v_; +}; + +class StateBool : virtual public State { + public: + StateBool() : v_(false) {} + StateBool(bool value) : v_(value) {} + + bool get_value() override { return v_; } + void set_value(bool value) override { v_ = value; } + + protected: + bool v_; +}; + +class StateReal : virtual public State { + public: + StateReal() : v_(0.0) {} + + StateReal(double value) : v_(value) {} + + double get_value() override { return v_; } + void set_value(double value) override { v_ = value; } + + protected: + double v_; +}; + +} // namespace dwave::optimization::cp diff --git a/dwave/optimization/include/dwave-optimization/cp/state/state_entry.hpp b/dwave/optimization/include/dwave-optimization/cp/state/state_entry.hpp new file mode 100644 index 00000000..392f71d0 --- /dev/null +++ b/dwave/optimization/include/dwave-optimization/cp/state/state_entry.hpp @@ -0,0 +1,10 @@ +#pragma once + +namespace dwave::optimization::cp { +class StateEntry { + public: + virtual ~StateEntry() = default; + virtual void restore() = 0; +}; + +} // namespace dwave::optimization::cp \ No newline at end of file diff --git a/dwave/optimization/include/dwave-optimization/cp/state/state_manager.hpp b/dwave/optimization/include/dwave-optimization/cp/state/state_manager.hpp new file mode 100644 index 00000000..86d7d56f --- /dev/null +++ b/dwave/optimization/include/dwave-optimization/cp/state/state_manager.hpp @@ -0,0 +1,20 @@ +#pragma once +#include + +#include "dwave-optimization/cp/state/state.hpp" + +namespace dwave::optimization::cp { +class StateManager { + public: + virtual ~StateManager() = default; + + virtual int get_level() = 0; + virtual void with_new_state(std::function body) = 0; + virtual void save_state() = 0; + virtual void restore_state() = 0; + virtual void restore_state_until(int level) = 0; + virtual StateInt* make_state_int(int init_value) = 0; + virtual StateBool* make_state_bool(bool init_value) = 0; + virtual StateReal* make_state_real(double init_value) = 0; +}; +} // namespace dwave::optimization::cp diff --git a/dwave/optimization/include/dwave-optimization/cp/state/state_stack.hpp b/dwave/optimization/include/dwave-optimization/cp/state/state_stack.hpp new file mode 100644 index 00000000..cb6f6005 --- /dev/null +++ b/dwave/optimization/include/dwave-optimization/cp/state/state_stack.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include "dwave-optimization/cp/state/state.hpp" +#include "dwave-optimization/cp/state/state_manager.hpp" + +namespace dwave::optimization::cp { +template +class StateStack { + public: + // constructor + StateStack(StateManager* sm); + + void push(T* elem); + + int size() const; + T* get(int index) const; + + protected: + // ownership of the objects T is shared between the objects owning the stack_ + // std::vector> stack_; + std::vector stack_; + // the size_ int is owned by the state manager + StateInt* size_; +}; +} // namespace dwave::optimization::cp diff --git a/dwave/optimization/include/dwave-optimization/cp/state/storage.hpp b/dwave/optimization/include/dwave-optimization/cp/state/storage.hpp new file mode 100644 index 00000000..016feb40 --- /dev/null +++ b/dwave/optimization/include/dwave-optimization/cp/state/storage.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include + +#include "dwave-optimization/cp/state/state_entry.hpp" + +namespace dwave::optimization::cp { +class Storage { + public: + // constructor should be default + // destructor + virtual ~Storage() = default; + + virtual std::unique_ptr save() = 0; +}; + +} // namespace dwave::optimization::cp diff --git a/dwave/optimization/src/cp/core/cpvar.cpp b/dwave/optimization/src/cp/core/cpvar.cpp new file mode 100644 index 00000000..4baacbb3 --- /dev/null +++ b/dwave/optimization/src/cp/core/cpvar.cpp @@ -0,0 +1,80 @@ +#include "dwave-optimization/cp/core/cpvar.hpp" + +#include +namespace dwave::optimization::cp { + +CPVar::CPVar(const CPModel& model, const dwave::optimization::ArrayNode* node_ptr, int index) + : model_(model), node_(node_ptr), cp_var_index_(index) {} + +// ------ CPVar ------- + +double CPVar::min(const CPVarsState& state, int index) const { + const CPVarData* data = data_ptr(state); + return data->min(index); +} + +double CPVar::max(const CPVarsState& state, int index) const { + const CPVarData* data = data_ptr(state); + return data->max(index); +} + +double CPVar::size(const CPVarsState& state, int index) const { + const CPVarData* data = data_ptr(state); + return data->size(index); +} + +bool CPVar::is_bound(const CPVarsState& state, int index) const { + const CPVarData* data = data_ptr(state); + return data->is_bound(index); +} + +bool CPVar::contains(const CPVarsState& state, double value, int index) const { + const CPVarData* data = data_ptr(state); + return data->contains(value, index); +} + +CPStatus CPVar::remove(CPVarsState& state, double value, int index) const { + CPVarData* data = data_ptr(state); + return data->remove(value, index); +} + +CPStatus CPVar::remove_above(CPVarsState& state, double value, int index) const { + CPVarData* data = data_ptr(state); + return data->remove_above(value, index); +} + +CPStatus CPVar::remove_below(CPVarsState& state, double value, int index) const { + CPVarData* data = data_ptr(state); + return data->remove_below(value, index); +} + +CPStatus CPVar::assign(CPVarsState& state, double value, int index) const { + CPVarData* data = data_ptr(state); + return data->remove_all_but(value, index); +} + +void CPVar::schedule_all(CPState& state, const std::vector& propagators) const { + for (auto p : propagators) { + state.schedule(p); + } +} + +void CPVar::propagate_on_assignment(Propagator* p) { on_bind.push_back(p); } + +void CPVar::propagate_on_bounds_change(Propagator* p) { on_bounds.push_back(p); } + +void CPVar::propagate_on_domain_change(Propagator* p) { on_domain.push_back(p); } + +void CPVar::initialize_state(CPState& state) const { + assert(this->cp_var_index_ >= 0); + assert(state.var_state_.size() > cp_var_index_); + + // TODO: for now we don't handle dynamically sized nodes + assert(node_->size() > 0); + + state.var_state_[cp_var_index_] = std::make_unique( + state.get_state_manager(), node_->size(), node_->min(), node_->max(), + std::make_unique(this, state), node_->integral()); +} + +} // namespace dwave::optimization::cp diff --git a/dwave/optimization/src/cp/core/engine.cpp b/dwave/optimization/src/cp/core/engine.cpp new file mode 100644 index 00000000..1f142286 --- /dev/null +++ b/dwave/optimization/src/cp/core/engine.cpp @@ -0,0 +1,40 @@ +#include "dwave-optimization/cp/core/engine.hpp" + +namespace dwave::optimization::cp { + +CPStatus CPEngine::fix_point(CPState& state) const { + CPStatus status = CPStatus::OK; + + CPPropagatorsState& p_state = state.propagator_state_; + CPVarsState& v_state = state.var_state_; + + while (state.propagation_queue_.size() > 0) { + Propagator* p = state.propagation_queue_.front(); + status = this->propagate(state, p); + + if (status == CPStatus::Inconsistency) { + while (state.propagation_queue_.size() > 0) { + Propagator* p = state.propagation_queue_.front(); + // state.propagation_queue_.front()->set_scheduled(state, false); + state.propagation_queue_.pop_front(); + } + + // inconsistency detected, return! + return status; + } + } + + return status; +} + +CPStatus CPEngine::propagate(CPState& state, Propagator* p) const { + auto& p_state = state.propagator_state_; + auto& v_state = state.var_state_; + p->propagate(p_state, v_state); +} + +StateManager* CPEngine::get_state_manager(const CPState& state) const { + return state.get_state_manager(); +} + +} // namespace dwave::optimization::cp diff --git a/dwave/optimization/src/cp/core/interval_array.cpp b/dwave/optimization/src/cp/core/interval_array.cpp new file mode 100644 index 00000000..cae19f0d --- /dev/null +++ b/dwave/optimization/src/cp/core/interval_array.cpp @@ -0,0 +1,255 @@ +#include "dwave-optimization/cp/core/interval_array.hpp" + +#include +#include +#include +#include + +namespace dwave::optimization::cp { +template <> +IntervalArray::IntervalArray(StateManager* sm, ssize_t size) { + min_.resize(size); + max_.resize(size); + for (size_t i = 0; i < min_.size(); ++i) { + min_[i] = sm->make_state_real(-std::numeric_limits::max() / 2); + max_[i] = sm->make_state_real(std::numeric_limits::max() / 2); + } +} + +template <> +IntervalArray::IntervalArray(StateManager* sm, ssize_t size) { + min_.resize(size, sm->make_state_int(0)); + max_.resize(size, sm->make_state_int((int64_t)1 << 51)); + + min_.resize(size); + max_.resize(size); + for (size_t i = 0; i < min_.size(); ++i) { + min_[i] = sm->make_state_int(0); + max_[i] = sm->make_state_int((int64_t)1 << 51); + } +} + +template <> +IntervalArray::IntervalArray(StateManager* sm, ssize_t size, double lb, double ub) { + if (lb > ub) { + throw std::invalid_argument("lower bound larger than upper bound"); + } + + min_.resize(size); + max_.resize(size); + + for (size_t i = 0; i < min_.size(); ++i) { + min_[i] = sm->make_state_real(lb); + max_[i] = sm->make_state_real(ub); + } +} + +template <> +IntervalArray::IntervalArray(StateManager* sm, ssize_t size, double lb, double ub) { + if (lb < std::numeric_limits::min()) + throw std::invalid_argument("lower bound too small for int64_t"); + if (ub > std::numeric_limits::max()) + throw std::invalid_argument("upper bound too big for int64_t"); + + if (std::ceil(lb) > std::floor(ub)) { + throw std::invalid_argument("lower bound larger than upper bound"); + } + + min_.resize(size); + max_.resize(size); + + for (size_t i = 0; i < min_.size(); ++i) { + min_[i] = sm->make_state_int(std::ceil(lb)); + max_[i] = sm->make_state_int(std::floor(ub)); + } +} + +template <> +IntervalArray::IntervalArray(StateManager* sm, std::vector lb, + std::vector ub) { + if (lb.size() != ub.size()) { + throw std::invalid_argument("lower bounds and upper bounds have different sizes"); + } + + min_.resize(lb.size()); + max_.resize(ub.size()); + for (size_t i = 0; i < lb.size(); ++i) { + if (lb[i] > ub[i]) { + throw std::invalid_argument("lower bound larger than upper bound"); + } + + min_[i] = sm->make_state_real(lb[i]); + max_[i] = sm->make_state_real(ub[i]); + } +} + +template <> +IntervalArray::IntervalArray(StateManager* sm, std::vector lb, + std::vector ub) { + if (lb.size() != ub.size()) { + throw std::invalid_argument("lower bounds and upper bounds have different sizes"); + } + + min_.resize(lb.size()); + max_.resize(ub.size()); + + for (size_t i = 0; i < lb.size(); ++i) { + if (lb[i] < std::numeric_limits::min()) + throw std::invalid_argument("lower bound too small for int64_t"); + if (ub[i] > std::numeric_limits::max()) + throw std::invalid_argument("upper bound too big for int64_t"); + if (lb[i] > ub[i]) throw std::invalid_argument("lower bound larger than upper bound"); + min_[i] = sm->make_state_int(std::ceil(lb[i])); + max_[i] = sm->make_state_int(std::floor(ub[i])); + } +} + +template +double IntervalArray::min(int index) const { + assert(index < min_.size()); + return min_[index]->get_value(); +} + +template +double IntervalArray::max(int index) const { + assert(index < max_.size()); + return max_[index]->get_value(); +} + +template <> +double IntervalArray::size(int index) const { + assert(index < min_.size()); + return max_[index]->get_value() - min_[index]->get_value(); +} + +template <> +double IntervalArray::size(int index) const { + assert(index < min_.size()); + return max_[index]->get_value() - min_[index]->get_value() + 1; +} + +template <> +bool IntervalArray::is_bound(int index) const { + return (this->size(index) == 0); +} + +template <> +bool IntervalArray::is_bound(int index) const { + return (this->size(index) == 1); +} + +template <> +bool IntervalArray::contains(double value, int index) const { + if (value > max_[index]->get_value()) return false; + if (value < min_[index]->get_value()) return false; + return true; +} + +template <> +bool IntervalArray::contains(double value, int index) const { + if (value > max_[index]->get_value()) return false; + if (value < min_[index]->get_value()) return false; + if (std::floor(value) != std::ceil(value)) return false; + return true; +} + +template <> +CPStatus IntervalArray::remove(double value, int index, DomainListener* l) { + // can't really remove a double, even from the boundary + return CPStatus::OK; +} + +template <> +CPStatus IntervalArray::remove(double value, int index, DomainListener* l) { + // can only remove from the boundary + if (this->contains(value, index)) { + // if there's only this value, then domain is wiped out + if (this->is_bound(index)) return CPStatus::Inconsistency; + + bool change_max = value == max_[index]->get_value(); + bool change_min = value == min_[index]->get_value(); + + if (change_max) { + max_[index]->set_value(value - 1); + l->change_max(); + } else if (change_min) { + min_[index]->set_value(value + 1); + l->change_min(); + } + } + return CPStatus::OK; +} + +template <> +CPStatus IntervalArray::remove_above(double value, int index, DomainListener* l) { + // wipe-out all domain + if (min_[index]->get_value() > value) return CPStatus::Inconsistency; + + // nothing to do + if (max_[index]->get_value() <= value) return CPStatus::OK; + + max_[index]->set_value(value); + l->change_max(); + return CPStatus::OK; +} + +template <> +CPStatus IntervalArray::remove_above(double value, int index, DomainListener* l) { + // wipe-out all domain + if (min_[index]->get_value() > value) return CPStatus::Inconsistency; + + // nothing to do + if (max_[index]->get_value() <= value) return CPStatus::OK; + + max_[index]->set_value(std::floor(value)); + l->change_max(); + return CPStatus::OK; +} + +template <> +CPStatus IntervalArray::remove_below(double value, int index, DomainListener* l) { + // wipe-out all domain + if (max_[index]->get_value() < value) return CPStatus::Inconsistency; + + // nothing to do + if (min_[index]->get_value() >= value) return CPStatus::OK; + + min_[index]->set_value(value); + l->change_min(); + return CPStatus::OK; +} + +template <> +CPStatus IntervalArray::remove_below(double value, int index, DomainListener* l) { + // wipe-out all domain + if (max_[index]->get_value() < value) return CPStatus::Inconsistency; + + // nothing to do + if (min_[index]->get_value() >= value) return CPStatus::OK; + + min_[index]->set_value(std::ceil(value)); + l->change_min(); + return CPStatus::OK; +} + +template +CPStatus IntervalArray::remove_all_but(double value, int index, DomainListener* l) { + // if the value is not contained, wipe-out + if (not this->contains(value, index)) return CPStatus::Inconsistency; + + bool changed_min = value = min_[index]->get_value(); + bool changed_max = value = max_[index]->get_value(); + + min_[index]->set_value(value); + max_[index]->set_value(value); + + if (changed_max) l->change_max(); + if (changed_min) l->change_min(); + + return CPStatus::OK; +} + +template class IntervalArray; +template class IntervalArray; + +} // namespace dwave::optimization::cp diff --git a/dwave/optimization/src/cp/core/propagator.cpp b/dwave/optimization/src/cp/core/propagator.cpp new file mode 100644 index 00000000..5ebf9a73 --- /dev/null +++ b/dwave/optimization/src/cp/core/propagator.cpp @@ -0,0 +1,42 @@ +#include "dwave-optimization/cp/core/propagator.hpp" + +namespace dwave::optimization::cp { + +template PData> +PData* Propagator::data_ptr(CPPropagatorsState& state) const { + assert(propagator_index_ >= 0); + assert(propagator_index_ < state.size()); + return static_cast(state[propagator_index_].get()); +} + +template PData> +const PData* Propagator::data_ptr(const CPPropagatorsState& state) const { + assert(propagator_index_ >= 0); + assert(propagator_index_ < state.size()); + return static_cast(state[propagator_index_].get()); +} + +bool Propagator::scheduled(const CPPropagatorsState& state) const { + assert(propagator_index_ >= 0); + assert(propagator_index_ < state.size()); + return state[propagator_index_]->scheduled(); +} + +void Propagator::set_scheduled(CPPropagatorsState& state, bool scheduled) const { + assert(propagator_index_ >= 0); + assert(propagator_index_ < state.size()); + state[propagator_index_]->set_scheduled(scheduled); +} + +bool Propagator::active(const CPPropagatorsState& state) const { + assert(propagator_index_ >= 0); + assert(propagator_index_ < state.size()); + return state[propagator_index_]->active(); +} + +bool Propagator::set_active(CPPropagatorsState& state, bool active) const { + assert(propagator_index_ >= 0); + assert(propagator_index_ < state.size()); + state[propagator_index_]->set_active(active); +} +} // namespace dwave::optimization::cp diff --git a/dwave/optimization/src/cp/state/copier.cpp b/dwave/optimization/src/cp/state/copier.cpp new file mode 100644 index 00000000..7201a345 --- /dev/null +++ b/dwave/optimization/src/cp/state/copier.cpp @@ -0,0 +1,48 @@ +#include "dwave-optimization/cp/state/copier.hpp" + +#include "dwave-optimization/cp/state/copy.hpp" +namespace dwave::optimization::cp { +int Copier::get_level() { return prior.size() - 1; } + +void Copier::restore_state() { + prior.back().restore(); + prior.pop_back(); + this->notify_restore(); +} + +void Copier::save_state() { prior.emplace_back(Backup(*this)); } + +void Copier::restore_state_until(int level) { + while (get_level() > level) { + this->restore_state(); + } +} + +void Copier::with_new_state(std::function body) { + int level = this->get_level(); + this->save_state(); + body(); + this->restore_state_until(level); +} + +StateInt* Copier::make_state_int(int init_value) { + store.emplace_back(std::make_unique(init_value)); + return dynamic_cast(store.back().get()); +} + +StateBool* Copier::make_state_bool(bool init_value) { + store.emplace_back(std::make_unique(init_value)); + return dynamic_cast(store.back().get()); +} + +StateReal* Copier::make_state_real(double init_value) { + store.emplace_back(std::make_unique(init_value)); + return dynamic_cast(store.back().get()); +} + +void Copier::notify_restore() { + for (auto l : on_restore_listeners) { + l(); + } +} +} // namespace dwave::optimization::cp diff --git a/dwave/optimization/src/cp/state/copy.cpp b/dwave/optimization/src/cp/state/copy.cpp new file mode 100644 index 00000000..8660b89b --- /dev/null +++ b/dwave/optimization/src/cp/state/copy.cpp @@ -0,0 +1,14 @@ +#include "dwave-optimization/cp/state/copy.hpp" + +namespace dwave::optimization::cp { + +/// TODO: I have to think about ownership of this part here +template +std::unique_ptr Copy::save() { + return std::make_unique(this->get_value(), this); +} + +template class Copy; +template class Copy; +template class Copy; +} // namespace dwave::optimization::cp diff --git a/dwave/optimization/src/cp/state/cpvar_state.cpp b/dwave/optimization/src/cp/state/cpvar_state.cpp new file mode 100644 index 00000000..2db4439e --- /dev/null +++ b/dwave/optimization/src/cp/state/cpvar_state.cpp @@ -0,0 +1,49 @@ +#include "dwave-optimization/cp/state/cpvar_state.hpp" + +#include + +#include "dwave-optimization/cp/core/interval_array.hpp" + +namespace dwave::optimization::cp { + +CPVarData::CPVarData(StateManager* sm, ssize_t size, double lb, double ub, + std::unique_ptr listener, bool integral) { + // Set a real interval + if (integral) { + domains_ = std::make_unique(sm, size, lb, ub); + } else { + domains_ = std::make_unique(sm, size, lb, ub); + } + + listen_ = std::move(listener); +} + +size_t CPVarData::num_domains() const { return domains_->num_domains(); } + +double CPVarData::min(int index) const { return domains_->min(index); } + +double CPVarData::max(int index) const { return domains_->max(index); } + +double CPVarData::size(int index) const { return domains_->size(index); } + +bool CPVarData::is_bound(int index) const { return domains_->is_bound(index); } + +bool CPVarData::contains(double value, int index) const { return domains_->contains(value, index); } + +CPStatus CPVarData::remove(double value, int index) { + return domains_->remove(value, index, listen_.get()); +} + +CPStatus CPVarData::remove_above(double value, int index) { + return domains_->remove_above(value, index, listen_.get()); +} + +CPStatus CPVarData::remove_below(double value, int index) { + return domains_->remove_below(value, index, listen_.get()); +} + +CPStatus CPVarData::remove_all_but(double value, int index) { + return domains_->remove_all_but(value, index, listen_.get()); +} + +} // namespace dwave::optimization::cp diff --git a/dwave/optimization/src/cp/state/state_stack.cpp b/dwave/optimization/src/cp/state/state_stack.cpp new file mode 100644 index 00000000..b336005e --- /dev/null +++ b/dwave/optimization/src/cp/state/state_stack.cpp @@ -0,0 +1,36 @@ +#include "dwave-optimization/cp/state/state_stack.hpp" + +#include "dwave-optimization/cp/core/cpvar.hpp" +#include "dwave-optimization/cp/core/propagator.hpp" + +namespace dwave::optimization::cp { +template +StateStack::StateStack(StateManager* sm_ptr) { + size_ = sm_ptr->make_state_int(0); +} + +template +void StateStack::push(T* elem) { + int s = size_->get_value(); + if (static_cast(stack_.size()) > s) { + stack_[s] = elem; + } else { + stack_.push_back(elem); + } + size_->increment(); +} + +template +int StateStack::size() const { + return size_->get_value(); +} + +template +T* StateStack::get(int index) const { + return stack_[index]; +} + +template class StateStack; +template class StateStack; + +} // namespace dwave::optimization::cp \ No newline at end of file diff --git a/meson.build b/meson.build index 690d7165..ebe05aeb 100644 --- a/meson.build +++ b/meson.build @@ -53,6 +53,17 @@ dwave_optimization_src = [ 'dwave/optimization/src/fraction.cpp', 'dwave/optimization/src/graph.cpp', 'dwave/optimization/src/simplex.cpp', + + 'dwave/optimization/src/cp/core/interval_array.cpp', + 'dwave/optimization/src/cp/core/propagator.cpp', + 'dwave/optimization/src/cp/core/engine.cpp', + 'dwave/optimization/src/cp/core/cpvar.cpp', + + 'dwave/optimization/src/cp/state/copier.cpp', + 'dwave/optimization/src/cp/state/copy.cpp', + 'dwave/optimization/src/cp/state/cpvar_state.cpp', + 'dwave/optimization/src/cp/state/state_stack.cpp', + ] dwave_optimization_dependencies = [] diff --git a/tests/cpp/cp/test_copier.cpp b/tests/cpp/cp/test_copier.cpp new file mode 100644 index 00000000..81297e4c --- /dev/null +++ b/tests/cpp/cp/test_copier.cpp @@ -0,0 +1,97 @@ +// Copyright 2023 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. + +#include +#include +#include +#include + +#include "catch2/catch_test_macros.hpp" +#include "catch2/matchers/catch_matchers_all.hpp" +#include "dwave-optimization/cp/state/copier.hpp" +#include "dwave-optimization/cp/state/copy.hpp" +#include "dwave-optimization/cp/state/state.hpp" +#include "dwave-optimization/cp/state/state_manager.hpp" +#include "utils.hpp" + +using Catch::Matchers::RangeEquals; + +namespace dwave::optimization::cp { + +TEST_CASE("State") { + StateInt si = StateInt(8); + REQUIRE(si.get_value() == 8); + + si.increment(); + REQUIRE(si.get_value() == 9); + + si.decrement(); + REQUIRE(si.get_value() == 8); + + StateBool sb = StateBool(true); + REQUIRE(sb.get_value()); + sb.set_value(false); + REQUIRE_FALSE(sb.get_value()); +} + +TEST_CASE("Copy") { + CopyInt ci = CopyInt(0); + REQUIRE(ci.get_value() == 0); + + auto cci = ci.save(); + + ci.increment(); + REQUIRE(ci.get_value() == 1); + + cci->restore(); + REQUIRE(ci.get_value() == 0); +} + +TEST_CASE("Copier") { + Copier sm = Copier(); + REQUIRE(sm.get_level() == -1); + + StateInt* si = sm.make_state_int(0); + REQUIRE(si->get_value() == 0); + + StateBool* sb = sm.make_state_bool(false); + REQUIRE(!sb->get_value()); + + sm.save_state(); + REQUIRE(sm.get_level() == 0); + + si->increment(); + REQUIRE(si->get_value() == 1); + + sm.save_state(); + + sm.restore_state_until(-1); + REQUIRE(sm.get_level() == -1); + REQUIRE(si->get_value() == 0); + REQUIRE(!sb->get_value()); + + // try with new state method + auto f = [&]() { + si->decrement(); + sb->set_value(true); + REQUIRE(si->get_value() == -1); + REQUIRE(sb->get_value()); + }; + + sm.with_new_state(f); + REQUIRE(si->get_value() == 0); + REQUIRE(!sb->get_value()); +} + +} // namespace dwave::optimization::cp diff --git a/tests/cpp/cp/test_cp_var.cpp b/tests/cpp/cp/test_cp_var.cpp new file mode 100644 index 00000000..3dfc3f15 --- /dev/null +++ b/tests/cpp/cp/test_cp_var.cpp @@ -0,0 +1,51 @@ +// Copyright 2023 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. + +#include +#include +#include + +#include "catch2/catch_test_macros.hpp" +#include "catch2/matchers/catch_matchers_all.hpp" +#include "dwave-optimization/array.hpp" +#include "dwave-optimization/cp/core/cpvar.hpp" +#include "dwave-optimization/cp/core/interval_array.hpp" +#include "dwave-optimization/cp/state/copier.hpp" +#include "dwave-optimization/graph.hpp" +#include "dwave-optimization/nodes.hpp" +#include "dwave-optimization/state.hpp" +#include "utils.hpp" + +using Catch::Matchers::RangeEquals; + +namespace dwave::optimization::cp { + +TEST_CASE("CP Variable") { + dwave::optimization::Graph graph; + CPModel model; + + // Add an integer node + IntegerNode* i_ptr = graph.emplace_node(10, 0, 2); + CPVar* var = model.emplace_variable(model, i_ptr, model.num_variables()); + CPState state = model.initialize_state(); + var->initialize_state(state); + + for (ssize_t i = 0; i < 10; ++i) { + REQUIRE(var->min(state.get_variables_state(), i) == 0); + REQUIRE(var->max(state.get_variables_state(), i) == 2); + REQUIRE(var->size(state.get_variables_state(), i) == 3); + } +} + +} // namespace dwave::optimization::cp diff --git a/tests/cpp/cp/test_interval.cpp b/tests/cpp/cp/test_interval.cpp new file mode 100644 index 00000000..e5b2d182 --- /dev/null +++ b/tests/cpp/cp/test_interval.cpp @@ -0,0 +1,119 @@ +// 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. + +#include +#include +#include + +#include "catch2/catch_test_macros.hpp" +#include "catch2/matchers/catch_matchers_all.hpp" +#include "dwave-optimization/array.hpp" +#include "dwave-optimization/cp/core/interval_array.hpp" +#include "dwave-optimization/cp/state/copier.hpp" +#include "dwave-optimization/state.hpp" +#include "utils.hpp" + +using Catch::Matchers::RangeEquals; + +namespace dwave::optimization::cp { + +TEST_CASE("Interval double") { + GIVEN("A state manager and 10 intervals with default boundary") { + std::unique_ptr sm = std::make_unique(); + IntervalArray interval = IntervalArray(sm.get(), 10); + REQUIRE(interval.num_domains() == 10); + REQUIRE(sm->store.size() == 20); + for (size_t i = 0; i < interval.num_domains(); ++i) { + REQUIRE(interval.min(i) == -std::numeric_limits::max() / 2); + REQUIRE(interval.max(i) == std::numeric_limits::max() / 2); + REQUIRE(interval.size(i) == std::numeric_limits::max()); + } + + AND_GIVEN("A test domain listener") { + std::unique_ptr tl = std::make_unique(); + TestListener* tl_ptr = tl.get(); + + WHEN("We remove below 0 from all indices") { + for (int i = 0; i < 10; ++i) { + auto status = interval.remove_below(0., i, tl_ptr); + REQUIRE(status == CPStatus::OK); + } + + THEN("The minimum is correctly changed and the maximum is left unchanged") { + for (int i = 0; i < 10; ++i) { + REQUIRE(interval.min(i) == 0.); + REQUIRE(interval.max(i) == std::numeric_limits::max() / 2); + } + } + + AND_WHEN("We remove above 3 from the fourth index") { + auto status = interval.remove_above(3, 4, tl_ptr); + REQUIRE(status == CPStatus::OK); + REQUIRE(interval.max(4) == 3); + + THEN("The other indices are unchanged") { + for (int i = 0; i < 10; ++i) { + if (i == 4) continue; + REQUIRE(interval.min(i) == 0); + REQUIRE(interval.max(i) == std::numeric_limits::max() / 2); + } + } + + AND_WHEN("We remove above -3 from the first index") { + auto status = interval.remove_above(-3, 1, tl_ptr); + REQUIRE(status == CPStatus::Inconsistency); + } + } + } + } + } + + GIVEN("An array of intervals initialized by two vectors") { + std::unique_ptr sm = std::make_unique(); + std::vector lb(10); + std::vector ub(10); + + for (int i = 0; i < 10; ++i) { + lb[i] = -i; + ub[i] = i; + } + + IntervalArray interval2 = IntervalArray(sm.get(), lb, ub); + REQUIRE(sm->store.size() == 20); + THEN("The fomains are correctly initialized") { + REQUIRE(interval2.num_domains() == lb.size()); + for (int i = 0; i < 10; ++i) { + REQUIRE(interval2.min(i) == -i); + REQUIRE(interval2.max(i) == i); + REQUIRE(interval2.size(i) == 2 * i); + } + } + } + + GIVEN("An array of 15 intervals between 0.1 and 0.5") { + std::unique_ptr sm = std::make_unique(); + IntervalArray interval3 = IntervalArray(sm.get(), 15, 0.1, 0.5); + REQUIRE(sm->store.size() == 30); + THEN("The domains are correctly set") { + REQUIRE(interval3.num_domains() == 15); + for (int i = 0; i < 15; ++i) { + REQUIRE(interval3.min(i) == 0.1); + REQUIRE(interval3.max(i) == 0.5); + REQUIRE(interval3.size(i) == 0.4); + } + } + } +} + +} // namespace dwave::optimization::cp diff --git a/tests/cpp/cp/utils.hpp b/tests/cpp/cp/utils.hpp new file mode 100644 index 00000000..ae894dc6 --- /dev/null +++ b/tests/cpp/cp/utils.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include "dwave-optimization/cp/core/domain_listener.hpp" + +namespace dwave::optimization::cp +{ + class TestListener : public DomainListener { + public: + // void empty() override {} + void bind() override {} + void change() override {} + void change_max() override {} + void change_min() override {} + }; +} // namespace dwave::optimization::cp diff --git a/tests/cpp/meson.build b/tests/cpp/meson.build index ef23f764..351cf021 100644 --- a/tests/cpp/meson.build +++ b/tests/cpp/meson.build @@ -7,6 +7,10 @@ catch2 = subproject( tests_all = executable( 'tests-all', [ + 'cp/test_interval.cpp', + 'cp/test_copier.cpp', + 'cp/test_cp_var.cpp', + 'nodes/indexing/test_advanced.cpp', 'nodes/indexing/test_basic.cpp', 'nodes/test_binaryop.cpp', @@ -29,6 +33,7 @@ tests_all = executable( 'nodes/test_sorting.cpp', 'nodes/test_statistics.cpp', 'nodes/test_unaryop.cpp', + 'test_array.cpp', 'test_double_kahan.cpp', 'test_fraction.cpp', From ef5127afd194929219603d30e751b42681baf856 Mon Sep 17 00:00:00 2001 From: azucca Date: Fri, 27 Feb 2026 16:17:05 -0800 Subject: [PATCH 02/19] Add licence header --- .../dwave-optimization/cp/core/base_node.hpp | 14 ++++++++++++++ .../include/dwave-optimization/cp/core/cpvar.hpp | 13 +++++++++++++ .../dwave-optimization/cp/core/domain_array.hpp | 14 ++++++++++++++ .../cp/core/domain_listener.hpp | 14 ++++++++++++++ .../dwave-optimization/cp/core/engine.hpp | 14 ++++++++++++++ .../cp/core/interval_array.hpp | 14 ++++++++++++++ .../dwave-optimization/cp/core/propagator.hpp | 15 ++++++++++++++- .../dwave-optimization/cp/core/status.hpp | 14 ++++++++++++++ .../dwave-optimization/cp/state/copier.hpp | 14 ++++++++++++++ .../include/dwave-optimization/cp/state/copy.hpp | 14 ++++++++++++++ .../dwave-optimization/cp/state/cpvar_state.hpp | 14 ++++++++++++++ .../dwave-optimization/cp/state/state.hpp | 14 ++++++++++++++ .../dwave-optimization/cp/state/state_entry.hpp | 14 ++++++++++++++ .../cp/state/state_manager.hpp | 14 ++++++++++++++ .../dwave-optimization/cp/state/state_stack.hpp | 14 ++++++++++++++ .../dwave-optimization/cp/state/storage.hpp | 14 ++++++++++++++ dwave/optimization/src/cp/core/cpvar.cpp | 14 ++++++++++++++ dwave/optimization/src/cp/core/engine.cpp | 14 ++++++++++++++ .../optimization/src/cp/core/interval_array.cpp | 14 ++++++++++++++ dwave/optimization/src/cp/core/propagator.cpp | 14 ++++++++++++++ dwave/optimization/src/cp/state/copier.cpp | 14 ++++++++++++++ dwave/optimization/src/cp/state/copy.cpp | 14 ++++++++++++++ dwave/optimization/src/cp/state/cpvar_state.cpp | 14 ++++++++++++++ dwave/optimization/src/cp/state/state_stack.cpp | 14 ++++++++++++++ tests/cpp/cp/test_copier.cpp | 2 +- tests/cpp/cp/test_cp_var.cpp | 2 +- tests/cpp/cp/utils.hpp | 16 +++++++++++++++- 27 files changed, 352 insertions(+), 4 deletions(-) diff --git a/dwave/optimization/include/dwave-optimization/cp/core/base_node.hpp b/dwave/optimization/include/dwave-optimization/cp/core/base_node.hpp index 52b42fe6..cbd05d8b 100644 --- a/dwave/optimization/include/dwave-optimization/cp/core/base_node.hpp +++ b/dwave/optimization/include/dwave-optimization/cp/core/base_node.hpp @@ -1,3 +1,17 @@ +// 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" diff --git a/dwave/optimization/include/dwave-optimization/cp/core/cpvar.hpp b/dwave/optimization/include/dwave-optimization/cp/core/cpvar.hpp index 6b3f895a..5a52f8d6 100644 --- a/dwave/optimization/include/dwave-optimization/cp/core/cpvar.hpp +++ b/dwave/optimization/include/dwave-optimization/cp/core/cpvar.hpp @@ -1,3 +1,16 @@ +// 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 diff --git a/dwave/optimization/include/dwave-optimization/cp/core/domain_array.hpp b/dwave/optimization/include/dwave-optimization/cp/core/domain_array.hpp index 1d8753c4..c029bcf9 100644 --- a/dwave/optimization/include/dwave-optimization/cp/core/domain_array.hpp +++ b/dwave/optimization/include/dwave-optimization/cp/core/domain_array.hpp @@ -1,3 +1,17 @@ +// 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/domain_listener.hpp" diff --git a/dwave/optimization/include/dwave-optimization/cp/core/domain_listener.hpp b/dwave/optimization/include/dwave-optimization/cp/core/domain_listener.hpp index 7731325f..97a2a243 100644 --- a/dwave/optimization/include/dwave-optimization/cp/core/domain_listener.hpp +++ b/dwave/optimization/include/dwave-optimization/cp/core/domain_listener.hpp @@ -1,3 +1,17 @@ +// 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 { diff --git a/dwave/optimization/include/dwave-optimization/cp/core/engine.hpp b/dwave/optimization/include/dwave-optimization/cp/core/engine.hpp index 487eff34..6aa2414c 100644 --- a/dwave/optimization/include/dwave-optimization/cp/core/engine.hpp +++ b/dwave/optimization/include/dwave-optimization/cp/core/engine.hpp @@ -1,3 +1,17 @@ +// 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 diff --git a/dwave/optimization/include/dwave-optimization/cp/core/interval_array.hpp b/dwave/optimization/include/dwave-optimization/cp/core/interval_array.hpp index 6d1a5f64..656d3362 100644 --- a/dwave/optimization/include/dwave-optimization/cp/core/interval_array.hpp +++ b/dwave/optimization/include/dwave-optimization/cp/core/interval_array.hpp @@ -1,3 +1,17 @@ +// 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 diff --git a/dwave/optimization/include/dwave-optimization/cp/core/propagator.hpp b/dwave/optimization/include/dwave-optimization/cp/core/propagator.hpp index b5bd55bb..e6979cd6 100644 --- a/dwave/optimization/include/dwave-optimization/cp/core/propagator.hpp +++ b/dwave/optimization/include/dwave-optimization/cp/core/propagator.hpp @@ -1,9 +1,22 @@ +// 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 #include -//#include "dwave-optimization/cp/core/status.hpp" #include "dwave-optimization/cp/state/cpvar_state.hpp" #include "dwave-optimization/cp/state/state.hpp" diff --git a/dwave/optimization/include/dwave-optimization/cp/core/status.hpp b/dwave/optimization/include/dwave-optimization/cp/core/status.hpp index 3b494458..ddeaf031 100644 --- a/dwave/optimization/include/dwave-optimization/cp/core/status.hpp +++ b/dwave/optimization/include/dwave-optimization/cp/core/status.hpp @@ -1,3 +1,17 @@ +// 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 { diff --git a/dwave/optimization/include/dwave-optimization/cp/state/copier.hpp b/dwave/optimization/include/dwave-optimization/cp/state/copier.hpp index 1f587b74..2f9cc342 100644 --- a/dwave/optimization/include/dwave-optimization/cp/state/copier.hpp +++ b/dwave/optimization/include/dwave-optimization/cp/state/copier.hpp @@ -1,3 +1,17 @@ +// 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 diff --git a/dwave/optimization/include/dwave-optimization/cp/state/copy.hpp b/dwave/optimization/include/dwave-optimization/cp/state/copy.hpp index 81a53503..d919d48a 100644 --- a/dwave/optimization/include/dwave-optimization/cp/state/copy.hpp +++ b/dwave/optimization/include/dwave-optimization/cp/state/copy.hpp @@ -1,3 +1,17 @@ +// 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 diff --git a/dwave/optimization/include/dwave-optimization/cp/state/cpvar_state.hpp b/dwave/optimization/include/dwave-optimization/cp/state/cpvar_state.hpp index 869a3ac6..f9c295df 100644 --- a/dwave/optimization/include/dwave-optimization/cp/state/cpvar_state.hpp +++ b/dwave/optimization/include/dwave-optimization/cp/state/cpvar_state.hpp @@ -1,3 +1,17 @@ +// 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 #include diff --git a/dwave/optimization/include/dwave-optimization/cp/state/state.hpp b/dwave/optimization/include/dwave-optimization/cp/state/state.hpp index fc1eaf6c..ab55f094 100644 --- a/dwave/optimization/include/dwave-optimization/cp/state/state.hpp +++ b/dwave/optimization/include/dwave-optimization/cp/state/state.hpp @@ -1,3 +1,17 @@ +// 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 diff --git a/dwave/optimization/include/dwave-optimization/cp/state/state_entry.hpp b/dwave/optimization/include/dwave-optimization/cp/state/state_entry.hpp index 392f71d0..dd9cc0b5 100644 --- a/dwave/optimization/include/dwave-optimization/cp/state/state_entry.hpp +++ b/dwave/optimization/include/dwave-optimization/cp/state/state_entry.hpp @@ -1,3 +1,17 @@ +// 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 { diff --git a/dwave/optimization/include/dwave-optimization/cp/state/state_manager.hpp b/dwave/optimization/include/dwave-optimization/cp/state/state_manager.hpp index 86d7d56f..5d60ad1f 100644 --- a/dwave/optimization/include/dwave-optimization/cp/state/state_manager.hpp +++ b/dwave/optimization/include/dwave-optimization/cp/state/state_manager.hpp @@ -1,3 +1,17 @@ +// 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 diff --git a/dwave/optimization/include/dwave-optimization/cp/state/state_stack.hpp b/dwave/optimization/include/dwave-optimization/cp/state/state_stack.hpp index cb6f6005..a2ca0a58 100644 --- a/dwave/optimization/include/dwave-optimization/cp/state/state_stack.hpp +++ b/dwave/optimization/include/dwave-optimization/cp/state/state_stack.hpp @@ -1,3 +1,17 @@ +// 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/state/state.hpp" diff --git a/dwave/optimization/include/dwave-optimization/cp/state/storage.hpp b/dwave/optimization/include/dwave-optimization/cp/state/storage.hpp index 016feb40..02711680 100644 --- a/dwave/optimization/include/dwave-optimization/cp/state/storage.hpp +++ b/dwave/optimization/include/dwave-optimization/cp/state/storage.hpp @@ -1,3 +1,17 @@ +// 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 diff --git a/dwave/optimization/src/cp/core/cpvar.cpp b/dwave/optimization/src/cp/core/cpvar.cpp index 4baacbb3..5d9b5c44 100644 --- a/dwave/optimization/src/cp/core/cpvar.cpp +++ b/dwave/optimization/src/cp/core/cpvar.cpp @@ -1,3 +1,17 @@ +// 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. + #include "dwave-optimization/cp/core/cpvar.hpp" #include diff --git a/dwave/optimization/src/cp/core/engine.cpp b/dwave/optimization/src/cp/core/engine.cpp index 1f142286..69b3023c 100644 --- a/dwave/optimization/src/cp/core/engine.cpp +++ b/dwave/optimization/src/cp/core/engine.cpp @@ -1,3 +1,17 @@ +// 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. + #include "dwave-optimization/cp/core/engine.hpp" namespace dwave::optimization::cp { diff --git a/dwave/optimization/src/cp/core/interval_array.cpp b/dwave/optimization/src/cp/core/interval_array.cpp index cae19f0d..3e45d70d 100644 --- a/dwave/optimization/src/cp/core/interval_array.cpp +++ b/dwave/optimization/src/cp/core/interval_array.cpp @@ -1,3 +1,17 @@ +// 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. + #include "dwave-optimization/cp/core/interval_array.hpp" #include diff --git a/dwave/optimization/src/cp/core/propagator.cpp b/dwave/optimization/src/cp/core/propagator.cpp index 5ebf9a73..229dafd3 100644 --- a/dwave/optimization/src/cp/core/propagator.cpp +++ b/dwave/optimization/src/cp/core/propagator.cpp @@ -1,3 +1,17 @@ +// 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. + #include "dwave-optimization/cp/core/propagator.hpp" namespace dwave::optimization::cp { diff --git a/dwave/optimization/src/cp/state/copier.cpp b/dwave/optimization/src/cp/state/copier.cpp index 7201a345..0101442a 100644 --- a/dwave/optimization/src/cp/state/copier.cpp +++ b/dwave/optimization/src/cp/state/copier.cpp @@ -1,3 +1,17 @@ +// 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. + #include "dwave-optimization/cp/state/copier.hpp" #include "dwave-optimization/cp/state/copy.hpp" diff --git a/dwave/optimization/src/cp/state/copy.cpp b/dwave/optimization/src/cp/state/copy.cpp index 8660b89b..cbc76b9b 100644 --- a/dwave/optimization/src/cp/state/copy.cpp +++ b/dwave/optimization/src/cp/state/copy.cpp @@ -1,3 +1,17 @@ +// 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. + #include "dwave-optimization/cp/state/copy.hpp" namespace dwave::optimization::cp { diff --git a/dwave/optimization/src/cp/state/cpvar_state.cpp b/dwave/optimization/src/cp/state/cpvar_state.cpp index 2db4439e..7a85347d 100644 --- a/dwave/optimization/src/cp/state/cpvar_state.cpp +++ b/dwave/optimization/src/cp/state/cpvar_state.cpp @@ -1,3 +1,17 @@ +// 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. + #include "dwave-optimization/cp/state/cpvar_state.hpp" #include diff --git a/dwave/optimization/src/cp/state/state_stack.cpp b/dwave/optimization/src/cp/state/state_stack.cpp index b336005e..92194f39 100644 --- a/dwave/optimization/src/cp/state/state_stack.cpp +++ b/dwave/optimization/src/cp/state/state_stack.cpp @@ -1,3 +1,17 @@ +// 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. + #include "dwave-optimization/cp/state/state_stack.hpp" #include "dwave-optimization/cp/core/cpvar.hpp" diff --git a/tests/cpp/cp/test_copier.cpp b/tests/cpp/cp/test_copier.cpp index 81297e4c..c8f7216a 100644 --- a/tests/cpp/cp/test_copier.cpp +++ b/tests/cpp/cp/test_copier.cpp @@ -1,4 +1,4 @@ -// Copyright 2023 D-Wave Systems Inc. +// 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. diff --git a/tests/cpp/cp/test_cp_var.cpp b/tests/cpp/cp/test_cp_var.cpp index 3dfc3f15..7614cccf 100644 --- a/tests/cpp/cp/test_cp_var.cpp +++ b/tests/cpp/cp/test_cp_var.cpp @@ -1,4 +1,4 @@ -// Copyright 2023 D-Wave Systems Inc. +// 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. diff --git a/tests/cpp/cp/utils.hpp b/tests/cpp/cp/utils.hpp index ae894dc6..f58b947a 100644 --- a/tests/cpp/cp/utils.hpp +++ b/tests/cpp/cp/utils.hpp @@ -1,3 +1,17 @@ +// 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/domain_listener.hpp" @@ -6,10 +20,10 @@ namespace dwave::optimization::cp { class TestListener : public DomainListener { public: - // void empty() override {} void bind() override {} void change() override {} void change_max() override {} void change_min() override {} }; + } // namespace dwave::optimization::cp From 612a06920318c45f0d2328e0df53f00df9ee1e48 Mon Sep 17 00:00:00 2001 From: azucca Date: Mon, 2 Mar 2026 14:44:10 -0800 Subject: [PATCH 03/19] Unify int64_t->ssize_t --- .../cp/core/interval_array.hpp | 2 +- .../dwave-optimization/cp/state/copier.hpp | 2 +- .../dwave-optimization/cp/state/state.hpp | 8 ++-- .../cp/state/state_manager.hpp | 2 +- .../src/cp/core/interval_array.cpp | 40 +++++++++---------- dwave/optimization/src/cp/state/copier.cpp | 2 +- 6 files changed, 28 insertions(+), 28 deletions(-) diff --git a/dwave/optimization/include/dwave-optimization/cp/core/interval_array.hpp b/dwave/optimization/include/dwave-optimization/cp/core/interval_array.hpp index 656d3362..7a66c489 100644 --- a/dwave/optimization/include/dwave-optimization/cp/core/interval_array.hpp +++ b/dwave/optimization/include/dwave-optimization/cp/core/interval_array.hpp @@ -47,7 +47,7 @@ class IntervalArray : public DomainArray { std::vector*> max_; }; -using IntIntervalArray = IntervalArray; +using IntIntervalArray = IntervalArray; using RealIntervalArray = IntervalArray; } // namespace dwave::optimization::cp diff --git a/dwave/optimization/include/dwave-optimization/cp/state/copier.hpp b/dwave/optimization/include/dwave-optimization/cp/state/copier.hpp index 2f9cc342..f9fb8c4a 100644 --- a/dwave/optimization/include/dwave-optimization/cp/state/copier.hpp +++ b/dwave/optimization/include/dwave-optimization/cp/state/copier.hpp @@ -74,7 +74,7 @@ class Copier : public StateManager { void with_new_state(std::function body) override; // TODO: the next two methods have a dynamic cast that doesn't make me really comfortable - StateInt* make_state_int(int init_value) override; + StateInt* make_state_int(ssize_t init_value) override; StateBool* make_state_bool(bool init_value) override; diff --git a/dwave/optimization/include/dwave-optimization/cp/state/state.hpp b/dwave/optimization/include/dwave-optimization/cp/state/state.hpp index ab55f094..e7b1ee0a 100644 --- a/dwave/optimization/include/dwave-optimization/cp/state/state.hpp +++ b/dwave/optimization/include/dwave-optimization/cp/state/state.hpp @@ -29,19 +29,19 @@ class State { virtual void set_value(T v) = 0; }; -class StateInt : virtual public State { +class StateInt : virtual public State { public: StateInt() : v_(0) {} - StateInt(int value) : v_(value) {} + StateInt(ssize_t value) : v_(value) {} - int64_t get_value() override { return v_; } + ssize_t get_value() override { return v_; } void set_value(int64_t value) override { v_ = value; } virtual void increment() { v_++; } virtual void decrement() { v_--; } protected: - int v_; + ssize_t v_; }; class StateBool : virtual public State { diff --git a/dwave/optimization/include/dwave-optimization/cp/state/state_manager.hpp b/dwave/optimization/include/dwave-optimization/cp/state/state_manager.hpp index 5d60ad1f..2031df1d 100644 --- a/dwave/optimization/include/dwave-optimization/cp/state/state_manager.hpp +++ b/dwave/optimization/include/dwave-optimization/cp/state/state_manager.hpp @@ -27,7 +27,7 @@ class StateManager { virtual void save_state() = 0; virtual void restore_state() = 0; virtual void restore_state_until(int level) = 0; - virtual StateInt* make_state_int(int init_value) = 0; + virtual StateInt* make_state_int(ssize_t init_value) = 0; virtual StateBool* make_state_bool(bool init_value) = 0; virtual StateReal* make_state_real(double init_value) = 0; }; diff --git a/dwave/optimization/src/cp/core/interval_array.cpp b/dwave/optimization/src/cp/core/interval_array.cpp index 3e45d70d..903e3592 100644 --- a/dwave/optimization/src/cp/core/interval_array.cpp +++ b/dwave/optimization/src/cp/core/interval_array.cpp @@ -31,15 +31,15 @@ IntervalArray::IntervalArray(StateManager* sm, ssize_t size) { } template <> -IntervalArray::IntervalArray(StateManager* sm, ssize_t size) { +IntervalArray::IntervalArray(StateManager* sm, ssize_t size) { min_.resize(size, sm->make_state_int(0)); - max_.resize(size, sm->make_state_int((int64_t)1 << 51)); + max_.resize(size, sm->make_state_int((ssize_t)1 << 51)); min_.resize(size); max_.resize(size); for (size_t i = 0; i < min_.size(); ++i) { min_[i] = sm->make_state_int(0); - max_[i] = sm->make_state_int((int64_t)1 << 51); + max_[i] = sm->make_state_int((ssize_t)1 << 51); } } @@ -59,11 +59,11 @@ IntervalArray::IntervalArray(StateManager* sm, ssize_t size, double lb, } template <> -IntervalArray::IntervalArray(StateManager* sm, ssize_t size, double lb, double ub) { - if (lb < std::numeric_limits::min()) - throw std::invalid_argument("lower bound too small for int64_t"); - if (ub > std::numeric_limits::max()) - throw std::invalid_argument("upper bound too big for int64_t"); +IntervalArray::IntervalArray(StateManager* sm, ssize_t size, double lb, double ub) { + if (lb < std::numeric_limits::min()) + throw std::invalid_argument("lower bound too small for ssize_t"); + if (ub > std::numeric_limits::max()) + throw std::invalid_argument("upper bound too big for ssize_t"); if (std::ceil(lb) > std::floor(ub)) { throw std::invalid_argument("lower bound larger than upper bound"); @@ -98,7 +98,7 @@ IntervalArray::IntervalArray(StateManager* sm, std::vector lb, } template <> -IntervalArray::IntervalArray(StateManager* sm, std::vector lb, +IntervalArray::IntervalArray(StateManager* sm, std::vector lb, std::vector ub) { if (lb.size() != ub.size()) { throw std::invalid_argument("lower bounds and upper bounds have different sizes"); @@ -108,10 +108,10 @@ IntervalArray::IntervalArray(StateManager* sm, std::vector lb, max_.resize(ub.size()); for (size_t i = 0; i < lb.size(); ++i) { - if (lb[i] < std::numeric_limits::min()) - throw std::invalid_argument("lower bound too small for int64_t"); - if (ub[i] > std::numeric_limits::max()) - throw std::invalid_argument("upper bound too big for int64_t"); + if (lb[i] < std::numeric_limits::min()) + throw std::invalid_argument("lower bound too small for ssize_t"); + if (ub[i] > std::numeric_limits::max()) + throw std::invalid_argument("upper bound too big for ssize_t"); if (lb[i] > ub[i]) throw std::invalid_argument("lower bound larger than upper bound"); min_[i] = sm->make_state_int(std::ceil(lb[i])); max_[i] = sm->make_state_int(std::floor(ub[i])); @@ -137,7 +137,7 @@ double IntervalArray::size(int index) const { } template <> -double IntervalArray::size(int index) const { +double IntervalArray::size(int index) const { assert(index < min_.size()); return max_[index]->get_value() - min_[index]->get_value() + 1; } @@ -148,7 +148,7 @@ bool IntervalArray::is_bound(int index) const { } template <> -bool IntervalArray::is_bound(int index) const { +bool IntervalArray::is_bound(int index) const { return (this->size(index) == 1); } @@ -160,7 +160,7 @@ bool IntervalArray::contains(double value, int index) const { } template <> -bool IntervalArray::contains(double value, int index) const { +bool IntervalArray::contains(double value, int index) const { if (value > max_[index]->get_value()) return false; if (value < min_[index]->get_value()) return false; if (std::floor(value) != std::ceil(value)) return false; @@ -174,7 +174,7 @@ CPStatus IntervalArray::remove(double value, int index, DomainListener* } template <> -CPStatus IntervalArray::remove(double value, int index, DomainListener* l) { +CPStatus IntervalArray::remove(double value, int index, DomainListener* l) { // can only remove from the boundary if (this->contains(value, index)) { // if there's only this value, then domain is wiped out @@ -208,7 +208,7 @@ CPStatus IntervalArray::remove_above(double value, int index, DomainList } template <> -CPStatus IntervalArray::remove_above(double value, int index, DomainListener* l) { +CPStatus IntervalArray::remove_above(double value, int index, DomainListener* l) { // wipe-out all domain if (min_[index]->get_value() > value) return CPStatus::Inconsistency; @@ -234,7 +234,7 @@ CPStatus IntervalArray::remove_below(double value, int index, DomainList } template <> -CPStatus IntervalArray::remove_below(double value, int index, DomainListener* l) { +CPStatus IntervalArray::remove_below(double value, int index, DomainListener* l) { // wipe-out all domain if (max_[index]->get_value() < value) return CPStatus::Inconsistency; @@ -264,6 +264,6 @@ CPStatus IntervalArray::remove_all_but(double value, int index, DomainListene } template class IntervalArray; -template class IntervalArray; +template class IntervalArray; } // namespace dwave::optimization::cp diff --git a/dwave/optimization/src/cp/state/copier.cpp b/dwave/optimization/src/cp/state/copier.cpp index 0101442a..3ca74a41 100644 --- a/dwave/optimization/src/cp/state/copier.cpp +++ b/dwave/optimization/src/cp/state/copier.cpp @@ -39,7 +39,7 @@ void Copier::with_new_state(std::function body) { this->restore_state_until(level); } -StateInt* Copier::make_state_int(int init_value) { +StateInt* Copier::make_state_int(ssize_t init_value) { store.emplace_back(std::make_unique(init_value)); return dynamic_cast(store.back().get()); } From bca5f13a8849f5410c2d98eddef7cb460eea2e5e Mon Sep 17 00:00:00 2001 From: azucca Date: Mon, 2 Mar 2026 14:47:17 -0800 Subject: [PATCH 04/19] Fix warnings --- dwave/optimization/src/cp/core/engine.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/dwave/optimization/src/cp/core/engine.cpp b/dwave/optimization/src/cp/core/engine.cpp index 69b3023c..215ed955 100644 --- a/dwave/optimization/src/cp/core/engine.cpp +++ b/dwave/optimization/src/cp/core/engine.cpp @@ -20,7 +20,6 @@ CPStatus CPEngine::fix_point(CPState& state) const { CPStatus status = CPStatus::OK; CPPropagatorsState& p_state = state.propagator_state_; - CPVarsState& v_state = state.var_state_; while (state.propagation_queue_.size() > 0) { Propagator* p = state.propagation_queue_.front(); @@ -28,8 +27,7 @@ CPStatus CPEngine::fix_point(CPState& state) const { if (status == CPStatus::Inconsistency) { while (state.propagation_queue_.size() > 0) { - Propagator* p = state.propagation_queue_.front(); - // state.propagation_queue_.front()->set_scheduled(state, false); + state.propagation_queue_.front()->set_scheduled(p_state, false); state.propagation_queue_.pop_front(); } @@ -44,7 +42,7 @@ CPStatus CPEngine::fix_point(CPState& state) const { CPStatus CPEngine::propagate(CPState& state, Propagator* p) const { auto& p_state = state.propagator_state_; auto& v_state = state.var_state_; - p->propagate(p_state, v_state); + return p->propagate(p_state, v_state); } StateManager* CPEngine::get_state_manager(const CPState& state) const { From 8ef6bc75289e65be1809a4c6b901272db2c48750 Mon Sep 17 00:00:00 2001 From: azucca Date: Mon, 2 Mar 2026 15:11:46 -0800 Subject: [PATCH 05/19] Fix ssize_t and bool warnings --- .../dwave-optimization/cp/core/propagator.hpp | 2 +- dwave/optimization/src/cp/core/cpvar.cpp | 2 +- dwave/optimization/src/cp/core/interval_array.cpp | 12 ++++++------ dwave/optimization/src/cp/core/propagator.cpp | 14 +++++++------- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/dwave/optimization/include/dwave-optimization/cp/core/propagator.hpp b/dwave/optimization/include/dwave-optimization/cp/core/propagator.hpp index e6979cd6..88276d19 100644 --- a/dwave/optimization/include/dwave-optimization/cp/core/propagator.hpp +++ b/dwave/optimization/include/dwave-optimization/cp/core/propagator.hpp @@ -59,7 +59,7 @@ class Propagator { bool active(const CPPropagatorsState& state) const; - bool set_active(CPPropagatorsState& state, bool active) const; + void set_active(CPPropagatorsState& state, bool active) const; virtual CPStatus propagate(CPPropagatorsState& p_state, CPVarsState& v_state) = 0; diff --git a/dwave/optimization/src/cp/core/cpvar.cpp b/dwave/optimization/src/cp/core/cpvar.cpp index 5d9b5c44..df57406e 100644 --- a/dwave/optimization/src/cp/core/cpvar.cpp +++ b/dwave/optimization/src/cp/core/cpvar.cpp @@ -81,7 +81,7 @@ void CPVar::propagate_on_domain_change(Propagator* p) { on_domain.push_back(p); void CPVar::initialize_state(CPState& state) const { assert(this->cp_var_index_ >= 0); - assert(state.var_state_.size() > cp_var_index_); + assert(static_cast(state.var_state_.size()) > cp_var_index_); // TODO: for now we don't handle dynamically sized nodes assert(node_->size() > 0); diff --git a/dwave/optimization/src/cp/core/interval_array.cpp b/dwave/optimization/src/cp/core/interval_array.cpp index 903e3592..8ffc509d 100644 --- a/dwave/optimization/src/cp/core/interval_array.cpp +++ b/dwave/optimization/src/cp/core/interval_array.cpp @@ -120,25 +120,25 @@ IntervalArray::IntervalArray(StateManager* sm, std::vector lb, template double IntervalArray::min(int index) const { - assert(index < min_.size()); + assert(index < static_cast(min_.size())); return min_[index]->get_value(); } template double IntervalArray::max(int index) const { - assert(index < max_.size()); + assert(index < static_cast(max_.size())); return max_[index]->get_value(); } template <> double IntervalArray::size(int index) const { - assert(index < min_.size()); + assert(index < static_cast(min_.size())); return max_[index]->get_value() - min_[index]->get_value(); } template <> double IntervalArray::size(int index) const { - assert(index < min_.size()); + assert(index < static_cast(min_.size())); return max_[index]->get_value() - min_[index]->get_value() + 1; } @@ -251,8 +251,8 @@ CPStatus IntervalArray::remove_all_but(double value, int index, DomainListene // if the value is not contained, wipe-out if (not this->contains(value, index)) return CPStatus::Inconsistency; - bool changed_min = value = min_[index]->get_value(); - bool changed_max = value = max_[index]->get_value(); + bool changed_min = (value = min_[index]->get_value()); + bool changed_max = (value = max_[index]->get_value()); min_[index]->set_value(value); max_[index]->set_value(value); diff --git a/dwave/optimization/src/cp/core/propagator.cpp b/dwave/optimization/src/cp/core/propagator.cpp index 229dafd3..ebd402c1 100644 --- a/dwave/optimization/src/cp/core/propagator.cpp +++ b/dwave/optimization/src/cp/core/propagator.cpp @@ -19,38 +19,38 @@ namespace dwave::optimization::cp { template PData> PData* Propagator::data_ptr(CPPropagatorsState& state) const { assert(propagator_index_ >= 0); - assert(propagator_index_ < state.size()); + assert(propagator_index_ < static_cast(state.size())); return static_cast(state[propagator_index_].get()); } template PData> const PData* Propagator::data_ptr(const CPPropagatorsState& state) const { assert(propagator_index_ >= 0); - assert(propagator_index_ < state.size()); + assert(propagator_index_ < static_cast(state.size())); return static_cast(state[propagator_index_].get()); } bool Propagator::scheduled(const CPPropagatorsState& state) const { assert(propagator_index_ >= 0); - assert(propagator_index_ < state.size()); + assert(propagator_index_ < static_cast(state.size())); return state[propagator_index_]->scheduled(); } void Propagator::set_scheduled(CPPropagatorsState& state, bool scheduled) const { assert(propagator_index_ >= 0); - assert(propagator_index_ < state.size()); + assert(propagator_index_ < static_cast(state.size())); state[propagator_index_]->set_scheduled(scheduled); } bool Propagator::active(const CPPropagatorsState& state) const { assert(propagator_index_ >= 0); - assert(propagator_index_ < state.size()); + assert(propagator_index_ < static_cast(state.size())); return state[propagator_index_]->active(); } -bool Propagator::set_active(CPPropagatorsState& state, bool active) const { +void Propagator::set_active(CPPropagatorsState& state, bool active) const { assert(propagator_index_ >= 0); - assert(propagator_index_ < state.size()); + assert(propagator_index_ < static_cast(state.size())); state[propagator_index_]->set_active(active); } } // namespace dwave::optimization::cp From 3e1fa4288f8f9dd726cbebafbb64eacad2b61e75 Mon Sep 17 00:00:00 2001 From: azucca Date: Mon, 2 Mar 2026 15:19:18 -0800 Subject: [PATCH 06/19] Include header --- .../include/dwave-optimization/cp/core/domain_array.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/dwave/optimization/include/dwave-optimization/cp/core/domain_array.hpp b/dwave/optimization/include/dwave-optimization/cp/core/domain_array.hpp index c029bcf9..20d76f3d 100644 --- a/dwave/optimization/include/dwave-optimization/cp/core/domain_array.hpp +++ b/dwave/optimization/include/dwave-optimization/cp/core/domain_array.hpp @@ -13,6 +13,7 @@ // limitations under the License. #pragma once +#include #include "dwave-optimization/cp/core/domain_listener.hpp" #include "dwave-optimization/cp/core/status.hpp" From abff5d19bff740f9c5dea01eaf4b2f628bda6326 Mon Sep 17 00:00:00 2001 From: azucca Date: Mon, 2 Mar 2026 15:24:42 -0800 Subject: [PATCH 07/19] Fix int64_t -> ssize_t --- .../optimization/include/dwave-optimization/cp/state/state.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dwave/optimization/include/dwave-optimization/cp/state/state.hpp b/dwave/optimization/include/dwave-optimization/cp/state/state.hpp index e7b1ee0a..63307c59 100644 --- a/dwave/optimization/include/dwave-optimization/cp/state/state.hpp +++ b/dwave/optimization/include/dwave-optimization/cp/state/state.hpp @@ -35,7 +35,7 @@ class StateInt : virtual public State { StateInt(ssize_t value) : v_(value) {} ssize_t get_value() override { return v_; } - void set_value(int64_t value) override { v_ = value; } + void set_value(ssize_t value) override { v_ = value; } virtual void increment() { v_++; } virtual void decrement() { v_--; } From 9e0de2b8cc70bb8c7321f715d0f3b8fffa3d450b Mon Sep 17 00:00:00 2001 From: azucca Date: Mon, 2 Mar 2026 15:49:47 -0800 Subject: [PATCH 08/19] Remove std::move in emplace_back --- .../optimization/include/dwave-optimization/cp/state/copier.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dwave/optimization/include/dwave-optimization/cp/state/copier.hpp b/dwave/optimization/include/dwave-optimization/cp/state/copier.hpp index f9fb8c4a..7a37ab1a 100644 --- a/dwave/optimization/include/dwave-optimization/cp/state/copier.hpp +++ b/dwave/optimization/include/dwave-optimization/cp/state/copier.hpp @@ -36,7 +36,7 @@ class Copier : public StateManager { Backup(Copier& outer) : copier_(outer) { size_ = copier_.store.size(); for (const auto& st_ptr : copier_.store) { - backup_store_.emplace_back(std::move(st_ptr->save())); + backup_store_.emplace_back(st_ptr->save()); } } From 2d5cf5f9e578810ae21216632b213593a64b2755 Mon Sep 17 00:00:00 2001 From: azucca Date: Mon, 2 Mar 2026 16:07:03 -0800 Subject: [PATCH 09/19] Fix int64_t -> ssize_t --- dwave/optimization/src/cp/state/copy.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dwave/optimization/src/cp/state/copy.cpp b/dwave/optimization/src/cp/state/copy.cpp index cbc76b9b..705f20cf 100644 --- a/dwave/optimization/src/cp/state/copy.cpp +++ b/dwave/optimization/src/cp/state/copy.cpp @@ -22,7 +22,7 @@ std::unique_ptr Copy::save() { return std::make_unique(this->get_value(), this); } -template class Copy; +template class Copy; template class Copy; template class Copy; } // namespace dwave::optimization::cp From baacb91a0755a652ba9682cb772de3bb1cea6f72 Mon Sep 17 00:00:00 2001 From: azucca Date: Mon, 2 Mar 2026 16:43:19 -0800 Subject: [PATCH 10/19] Fix int64_t -> ssize_t --- .../optimization/include/dwave-optimization/cp/state/copy.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dwave/optimization/include/dwave-optimization/cp/state/copy.hpp b/dwave/optimization/include/dwave-optimization/cp/state/copy.hpp index d919d48a..e2247160 100644 --- a/dwave/optimization/include/dwave-optimization/cp/state/copy.hpp +++ b/dwave/optimization/include/dwave-optimization/cp/state/copy.hpp @@ -41,10 +41,10 @@ class Copy : virtual public State, public Storage { virtual std::unique_ptr save() override; }; -class CopyInt : virtual public StateInt, virtual public Copy { +class CopyInt : virtual public StateInt, virtual public Copy { public: // constructor - CopyInt(int initial) : StateInt(initial) {} + CopyInt(ssize_t initial) : StateInt(initial) {} using StateInt::decrement; using StateInt::get_value; using StateInt::increment; From aae8673946454e6361f0ab3d11f201ed93aa37fc Mon Sep 17 00:00:00 2001 From: azucca Date: Mon, 2 Mar 2026 16:50:41 -0800 Subject: [PATCH 11/19] Include cstddef header --- dwave/optimization/include/dwave-optimization/cp/state/state.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/dwave/optimization/include/dwave-optimization/cp/state/state.hpp b/dwave/optimization/include/dwave-optimization/cp/state/state.hpp index 63307c59..b258f636 100644 --- a/dwave/optimization/include/dwave-optimization/cp/state/state.hpp +++ b/dwave/optimization/include/dwave-optimization/cp/state/state.hpp @@ -16,6 +16,7 @@ #include #include +#include #include #include From 1b421078fe9b44b8583b31fba016e37137a67df2 Mon Sep 17 00:00:00 2001 From: azucca Date: Mon, 2 Mar 2026 16:58:32 -0800 Subject: [PATCH 12/19] Include common --- .../include/dwave-optimization/cp/core/domain_array.hpp | 1 + .../optimization/include/dwave-optimization/cp/state/state.hpp | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/dwave/optimization/include/dwave-optimization/cp/core/domain_array.hpp b/dwave/optimization/include/dwave-optimization/cp/core/domain_array.hpp index 20d76f3d..74e1127c 100644 --- a/dwave/optimization/include/dwave-optimization/cp/core/domain_array.hpp +++ b/dwave/optimization/include/dwave-optimization/cp/core/domain_array.hpp @@ -15,6 +15,7 @@ #pragma once #include +#include "dwave-optimization/common.hpp" #include "dwave-optimization/cp/core/domain_listener.hpp" #include "dwave-optimization/cp/core/status.hpp" diff --git a/dwave/optimization/include/dwave-optimization/cp/state/state.hpp b/dwave/optimization/include/dwave-optimization/cp/state/state.hpp index b258f636..3d4b8d4b 100644 --- a/dwave/optimization/include/dwave-optimization/cp/state/state.hpp +++ b/dwave/optimization/include/dwave-optimization/cp/state/state.hpp @@ -16,10 +16,11 @@ #include #include -#include #include #include +#include "dwave-optimization/common.hpp" + namespace dwave::optimization::cp { template class State { From 42aa8b44fbad7490cb15a0080304d8c28e12bda7 Mon Sep 17 00:00:00 2001 From: azucca Date: Wed, 4 Mar 2026 15:21:39 -0800 Subject: [PATCH 13/19] Support dynamically sized arrays in CP --- .../dwave-optimization/cp/core/cpvar.hpp | 17 +- .../cp/core/domain_array.hpp | 39 ++++ .../cp/core/domain_listener.hpp | 1 + .../cp/core/interval_array.hpp | 17 ++ .../cp/state/cpvar_state.hpp | 8 +- dwave/optimization/src/cp/core/cpvar.cpp | 58 +++++- .../src/cp/core/interval_array.cpp | 197 +++++++++++++++--- .../optimization/src/cp/state/cpvar_state.cpp | 22 +- tests/cpp/cp/test_cp_var.cpp | 73 ++++++- tests/cpp/cp/test_interval.cpp | 142 ++++++++++++- tests/cpp/cp/utils.hpp | 21 +- 11 files changed, 536 insertions(+), 59 deletions(-) diff --git a/dwave/optimization/include/dwave-optimization/cp/core/cpvar.hpp b/dwave/optimization/include/dwave-optimization/cp/core/cpvar.hpp index 5a52f8d6..82013b27 100644 --- a/dwave/optimization/include/dwave-optimization/cp/core/cpvar.hpp +++ b/dwave/optimization/include/dwave-optimization/cp/core/cpvar.hpp @@ -83,7 +83,7 @@ class CPModel { }; /// Interface for CP variables, allows query and modify their domain. It assumes a flattened array -/// of variables +/// of variables. class CPVar { public: virtual ~CPVar() = default; @@ -111,18 +111,27 @@ class CPVar { 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; // 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& propagators) 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(Propagator* p); void propagate_on_bounds_change(Propagator* p); void propagate_on_assignment(Propagator* p); @@ -136,6 +145,7 @@ class CPVar { std::vector on_bind; std::vector on_domain; std::vector on_bounds; + std::vector on_array_size_change; protected: const CPModel& model_; @@ -149,6 +159,7 @@ class CPVar { Listener(const CPVar* var, CPState& state) : var_(var), state_(state) {} // domain listener overrides + void bind() override { var_->schedule_all(state_, var_->on_bind); } void change() override { var_->schedule_all(state_, var_->on_domain); } @@ -157,7 +168,9 @@ class CPVar { void change_max() override { var_->schedule_all(state_, var_->on_bounds); } - // void empty() override { return CPStatus::Inconsistency; } + void change_array_size() override { + var_->schedule_all(state_, var_->on_array_size_change); + } private: const CPVar* var_; diff --git a/dwave/optimization/include/dwave-optimization/cp/core/domain_array.hpp b/dwave/optimization/include/dwave-optimization/cp/core/domain_array.hpp index 74e1127c..c93458e5 100644 --- a/dwave/optimization/include/dwave-optimization/cp/core/domain_array.hpp +++ b/dwave/optimization/include/dwave-optimization/cp/core/domain_array.hpp @@ -20,23 +20,62 @@ #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; + + /// Return the minimum size of the array + virtual ssize_t min_size() const = 0; + + /// Return the minimum size of the array + 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; diff --git a/dwave/optimization/include/dwave-optimization/cp/core/domain_listener.hpp b/dwave/optimization/include/dwave-optimization/cp/core/domain_listener.hpp index 97a2a243..22323418 100644 --- a/dwave/optimization/include/dwave-optimization/cp/core/domain_listener.hpp +++ b/dwave/optimization/include/dwave-optimization/cp/core/domain_listener.hpp @@ -25,5 +25,6 @@ class DomainListener { virtual void change() = 0; virtual void change_max() = 0; virtual void change_min() = 0; + virtual void change_array_size() = 0; }; } // namespace dwave::optimization::cp diff --git a/dwave/optimization/include/dwave-optimization/cp/core/interval_array.hpp b/dwave/optimization/include/dwave-optimization/cp/core/interval_array.hpp index 7a66c489..c6603f74 100644 --- a/dwave/optimization/include/dwave-optimization/cp/core/interval_array.hpp +++ b/dwave/optimization/include/dwave-optimization/cp/core/interval_array.hpp @@ -25,26 +25,43 @@ template class IntervalArray : public DomainArray { public: IntervalArray(StateManager* sm, ssize_t size); + IntervalArray(StateManager* sm, ssize_t min_size, ssize_t max_size); + IntervalArray(StateManager* sm, ssize_t min_size, ssize_t max_size, double lb, double up); IntervalArray(StateManager* sm, ssize_t size, double lb, double up); + IntervalArray(StateManager* sm, ssize_t min_size, std::vector lb, + std::vector ub); IntervalArray(StateManager* sm, std::vector lb, std::vector ub); size_t num_domains() const override { return min_.size(); } + + ssize_t min_size() const override { return min_size_->get_value(); } + ssize_t max_size() const override { return max_size_->get_value(); } double min(int index) const override; double max(int index) const override; double size(int index) const override; bool is_bound(int index) const override; bool contains(double value, int index) const override; + bool is_active(int index) const override; + bool maybe_active(int index) const override; + CPStatus remove(double value, int index, DomainListener* l) override; CPStatus remove_above(double value, int index, DomainListener* l) override; CPStatus remove_below(double value, int index, DomainListener* l) override; CPStatus remove_all_but(double value, int index, DomainListener* l) override; + CPStatus update_min_size(int new_min_size, DomainListener* l) override; + CPStatus update_max_size(int new_max_size, DomainListener* l) override; private: // Change double do an object that can be backtracked. // And maybe get std::vector*> min_; std::vector*> max_; + + StateInt* min_size_; + StateInt* max_size_; + + void set_sizes(ssize_t size, ssize_t min_size, ssize_t max_size); }; using IntIntervalArray = IntervalArray; diff --git a/dwave/optimization/include/dwave-optimization/cp/state/cpvar_state.hpp b/dwave/optimization/include/dwave-optimization/cp/state/cpvar_state.hpp index f9c295df..349221ae 100644 --- a/dwave/optimization/include/dwave-optimization/cp/state/cpvar_state.hpp +++ b/dwave/optimization/include/dwave-optimization/cp/state/cpvar_state.hpp @@ -23,7 +23,7 @@ namespace dwave::optimization::cp { class CPVarData { public: - CPVarData(StateManager* sm, ssize_t size, double lb, double ub, + CPVarData(StateManager* sm, ssize_t min_size, ssize_t max_size, double lb, double ub, std::unique_ptr listener, bool integral); // actions on the underlying domain @@ -34,11 +34,17 @@ class CPVarData { double size(int index) const; bool is_bound(int index) const; bool contains(double value, int index) const; + bool is_active(int index) const; + bool maybe_active(int index) const; + ssize_t min_size() const; + ssize_t max_size() const; CPStatus remove(double value, int index); CPStatus remove_above(double value, int index); CPStatus remove_below(double value, int index); CPStatus remove_all_but(double value, int index); + CPStatus update_min_size(ssize_t new_min_size); + CPStatus update_max_size(ssize_t new_min_size); protected: // keeping it as a unique pointer in case we wanna have different types of domains.. diff --git a/dwave/optimization/src/cp/core/cpvar.cpp b/dwave/optimization/src/cp/core/cpvar.cpp index df57406e..310e1e02 100644 --- a/dwave/optimization/src/cp/core/cpvar.cpp +++ b/dwave/optimization/src/cp/core/cpvar.cpp @@ -17,11 +17,11 @@ #include namespace dwave::optimization::cp { +// ------ CPVar ------- + CPVar::CPVar(const CPModel& model, const dwave::optimization::ArrayNode* node_ptr, int index) : model_(model), node_(node_ptr), cp_var_index_(index) {} -// ------ CPVar ------- - double CPVar::min(const CPVarsState& state, int index) const { const CPVarData* data = data_ptr(state); return data->min(index); @@ -47,6 +47,26 @@ bool CPVar::contains(const CPVarsState& state, double value, int index) const { return data->contains(value, index); } +bool CPVar::is_active(const CPVarsState& state, int index) const { + const CPVarData* data = data_ptr(state); + return data->is_active(index); +} + +bool CPVar::maybe_active(const CPVarsState& state, int index) const { + const CPVarData* data = data_ptr(state); + return data->maybe_active(index); +} + +ssize_t CPVar::min_size(const CPVarsState& state) const { + const CPVarData* data = data_ptr(state); + return data->min_size(); +} + +ssize_t CPVar::max_size(const CPVarsState& state) const { + const CPVarData* data = data_ptr(state); + return data->max_size(); +} + CPStatus CPVar::remove(CPVarsState& state, double value, int index) const { CPVarData* data = data_ptr(state); return data->remove(value, index); @@ -67,6 +87,16 @@ CPStatus CPVar::assign(CPVarsState& state, double value, int index) const { return data->remove_all_but(value, index); } +CPStatus CPVar::set_min_size(CPVarsState& state, int new_min_size) const { + CPVarData* data = data_ptr(state); + return data->update_min_size(new_min_size); +} + +CPStatus CPVar::set_max_size(CPVarsState& state, int new_max_size) const { + CPVarData* data = data_ptr(state); + return data->update_max_size(new_max_size); +} + void CPVar::schedule_all(CPState& state, const std::vector& propagators) const { for (auto p : propagators) { state.schedule(p); @@ -84,10 +114,30 @@ void CPVar::initialize_state(CPState& state) const { assert(static_cast(state.var_state_.size()) > cp_var_index_); // TODO: for now we don't handle dynamically sized nodes - assert(node_->size() > 0); + + ssize_t max_size, min_size; + + if (node_->size() > 0) { + // Static array + max_size = node_->size(); + min_size = node_->size(); + } else { + // Dynamic arrays + SizeInfo sizeinfo = node_->sizeinfo(); + if (not sizeinfo.max.has_value()) { + throw std::invalid_argument("Array has no max size available"); + } + + if (not sizeinfo.min.has_value()) { + throw std::invalid_argument("Array has no min size available"); + } + + max_size = sizeinfo.max.value(); + min_size = sizeinfo.min.value(); + } state.var_state_[cp_var_index_] = std::make_unique( - state.get_state_manager(), node_->size(), node_->min(), node_->max(), + state.get_state_manager(), min_size, max_size, node_->min(), node_->max(), std::make_unique(this, state), node_->integral()); } diff --git a/dwave/optimization/src/cp/core/interval_array.cpp b/dwave/optimization/src/cp/core/interval_array.cpp index 8ffc509d..cae1208b 100644 --- a/dwave/optimization/src/cp/core/interval_array.cpp +++ b/dwave/optimization/src/cp/core/interval_array.cpp @@ -20,37 +20,65 @@ #include namespace dwave::optimization::cp { + template <> -IntervalArray::IntervalArray(StateManager* sm, ssize_t size) { - min_.resize(size); - max_.resize(size); - for (size_t i = 0; i < min_.size(); ++i) { +IntervalArray::IntervalArray(StateManager* sm, ssize_t min_size, ssize_t max_size) { + if (min_size > max_size) { + throw std::invalid_argument("Min size larger than max size"); + } + + assert(min_size <= max_size); + min_size_ = sm->make_state_int(min_size); + max_size_ = sm->make_state_int(max_size); + + // Set the bounds for each index + min_.resize(max_size); + max_.resize(max_size); + for (ssize_t i = 0; i < max_size; ++i) { min_[i] = sm->make_state_real(-std::numeric_limits::max() / 2); max_[i] = sm->make_state_real(std::numeric_limits::max() / 2); } } template <> -IntervalArray::IntervalArray(StateManager* sm, ssize_t size) { - min_.resize(size, sm->make_state_int(0)); - max_.resize(size, sm->make_state_int((ssize_t)1 << 51)); +IntervalArray::IntervalArray(StateManager* sm, ssize_t min_size, ssize_t max_size) { + if (min_size > max_size) { + throw std::invalid_argument("Min size larger than max size"); + } - min_.resize(size); - max_.resize(size); + assert(min_size <= max_size); + min_size_ = sm->make_state_int(min_size); + max_size_ = sm->make_state_int(max_size); + + min_.resize(max_size); + max_.resize(max_size); for (size_t i = 0; i < min_.size(); ++i) { min_[i] = sm->make_state_int(0); max_[i] = sm->make_state_int((ssize_t)1 << 51); } } +template +IntervalArray::IntervalArray(StateManager* sm, ssize_t size) + : IntervalArray(sm, size, size) {} + template <> -IntervalArray::IntervalArray(StateManager* sm, ssize_t size, double lb, double ub) { +IntervalArray::IntervalArray(StateManager* sm, ssize_t min_size, ssize_t max_size, + double lb, double ub) { + if (min_size > max_size) { + throw std::invalid_argument("Min size larger than max size"); + } + if (lb > ub) { throw std::invalid_argument("lower bound larger than upper bound"); } - min_.resize(size); - max_.resize(size); + assert(min_size <= max_size); + min_size_ = sm->make_state_int(min_size); + max_size_ = sm->make_state_int(max_size); + + min_.resize(max_size); + max_.resize(max_size); for (size_t i = 0; i < min_.size(); ++i) { min_[i] = sm->make_state_real(lb); @@ -59,7 +87,10 @@ IntervalArray::IntervalArray(StateManager* sm, ssize_t size, double lb, } template <> -IntervalArray::IntervalArray(StateManager* sm, ssize_t size, double lb, double ub) { +IntervalArray::IntervalArray(StateManager* sm, ssize_t min_size, ssize_t max_size, + double lb, double ub) { + if (min_size > max_size) throw std::invalid_argument("Min size larger than max size"); + if (lb < std::numeric_limits::min()) throw std::invalid_argument("lower bound too small for ssize_t"); if (ub > std::numeric_limits::max()) @@ -69,8 +100,13 @@ IntervalArray::IntervalArray(StateManager* sm, ssize_t size, double lb, throw std::invalid_argument("lower bound larger than upper bound"); } - min_.resize(size); - max_.resize(size); + assert(min_size <= max_size); + min_size_ = sm->make_state_int(min_size); + max_size_ = sm->make_state_int(max_size); + + // Set the bounds for each index + min_.resize(max_size); + max_.resize(max_size); for (size_t i = 0; i < min_.size(); ++i) { min_[i] = sm->make_state_int(std::ceil(lb)); @@ -78,15 +114,27 @@ IntervalArray::IntervalArray(StateManager* sm, ssize_t size, double lb, } } +template +IntervalArray::IntervalArray(StateManager* sm, ssize_t size, double lb, double ub) + : IntervalArray(sm, size, size, lb, ub) {} + template <> -IntervalArray::IntervalArray(StateManager* sm, std::vector lb, +IntervalArray::IntervalArray(StateManager* sm, ssize_t min_size, std::vector lb, std::vector ub) { if (lb.size() != ub.size()) { throw std::invalid_argument("lower bounds and upper bounds have different sizes"); } + if (min_size > static_cast(lb.size())) { + throw std::invalid_argument("Min size larger than max size"); + } + + assert(min_size <= static_cast(lb.size())); + min_size_ = sm->make_state_int(min_size); + max_size_ = sm->make_state_int(static_cast(lb.size())); + min_.resize(lb.size()); - max_.resize(ub.size()); + max_.resize(lb.size()); for (size_t i = 0; i < lb.size(); ++i) { if (lb[i] > ub[i]) { throw std::invalid_argument("lower bound larger than upper bound"); @@ -98,12 +146,20 @@ IntervalArray::IntervalArray(StateManager* sm, std::vector lb, } template <> -IntervalArray::IntervalArray(StateManager* sm, std::vector lb, +IntervalArray::IntervalArray(StateManager* sm, ssize_t min_size, std::vector lb, std::vector ub) { if (lb.size() != ub.size()) { throw std::invalid_argument("lower bounds and upper bounds have different sizes"); } + if (min_size > static_cast(lb.size())) { + throw std::invalid_argument("Min size larger than max size"); + } + + assert(min_size <= static_cast(lb.size())); + min_size_ = sm->make_state_int(min_size); + max_size_ = sm->make_state_int(lb.size()); + min_.resize(lb.size()); max_.resize(ub.size()); @@ -118,6 +174,10 @@ IntervalArray::IntervalArray(StateManager* sm, std::vector lb, } } +template +IntervalArray::IntervalArray(StateManager* sm, std::vector lb, std::vector ub) + : IntervalArray(sm, lb.size(), lb, ub) {} + template double IntervalArray::min(int index) const { assert(index < static_cast(min_.size())); @@ -167,6 +227,16 @@ bool IntervalArray::contains(double value, int index) const { return true; } +template +bool IntervalArray::is_active(int index) const { + return index < min_size_->get_value(); +} + +template +bool IntervalArray::maybe_active(int index) const { + return index < max_size_->get_value(); +} + template <> CPStatus IntervalArray::remove(double value, int index, DomainListener* l) { // can't really remove a double, even from the boundary @@ -175,10 +245,17 @@ CPStatus IntervalArray::remove(double value, int index, DomainListener* template <> CPStatus IntervalArray::remove(double value, int index, DomainListener* l) { - // can only remove from the boundary + // This is an interval so we can only remove from the boundary if (this->contains(value, index)) { // if there's only this value, then domain is wiped out - if (this->is_bound(index)) return CPStatus::Inconsistency; + if (this->is_bound(index) and this->is_active(index)) return CPStatus::Inconsistency; + + // The variable domain is fixed at the value we want to remove, but the variable is + // optional. Then this variabe becomes inactive. We can set the maximum size of the domain + // to the current index + if (this->is_bound(index) and this->maybe_active(index)) { + return this->update_max_size(index, l); + } bool change_max = value == max_[index]->get_value(); bool change_min = value == min_[index]->get_value(); @@ -196,8 +273,14 @@ CPStatus IntervalArray::remove(double value, int index, DomainListener* template <> CPStatus IntervalArray::remove_above(double value, int index, DomainListener* l) { - // wipe-out all domain - if (min_[index]->get_value() > value) return CPStatus::Inconsistency; + // Wipe-out all domain on a mandatory variable, inconsistency. + if (min_[index]->get_value() > value and this->is_active(index)) return CPStatus::Inconsistency; + + // Wipe out of the domain of an optional variable. We can set the maximum size of the domain + // to the current index + if (min_[index]->get_value() > value and this->maybe_active(index)) { + return this->update_max_size(index, l); + } // nothing to do if (max_[index]->get_value() <= value) return CPStatus::OK; @@ -209,8 +292,14 @@ CPStatus IntervalArray::remove_above(double value, int index, DomainList template <> CPStatus IntervalArray::remove_above(double value, int index, DomainListener* l) { - // wipe-out all domain - if (min_[index]->get_value() > value) return CPStatus::Inconsistency; + // Wipe-out all domain on a mandatory variable, inconsistency. + if (min_[index]->get_value() > value and this->is_active(index)) return CPStatus::Inconsistency; + + // Wipe out of the domain of an optional variable. We can set the maximum size of the domain + // to the current index + if (min_[index]->get_value() > value and this->maybe_active(index)) { + return this->update_max_size(index, l); + } // nothing to do if (max_[index]->get_value() <= value) return CPStatus::OK; @@ -222,8 +311,14 @@ CPStatus IntervalArray::remove_above(double value, int index, DomainLis template <> CPStatus IntervalArray::remove_below(double value, int index, DomainListener* l) { - // wipe-out all domain - if (max_[index]->get_value() < value) return CPStatus::Inconsistency; + // Wipe-out all domain on a mandatory variable, inconsistency. + if (max_[index]->get_value() < value and this->is_active(index)) return CPStatus::Inconsistency; + + // Wipe out of the domain of an optional variable. We can set the maximum size of the domain + // to the current index + if (max_[index]->get_value() < value and this->maybe_active(index)) { + return this->update_max_size(index, l); + } // nothing to do if (min_[index]->get_value() >= value) return CPStatus::OK; @@ -235,8 +330,14 @@ CPStatus IntervalArray::remove_below(double value, int index, DomainList template <> CPStatus IntervalArray::remove_below(double value, int index, DomainListener* l) { - // wipe-out all domain - if (max_[index]->get_value() < value) return CPStatus::Inconsistency; + // Wipe-out all domain on a mandatory variable, inconsistency. + if (max_[index]->get_value() < value and this->is_active(index)) return CPStatus::Inconsistency; + + // Wipe out of the domain of an optional variable. We can set the maximum size of the domain + // to the current index + if (max_[index]->get_value() < value and this->maybe_active(index)) { + return this->update_max_size(index, l); + } // nothing to do if (min_[index]->get_value() >= value) return CPStatus::OK; @@ -248,8 +349,14 @@ CPStatus IntervalArray::remove_below(double value, int index, DomainLis template CPStatus IntervalArray::remove_all_but(double value, int index, DomainListener* l) { - // if the value is not contained, wipe-out - if (not this->contains(value, index)) return CPStatus::Inconsistency; + // If the value is not contained, wipe-out a mandatory variable, cause an inconsistency + if (not this->contains(value, index) and this->is_active(index)) return CPStatus::Inconsistency; + + // Wipe out of the domain of an optional variable. We can set the maximum size of the domain + // to the current index + if (not this->contains(value, index) and this->maybe_active(index)) { + return this->update_max_size(index, l); + } bool changed_min = (value = min_[index]->get_value()); bool changed_max = (value = max_[index]->get_value()); @@ -263,6 +370,36 @@ CPStatus IntervalArray::remove_all_but(double value, int index, DomainListene return CPStatus::OK; } +template +CPStatus IntervalArray::update_min_size(int new_min_size, DomainListener* l) { + // Simple case, nothing to do + if (new_min_size < min_size_->get_value()) return CPStatus::OK; + + // Wipeout of the domain for the size variable + if (new_min_size > max_size_->get_value()) return CPStatus::Inconsistency; + + if (new_min_size != min_size_->get_value()) { + min_size_->set_value(new_min_size); + l->change_array_size(); + } + return CPStatus::OK; +} + +template +CPStatus IntervalArray::update_max_size(int new_max_size, DomainListener* l) { + // Simple case, nothing to do + if (new_max_size > max_size_->get_value()) return CPStatus::OK; + + // Wipeout of the domain for the size variable + if (new_max_size < min_size_->get_value()) return CPStatus::Inconsistency; + + if (new_max_size != max_size_->get_value()) { + max_size_->set_value(new_max_size); + l->change_array_size(); + } + return CPStatus::OK; +} + template class IntervalArray; template class IntervalArray; diff --git a/dwave/optimization/src/cp/state/cpvar_state.cpp b/dwave/optimization/src/cp/state/cpvar_state.cpp index 7a85347d..ec23f5f3 100644 --- a/dwave/optimization/src/cp/state/cpvar_state.cpp +++ b/dwave/optimization/src/cp/state/cpvar_state.cpp @@ -20,13 +20,13 @@ namespace dwave::optimization::cp { -CPVarData::CPVarData(StateManager* sm, ssize_t size, double lb, double ub, +CPVarData::CPVarData(StateManager* sm, ssize_t min_size, ssize_t max_size, double lb, double ub, std::unique_ptr listener, bool integral) { // Set a real interval if (integral) { - domains_ = std::make_unique(sm, size, lb, ub); + domains_ = std::make_unique(sm, min_size, max_size, lb, ub); } else { - domains_ = std::make_unique(sm, size, lb, ub); + domains_ = std::make_unique(sm, min_size, max_size, lb, ub); } listen_ = std::move(listener); @@ -44,6 +44,14 @@ bool CPVarData::is_bound(int index) const { return domains_->is_bound(index); } bool CPVarData::contains(double value, int index) const { return domains_->contains(value, index); } +bool CPVarData::is_active(int index) const { return domains_->is_active(index); } + +bool CPVarData::maybe_active(int index) const { return domains_->maybe_active(index); } + +ssize_t CPVarData::min_size() const { return domains_->min_size(); } + +ssize_t CPVarData::max_size() const { return domains_->max_size(); } + CPStatus CPVarData::remove(double value, int index) { return domains_->remove(value, index, listen_.get()); } @@ -60,4 +68,12 @@ CPStatus CPVarData::remove_all_but(double value, int index) { return domains_->remove_all_but(value, index, listen_.get()); } +CPStatus CPVarData::update_min_size(ssize_t new_min_size) { + return domains_->update_min_size(new_min_size, listen_.get()); +} + +CPStatus CPVarData::update_max_size(ssize_t new_max_size) { + return domains_->update_max_size(new_max_size, listen_.get()); +} + } // namespace dwave::optimization::cp diff --git a/tests/cpp/cp/test_cp_var.cpp b/tests/cpp/cp/test_cp_var.cpp index 7614cccf..d7db7ecc 100644 --- a/tests/cpp/cp/test_cp_var.cpp +++ b/tests/cpp/cp/test_cp_var.cpp @@ -31,20 +31,85 @@ using Catch::Matchers::RangeEquals; namespace dwave::optimization::cp { -TEST_CASE("CP Variable") { +TEST_CASE("IntegerNode -> CPVar") { dwave::optimization::Graph graph; - CPModel model; // Add an integer node - IntegerNode* i_ptr = graph.emplace_node(10, 0, 2); - CPVar* var = model.emplace_variable(model, i_ptr, model.num_variables()); + graph.emplace_node(10, 0, 2); + + // Lock the graph + graph.topological_sort(); + + // Construct the CP corresponding model + CPModel model; + + // Start adding the + std::vector vars; + for (const auto& n_uptr : graph.nodes()) { + const ArrayNode* ptr = dynamic_cast(n_uptr.get()); + REQUIRE(ptr); + vars.push_back(model.emplace_variable(model, ptr, ptr->topological_index())); + } + + // Initialize an empty state? CPState state = model.initialize_state(); + REQUIRE(state.get_propagators_state().size() == 0); + REQUIRE(state.get_variables_state().size() == 1); + + auto* var = vars[0]; var->initialize_state(state); + REQUIRE(var->min_size(state.get_variables_state()) == + var->max_size(state.get_variables_state())); + REQUIRE(var->min_size(state.get_variables_state()) == 10); for (ssize_t i = 0; i < 10; ++i) { REQUIRE(var->min(state.get_variables_state(), i) == 0); REQUIRE(var->max(state.get_variables_state(), i) == 2); REQUIRE(var->size(state.get_variables_state(), i) == 3); + REQUIRE(var->is_active(state.get_variables_state(), i)); + } +} + +TEST_CASE("ListNode -> CPVar") { + dwave::optimization::Graph graph; + + // Add an list node + graph.emplace_node(10, 2, 5); + + // Lock the graph + graph.topological_sort(); + + // Construct the CP corresponding model + CPModel model; + + // Start adding the + std::vector vars; + for (const auto& n_uptr : graph.nodes()) { + const ArrayNode* ptr = dynamic_cast(n_uptr.get()); + REQUIRE(ptr); + vars.push_back(model.emplace_variable(model, ptr, ptr->topological_index())); + } + + // Initialize an empty state? + CPState state = model.initialize_state(); + REQUIRE(state.get_propagators_state().size() == 0); + REQUIRE(state.get_variables_state().size() == 1); + + auto* var = vars[0]; + var->initialize_state(state); + REQUIRE(var->min_size(state.get_variables_state()) == 2); + REQUIRE(var->max_size(state.get_variables_state()) == 5); + + for (ssize_t i = 0; i < 5; ++i) { + REQUIRE(var->min(state.get_variables_state(), i) == 0); + REQUIRE(var->max(state.get_variables_state(), i) == 9); + REQUIRE(var->size(state.get_variables_state(), i) == 10); + if (i < 2) { + REQUIRE(var->is_active(state.get_variables_state(), i)); + } else { + REQUIRE_FALSE(var->is_active(state.get_variables_state(), i)); + REQUIRE(var->maybe_active(state.get_variables_state(), i)); + } } } diff --git a/tests/cpp/cp/test_interval.cpp b/tests/cpp/cp/test_interval.cpp index e5b2d182..abf91ff2 100644 --- a/tests/cpp/cp/test_interval.cpp +++ b/tests/cpp/cp/test_interval.cpp @@ -28,12 +28,16 @@ using Catch::Matchers::RangeEquals; namespace dwave::optimization::cp { -TEST_CASE("Interval double") { +TEST_CASE("Interval double (fixed size)") { GIVEN("A state manager and 10 intervals with default boundary") { std::unique_ptr sm = std::make_unique(); IntervalArray interval = IntervalArray(sm.get(), 10); REQUIRE(interval.num_domains() == 10); - REQUIRE(sm->store.size() == 20); + REQUIRE(interval.max_size() == 10); + REQUIRE(interval.min_size() == 10); + + // store size should be 2 x 10 + 2 for min and max size + REQUIRE(sm->store.size() == 22); for (size_t i = 0; i < interval.num_domains(); ++i) { REQUIRE(interval.min(i) == -std::numeric_limits::max() / 2); REQUIRE(interval.max(i) == std::numeric_limits::max() / 2); @@ -90,9 +94,11 @@ TEST_CASE("Interval double") { } IntervalArray interval2 = IntervalArray(sm.get(), lb, ub); - REQUIRE(sm->store.size() == 20); + REQUIRE(sm->store.size() == 22); THEN("The fomains are correctly initialized") { REQUIRE(interval2.num_domains() == lb.size()); + REQUIRE(interval2.min_size() == 10); + REQUIRE(interval2.max_size() == 10); for (int i = 0; i < 10; ++i) { REQUIRE(interval2.min(i) == -i); REQUIRE(interval2.max(i) == i); @@ -104,13 +110,141 @@ TEST_CASE("Interval double") { GIVEN("An array of 15 intervals between 0.1 and 0.5") { std::unique_ptr sm = std::make_unique(); IntervalArray interval3 = IntervalArray(sm.get(), 15, 0.1, 0.5); - REQUIRE(sm->store.size() == 30); + REQUIRE(sm->store.size() == 32); + THEN("The domains are correctly set") { + REQUIRE(interval3.num_domains() == 15); + REQUIRE(interval3.min_size() == 15); + REQUIRE(interval3.max_size() == 15); + for (int i = 0; i < 15; ++i) { + REQUIRE(interval3.min(i) == 0.1); + REQUIRE(interval3.max(i) == 0.5); + REQUIRE(interval3.size(i) == 0.4); + } + } + } +} + +TEST_CASE("Interval double (dynamic size)") { + GIVEN("A state manager and a dynamic array with minimum size 6 and max size 10") { + std::unique_ptr sm = std::make_unique(); + IntervalArray interval = IntervalArray(sm.get(), 6, 10); + REQUIRE(interval.num_domains() == 10); + REQUIRE(interval.min_size() == 6); + REQUIRE(interval.max_size() == 10); + // store size should be 2 x 10 + 2 for min and max + REQUIRE(sm->store.size() == 22); + for (size_t i = 0; i < interval.num_domains(); ++i) { + REQUIRE(interval.min(i) == -std::numeric_limits::max() / 2); + REQUIRE(interval.max(i) == std::numeric_limits::max() / 2); + REQUIRE(interval.size(i) == std::numeric_limits::max()); + } + + // Check the active and optional entries + for (size_t i = 0; i < 6; ++i) { + REQUIRE(interval.is_active(i)); + } + for (size_t i = 7; i < 10; ++i) { + REQUIRE_FALSE(interval.is_active(i)); + REQUIRE(interval.maybe_active(i)); + } + + AND_GIVEN("A test domain listener") { + std::unique_ptr tl = std::make_unique(); + TestListener* tl_ptr = tl.get(); + + WHEN("We remove below 0 from all indices") { + for (int i = 0; i < 10; ++i) { + auto status = interval.remove_below(0., i, tl_ptr); + REQUIRE(status == CPStatus::OK); + } + + THEN("The minimum is correctly changed and the maximum is left unchanged") { + for (int i = 0; i < 10; ++i) { + REQUIRE(interval.min(i) == 0.); + REQUIRE(interval.max(i) == std::numeric_limits::max() / 2); + } + } + + AND_WHEN("We remove above 3 from the fourth index") { + auto status = interval.remove_above(3, 4, tl_ptr); + REQUIRE(status == CPStatus::OK); + REQUIRE(interval.max(4) == 3); + + THEN("The other indices are unchanged") { + for (int i = 0; i < 10; ++i) { + if (i == 4) continue; + REQUIRE(interval.min(i) == 0); + REQUIRE(interval.max(i) == std::numeric_limits::max() / 2); + } + } + + AND_WHEN("We remove above -3 from the first index") { + auto status = interval.remove_above(-3, 1, tl_ptr); + REQUIRE(status == CPStatus::Inconsistency); + } + + AND_WHEN("We remove above -3 from the 7th index") { + auto status = interval.remove_above(-3, 7, tl_ptr); + REQUIRE(status == CPStatus::OK); + REQUIRE(interval.max_size() == 7); + for (int i = 7; i < 10; ++i) { + REQUIRE_FALSE(interval.is_active(i)); + REQUIRE_FALSE(interval.maybe_active(i)); + } + } + } + } + } + } + + GIVEN("An array of intervals initialized by two vectors") { + std::unique_ptr sm = std::make_unique(); + std::vector lb(10); + std::vector ub(10); + + for (int i = 0; i < 10; ++i) { + lb[i] = -i; + ub[i] = i; + } + + IntervalArray interval2 = IntervalArray(sm.get(), 6, lb, ub); + REQUIRE(sm->store.size() == 22); + THEN("The domains are correctly initialized") { + REQUIRE(interval2.num_domains() == lb.size()); + REQUIRE(interval2.min_size() == 6); + REQUIRE(interval2.max_size() == static_cast(lb.size())); + for (int i = 0; i < 10; ++i) { + REQUIRE(interval2.min(i) == -i); + REQUIRE(interval2.max(i) == i); + REQUIRE(interval2.size(i) == 2 * i); + if (i < 6) { + REQUIRE(interval2.is_active(i)); + } else { + REQUIRE_FALSE(interval2.is_active(i)); + REQUIRE(interval2.maybe_active(i)); + } + } + } + } + + GIVEN("An array of 15 intervals between 0.1 and 0.5") { + std::unique_ptr sm = std::make_unique(); + IntervalArray interval3 = IntervalArray(sm.get(), 6, 15, 0.1, 0.5); + REQUIRE(sm->store.size() == 32); THEN("The domains are correctly set") { REQUIRE(interval3.num_domains() == 15); + REQUIRE(interval3.min_size() == 6); + REQUIRE(interval3.max_size() == 15); for (int i = 0; i < 15; ++i) { REQUIRE(interval3.min(i) == 0.1); REQUIRE(interval3.max(i) == 0.5); REQUIRE(interval3.size(i) == 0.4); + if (i < 6) { + REQUIRE(interval3.is_active(i)); + } else { + REQUIRE_FALSE(interval3.is_active(i)); + REQUIRE(interval3.maybe_active(i)); + } } } } diff --git a/tests/cpp/cp/utils.hpp b/tests/cpp/cp/utils.hpp index f58b947a..c5043cda 100644 --- a/tests/cpp/cp/utils.hpp +++ b/tests/cpp/cp/utils.hpp @@ -16,14 +16,13 @@ #include "dwave-optimization/cp/core/domain_listener.hpp" -namespace dwave::optimization::cp -{ - class TestListener : public DomainListener { - public: - void bind() override {} - void change() override {} - void change_max() override {} - void change_min() override {} - }; - -} // namespace dwave::optimization::cp +namespace dwave::optimization::cp { +class TestListener : public DomainListener { + public: + void bind() override {} + void change() override {} + void change_max() override {} + void change_min() override {} + void change_array_size() override {} +}; +} // namespace dwave::optimization::cp From aa8e594d36072117329c8ad3f3a4a7d56ea676c0 Mon Sep 17 00:00:00 2001 From: azucca Date: Fri, 6 Mar 2026 15:29:55 -0800 Subject: [PATCH 14/19] Support scheduling propagators via indices --- .../dwave-optimization/cp/core/advisor.hpp | 43 ++++ .../dwave-optimization/cp/core/cpvar.hpp | 32 +-- .../cp/core/domain_listener.hpp | 10 +- .../dwave-optimization/cp/core/engine.hpp | 4 + .../cp/core/index_transform.hpp | 41 ++++ .../dwave-optimization/cp/core/propagator.hpp | 53 ++++- .../cp/propagators/identity_propagator.hpp | 48 ++++ dwave/optimization/src/cp/core/advisor.cpp | 30 +++ dwave/optimization/src/cp/core/cpvar.cpp | 36 ++- .../src/cp/core/interval_array.cpp | 27 ++- dwave/optimization/src/cp/core/propagator.cpp | 24 +- .../cp/propagators/identity_propagator.cpp | 75 ++++++ meson.build | 3 + tests/cpp/cp/test_cp_var.cpp | 221 ++++++++++++------ tests/cpp/cp/test_propagator.cpp | 95 ++++++++ tests/cpp/cp/utils.hpp | 10 +- tests/cpp/meson.build | 1 + 17 files changed, 622 insertions(+), 131 deletions(-) create mode 100644 dwave/optimization/include/dwave-optimization/cp/core/advisor.hpp create mode 100644 dwave/optimization/include/dwave-optimization/cp/core/index_transform.hpp create mode 100644 dwave/optimization/include/dwave-optimization/cp/propagators/identity_propagator.hpp create mode 100644 dwave/optimization/src/cp/core/advisor.cpp create mode 100644 dwave/optimization/src/cp/propagators/identity_propagator.cpp create mode 100644 tests/cpp/cp/test_propagator.cpp diff --git a/dwave/optimization/include/dwave-optimization/cp/core/advisor.hpp b/dwave/optimization/include/dwave-optimization/cp/core/advisor.hpp new file mode 100644 index 00000000..b683439f --- /dev/null +++ b/dwave/optimization/include/dwave-optimization/cp/core/advisor.hpp @@ -0,0 +1,43 @@ +// 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 { + +/// Utility class to help schedule propagators when variable 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 index_transform); + + void notify(CPPropagatorsState& p_state, ssize_t i) const; + + Propagator* get_propagator() const; + + 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 index_transform_; +}; + +} // namespace dwave::optimization::cp diff --git a/dwave/optimization/include/dwave-optimization/cp/core/cpvar.hpp b/dwave/optimization/include/dwave-optimization/cp/core/cpvar.hpp index 82013b27..b721b453 100644 --- a/dwave/optimization/include/dwave-optimization/cp/core/cpvar.hpp +++ b/dwave/optimization/include/dwave-optimization/cp/core/cpvar.hpp @@ -17,6 +17,7 @@ #include #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" @@ -116,6 +117,9 @@ class CPVar { 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; @@ -125,16 +129,16 @@ class CPVar { CPStatus set_max_size(CPVarsState& state, int index) const; // actions for the propagation engine - void schedule_all(CPState& state, const std::vector& propagators) const; + void schedule_all(CPState& state, const std::vector& 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(Propagator* p); - void propagate_on_bounds_change(Propagator* p); - void propagate_on_assignment(Propagator* p); + 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; @@ -142,10 +146,10 @@ class CPVar { /// 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 on_bind; - std::vector on_domain; - std::vector on_bounds; - std::vector on_array_size_change; + std::vector on_bind; + std::vector on_domain; + std::vector on_bounds; + std::vector on_array_size_change; protected: const CPModel& model_; @@ -160,16 +164,16 @@ class CPVar { // domain listener overrides - void bind() override { var_->schedule_all(state_, var_->on_bind); } + void bind(ssize_t i) override { var_->schedule_all(state_, var_->on_bind, i); } - void change() override { var_->schedule_all(state_, var_->on_domain); } + void change(ssize_t i) override { var_->schedule_all(state_, var_->on_domain, i); } - void change_min() override { var_->schedule_all(state_, var_->on_bounds); } + void change_min(ssize_t i) override { var_->schedule_all(state_, var_->on_bounds, i); } - void change_max() override { var_->schedule_all(state_, var_->on_bounds); } + void change_max(ssize_t i) override { var_->schedule_all(state_, var_->on_bounds, i); } - void change_array_size() override { - var_->schedule_all(state_, var_->on_array_size_change); + void change_array_size(ssize_t i) override { + var_->schedule_all(state_, var_->on_array_size_change, i); } private: diff --git a/dwave/optimization/include/dwave-optimization/cp/core/domain_listener.hpp b/dwave/optimization/include/dwave-optimization/cp/core/domain_listener.hpp index 22323418..1743da16 100644 --- a/dwave/optimization/include/dwave-optimization/cp/core/domain_listener.hpp +++ b/dwave/optimization/include/dwave-optimization/cp/core/domain_listener.hpp @@ -21,10 +21,10 @@ class DomainListener { // TODO: check whether this should be removed or not // virtual void empty() = 0; - virtual void bind() = 0; - virtual void change() = 0; - virtual void change_max() = 0; - virtual void change_min() = 0; - virtual void change_array_size() = 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 diff --git a/dwave/optimization/include/dwave-optimization/cp/core/engine.hpp b/dwave/optimization/include/dwave-optimization/cp/core/engine.hpp index 6aa2414c..c5881ecb 100644 --- a/dwave/optimization/include/dwave-optimization/cp/core/engine.hpp +++ b/dwave/optimization/include/dwave-optimization/cp/core/engine.hpp @@ -15,6 +15,7 @@ #pragma once #include +#include #include #include "dwave-optimization/cp/core/propagator.hpp" @@ -48,6 +49,9 @@ class CPState { } void schedule(Propagator* p) { + std::cout << "scheduling propagator from the state...\n"; + std::cout << "propagator active " << p->active(propagator_state_) << "\n"; + std::cout << "propagator scheduled " << p->scheduled(propagator_state_) << "\n"; if (p->active(propagator_state_) and not p->scheduled(propagator_state_)) { p->set_scheduled(propagator_state_, true); propagation_queue_.push_back(p); diff --git a/dwave/optimization/include/dwave-optimization/cp/core/index_transform.hpp b/dwave/optimization/include/dwave-optimization/cp/core/index_transform.hpp new file mode 100644 index 00000000..53bb482c --- /dev/null +++ b/dwave/optimization/include/dwave-optimization/cp/core/index_transform.hpp @@ -0,0 +1,41 @@ +// 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 + +#include "dwave-optimization/common.hpp" + +namespace dwave::optimization::cp { + +/// Interface to transform index of the changing variable to output propagator index +struct IndexTransform { + /// Return the affected indices of the propagator/constraint given the index of a changing + /// variable + /// TODO: keeping this as a vector of output indices, but could well change the signature to + /// ssize_t affected(ssize_t i) + virtual void affected(ssize_t i, std::vector& out) = 0; +}; + +/// Simple case, element-wise transform +struct ElementWiseTransform : IndexTransform { + void affected(ssize_t i, std::vector& out) override { out.push_back(i); } +}; + +/// This assumes that y = sum(x) with y.shape() == (0,) and x has any shape. +struct ReduceTransform : IndexTransform { + void affected(ssize_t i, std::vector& out) override { out.push_back(0); } +}; + +} // namespace dwave::optimization::cp diff --git a/dwave/optimization/include/dwave-optimization/cp/core/propagator.hpp b/dwave/optimization/include/dwave-optimization/cp/core/propagator.hpp index 88276d19..2a1e6ae3 100644 --- a/dwave/optimization/include/dwave-optimization/cp/core/propagator.hpp +++ b/dwave/optimization/include/dwave-optimization/cp/core/propagator.hpp @@ -14,6 +14,7 @@ #pragma once +#include #include #include @@ -22,20 +23,49 @@ namespace dwave::optimization::cp { + // forward declaration + + class CPState; + + // internal structure of propagators class PropagatorData { + protected: + /// Helper class to handle the indices to process + struct ProcessingIndexHelper { + // constructor + ProcessingIndexHelper(ssize_t n) { is_scheduled.resize(n, false); } + + std::deque to_process; + std::vector is_scheduled; + }; + public: virtual ~PropagatorData() = default; + + PropagatorData(StateManager* sm, ssize_t constraint_size) : indices(constraint_size) { + active_ = sm->make_state_bool(true); + } + + /// Check whether the propagator is already scheduled bool scheduled() const { return scheduled_; } + + /// Set the scheduled status of the propagator void set_scheduled(bool scheduled) { scheduled_ = scheduled; } + + /// Check whether the propagator is active (not active when the constraint is entailed) bool active() const { return active_->get_value(); } void set_active(bool active) { active_->set_value(active); } // indices of the constraint (corresponding to this propagator) to propagate. - std::vector indices; + void mark_index(ssize_t index); + + ProcessingIndexHelper indices; protected: bool scheduled_; + + // TODO: should this be a vector? Probably yes StateBool* active_; }; @@ -48,10 +78,18 @@ class Propagator { Propagator(int index) : propagator_index_(index) {} template PData> - PData* data_ptr(CPPropagatorsState& state) const; + PData* data_ptr(CPPropagatorsState& state) const { + assert(propagator_index_ >= 0); + assert(propagator_index_ < static_cast(state.size())); + return static_cast(state[propagator_index_].get()); + } template PData> - const PData* data_ptr(const CPPropagatorsState& state) const; + const PData* data_ptr(const CPPropagatorsState& state) const { + assert(propagator_index_ >= 0); + assert(propagator_index_ < static_cast(state.size())); + return static_cast(state[propagator_index_].get()); + } bool scheduled(const CPPropagatorsState& state) const; @@ -61,9 +99,14 @@ class Propagator { void set_active(CPPropagatorsState& state, bool active) const; - virtual CPStatus propagate(CPPropagatorsState& p_state, CPVarsState& v_state) = 0; + virtual CPStatus propagate(CPPropagatorsState& p_state, CPVarsState& v_state) const = 0; - private: + virtual void initialize_state(CPState& state) const = 0; + + void mark_index(CPPropagatorsState& p_state, ssize_t index) const; + + protected: + // Index of the propagator to access the respective propagator state const ssize_t propagator_index_; }; diff --git a/dwave/optimization/include/dwave-optimization/cp/propagators/identity_propagator.hpp b/dwave/optimization/include/dwave-optimization/cp/propagators/identity_propagator.hpp new file mode 100644 index 00000000..8dec00ab --- /dev/null +++ b/dwave/optimization/include/dwave-optimization/cp/propagators/identity_propagator.hpp @@ -0,0 +1,48 @@ +// 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/cpvar.hpp" +#include "dwave-optimization/cp/core/propagator.hpp" + +namespace dwave::optimization::cp { + +/// This is an identity propagator, it does nothing, leaves the domains untouched but it is used for +/// testing the "delta" propagation, where we mark indices of the constraints array to propagate. +class ElementWiseIdentityPropagator : public Propagator { + public: + // constructor + ElementWiseIdentityPropagator(ssize_t index, CPVar* var); + + void initialize_state(CPState& state) const override; + CPStatus propagate(CPPropagatorsState& p_state, CPVarsState& v_state) const override; + + private: + CPVar* var_; +}; + +/// This is an identity propagator, it does nothing, leaves the domains untouched but it is used for +/// testing the "delta" propagation, where we mark indices of the constraints array to propagate. +class ReductionIdentityPropagator : public Propagator { + public: + // constructor + ReductionIdentityPropagator(ssize_t index, CPVar* var); + + void initialize_state(CPState& state) const override; + CPStatus propagate(CPPropagatorsState& p_state, CPVarsState& v_state) const override; + + private: + CPVar* var_; +}; +} // namespace dwave::optimization::cp diff --git a/dwave/optimization/src/cp/core/advisor.cpp b/dwave/optimization/src/cp/core/advisor.cpp new file mode 100644 index 00000000..a63e63fe --- /dev/null +++ b/dwave/optimization/src/cp/core/advisor.cpp @@ -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. + +#include "dwave-optimization/cp/core/advisor.hpp" + +namespace dwave::optimization::cp { +Advisor::Advisor(Propagator* p, ssize_t p_input, std::unique_ptr index_transform) + : p_(p), p_input_(p_input), index_transform_(std::move(index_transform)) {} + +void Advisor::notify(CPPropagatorsState& p_state, ssize_t i) const { + std::vector out; + index_transform_->affected(i, out); + for (ssize_t j : out) { + p_->mark_index(p_state, j); + } +} + +Propagator* Advisor::get_propagator() const { return p_; } +} // namespace dwave::optimization::cp diff --git a/dwave/optimization/src/cp/core/cpvar.cpp b/dwave/optimization/src/cp/core/cpvar.cpp index 310e1e02..0513e735 100644 --- a/dwave/optimization/src/cp/core/cpvar.cpp +++ b/dwave/optimization/src/cp/core/cpvar.cpp @@ -67,6 +67,28 @@ ssize_t CPVar::max_size(const CPVarsState& state) const { return data->max_size(); } +ssize_t CPVar::min_size() const { + if (node_->size() > 0) { + // Static array + return node_->size(); + } + SizeInfo info = node_->sizeinfo(); + // The minimum size value existence should be guaranteed at construction of the CPVar + assert(info.min.has_value()); + return info.min.value(); +} + +ssize_t CPVar::max_size() const { + if (node_->size() > 0) { + // Static array + return node_->size(); + } + SizeInfo info = node_->sizeinfo(); + // The maximum size value existence should be guaranteed at construction of the CPVar + assert(info.max.has_value()); + return info.max.value(); +} + CPStatus CPVar::remove(CPVarsState& state, double value, int index) const { CPVarData* data = data_ptr(state); return data->remove(value, index); @@ -97,17 +119,19 @@ CPStatus CPVar::set_max_size(CPVarsState& state, int new_max_size) const { return data->update_max_size(new_max_size); } -void CPVar::schedule_all(CPState& state, const std::vector& propagators) const { - for (auto p : propagators) { - state.schedule(p); +void CPVar::schedule_all(CPState& state, const std::vector& advisors, ssize_t i) const { + CPPropagatorsState& p_state = state.get_propagators_state(); + for (const Advisor& adv : advisors) { + adv.notify(p_state, i); + state.schedule(adv.get_propagator()); } } -void CPVar::propagate_on_assignment(Propagator* p) { on_bind.push_back(p); } +void CPVar::propagate_on_assignment(Advisor&& adv) { on_bind.push_back(std::move(adv)); } -void CPVar::propagate_on_bounds_change(Propagator* p) { on_bounds.push_back(p); } +void CPVar::propagate_on_bounds_change(Advisor&& adv) { on_bounds.push_back(std::move(adv)); } -void CPVar::propagate_on_domain_change(Propagator* p) { on_domain.push_back(p); } +void CPVar::propagate_on_domain_change(Advisor&& adv) { on_domain.push_back(std::move(adv)); } void CPVar::initialize_state(CPState& state) const { assert(this->cp_var_index_ >= 0); diff --git a/dwave/optimization/src/cp/core/interval_array.cpp b/dwave/optimization/src/cp/core/interval_array.cpp index cae1208b..33718725 100644 --- a/dwave/optimization/src/cp/core/interval_array.cpp +++ b/dwave/optimization/src/cp/core/interval_array.cpp @@ -262,11 +262,12 @@ CPStatus IntervalArray::remove(double value, int index, DomainListener* if (change_max) { max_[index]->set_value(value - 1); - l->change_max(); + l->change_max(index); } else if (change_min) { min_[index]->set_value(value + 1); - l->change_min(); + l->change_min(index); } + l->change(index); } return CPStatus::OK; } @@ -286,7 +287,8 @@ CPStatus IntervalArray::remove_above(double value, int index, DomainList if (max_[index]->get_value() <= value) return CPStatus::OK; max_[index]->set_value(value); - l->change_max(); + l->change_max(index); + l->change(index); return CPStatus::OK; } @@ -305,7 +307,8 @@ CPStatus IntervalArray::remove_above(double value, int index, DomainLis if (max_[index]->get_value() <= value) return CPStatus::OK; max_[index]->set_value(std::floor(value)); - l->change_max(); + l->change_max(index); + l->change(index); return CPStatus::OK; } @@ -324,7 +327,8 @@ CPStatus IntervalArray::remove_below(double value, int index, DomainList if (min_[index]->get_value() >= value) return CPStatus::OK; min_[index]->set_value(value); - l->change_min(); + l->change_min(index); + l->change(index); return CPStatus::OK; } @@ -343,7 +347,8 @@ CPStatus IntervalArray::remove_below(double value, int index, DomainLis if (min_[index]->get_value() >= value) return CPStatus::OK; min_[index]->set_value(std::ceil(value)); - l->change_min(); + l->change_min(index); + l->change(index); return CPStatus::OK; } @@ -364,8 +369,10 @@ CPStatus IntervalArray::remove_all_but(double value, int index, DomainListene min_[index]->set_value(value); max_[index]->set_value(value); - if (changed_max) l->change_max(); - if (changed_min) l->change_min(); + if (changed_max) l->change_max(index); + if (changed_min) l->change_min(index); + l->change(index); + l->bind(index); return CPStatus::OK; } @@ -380,7 +387,7 @@ CPStatus IntervalArray::update_min_size(int new_min_size, DomainListener* l) if (new_min_size != min_size_->get_value()) { min_size_->set_value(new_min_size); - l->change_array_size(); + l->change_array_size(new_min_size); } return CPStatus::OK; } @@ -395,7 +402,7 @@ CPStatus IntervalArray::update_max_size(int new_max_size, DomainListener* l) if (new_max_size != max_size_->get_value()) { max_size_->set_value(new_max_size); - l->change_array_size(); + l->change_array_size(new_max_size); } return CPStatus::OK; } diff --git a/dwave/optimization/src/cp/core/propagator.cpp b/dwave/optimization/src/cp/core/propagator.cpp index ebd402c1..48582cde 100644 --- a/dwave/optimization/src/cp/core/propagator.cpp +++ b/dwave/optimization/src/cp/core/propagator.cpp @@ -13,21 +13,13 @@ // limitations under the License. #include "dwave-optimization/cp/core/propagator.hpp" - namespace dwave::optimization::cp { -template PData> -PData* Propagator::data_ptr(CPPropagatorsState& state) const { - assert(propagator_index_ >= 0); - assert(propagator_index_ < static_cast(state.size())); - return static_cast(state[propagator_index_].get()); -} - -template PData> -const PData* Propagator::data_ptr(const CPPropagatorsState& state) const { - assert(propagator_index_ >= 0); - assert(propagator_index_ < static_cast(state.size())); - return static_cast(state[propagator_index_].get()); +void PropagatorData::mark_index(ssize_t index) { + assert(index < static_cast(indices.is_scheduled.size())); + if (indices.is_scheduled[index]) return; + indices.is_scheduled[index] = true; + indices.to_process.push_back(index); } bool Propagator::scheduled(const CPPropagatorsState& state) const { @@ -53,4 +45,10 @@ void Propagator::set_active(CPPropagatorsState& state, bool active) const { assert(propagator_index_ < static_cast(state.size())); state[propagator_index_]->set_active(active); } + +void Propagator::mark_index(CPPropagatorsState& state, ssize_t index) const { + assert(propagator_index_ >= 0); + assert(index >= 0); + state[propagator_index_]->mark_index(index); +} } // namespace dwave::optimization::cp diff --git a/dwave/optimization/src/cp/propagators/identity_propagator.cpp b/dwave/optimization/src/cp/propagators/identity_propagator.cpp new file mode 100644 index 00000000..0dfa0e55 --- /dev/null +++ b/dwave/optimization/src/cp/propagators/identity_propagator.cpp @@ -0,0 +1,75 @@ +// 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. + +#include "dwave-optimization/cp/propagators/identity_propagator.hpp" + +#include +namespace dwave::optimization::cp { + +ElementWiseIdentityPropagator::ElementWiseIdentityPropagator(ssize_t index, CPVar* var) + : Propagator(index) { + // TODO: not supporting dynamic variables for now + assert(var->min_size() == var->max_size()); + var_ = var; +} + +void ElementWiseIdentityPropagator::initialize_state(CPState& state) const { + CPPropagatorsState& p_state = state.get_propagators_state(); + assert(propagator_index_ >= 0); + assert(propagator_index_ < static_cast(p_state.size())); + p_state[propagator_index_] = + std::make_unique(state.get_state_manager(), var_->max_size()); +} + +CPStatus ElementWiseIdentityPropagator::propagate(CPPropagatorsState& p_state, + CPVarsState& v_state) const { + auto data = data_ptr(p_state); + assert(data->indices.to_process.size() > 0); + while (data->indices.to_process.size() > 0) { + ssize_t i = data->indices.to_process.front(); + data->indices.to_process.pop_front(); + data->indices.is_scheduled[i] = false; + /// Note: this is where other propagator would filter domains.. + } + return CPStatus::OK; +} + +ReductionIdentityPropagator::ReductionIdentityPropagator(ssize_t index, CPVar* var) + : Propagator(index) { + // TODO: not supporting dynamic variables for now + assert(var->min_size() == var->max_size()); + var_ = var; +} + +void ReductionIdentityPropagator::initialize_state(CPState& state) const { + CPPropagatorsState& p_state = state.get_propagators_state(); + assert(propagator_index_ >= 0); + assert(propagator_index_ < static_cast(p_state.size())); + p_state[propagator_index_] = std::make_unique(state.get_state_manager(), 0); +} + +CPStatus ReductionIdentityPropagator::propagate(CPPropagatorsState& p_state, + CPVarsState& v_state) const { + auto data = data_ptr(p_state); + assert(data->indices.to_process.size() == 1); + while (data->indices.to_process.size() > 0) { + ssize_t i = data->indices.to_process.front(); + data->indices.to_process.pop_front(); + data->indices.is_scheduled[i] = false; + /// Note: this is where other propagator would filter domains.. + } + return CPStatus::OK; +} + +} // namespace dwave::optimization::cp diff --git a/meson.build b/meson.build index ebe05aeb..384a6a2c 100644 --- a/meson.build +++ b/meson.build @@ -54,11 +54,14 @@ dwave_optimization_src = [ 'dwave/optimization/src/graph.cpp', 'dwave/optimization/src/simplex.cpp', + 'dwave/optimization/src/cp/core/advisor.cpp', 'dwave/optimization/src/cp/core/interval_array.cpp', 'dwave/optimization/src/cp/core/propagator.cpp', 'dwave/optimization/src/cp/core/engine.cpp', 'dwave/optimization/src/cp/core/cpvar.cpp', + 'dwave/optimization/src/cp/propagators/identity_propagator.cpp', + 'dwave/optimization/src/cp/state/copier.cpp', 'dwave/optimization/src/cp/state/copy.cpp', 'dwave/optimization/src/cp/state/cpvar_state.cpp', diff --git a/tests/cpp/cp/test_cp_var.cpp b/tests/cpp/cp/test_cp_var.cpp index d7db7ecc..39c8f4d0 100644 --- a/tests/cpp/cp/test_cp_var.cpp +++ b/tests/cpp/cp/test_cp_var.cpp @@ -32,83 +32,158 @@ using Catch::Matchers::RangeEquals; namespace dwave::optimization::cp { TEST_CASE("IntegerNode -> CPVar") { - dwave::optimization::Graph graph; - - // Add an integer node - graph.emplace_node(10, 0, 2); - - // Lock the graph - graph.topological_sort(); - - // Construct the CP corresponding model - CPModel model; - - // Start adding the - std::vector vars; - for (const auto& n_uptr : graph.nodes()) { - const ArrayNode* ptr = dynamic_cast(n_uptr.get()); - REQUIRE(ptr); - vars.push_back(model.emplace_variable(model, ptr, ptr->topological_index())); - } - - // Initialize an empty state? - CPState state = model.initialize_state(); - REQUIRE(state.get_propagators_state().size() == 0); - REQUIRE(state.get_variables_state().size() == 1); - - auto* var = vars[0]; - var->initialize_state(state); - REQUIRE(var->min_size(state.get_variables_state()) == - var->max_size(state.get_variables_state())); - REQUIRE(var->min_size(state.get_variables_state()) == 10); - - for (ssize_t i = 0; i < 10; ++i) { - REQUIRE(var->min(state.get_variables_state(), i) == 0); - REQUIRE(var->max(state.get_variables_state(), i) == 2); - REQUIRE(var->size(state.get_variables_state(), i) == 3); - REQUIRE(var->is_active(state.get_variables_state(), i)); + GIVEN("A Graph and an integer node") { + dwave::optimization::Graph graph; + + // Add an integer node + graph.emplace_node(10, 0, 5); + + // Lock the graph + graph.topological_sort(); + + // Construct the CP corresponding model + AND_GIVEN("The CP Model") { + CPModel model; + + // Start adding the + std::vector vars; + for (const auto& n_uptr : graph.nodes()) { + const ArrayNode* ptr = dynamic_cast(n_uptr.get()); + REQUIRE(ptr); + vars.push_back(model.emplace_variable(model, ptr, ptr->topological_index())); + } + + // Initialize an empty state? + WHEN("We initialize the state") { + CPState state = model.initialize_state(); + REQUIRE(state.get_propagators_state().size() == 0); + REQUIRE(state.get_variables_state().size() == 1); + + auto* var = vars[0]; + var->initialize_state(state); + CPVarsState& s_state = state.get_variables_state(); + THEN("The size and domains are correct") { + REQUIRE(var->min_size(s_state) == var->max_size(s_state)); + REQUIRE(var->min_size(s_state) == 10); + + for (ssize_t i = 0; i < 10; ++i) { + REQUIRE(var->min(s_state, i) == 0); + REQUIRE(var->max(s_state, i) == 5); + REQUIRE(var->size(s_state, i) == 6); + REQUIRE(var->is_active(s_state, i)); + } + } + + AND_WHEN("We remove above 3 from variable 2") { + auto status = var->remove_above(s_state, 3, 2); + THEN("The domain is correctly set") { + REQUIRE(status == CPStatus::OK); + REQUIRE(var->is_active(s_state, 2)); + REQUIRE(var->min(s_state, 2) == 0); + REQUIRE(var->max(s_state, 2) == 3); + REQUIRE(var->size(s_state, 2) == 4); + } + } + + AND_WHEN("We remove below 3 from variable 1") { + auto status = var->remove_below(s_state, 3, 1); + THEN("The domain is correctly set") { + REQUIRE(status == CPStatus::OK); + REQUIRE(var->is_active(s_state, 1)); + REQUIRE(var->min(s_state, 1) == 3); + REQUIRE(var->max(s_state, 1) == 5); + REQUIRE(var->size(s_state, 1) == 3); + } + } + + AND_WHEN("We remove below 10 from variable 7") { + auto status = var->remove_below(s_state, 10, 7); + THEN("We wiped out the domain") { REQUIRE(status == CPStatus::Inconsistency); } + } + } + } } } TEST_CASE("ListNode -> CPVar") { - dwave::optimization::Graph graph; - - // Add an list node - graph.emplace_node(10, 2, 5); - - // Lock the graph - graph.topological_sort(); - - // Construct the CP corresponding model - CPModel model; - - // Start adding the - std::vector vars; - for (const auto& n_uptr : graph.nodes()) { - const ArrayNode* ptr = dynamic_cast(n_uptr.get()); - REQUIRE(ptr); - vars.push_back(model.emplace_variable(model, ptr, ptr->topological_index())); - } - - // Initialize an empty state? - CPState state = model.initialize_state(); - REQUIRE(state.get_propagators_state().size() == 0); - REQUIRE(state.get_variables_state().size() == 1); - - auto* var = vars[0]; - var->initialize_state(state); - REQUIRE(var->min_size(state.get_variables_state()) == 2); - REQUIRE(var->max_size(state.get_variables_state()) == 5); - - for (ssize_t i = 0; i < 5; ++i) { - REQUIRE(var->min(state.get_variables_state(), i) == 0); - REQUIRE(var->max(state.get_variables_state(), i) == 9); - REQUIRE(var->size(state.get_variables_state(), i) == 10); - if (i < 2) { - REQUIRE(var->is_active(state.get_variables_state(), i)); - } else { - REQUIRE_FALSE(var->is_active(state.get_variables_state(), i)); - REQUIRE(var->maybe_active(state.get_variables_state(), i)); + GIVEN("A Graph and a list node") { + dwave::optimization::Graph graph; + + // Add an list node + graph.emplace_node(10, 2, 5); + + // Lock the graph + graph.topological_sort(); + + // Construct the CP corresponding model + AND_GIVEN("The CP Model") { + CPModel model; + + // Start adding the + std::vector vars; + for (const auto& n_uptr : graph.nodes()) { + const ArrayNode* ptr = dynamic_cast(n_uptr.get()); + REQUIRE(ptr); + vars.push_back(model.emplace_variable(model, ptr, ptr->topological_index())); + } + + WHEN("We initialize the state") { + // Initialize an empty state? + CPState state = model.initialize_state(); + REQUIRE(state.get_propagators_state().size() == 0); + REQUIRE(state.get_variables_state().size() == 1); + + auto* var = vars[0]; + var->initialize_state(state); + CPVarsState& s_state = state.get_variables_state(); + THEN("The size and domains are correct") { + REQUIRE(var->min_size(s_state) == 2); + REQUIRE(var->max_size(s_state) == 5); + + for (ssize_t i = 0; i < 5; ++i) { + REQUIRE(var->min(s_state, i) == 0); + REQUIRE(var->max(s_state, i) == 9); + REQUIRE(var->size(s_state, i) == 10); + if (i < 2) { + REQUIRE(var->is_active(s_state, i)); + } else { + REQUIRE_FALSE(var->is_active(s_state, i)); + REQUIRE(var->maybe_active(s_state, i)); + } + } + } + + AND_WHEN("We remove above 3 from variable 2") { + auto status = var->remove_above(s_state, 3, 2); + THEN("The domain is correctly set") { + REQUIRE(status == CPStatus::OK); + REQUIRE_FALSE(var->is_active(s_state, 2)); + REQUIRE(var->maybe_active(s_state, 2)); + REQUIRE(var->min(s_state, 2) == 0); + REQUIRE(var->max(s_state, 2) == 3); + REQUIRE(var->size(s_state, 2) == 4); + } + } + + AND_WHEN("We remove below 3 from variable 1") { + auto status = var->remove_below(s_state, 3, 1); + THEN("The domain is correctly set") { + REQUIRE(status == CPStatus::OK); + REQUIRE(var->is_active(s_state, 1)); + REQUIRE(var->min(s_state, 1) == 3); + REQUIRE(var->max(s_state, 1) == 9); + REQUIRE(var->size(s_state, 1) == 7); + } + } + + AND_WHEN("We remove below 11 from variable 4") { + auto status = var->remove_below(s_state, 11, 4); + THEN("We wipe out an optional domain and removed the array size") { + REQUIRE(status == CPStatus::OK); + REQUIRE(var->max_size(s_state) == 4); + } + } + } } } } diff --git a/tests/cpp/cp/test_propagator.cpp b/tests/cpp/cp/test_propagator.cpp new file mode 100644 index 00000000..4b917c56 --- /dev/null +++ b/tests/cpp/cp/test_propagator.cpp @@ -0,0 +1,95 @@ +// 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. + +#include +#include +#include + +#include "catch2/catch_test_macros.hpp" +#include "catch2/matchers/catch_matchers_all.hpp" +#include "dwave-optimization/array.hpp" +#include "dwave-optimization/cp/core/cpvar.hpp" +#include "dwave-optimization/cp/core/index_transform.hpp" +#include "dwave-optimization/cp/core/interval_array.hpp" +#include "dwave-optimization/cp/propagators/identity_propagator.hpp" +#include "dwave-optimization/cp/state/copier.hpp" +#include "dwave-optimization/nodes.hpp" +#include "dwave-optimization/state.hpp" +#include "utils.hpp" + +using Catch::Matchers::RangeEquals; + +namespace dwave::optimization::cp { +TEST_CASE("ElementWiseIdentityPropagator") { + GIVEN("A Graph and an integer node") { + dwave::optimization::Graph graph; + + // Add an integer node + graph.emplace_node(10, 0, 5); + + // Lock the graph + graph.topological_sort(); + + // Construct the CP corresponding model + AND_GIVEN("The CP Model") { + CPModel model; + + // Add the variabbles to the model + std::vector vars; + for (const auto& n_uptr : graph.nodes()) { + const ArrayNode* ptr = dynamic_cast(n_uptr.get()); + REQUIRE(ptr); + vars.push_back(model.emplace_variable(model, ptr, ptr->topological_index())); + } + CPVar* var = vars[0]; + Propagator* p = model.emplace_propagator( + model.num_propagators(), var); + + // build the advisor for the propagator p aimed to the variable var + Advisor advisor(p, 0, std::make_unique()); + var->propagate_on_domain_change(std::move(advisor)); + REQUIRE(var->on_domain.size() == 1); + WHEN("We initialize the a state") { + CPState state = model.initialize_state(); + CPVarsState& s_state = state.get_variables_state(); + CPPropagatorsState& p_state = state.get_propagators_state(); + + REQUIRE(s_state.size() == 1); + REQUIRE(p_state.size() == 1); + + var->initialize_state(state); + p->initialize_state(state); + + AND_WHEN("We alter the domain of one variable") { + CPStatus status = var->assign(s_state, 3, 2); + REQUIRE(status == CPStatus::OK); + THEN("We see that the propagator is triggered to run on the same index") { + REQUIRE(p_state[0]->scheduled()); + REQUIRE(p_state[0]->indices.to_process.size() == 1); + + std::cout << "Checking the scheduled indices..."; + for (int i = 0; i < 10; ++i) { + if (i == 2) { + REQUIRE(p_state[0]->indices.is_scheduled[i]); + } else { + REQUIRE_FALSE(p_state[0]->indices.is_scheduled[i]); + } + } + } + } + } + } + } +} +} // namespace dwave::optimization::cp diff --git a/tests/cpp/cp/utils.hpp b/tests/cpp/cp/utils.hpp index c5043cda..6f7b64d4 100644 --- a/tests/cpp/cp/utils.hpp +++ b/tests/cpp/cp/utils.hpp @@ -19,10 +19,10 @@ namespace dwave::optimization::cp { class TestListener : public DomainListener { public: - void bind() override {} - void change() override {} - void change_max() override {} - void change_min() override {} - void change_array_size() override {} + void bind(ssize_t i) override {} + void change(ssize_t i) override {} + void change_max(ssize_t i) override {} + void change_min(ssize_t i) override {} + void change_array_size(ssize_t i) override {} }; } // namespace dwave::optimization::cp diff --git a/tests/cpp/meson.build b/tests/cpp/meson.build index 351cf021..2232e604 100644 --- a/tests/cpp/meson.build +++ b/tests/cpp/meson.build @@ -10,6 +10,7 @@ tests_all = executable( 'cp/test_interval.cpp', 'cp/test_copier.cpp', 'cp/test_cp_var.cpp', + 'cp/test_propagator.cpp', 'nodes/indexing/test_advanced.cpp', 'nodes/indexing/test_basic.cpp', From c6794ebdf4dcecdadf3baf6ccf13ba7abeb40760 Mon Sep 17 00:00:00 2001 From: azucca Date: Fri, 6 Mar 2026 15:59:05 -0800 Subject: [PATCH 15/19] Remove printouts --- .../include/dwave-optimization/cp/core/engine.hpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/dwave/optimization/include/dwave-optimization/cp/core/engine.hpp b/dwave/optimization/include/dwave-optimization/cp/core/engine.hpp index c5881ecb..6aa2414c 100644 --- a/dwave/optimization/include/dwave-optimization/cp/core/engine.hpp +++ b/dwave/optimization/include/dwave-optimization/cp/core/engine.hpp @@ -15,7 +15,6 @@ #pragma once #include -#include #include #include "dwave-optimization/cp/core/propagator.hpp" @@ -49,9 +48,6 @@ class CPState { } void schedule(Propagator* p) { - std::cout << "scheduling propagator from the state...\n"; - std::cout << "propagator active " << p->active(propagator_state_) << "\n"; - std::cout << "propagator scheduled " << p->scheduled(propagator_state_) << "\n"; if (p->active(propagator_state_) and not p->scheduled(propagator_state_)) { p->set_scheduled(propagator_state_, true); propagation_queue_.push_back(p); From 07e4afbccbe0b5dfa8f1e6b3f9e9c27887d705da Mon Sep 17 00:00:00 2001 From: azucca Date: Fri, 6 Mar 2026 16:10:10 -0800 Subject: [PATCH 16/19] Initialize scheduled_ bool to false --- .../include/dwave-optimization/cp/core/propagator.hpp | 1 + tests/cpp/cp/test_propagator.cpp | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/dwave/optimization/include/dwave-optimization/cp/core/propagator.hpp b/dwave/optimization/include/dwave-optimization/cp/core/propagator.hpp index 2a1e6ae3..56a1ea6e 100644 --- a/dwave/optimization/include/dwave-optimization/cp/core/propagator.hpp +++ b/dwave/optimization/include/dwave-optimization/cp/core/propagator.hpp @@ -45,6 +45,7 @@ class PropagatorData { PropagatorData(StateManager* sm, ssize_t constraint_size) : indices(constraint_size) { active_ = sm->make_state_bool(true); + scheduled_ = false; } /// Check whether the propagator is already scheduled diff --git a/tests/cpp/cp/test_propagator.cpp b/tests/cpp/cp/test_propagator.cpp index 4b917c56..9e276b7a 100644 --- a/tests/cpp/cp/test_propagator.cpp +++ b/tests/cpp/cp/test_propagator.cpp @@ -78,7 +78,6 @@ TEST_CASE("ElementWiseIdentityPropagator") { REQUIRE(p_state[0]->scheduled()); REQUIRE(p_state[0]->indices.to_process.size() == 1); - std::cout << "Checking the scheduled indices..."; for (int i = 0; i < 10; ++i) { if (i == 2) { REQUIRE(p_state[0]->indices.is_scheduled[i]); From 9f243400c1414f7d173a742c4b16e9c7449b2e67 Mon Sep 17 00:00:00 2001 From: azucca Date: Fri, 6 Mar 2026 16:16:10 -0800 Subject: [PATCH 17/19] Add getter method for input index --- .../optimization/include/dwave-optimization/cp/core/advisor.hpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dwave/optimization/include/dwave-optimization/cp/core/advisor.hpp b/dwave/optimization/include/dwave-optimization/cp/core/advisor.hpp index b683439f..e898a9bd 100644 --- a/dwave/optimization/include/dwave-optimization/cp/core/advisor.hpp +++ b/dwave/optimization/include/dwave-optimization/cp/core/advisor.hpp @@ -29,6 +29,8 @@ class Advisor { Propagator* get_propagator() const; + ssize_t input_index() const { return p_input_; } + private: // The propagator the advisor is watching Propagator* p_; From dafc3f01cd751b300eec6b1548da59f5ffaada86 Mon Sep 17 00:00:00 2001 From: azucca Date: Fri, 6 Mar 2026 16:18:49 -0800 Subject: [PATCH 18/19] Add virtual destructor --- .../include/dwave-optimization/cp/core/index_transform.hpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dwave/optimization/include/dwave-optimization/cp/core/index_transform.hpp b/dwave/optimization/include/dwave-optimization/cp/core/index_transform.hpp index 53bb482c..12f5d5d4 100644 --- a/dwave/optimization/include/dwave-optimization/cp/core/index_transform.hpp +++ b/dwave/optimization/include/dwave-optimization/cp/core/index_transform.hpp @@ -21,6 +21,10 @@ namespace dwave::optimization::cp { /// Interface to transform index of the changing variable to output propagator index struct IndexTransform { + + // Default destructor + virtual ~IndexTransform() = default; + /// Return the affected indices of the propagator/constraint given the index of a changing /// variable /// TODO: keeping this as a vector of output indices, but could well change the signature to From b3bcf72ba8fd9f4f192b13ada5734cdcc8a2d9f9 Mon Sep 17 00:00:00 2001 From: azucca Date: Thu, 12 Mar 2026 18:29:12 -0700 Subject: [PATCH 19/19] Add basic binary ops and reduce --- .../dwave-optimization/cp/core/cpvar.hpp | 3 + .../dwave-optimization/cp/core/engine.hpp | 2 +- .../dwave-optimization/cp/core/propagator.hpp | 60 +-- .../dwave-optimization/cp/core/status.hpp | 2 +- .../cp/propagators/binaryop.hpp | 38 ++ .../cp/propagators/reduce.hpp | 35 ++ dwave/optimization/src/cp/core/cpvar.cpp | 3 + dwave/optimization/src/cp/core/engine.cpp | 5 +- .../src/cp/core/interval_array.cpp | 10 +- dwave/optimization/src/cp/core/propagator.cpp | 55 ++- .../src/cp/propagators/binaryop.cpp | 157 +++++++ .../cp/propagators/identity_propagator.cpp | 22 +- .../src/cp/propagators/reduce.cpp | 147 +++++++ meson.build | 2 + tests/cpp/cp/propagators/test_binaryop.cpp | 397 ++++++++++++++++++ tests/cpp/cp/propagators/test_reduce.cpp | 127 ++++++ tests/cpp/cp/test_propagator.cpp | 26 +- tests/cpp/meson.build | 2 + 18 files changed, 1039 insertions(+), 54 deletions(-) create mode 100644 dwave/optimization/include/dwave-optimization/cp/propagators/binaryop.hpp create mode 100644 dwave/optimization/include/dwave-optimization/cp/propagators/reduce.hpp create mode 100644 dwave/optimization/src/cp/propagators/binaryop.cpp create mode 100644 dwave/optimization/src/cp/propagators/reduce.cpp create mode 100644 tests/cpp/cp/propagators/test_binaryop.cpp create mode 100644 tests/cpp/cp/propagators/test_reduce.cpp diff --git a/dwave/optimization/include/dwave-optimization/cp/core/cpvar.hpp b/dwave/optimization/include/dwave-optimization/cp/core/cpvar.hpp index b721b453..bbd6c0d2 100644 --- a/dwave/optimization/include/dwave-optimization/cp/core/cpvar.hpp +++ b/dwave/optimization/include/dwave-optimization/cp/core/cpvar.hpp @@ -92,6 +92,9 @@ class CPVar { // constructor CPVar(const CPModel& model, const dwave::optimization::ArrayNode* node_ptr, int index); + CPVar(const CPVar& other) = delete; + CPVar() = delete; + const CPModel& get_model() const { return model_; } template StateData> diff --git a/dwave/optimization/include/dwave-optimization/cp/core/engine.hpp b/dwave/optimization/include/dwave-optimization/cp/core/engine.hpp index 6aa2414c..a7cc17ad 100644 --- a/dwave/optimization/include/dwave-optimization/cp/core/engine.hpp +++ b/dwave/optimization/include/dwave-optimization/cp/core/engine.hpp @@ -48,7 +48,7 @@ class CPState { } void schedule(Propagator* p) { - if (p->active(propagator_state_) and not p->scheduled(propagator_state_)) { + if ((not p->scheduled(propagator_state_)) and (p->num_indices_to_process(propagator_state_) > 0)){ p->set_scheduled(propagator_state_, true); propagation_queue_.push_back(p); } diff --git a/dwave/optimization/include/dwave-optimization/cp/core/propagator.hpp b/dwave/optimization/include/dwave-optimization/cp/core/propagator.hpp index 56a1ea6e..e138bd7a 100644 --- a/dwave/optimization/include/dwave-optimization/cp/core/propagator.hpp +++ b/dwave/optimization/include/dwave-optimization/cp/core/propagator.hpp @@ -23,55 +23,56 @@ namespace dwave::optimization::cp { - // forward declaration - - class CPState; +// forward declaration +class CPState; +class Propagator; // internal structure of propagators class PropagatorData { - protected: - /// Helper class to handle the indices to process - struct ProcessingIndexHelper { - // constructor - ProcessingIndexHelper(ssize_t n) { is_scheduled.resize(n, false); } - - std::deque to_process; - std::vector is_scheduled; - }; - public: virtual ~PropagatorData() = default; - PropagatorData(StateManager* sm, ssize_t constraint_size) : indices(constraint_size) { - active_ = sm->make_state_bool(true); - scheduled_ = false; + PropagatorData(StateManager* sm, ssize_t constraint_size) : n_(constraint_size) { + scheduled_ = false; + is_scheduled_.resize(constraint_size, false); + active_.resize(constraint_size); + for (ssize_t i = 0; i < constraint_size; ++i) { + active_[i] = sm->make_state_bool(true); + } } /// Check whether the propagator is already scheduled - bool scheduled() const { return scheduled_; } + bool scheduled() const; + bool scheduled(ssize_t i) const; /// Set the scheduled status of the propagator - void set_scheduled(bool scheduled) { scheduled_ = scheduled; } + void set_scheduled(bool scheduled); + void set_scheduled(bool scheduled, ssize_t); /// Check whether the propagator is active (not active when the constraint is entailed) - bool active() const { return active_->get_value(); } - void set_active(bool active) { active_->set_value(active); } + bool active(ssize_t i) const; + void set_active(bool active, ssize_t i); // indices of the constraint (corresponding to this propagator) to propagate. void mark_index(ssize_t index); - ProcessingIndexHelper indices; + ssize_t num_indices_to_process() const { return to_process_.size(); } + std::deque& indices_to_process() { return to_process_; } protected: + // size of the constraint the propagator is associated with + const ssize_t n_; bool scheduled_; - - // TODO: should this be a vector? Probably yes - StateBool* active_; + std::deque to_process_; + std::vector is_scheduled_; + std::vector active_; }; using CPPropagatorsState = std::vector>; +/// Base class for propagators. A propagator, or filtering function, implements the inference done +/// on the variables domains from a constraint. class Propagator { public: virtual ~Propagator() = default; @@ -94,18 +95,25 @@ class Propagator { bool scheduled(const CPPropagatorsState& state) const; + bool scheduled(const CPPropagatorsState& state, int i) const; + void set_scheduled(CPPropagatorsState& state, bool scheduled) const; - bool active(const CPPropagatorsState& state) const; + bool active(const CPPropagatorsState& state, int i) const; - void set_active(CPPropagatorsState& state, bool active) const; + void set_active(CPPropagatorsState& state, bool active, int i) const; + /// Implementation of the filtering algorithm + /// IMPORTANT: The fix-point engine implementation in engine.hpp assumes that the propagate + /// function is idempotent virtual CPStatus propagate(CPPropagatorsState& p_state, CPVarsState& v_state) const = 0; virtual void initialize_state(CPState& state) const = 0; void mark_index(CPPropagatorsState& p_state, ssize_t index) const; + ssize_t num_indices_to_process(const CPPropagatorsState& state) const; + protected: // Index of the propagator to access the respective propagator state const ssize_t propagator_index_; diff --git a/dwave/optimization/include/dwave-optimization/cp/core/status.hpp b/dwave/optimization/include/dwave-optimization/cp/core/status.hpp index ddeaf031..7f5fa64f 100644 --- a/dwave/optimization/include/dwave-optimization/cp/core/status.hpp +++ b/dwave/optimization/include/dwave-optimization/cp/core/status.hpp @@ -15,5 +15,5 @@ #pragma once namespace dwave::optimization::cp { -enum CPStatus { OK, Inconsistency, Complete }; +enum class CPStatus { OK, Inconsistency, Complete }; } // namespace dwave::optimization::cp diff --git a/dwave/optimization/include/dwave-optimization/cp/propagators/binaryop.hpp b/dwave/optimization/include/dwave-optimization/cp/propagators/binaryop.hpp new file mode 100644 index 00000000..91b06af0 --- /dev/null +++ b/dwave/optimization/include/dwave-optimization/cp/propagators/binaryop.hpp @@ -0,0 +1,38 @@ +// 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/cpvar.hpp" +#include "dwave-optimization/cp/core/propagator.hpp" + +namespace dwave::optimization::cp { + +/// Propagator for a on-way constraint out = BinaryOp(lhs, rhs) +template +class BinaryOpPropagator : public Propagator { + public: + BinaryOpPropagator(ssize_t index, CPVar* lhs, CPVar* rhs, CPVar* out); + void initialize_state(CPState& state) const override; + CPStatus propagate(CPPropagatorsState& p_state, CPVarsState& v_state) const override; + + private: + // The variables entering the binary op + CPVar *lhs_, *rhs_, *out_; +}; + +using AddPropagator = BinaryOpPropagator>; +using LessEqualPropagator = BinaryOpPropagator>; + +} // namespace dwave::optimization::cp diff --git a/dwave/optimization/include/dwave-optimization/cp/propagators/reduce.hpp b/dwave/optimization/include/dwave-optimization/cp/propagators/reduce.hpp new file mode 100644 index 00000000..f97454e9 --- /dev/null +++ b/dwave/optimization/include/dwave-optimization/cp/propagators/reduce.hpp @@ -0,0 +1,35 @@ +// 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/cpvar.hpp" +#include "dwave-optimization/cp/core/propagator.hpp" + +namespace dwave::optimization::cp { + +template +class ReducePropagator : public Propagator { + public: + ReducePropagator(ssize_t index, CPVar* in, CPVar* out); + void initialize_state(CPState& state) const override; + CPStatus propagate(CPPropagatorsState& p_state, CPVarsState& v_state) const override; + + private: + // The variables entering the binary op + CPVar *in_, *out_; +}; + +using SumPropagator = ReducePropagator>; +} // namespace dwave::optimization::cp diff --git a/dwave/optimization/src/cp/core/cpvar.cpp b/dwave/optimization/src/cp/core/cpvar.cpp index 0513e735..b1b42170 100644 --- a/dwave/optimization/src/cp/core/cpvar.cpp +++ b/dwave/optimization/src/cp/core/cpvar.cpp @@ -160,6 +160,9 @@ void CPVar::initialize_state(CPState& state) const { min_size = sizeinfo.min.value(); } + std::vector min_node; + std::vector max_node; + state.var_state_[cp_var_index_] = std::make_unique( state.get_state_manager(), min_size, max_size, node_->min(), node_->max(), std::make_unique(this, state), node_->integral()); diff --git a/dwave/optimization/src/cp/core/engine.cpp b/dwave/optimization/src/cp/core/engine.cpp index 215ed955..d04bff4c 100644 --- a/dwave/optimization/src/cp/core/engine.cpp +++ b/dwave/optimization/src/cp/core/engine.cpp @@ -13,7 +13,6 @@ // limitations under the License. #include "dwave-optimization/cp/core/engine.hpp" - namespace dwave::optimization::cp { CPStatus CPEngine::fix_point(CPState& state) const { @@ -25,6 +24,10 @@ CPStatus CPEngine::fix_point(CPState& state) const { Propagator* p = state.propagation_queue_.front(); status = this->propagate(state, p); + // Note: After propagation, unscheduling the propagator manually + p->set_scheduled(p_state, false); + state.propagation_queue_.pop_front(); + if (status == CPStatus::Inconsistency) { while (state.propagation_queue_.size() > 0) { state.propagation_queue_.front()->set_scheduled(p_state, false); diff --git a/dwave/optimization/src/cp/core/interval_array.cpp b/dwave/optimization/src/cp/core/interval_array.cpp index 33718725..db09a5f7 100644 --- a/dwave/optimization/src/cp/core/interval_array.cpp +++ b/dwave/optimization/src/cp/core/interval_array.cpp @@ -13,7 +13,6 @@ // limitations under the License. #include "dwave-optimization/cp/core/interval_array.hpp" - #include #include #include @@ -363,8 +362,13 @@ CPStatus IntervalArray::remove_all_but(double value, int index, DomainListene return this->update_max_size(index, l); } - bool changed_min = (value = min_[index]->get_value()); - bool changed_max = (value = max_[index]->get_value()); + if (this->contains(value, index) and this->is_bound(index)){ + // nothing to do here, the domain is already fixed to this value + return CPStatus::OK; + } + + bool changed_min = (value == min_[index]->get_value()); + bool changed_max = (value == max_[index]->get_value()); min_[index]->set_value(value); max_[index]->set_value(value); diff --git a/dwave/optimization/src/cp/core/propagator.cpp b/dwave/optimization/src/cp/core/propagator.cpp index 48582cde..163ac1d6 100644 --- a/dwave/optimization/src/cp/core/propagator.cpp +++ b/dwave/optimization/src/cp/core/propagator.cpp @@ -15,35 +15,69 @@ #include "dwave-optimization/cp/core/propagator.hpp" namespace dwave::optimization::cp { +// ------- PropagatorData -------- + void PropagatorData::mark_index(ssize_t index) { - assert(index < static_cast(indices.is_scheduled.size())); - if (indices.is_scheduled[index]) return; - indices.is_scheduled[index] = true; - indices.to_process.push_back(index); + assert(index < static_cast(is_scheduled_.size())); + if (is_scheduled_[index] or not active_[index]) return; + is_scheduled_[index] = true; + to_process_.push_back(index); +} + +bool PropagatorData::scheduled() const { return scheduled_; } + +bool PropagatorData::scheduled(ssize_t i) const { + assert(i < static_cast(is_scheduled_.size())); + return is_scheduled_[i]; +} + +void PropagatorData::set_scheduled(bool scheduled) { scheduled_ = scheduled; } + +void PropagatorData::set_scheduled(bool scheduled, ssize_t i) { + assert(i < static_cast(is_scheduled_.size())); + is_scheduled_[i] = scheduled; } +bool PropagatorData::active(ssize_t i) const { + assert(i < static_cast(active_.size())); + return active_[i]->get_value(); +} + +void PropagatorData::set_active(bool active, ssize_t i) { + assert(i < static_cast(active_.size())); + active_[i]->set_value(active); +} + +// ------- Propagator -------- + bool Propagator::scheduled(const CPPropagatorsState& state) const { assert(propagator_index_ >= 0); assert(propagator_index_ < static_cast(state.size())); return state[propagator_index_]->scheduled(); } +bool Propagator::scheduled(const CPPropagatorsState& state, int i) const { + assert(propagator_index_ >= 0); + assert(propagator_index_ < static_cast(state.size())); + return state[propagator_index_]->scheduled(i); +} + void Propagator::set_scheduled(CPPropagatorsState& state, bool scheduled) const { assert(propagator_index_ >= 0); assert(propagator_index_ < static_cast(state.size())); state[propagator_index_]->set_scheduled(scheduled); } -bool Propagator::active(const CPPropagatorsState& state) const { +bool Propagator::active(const CPPropagatorsState& state, int i) const { assert(propagator_index_ >= 0); assert(propagator_index_ < static_cast(state.size())); - return state[propagator_index_]->active(); + return state[propagator_index_]->active(i); } -void Propagator::set_active(CPPropagatorsState& state, bool active) const { +void Propagator::set_active(CPPropagatorsState& state, bool active, int i) const { assert(propagator_index_ >= 0); assert(propagator_index_ < static_cast(state.size())); - state[propagator_index_]->set_active(active); + state[propagator_index_]->set_active(active, i); } void Propagator::mark_index(CPPropagatorsState& state, ssize_t index) const { @@ -51,4 +85,9 @@ void Propagator::mark_index(CPPropagatorsState& state, ssize_t index) const { assert(index >= 0); state[propagator_index_]->mark_index(index); } + +ssize_t Propagator::num_indices_to_process(const CPPropagatorsState& state) const { + assert(propagator_index_ >= 0); + return state[propagator_index_]->num_indices_to_process(); +} } // namespace dwave::optimization::cp diff --git a/dwave/optimization/src/cp/propagators/binaryop.cpp b/dwave/optimization/src/cp/propagators/binaryop.cpp new file mode 100644 index 00000000..834f8dc3 --- /dev/null +++ b/dwave/optimization/src/cp/propagators/binaryop.cpp @@ -0,0 +1,157 @@ +// 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. + +#include "dwave-optimization/cp/propagators/binaryop.hpp" + +#include +namespace dwave::optimization::cp { + +template +BinaryOpPropagator::BinaryOpPropagator(ssize_t index, CPVar* lhs, CPVar* rhs, CPVar* out) + : Propagator(index), lhs_(lhs), rhs_(rhs), out_(out) { + // TODO: support only static sizes for now + if ((lhs_->max_size() != lhs_->min_size()) or (rhs_->max_size() != rhs_->min_size()) or + (out_->max_size() != out_->min_size())) { + throw std::invalid_argument("BinaryOpPropagator only supports static arrays currently"); + } + + // Default to bound-consistency + Advisor lhs_advisor(this, 0, std::make_unique()); + Advisor rhs_advisor(this, 1, std::make_unique()); + Advisor out_advisor(this, 2, std::make_unique()); + lhs_->propagate_on_bounds_change(std::move(lhs_advisor)); + rhs_->propagate_on_bounds_change(std::move(rhs_advisor)); + out_->propagate_on_bounds_change(std::move(out_advisor)); +} + +template +void BinaryOpPropagator::initialize_state(CPState& state) const { + CPPropagatorsState& p_state = state.get_propagators_state(); + assert(propagator_index_ >= 0); + assert(propagator_index_ < static_cast(p_state.size())); + p_state[propagator_index_] = + std::make_unique(state.get_state_manager(), out_->max_size()); +} + +template <> +CPStatus BinaryOpPropagator>::propagate(CPPropagatorsState& p_state, + CPVarsState& v_state) const { + auto data = data_ptr(p_state); + + CPStatus status = CPStatus::OK; + + std::deque& indices_to_process = data->indices_to_process(); + + while (data->num_indices_to_process() > 0) { + ssize_t i = indices_to_process.front(); + indices_to_process.pop_front(); + data->set_scheduled(false, i); + + // forward + double min_i = lhs_->min(v_state, i) + rhs_->min(v_state, i); + double max_i = lhs_->max(v_state, i) + rhs_->max(v_state, i); + + // prune the output + status = out_->remove_above(v_state, max_i, i); + if (status == CPStatus::Inconsistency) return status; + + status = out_->remove_below(v_state, min_i, i); + if (status == CPStatus::Inconsistency) return status; + + // backward + // prune lhs i + double lhs_up = out_->max(v_state, i) - rhs_->min(v_state, i); + double lhs_lo = out_->min(v_state, i) - rhs_->max(v_state, i); + status = lhs_->remove_above(v_state, lhs_up, i); + status = lhs_->remove_below(v_state, lhs_lo, i); + + if (status == CPStatus::Inconsistency) return status; + + // prube rhs i + double rhs_up = out_->max(v_state, i) - lhs_->min(v_state, i); + double rhs_lo = out_->min(v_state, i) - lhs_->max(v_state, i); + + status = rhs_->remove_above(v_state, rhs_up, i); + if (status == CPStatus::Inconsistency) return status; + + status = rhs_->remove_below(v_state, rhs_lo, i); + if (status == CPStatus::Inconsistency) return status; + } + return status; +} + +template <> +CPStatus BinaryOpPropagator>::propagate(CPPropagatorsState& p_state, + CPVarsState& v_state) const { + auto data = data_ptr(p_state); + assert(data->num_indices_to_process() > 0); + + CPStatus status = CPStatus::OK; + + std::deque& indices_to_process = data->indices_to_process(); + + while (data->num_indices_to_process() > 0) { + + ssize_t i = indices_to_process.front(); + indices_to_process.pop_front(); + data->set_scheduled(false, i); + + // forward + if (rhs_->max(v_state, i) < lhs_->min(v_state, i)) { + status = out_->assign(v_state, 0, i); + } else if (lhs_->max(v_state, i) <= rhs_->min(v_state, i)) { + status = out_->assign(v_state, 1, i); + } + + if (status == CPStatus::Inconsistency) return status; + + // backward + if (out_->min(v_state, i) == 1) { + // then we can prune the domain to guarantee the constraint is satisfied + status = lhs_->remove_above(v_state, rhs_->max(v_state, i), i); + if (status == CPStatus::Inconsistency) return status; + + status = rhs_->remove_below(v_state, lhs_->min(v_state, i), i); + if (status == CPStatus::Inconsistency) return status; + + this->set_active(p_state, lhs_->max(v_state, i) > rhs_->min(v_state, i), i); + + } else if (out_->max(v_state, i) == 0) { + // if the max is 0, then I have to prune in the other directions, in order to guarantee + // the constraint is always false + + status = lhs_->remove_below(v_state, rhs_->min(v_state, i), i); + if (status == CPStatus::Inconsistency) return status; + + status = rhs_->remove_above(v_state, lhs_->max(v_state, i), i); + if (status == CPStatus::Inconsistency) return status; + + // The constraint won't be active anymore, since after this pruning, the constraint + // won't be able to prune further + this->set_active(p_state, false, i); + } else { + // Cannot prune but the constraint will be active + // and probably this instruction is useless + this->set_active(p_state, true, i); + } + + } + + return status; +} + +template class BinaryOpPropagator>; +template class BinaryOpPropagator>; + +} // namespace dwave::optimization::cp diff --git a/dwave/optimization/src/cp/propagators/identity_propagator.cpp b/dwave/optimization/src/cp/propagators/identity_propagator.cpp index 0dfa0e55..73700b11 100644 --- a/dwave/optimization/src/cp/propagators/identity_propagator.cpp +++ b/dwave/optimization/src/cp/propagators/identity_propagator.cpp @@ -35,11 +35,12 @@ void ElementWiseIdentityPropagator::initialize_state(CPState& state) const { CPStatus ElementWiseIdentityPropagator::propagate(CPPropagatorsState& p_state, CPVarsState& v_state) const { auto data = data_ptr(p_state); - assert(data->indices.to_process.size() > 0); - while (data->indices.to_process.size() > 0) { - ssize_t i = data->indices.to_process.front(); - data->indices.to_process.pop_front(); - data->indices.is_scheduled[i] = false; + assert(data->num_indices_to_process() > 0); + std::deque indices_to_process = data->indices_to_process(); + while (data->num_indices_to_process() > 0) { + ssize_t i = indices_to_process.front(); + indices_to_process.pop_front(); + data->set_scheduled(false, i); /// Note: this is where other propagator would filter domains.. } return CPStatus::OK; @@ -62,11 +63,12 @@ void ReductionIdentityPropagator::initialize_state(CPState& state) const { CPStatus ReductionIdentityPropagator::propagate(CPPropagatorsState& p_state, CPVarsState& v_state) const { auto data = data_ptr(p_state); - assert(data->indices.to_process.size() == 1); - while (data->indices.to_process.size() > 0) { - ssize_t i = data->indices.to_process.front(); - data->indices.to_process.pop_front(); - data->indices.is_scheduled[i] = false; + std::deque indices_to_process = data->indices_to_process(); + assert(indices_to_process.size() == 1); + while (data->num_indices_to_process() > 0) { + ssize_t i = indices_to_process.front(); + indices_to_process.pop_front(); + data->set_scheduled(false, i); /// Note: this is where other propagator would filter domains.. } return CPStatus::OK; diff --git a/dwave/optimization/src/cp/propagators/reduce.cpp b/dwave/optimization/src/cp/propagators/reduce.cpp new file mode 100644 index 00000000..f6cb9281 --- /dev/null +++ b/dwave/optimization/src/cp/propagators/reduce.cpp @@ -0,0 +1,147 @@ +// 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. + +#include "dwave-optimization/cp/propagators/reduce.hpp" + +namespace dwave::optimization::cp { +class SumPropagatorData : public PropagatorData { + public: + SumPropagatorData(StateManager* sm, CPVar* predecessor, const CPVarsState& state, + ssize_t constraint_size) + : PropagatorData(sm, constraint_size) { + assert(predecessor->min_size() == predecessor->max_size()); + auto n_inputs = predecessor->max_size(); + min.resize(n_inputs, 0); + max.resize(n_inputs, 0); + fixed_idx.resize(n_inputs); + std::iota(fixed_idx.begin(), fixed_idx.end(), 0); + + for (ssize_t i = 0; i < n_inputs; ++i) { + min[i] = predecessor->min(state, i); + max[i] = predecessor->max(state, i); + } + + n_fixed = sm->make_state_int(0); + sum_fixed = sm->make_state_real(0); + } + + // Indices of the predecessor that are fixed + std::vector fixed_idx; + + // How many indices of the predecessor array are fixed + StateInt* n_fixed; + + // What is the sum of the fixed indices? + StateReal* sum_fixed; + + // Cached min and max for the predecessor indices + std::vector min, max; +}; + +template +ReducePropagator::ReducePropagator(ssize_t index, CPVar* in, CPVar* out) + : Propagator(index), in_(in), out_(out) { + // TODO: not supporting dynamic arrays for now + if (in_->max_size() != in_->min_size()) { + throw std::invalid_argument( + "ReducePropagator not currently compatible with dynamic arrays"); + } + + // Default to bound consistency + Advisor in_adv(this, 0, std::make_unique()); + Advisor out_adv(this, 1, std::make_unique()); + + in_->propagate_on_bounds_change(std::move(in_adv)); + out_->propagate_on_bounds_change(std::move(out_adv)); +} + +template <> +void ReducePropagator>::initialize_state(CPState& state) const { + CPPropagatorsState& p_state = state.get_propagators_state(); + CPVarsState& v_state = state.get_variables_state(); + assert(propagator_index_ >= 0); + assert(propagator_index_ < static_cast(p_state.size())); + p_state[propagator_index_] = + std::make_unique(state.get_state_manager(), in_, v_state, 1); +} + +template <> +CPStatus ReducePropagator>::propagate(CPPropagatorsState& p_state, + CPVarsState& v_state) const { + auto data = data_ptr(p_state); + auto n = in_->max_size(); + + CPStatus status = CPStatus::OK; + + std::deque& indices_to_process = data->indices_to_process(); + + while (data->num_indices_to_process() > 0) { + ssize_t i = indices_to_process.front(); + indices_to_process.pop_front(); + data->set_scheduled(false, i); + assert(i == 0); + + int nf = data->n_fixed->get_value(); + double sum_min = data->sum_fixed->get_value(); + double sum_max = data->sum_fixed->get_value(); + + for (int j = nf; j < n; ++j) { + int idx = data->fixed_idx[j]; + + data->min[idx] = in_->min(v_state, idx); + data->max[idx] = in_->max(v_state, idx); + + // update partial sum + sum_min += data->min[idx]; + sum_max += data->max[idx]; + + if (in_->is_bound(v_state, idx)) { + data->sum_fixed->set_value(data->sum_fixed->get_value() + in_->min(v_state, idx)); + std::swap(data->fixed_idx[j], data->fixed_idx[nf]); + nf++; + } + } + + data->n_fixed->set_value(nf); + if ((sum_min > out_->max(v_state, 0)) or (sum_max < out_->min(v_state, 0))) { + // TODO: I guess this may not happen in the DAG + return CPStatus::Inconsistency; + } + + status = out_->remove_above(v_state, sum_max, 0); + if (status == CPStatus::Inconsistency) return status; + + status = out_->remove_below(v_state, sum_min, 0); + if (status == CPStatus::Inconsistency) return status; + + double out_min = out_->min(v_state, 0); + double out_max = out_->max(v_state, 0); + + for (int j = nf; j < n; ++j) { + int idx = data->fixed_idx[j]; + + status = in_->remove_above(v_state, out_max - (sum_min - data->min[idx]), idx); + if (status == CPStatus::Inconsistency) return status; + + status = in_->remove_below(v_state, out_min - (sum_max - data->max[idx]), idx); + if (status == CPStatus::Inconsistency) return status; + } + } + + return status; +} + +template class ReducePropagator>; + +} // namespace dwave::optimization::cp diff --git a/meson.build b/meson.build index 384a6a2c..78403b99 100644 --- a/meson.build +++ b/meson.build @@ -60,7 +60,9 @@ dwave_optimization_src = [ 'dwave/optimization/src/cp/core/engine.cpp', 'dwave/optimization/src/cp/core/cpvar.cpp', + 'dwave/optimization/src/cp/propagators/binaryop.cpp', 'dwave/optimization/src/cp/propagators/identity_propagator.cpp', + 'dwave/optimization/src/cp/propagators/reduce.cpp', 'dwave/optimization/src/cp/state/copier.cpp', 'dwave/optimization/src/cp/state/copy.cpp', diff --git a/tests/cpp/cp/propagators/test_binaryop.cpp b/tests/cpp/cp/propagators/test_binaryop.cpp new file mode 100644 index 00000000..fa6a2a95 --- /dev/null +++ b/tests/cpp/cp/propagators/test_binaryop.cpp @@ -0,0 +1,397 @@ +// 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. + +#include +#include +#include + +#include "../utils.hpp" +#include "catch2/catch_test_macros.hpp" +#include "catch2/matchers/catch_matchers_all.hpp" +#include "dwave-optimization/array.hpp" +#include "dwave-optimization/cp/core/cpvar.hpp" +#include "dwave-optimization/cp/core/index_transform.hpp" +#include "dwave-optimization/cp/core/interval_array.hpp" +#include "dwave-optimization/cp/propagators/binaryop.hpp" +#include "dwave-optimization/cp/state/copier.hpp" +#include "dwave-optimization/nodes.hpp" +#include "dwave-optimization/state.hpp" + +using Catch::Matchers::RangeEquals; + +namespace dwave::optimization::cp { +TEST_CASE("AddPropagator") { + GIVEN("A Graph and an integer node") { + dwave::optimization::Graph graph; + + // Add an integer node + IntegerNode* x = graph.emplace_node(10, 0, 5); + IntegerNode* y = graph.emplace_node(10, -2, 3); + auto z = graph.emplace_node(x, y); + + // Lock the graph + graph.topological_sort(); + + // Construct the CP corresponding model + AND_GIVEN("A CP Model and fix-point engine") { + CPModel model; + CPEngine engine; + + // Add the variables to the model (manually) + CPVar* cp_x = model.emplace_variable(model, x, x->topological_index()); + CPVar* cp_y = model.emplace_variable(model, y, y->topological_index()); + CPVar* cp_z = model.emplace_variable(model, z, z->topological_index()); + + REQUIRE(cp_z->min_size() == 10); + REQUIRE(cp_z->max_size() == 10); + + // Add the propagator for the add node + Propagator* p_add = model.emplace_propagator(model.num_propagators(), + cp_x, cp_y, cp_z); + + REQUIRE(cp_x->on_bounds.size() == 1); + REQUIRE(cp_y->on_bounds.size() == 1); + REQUIRE(cp_z->on_bounds.size() == 1); + + WHEN("We initialize a state") { + CPState state = model.initialize_state(); + CPVarsState& v_state = state.get_variables_state(); + CPPropagatorsState& p_state = state.get_propagators_state(); + + REQUIRE(v_state.size() == 3); + REQUIRE(p_state.size() == 1); + + cp_x->initialize_state(state); + cp_y->initialize_state(state); + cp_z->initialize_state(state); + p_add->initialize_state(state); + + THEN("The interval of z is correctly set") { + for (int i = 0; i < 10; ++i) { + REQUIRE(cp_z->min(v_state, i) == -2); + REQUIRE(cp_z->max(v_state, i) == 8); + REQUIRE(cp_z->size(v_state, i) == 11); + } + } + + AND_WHEN("We restrict x[0] to [2, 5] and we propagate") { + cp_x->remove_below(v_state, 2, 0); + REQUIRE(cp_x->min(v_state, 0) == 2); + REQUIRE(cp_x->max(v_state, 0) == 5); + engine.fix_point(state); + + THEN("The sum output variable 0 is correctly set to [0, 8]") { + REQUIRE(cp_z->min(v_state, 0) == 0); + REQUIRE(cp_z->max(v_state, 0) == 8); + } + } + + AND_WHEN("We restrict y[1] to [-2, 1] and we propagate") { + cp_y->remove_above(v_state, 1, 1); + REQUIRE(cp_y->min(v_state, 1) == -2); + REQUIRE(cp_y->max(v_state, 1) == 1); + + engine.fix_point(state); + THEN("The sum output variable is correctly set to [-2, 6]") { + REQUIRE(cp_z->min(v_state, 1) == -2); + REQUIRE(cp_z->max(v_state, 1) == 6); + } + } + + AND_WHEN("We restrict the sum[2] to [-1, 1]") { + cp_z->remove_below(v_state, -1, 2); + cp_z->remove_above(v_state, 1, 2); + REQUIRE(cp_z->max(v_state, 2) == 1); + REQUIRE(cp_z->min(v_state, 2) == -1); + + AND_WHEN("We propagate") { + engine.fix_point(state); + THEN("The input variables gets changed accordingly to x[2] in [0, 3] and " + "y[2] in " + "[-2, 1]") { + REQUIRE(cp_x->min(v_state, 2) == 0); + REQUIRE(cp_x->max(v_state, 2) == 3); + REQUIRE(cp_y->min(v_state, 2) == -2); + REQUIRE(cp_y->max(v_state, 2) == 1); + } + } + } + } + } + } +} + +TEST_CASE("LessEqualPropagator") { + GIVEN("A Graph with two integer nodes x in [0, 2] and y in [3, 5] and their <= expression") { + dwave::optimization::Graph graph; + + // Add an integer node + IntegerNode* x = graph.emplace_node(3, 0, 2); + IntegerNode* y = graph.emplace_node(3, 3, 5); + auto z = graph.emplace_node(x, y); + + // Lock the graph + graph.topological_sort(); + + // Construct the CP corresponding model + AND_GIVEN("A CP Model and fix-point engine") { + CPModel model; + CPEngine engine; + + // Add the variables to the model (manually) + CPVar* cp_x = model.emplace_variable(model, x, x->topological_index()); + CPVar* cp_y = model.emplace_variable(model, y, y->topological_index()); + CPVar* cp_z = model.emplace_variable(model, z, z->topological_index()); + + REQUIRE(cp_z->min_size() == 3); + REQUIRE(cp_z->max_size() == 3); + + // Add the propagator for the add node + Propagator* p_add = model.emplace_propagator( + model.num_propagators(), cp_x, cp_y, cp_z); + + REQUIRE(cp_x->on_bounds.size() == 1); + REQUIRE(cp_y->on_bounds.size() == 1); + REQUIRE(cp_z->on_bounds.size() == 1); + + WHEN("We initialize a state") { + CPState state = model.initialize_state(); + CPVarsState& v_state = state.get_variables_state(); + CPPropagatorsState& p_state = state.get_propagators_state(); + + REQUIRE(v_state.size() == 3); + REQUIRE(p_state.size() == 1); + + cp_x->initialize_state(state); + cp_y->initialize_state(state); + cp_z->initialize_state(state); + p_add->initialize_state(state); + + THEN("The interval of z is correctly set") { + for (int i = 0; i < 3; ++i) { + REQUIRE(cp_z->min(v_state, i) == 0); + REQUIRE(cp_z->max(v_state, i) == 1); + REQUIRE(cp_z->size(v_state, i) == 2); + } + } + + AND_WHEN("We restrict y[0] to [4, 5] and we propagate") { + cp_y->remove_below(v_state, 4, 0); + REQUIRE(cp_y->min(v_state, 0) == 4); + REQUIRE(cp_y->max(v_state, 0) == 5); + engine.fix_point(state); + + THEN("The <= output variable 0 is left unchanged to [1, 1]") { + REQUIRE(cp_z->min(v_state, 0) == 1); + REQUIRE(cp_z->max(v_state, 0) == 1); + } + } + } + } + } + + GIVEN("A Graph with two integer nodes x in [0, 3] and y in [2, 5] and their <= expression") { + dwave::optimization::Graph graph; + + // Add an integer node + IntegerNode* x = graph.emplace_node(3, 0, 3); + IntegerNode* y = graph.emplace_node(3, 2, 5); + auto z = graph.emplace_node(x, y); + + // Lock the graph + graph.topological_sort(); + + // Construct the CP corresponding model + AND_GIVEN("A CP Model and fix-point engine") { + CPModel model; + CPEngine engine; + + // Add the variables to the model (manually) + CPVar* cp_x = model.emplace_variable(model, x, x->topological_index()); + CPVar* cp_y = model.emplace_variable(model, y, y->topological_index()); + CPVar* cp_z = model.emplace_variable(model, z, z->topological_index()); + + REQUIRE(cp_z->min_size() == 3); + REQUIRE(cp_z->max_size() == 3); + + // Add the propagator for the add node + Propagator* p_add = model.emplace_propagator( + model.num_propagators(), cp_x, cp_y, cp_z); + + REQUIRE(cp_x->on_bounds.size() == 1); + REQUIRE(cp_y->on_bounds.size() == 1); + REQUIRE(cp_z->on_bounds.size() == 1); + + WHEN("We initialize a state") { + CPState state = model.initialize_state(); + CPVarsState& v_state = state.get_variables_state(); + CPPropagatorsState& p_state = state.get_propagators_state(); + + REQUIRE(v_state.size() == 3); + REQUIRE(p_state.size() == 1); + + cp_x->initialize_state(state); + cp_y->initialize_state(state); + cp_z->initialize_state(state); + p_add->initialize_state(state); + + THEN("The interval of z is correctly set") { + for (int i = 0; i < 3; ++i) { + REQUIRE(cp_z->min(v_state, i) == 0); + REQUIRE(cp_z->max(v_state, i) == 1); + REQUIRE(cp_z->size(v_state, i) == 2); + } + } + + AND_WHEN("We restrict y[0] to [4, 5] and we propagate") { + cp_y->remove_below(v_state, 4, 0); + REQUIRE(cp_y->min(v_state, 0) == 4); + REQUIRE(cp_y->max(v_state, 0) == 5); + engine.fix_point(state); + + THEN("The <= output variable 0 is changed to [1, 1]") { + REQUIRE(cp_z->min(v_state, 0) == 1); + REQUIRE(cp_z->max(v_state, 0) == 1); + } + } + + AND_WHEN("We restrict z[1] to [1, 1] and we propagate") { + cp_z->assign(v_state, 1, 1); + REQUIRE(cp_z->min(v_state, 1) == 1); + REQUIRE(cp_z->max(v_state, 1) == 1); + engine.fix_point(state); + + THEN("Input nodes are left unchanged") { + REQUIRE(cp_x->min(v_state, 1) == 0); + REQUIRE(cp_x->max(v_state, 1) == 3); + REQUIRE(cp_y->min(v_state, 1) == 2); + REQUIRE(cp_y->max(v_state, 1) == 5); + } + } + + AND_WHEN("We restrict z[2] to [0, 0] and we propagate") { + cp_z->assign(v_state, 0, 2); + REQUIRE(cp_z->min(v_state, 2) == 0); + REQUIRE(cp_z->max(v_state, 2) == 0); + engine.fix_point(state); + + THEN("Input nodes are shrunk accordingly") { + REQUIRE(cp_x->min(v_state, 2) == 2); + REQUIRE(cp_x->max(v_state, 2) == 3); + REQUIRE(cp_y->min(v_state, 2) == 2); + REQUIRE(cp_y->max(v_state, 2) == 3); + } + } + } + } + } + + GIVEN("A Graph with two integer nodes x in [2, 5] and y in [0, 3] and their <= expression") { + dwave::optimization::Graph graph; + + // Add an integer node + IntegerNode* x = graph.emplace_node(3, 2, 5); + IntegerNode* y = graph.emplace_node(3, 0, 3); + auto z = graph.emplace_node(x, y); + + // Lock the graph + graph.topological_sort(); + + // Construct the CP corresponding model + AND_GIVEN("A CP Model and fix-point engine") { + CPModel model; + CPEngine engine; + + // Add the variables to the model (manually) + CPVar* cp_x = model.emplace_variable(model, x, x->topological_index()); + CPVar* cp_y = model.emplace_variable(model, y, y->topological_index()); + CPVar* cp_z = model.emplace_variable(model, z, z->topological_index()); + + REQUIRE(cp_z->min_size() == 3); + REQUIRE(cp_z->max_size() == 3); + + // Add the propagator for the add node + Propagator* p_add = model.emplace_propagator( + model.num_propagators(), cp_x, cp_y, cp_z); + + REQUIRE(cp_x->on_bounds.size() == 1); + REQUIRE(cp_y->on_bounds.size() == 1); + REQUIRE(cp_z->on_bounds.size() == 1); + + WHEN("We initialize a state") { + CPState state = model.initialize_state(); + CPVarsState& v_state = state.get_variables_state(); + CPPropagatorsState& p_state = state.get_propagators_state(); + + REQUIRE(v_state.size() == 3); + REQUIRE(p_state.size() == 1); + + cp_x->initialize_state(state); + cp_y->initialize_state(state); + cp_z->initialize_state(state); + p_add->initialize_state(state); + + THEN("The interval of z is correctly set") { + for (int i = 0; i < 3; ++i) { + REQUIRE(cp_z->min(v_state, i) == 0); + REQUIRE(cp_z->max(v_state, i) == 1); + REQUIRE(cp_z->size(v_state, i) == 2); + } + } + + AND_WHEN("We restrict x[0] to [4, 5] and we propagate") { + cp_x->remove_below(v_state, 4, 0); + REQUIRE(cp_x->min(v_state, 0) == 4); + REQUIRE(cp_x->max(v_state, 0) == 5); + engine.fix_point(state); + + THEN("The <= output variable 0 is changed to [0, 0]") { + REQUIRE(cp_z->min(v_state, 0) == 0); + REQUIRE(cp_z->max(v_state, 0) == 0); + } + } + + AND_WHEN("We restrict z[1] to [1, 1] and we propagate") { + cp_z->assign(v_state, 1, 1); + REQUIRE(cp_z->min(v_state, 1) == 1); + REQUIRE(cp_z->max(v_state, 1) == 1); + engine.fix_point(state); + + THEN("Input nodes are shrunk accordingly") { + REQUIRE(cp_x->min(v_state, 1) == 2); + REQUIRE(cp_x->max(v_state, 1) == 3); + REQUIRE(cp_y->min(v_state, 1) == 2); + REQUIRE(cp_y->max(v_state, 1) == 3); + } + } + + AND_WHEN("We restrict z[2] to [0, 0] and we propagate") { + cp_z->assign(v_state, 0, 2); + REQUIRE(cp_z->min(v_state, 2) == 0); + REQUIRE(cp_z->max(v_state, 2) == 0); + engine.fix_point(state); + + THEN("Input nodes are left unchanged") { + REQUIRE(cp_x->min(v_state, 2) == 2); + REQUIRE(cp_x->max(v_state, 2) == 5); + REQUIRE(cp_y->min(v_state, 2) == 0); + REQUIRE(cp_y->max(v_state, 2) == 3); + } + } + } + } + } +} + +} // namespace dwave::optimization::cp diff --git a/tests/cpp/cp/propagators/test_reduce.cpp b/tests/cpp/cp/propagators/test_reduce.cpp new file mode 100644 index 00000000..72412f39 --- /dev/null +++ b/tests/cpp/cp/propagators/test_reduce.cpp @@ -0,0 +1,127 @@ +// 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. + +#include +#include +#include + +#include "../utils.hpp" +#include "catch2/catch_test_macros.hpp" +#include "catch2/matchers/catch_matchers_all.hpp" +#include "dwave-optimization/array.hpp" +#include "dwave-optimization/cp/core/cpvar.hpp" +#include "dwave-optimization/cp/core/index_transform.hpp" +#include "dwave-optimization/cp/core/interval_array.hpp" +#include "dwave-optimization/cp/propagators/reduce.hpp" +#include "dwave-optimization/cp/state/copier.hpp" +#include "dwave-optimization/nodes.hpp" +#include "dwave-optimization/state.hpp" + +using Catch::Matchers::RangeEquals; + +namespace dwave::optimization::cp { +TEST_CASE("SumPropagator") { + GIVEN("A Graph and an integer node") { + dwave::optimization::Graph graph; + + // Add an integer node + IntegerNode* x = graph.emplace_node(3, 0, 5); + auto y = graph.emplace_node(x); + + // Lock the graph + graph.topological_sort(); + + // Construct the CP corresponding model + AND_GIVEN("A CP Model and fix-point engine") { + CPModel model; + CPEngine engine; + + // Add the variables to the model (manually) + CPVar* cp_x = model.emplace_variable(model, x, x->topological_index()); + CPVar* cp_y = model.emplace_variable(model, y, y->topological_index()); + + REQUIRE(cp_y->min_size() == 1); + REQUIRE(cp_y->max_size() == 1); + + // Add the propagator for the add node + Propagator* p_add = + model.emplace_propagator(model.num_propagators(), cp_x, cp_y); + + REQUIRE(cp_x->on_bounds.size() == 1); + REQUIRE(cp_y->on_bounds.size() == 1); + + WHEN("We initialize a state") { + CPState state = model.initialize_state(); + CPVarsState& v_state = state.get_variables_state(); + CPPropagatorsState& p_state = state.get_propagators_state(); + + REQUIRE(v_state.size() == 2); + REQUIRE(p_state.size() == 1); + + cp_x->initialize_state(state); + cp_y->initialize_state(state); + p_add->initialize_state(state); + + THEN("The interval of y is correctly set") { + REQUIRE(cp_y->min(v_state, 0) == 0); + REQUIRE(cp_y->max(v_state, 0) == 15); + REQUIRE(cp_y->size(v_state, 0) == 16); + } + + AND_WHEN("We restrict x[0] to [2, 5] and we propagate") { + cp_x->remove_below(v_state, 2, 0); + REQUIRE(cp_x->min(v_state, 0) == 2); + REQUIRE(cp_x->max(v_state, 0) == 5); + engine.fix_point(state); + + THEN("The sum output variable is correctly set to [2, 15]") { + REQUIRE(cp_y->min(v_state, 0) == 2); + REQUIRE(cp_y->max(v_state, 0) == 15); + } + } + + AND_WHEN("We restrict x[1] to [0, 1] and we propagate") { + cp_x->remove_above(v_state, 1, 1); + REQUIRE(cp_x->min(v_state, 1) == 0); + REQUIRE(cp_x->max(v_state, 1) == 1); + + engine.fix_point(state); + THEN("The sum output variable is correctly set to [0, 11]") { + REQUIRE(cp_y->min(v_state, 0) == 0); + REQUIRE(cp_y->max(v_state, 0) == 11); + } + } + + AND_WHEN("We restrict the sum to [3, 10]") { + cp_y->remove_below(v_state, 3, 0); + cp_y->remove_above(v_state, 10, 0); + REQUIRE(cp_y->max(v_state, 0) == 10); + REQUIRE(cp_y->min(v_state, 0) == 3); + + AND_WHEN("We propagate") { + engine.fix_point(state); + + THEN("The input variables gets unchanged]") { + for (int i = 0; i < 3; ++i) { + REQUIRE(cp_x->min(v_state, i) == 0); + REQUIRE(cp_x->max(v_state, i) == 5); + } + } + } + } + } + } + } +} +} // namespace dwave::optimization::cp \ No newline at end of file diff --git a/tests/cpp/cp/test_propagator.cpp b/tests/cpp/cp/test_propagator.cpp index 9e276b7a..ce0f7c03 100644 --- a/tests/cpp/cp/test_propagator.cpp +++ b/tests/cpp/cp/test_propagator.cpp @@ -60,7 +60,7 @@ TEST_CASE("ElementWiseIdentityPropagator") { Advisor advisor(p, 0, std::make_unique()); var->propagate_on_domain_change(std::move(advisor)); REQUIRE(var->on_domain.size() == 1); - WHEN("We initialize the a state") { + WHEN("We initialize a state") { CPState state = model.initialize_state(); CPVarsState& s_state = state.get_variables_state(); CPPropagatorsState& p_state = state.get_propagators_state(); @@ -76,17 +76,35 @@ TEST_CASE("ElementWiseIdentityPropagator") { REQUIRE(status == CPStatus::OK); THEN("We see that the propagator is triggered to run on the same index") { REQUIRE(p_state[0]->scheduled()); - REQUIRE(p_state[0]->indices.to_process.size() == 1); + REQUIRE(p->num_indices_to_process(p_state) == 1); for (int i = 0; i < 10; ++i) { + REQUIRE(p->active(p_state, i)); if (i == 2) { - REQUIRE(p_state[0]->indices.is_scheduled[i]); + REQUIRE(p->scheduled(p_state, i)); } else { - REQUIRE_FALSE(p_state[0]->indices.is_scheduled[i]); + REQUIRE_FALSE(p->scheduled(p_state, i)); } } } } + + AND_WHEN("We alter the domain of a variable twice") { + CPStatus status = var->remove_above(s_state, 4, 0); + REQUIRE(status == CPStatus::OK); + status = var->remove_below(s_state, 2, 0); + REQUIRE(status == CPStatus::OK); + + THEN("We see that the propagator is triggered to run on the same index only " + "once") { + REQUIRE(p_state[0]->scheduled()); + REQUIRE(p->num_indices_to_process(p_state) == 1); + REQUIRE(p->scheduled(p_state, 0)); + for (int i = 1; i < 10; ++i) { + REQUIRE_FALSE(p->scheduled(p_state, i)); + } + } + } } } } diff --git a/tests/cpp/meson.build b/tests/cpp/meson.build index 2232e604..448fd184 100644 --- a/tests/cpp/meson.build +++ b/tests/cpp/meson.build @@ -11,6 +11,8 @@ tests_all = executable( 'cp/test_copier.cpp', 'cp/test_cp_var.cpp', 'cp/test_propagator.cpp', + 'cp/propagators/test_binaryop.cpp', + 'cp/propagators/test_reduce.cpp', 'nodes/indexing/test_advanced.cpp', 'nodes/indexing/test_basic.cpp',