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..e898a9bd --- /dev/null +++ b/dwave/optimization/include/dwave-optimization/cp/core/advisor.hpp @@ -0,0 +1,45 @@ +// Copyright 2026 D-Wave Systems Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once +#include "dwave-optimization/cp/core/index_transform.hpp" +#include "dwave-optimization/cp/core/propagator.hpp" +namespace dwave::optimization::cp { + +/// 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; + + ssize_t input_index() const { return p_input_; } + + private: + // The propagator the advisor is watching + Propagator* p_; + + // Which input + const ssize_t p_input_; + + // Maps of the observed variable indices to the observed propagator indices + std::unique_ptr index_transform_; +}; + +} // namespace dwave::optimization::cp 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..cbd05d8b --- /dev/null +++ b/dwave/optimization/include/dwave-optimization/cp/core/base_node.hpp @@ -0,0 +1,25 @@ +// Copyright 2026 D-Wave Systems Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include "dwave-optimization/array.hpp" +#include "dwave-optimization/cp/core/cpvar.hpp" +#include "dwave-optimization/cp/core/engine.hpp" +#include "dwave-optimization/cp/core/interval_array.hpp" +#include "dwave-optimization/cp/core/propagator.hpp" +#include "dwave-optimization/cp/state/cpvar_state.hpp" +#include "dwave-optimization/graph.hpp" + +namespace dwave::optimization::cp {} // namespace dwave::optimization::cp 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..bbd6c0d2 --- /dev/null +++ b/dwave/optimization/include/dwave-optimization/cp/core/cpvar.hpp @@ -0,0 +1,187 @@ +// Copyright 2026 D-Wave Systems Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include + +#include "dwave-optimization/array.hpp" +#include "dwave-optimization/cp/core/advisor.hpp" +#include "dwave-optimization/cp/core/engine.hpp" +#include "dwave-optimization/cp/core/propagator.hpp" +#include "dwave-optimization/cp/core/status.hpp" +#include "dwave-optimization/cp/state/cpvar_state.hpp" +#include "dwave-optimization/graph.hpp" + +namespace dwave::optimization::cp { + +// forward declaration +class CPVar; + +class CPModel { + public: + template + 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); + + CPVar(const CPVar& other) = delete; + CPVar() = delete; + + 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; + bool is_active(const CPVarsState& state, int index) const; + bool maybe_active(const CPVarsState& state, int index) const; + ssize_t max_size(const CPVarsState& state) const; + ssize_t min_size(const CPVarsState& state) const; + + ssize_t max_size() const; + ssize_t min_size() const; + + // actions on the domains + CPStatus remove(CPVarsState& state, double value, int index) const; + CPStatus remove_above(CPVarsState& state, double value, int index) const; + CPStatus remove_below(CPVarsState& state, double value, int index) const; + CPStatus assign(CPVarsState& state, double value, int index) const; + CPStatus set_min_size(CPVarsState& state, int index) const; + CPStatus set_max_size(CPVarsState& state, int index) const; + + // actions for the propagation engine + void schedule_all(CPState& state, const std::vector& advisors, ssize_t index) const; + + // Note: maybe we need the state here.. especially if we want to attach propagators dynamically + // during the search... + // Another note: it is als possible to use a vector or struct where the struct holds the + // propagator and the event type (as an enum) that triggers it. Still considering what to do + // here.. + void propagate_on_domain_change(Advisor&& advisor); + void propagate_on_bounds_change(Advisor&& advisor); + void propagate_on_assignment(Advisor&& advisor); + + void initialize_state(CPState& state) const; + + /// Note: these could be state stacks that can be updated through the search. Keeping them as + /// simple vectors (static in terms of the search for now) + // They represent the propagators to trigger when the variable gets-assigned/changes + // domain/bounds change + std::vector on_bind; + std::vector on_domain; + std::vector on_bounds; + std::vector on_array_size_change; + + protected: + const CPModel& model_; + + // but should I have this? + const dwave::optimization::ArrayNode* node_; + const ssize_t cp_var_index_; + + class Listener : public DomainListener { + public: + Listener(const CPVar* var, CPState& state) : var_(var), state_(state) {} + + // domain listener overrides + + void bind(ssize_t i) override { var_->schedule_all(state_, var_->on_bind, i); } + + void change(ssize_t i) override { var_->schedule_all(state_, var_->on_domain, i); } + + void change_min(ssize_t i) override { var_->schedule_all(state_, var_->on_bounds, i); } + + void change_max(ssize_t i) override { var_->schedule_all(state_, var_->on_bounds, i); } + + void change_array_size(ssize_t i) override { + var_->schedule_all(state_, var_->on_array_size_change, i); + } + + private: + const CPVar* var_; + CPState& state_; + }; +}; +} // namespace dwave::optimization::cp 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..c93458e5 --- /dev/null +++ b/dwave/optimization/include/dwave-optimization/cp/core/domain_array.hpp @@ -0,0 +1,84 @@ +// Copyright 2026 D-Wave Systems Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once +#include + +#include "dwave-optimization/common.hpp" +#include "dwave-optimization/cp/core/domain_listener.hpp" +#include "dwave-optimization/cp/core/status.hpp" + +namespace dwave::optimization::cp { + +/// Interface for the domains of a contiguous array of variables. It is assumed that the variables +/// are flattened and they can be optional (maybe active). This is to mimic the dynamic arrays in +/// dwave-optimization. +class DomainArray { + public: + virtual ~DomainArray() = default; + + /// Return the total number of domains + virtual size_t num_domains() const = 0; + + /// Return the minimum value of the index-th variable + virtual double min(int index) const = 0; + + /// Return the maximum value of the index-th variable + virtual double max(int index) const = 0; + + /// Return the size of the domain of the index-th variable + virtual double size(int index) const = 0; + + /// 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; +}; + +} // 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..1743da16 --- /dev/null +++ b/dwave/optimization/include/dwave-optimization/cp/core/domain_listener.hpp @@ -0,0 +1,30 @@ +// Copyright 2026 D-Wave Systems Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +namespace dwave::optimization::cp { +class DomainListener { + public: + virtual ~DomainListener() = default; + + // TODO: check whether this should be removed or not + // virtual void empty() = 0; + virtual void bind(ssize_t i) = 0; + virtual void change(ssize_t i) = 0; + virtual void change_max(ssize_t i) = 0; + virtual void change_min(ssize_t i) = 0; + virtual void change_array_size(ssize_t i) = 0; +}; +} // namespace dwave::optimization::cp 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..a7cc17ad --- /dev/null +++ b/dwave/optimization/include/dwave-optimization/cp/core/engine.hpp @@ -0,0 +1,79 @@ +// 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/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 ((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); + } + } + + 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/index_transform.hpp b/dwave/optimization/include/dwave-optimization/cp/core/index_transform.hpp new file mode 100644 index 00000000..12f5d5d4 --- /dev/null +++ b/dwave/optimization/include/dwave-optimization/cp/core/index_transform.hpp @@ -0,0 +1,45 @@ +// Copyright 2026 D-Wave Systems Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once +#include + +#include "dwave-optimization/common.hpp" + +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 + /// 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/interval_array.hpp b/dwave/optimization/include/dwave-optimization/cp/core/interval_array.hpp new file mode 100644 index 00000000..c6603f74 --- /dev/null +++ b/dwave/optimization/include/dwave-optimization/cp/core/interval_array.hpp @@ -0,0 +1,70 @@ +// 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/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 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; +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..e138bd7a --- /dev/null +++ b/dwave/optimization/include/dwave-optimization/cp/core/propagator.hpp @@ -0,0 +1,122 @@ +// 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 + +#include "dwave-optimization/cp/state/cpvar_state.hpp" +#include "dwave-optimization/cp/state/state.hpp" + +namespace dwave::optimization::cp { + +// forward declaration + +class CPState; +class Propagator; + +// internal structure of propagators +class PropagatorData { + public: + virtual ~PropagatorData() = default; + + 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; + bool scheduled(ssize_t i) const; + + /// Set the scheduled status of the propagator + 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(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); + + 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_; + 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; + + Propagator(int index) : propagator_index_(index) {} + + template PData> + 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 { + assert(propagator_index_ >= 0); + assert(propagator_index_ < static_cast(state.size())); + return static_cast(state[propagator_index_].get()); + } + + 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, int i) 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_; +}; + +} // 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..7f5fa64f --- /dev/null +++ b/dwave/optimization/include/dwave-optimization/cp/core/status.hpp @@ -0,0 +1,19 @@ +// 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 { +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/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/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/include/dwave-optimization/cp/state/copier.hpp b/dwave/optimization/include/dwave-optimization/cp/state/copier.hpp new file mode 100644 index 00000000..7a37ab1a --- /dev/null +++ b/dwave/optimization/include/dwave-optimization/cp/state/copier.hpp @@ -0,0 +1,93 @@ +// 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 + +#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(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(ssize_t 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..e2247160 --- /dev/null +++ b/dwave/optimization/include/dwave-optimization/cp/state/copy.hpp @@ -0,0 +1,69 @@ +// 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/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(ssize_t 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..349221ae --- /dev/null +++ b/dwave/optimization/include/dwave-optimization/cp/state/cpvar_state.hpp @@ -0,0 +1,57 @@ +// 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/domain_array.hpp" +#include "dwave-optimization/cp/state/state_manager.hpp" + +namespace dwave::optimization::cp { + +class CPVarData { + public: + 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 + 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; + 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.. + 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..3d4b8d4b --- /dev/null +++ b/dwave/optimization/include/dwave-optimization/cp/state/state.hpp @@ -0,0 +1,74 @@ +// 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 +#include + +#include "dwave-optimization/common.hpp" + +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(ssize_t value) : v_(value) {} + + ssize_t get_value() override { return v_; } + void set_value(ssize_t value) override { v_ = value; } + + virtual void increment() { v_++; } + virtual void decrement() { v_--; } + + protected: + ssize_t 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..dd9cc0b5 --- /dev/null +++ b/dwave/optimization/include/dwave-optimization/cp/state/state_entry.hpp @@ -0,0 +1,24 @@ +// Copyright 2026 D-Wave Systems Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +namespace dwave::optimization::cp { +class 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..2031df1d --- /dev/null +++ b/dwave/optimization/include/dwave-optimization/cp/state/state_manager.hpp @@ -0,0 +1,34 @@ +// 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/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(ssize_t 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..a2ca0a58 --- /dev/null +++ b/dwave/optimization/include/dwave-optimization/cp/state/state_stack.hpp @@ -0,0 +1,39 @@ +// 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" +#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..02711680 --- /dev/null +++ b/dwave/optimization/include/dwave-optimization/cp/state/storage.hpp @@ -0,0 +1,31 @@ +// 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/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/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 new file mode 100644 index 00000000..b1b42170 --- /dev/null +++ b/dwave/optimization/src/cp/core/cpvar.cpp @@ -0,0 +1,171 @@ +// 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 +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) {} + +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); +} + +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(); +} + +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); +} + +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); +} + +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& 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(Advisor&& adv) { on_bind.push_back(std::move(adv)); } + +void CPVar::propagate_on_bounds_change(Advisor&& adv) { on_bounds.push_back(std::move(adv)); } + +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); + assert(static_cast(state.var_state_.size()) > cp_var_index_); + + // TODO: for now we don't handle dynamically sized nodes + + 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(); + } + + 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()); +} + +} // 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..d04bff4c --- /dev/null +++ b/dwave/optimization/src/cp/core/engine.cpp @@ -0,0 +1,55 @@ +// 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 { + +CPStatus CPEngine::fix_point(CPState& state) const { + CPStatus status = CPStatus::OK; + + CPPropagatorsState& p_state = state.propagator_state_; + + while (state.propagation_queue_.size() > 0) { + 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); + 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_; + return 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..db09a5f7 --- /dev/null +++ b/dwave/optimization/src/cp/core/interval_array.cpp @@ -0,0 +1,417 @@ +// 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 +#include +#include +#include + +namespace dwave::optimization::cp { + +template <> +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 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); + + 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 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"); + } + + 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); + max_[i] = sm->make_state_real(ub); + } +} + +template <> +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()) + 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"); + } + + 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)); + max_[i] = sm->make_state_int(std::floor(ub)); + } +} + +template +IntervalArray::IntervalArray(StateManager* sm, ssize_t size, double lb, double ub) + : IntervalArray(sm, size, size, lb, ub) {} + +template <> +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(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"); + } + + min_[i] = sm->make_state_real(lb[i]); + max_[i] = sm->make_state_real(ub[i]); + } +} + +template <> +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()); + + 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 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])); + } +} + +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())); + return min_[index]->get_value(); +} + +template +double IntervalArray::max(int index) const { + assert(index < static_cast(max_.size())); + return max_[index]->get_value(); +} + +template <> +double IntervalArray::size(int index) const { + assert(index < static_cast(min_.size())); + return max_[index]->get_value() - min_[index]->get_value(); +} + +template <> +double IntervalArray::size(int index) const { + assert(index < static_cast(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 +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 + return CPStatus::OK; +} + +template <> +CPStatus IntervalArray::remove(double value, int index, DomainListener* l) { + // 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) 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(); + + if (change_max) { + max_[index]->set_value(value - 1); + l->change_max(index); + } else if (change_min) { + min_[index]->set_value(value + 1); + l->change_min(index); + } + l->change(index); + } + return CPStatus::OK; +} + +template <> +CPStatus IntervalArray::remove_above(double value, int index, DomainListener* l) { + // 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; + + max_[index]->set_value(value); + l->change_max(index); + l->change(index); + return CPStatus::OK; +} + +template <> +CPStatus IntervalArray::remove_above(double value, int index, DomainListener* l) { + // 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; + + max_[index]->set_value(std::floor(value)); + l->change_max(index); + l->change(index); + return CPStatus::OK; +} + +template <> +CPStatus IntervalArray::remove_below(double value, int index, DomainListener* l) { + // 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; + + min_[index]->set_value(value); + l->change_min(index); + l->change(index); + return CPStatus::OK; +} + +template <> +CPStatus IntervalArray::remove_below(double value, int index, DomainListener* l) { + // 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; + + min_[index]->set_value(std::ceil(value)); + l->change_min(index); + l->change(index); + return CPStatus::OK; +} + +template +CPStatus IntervalArray::remove_all_but(double value, int index, DomainListener* l) { + // 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); + } + + 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); + + if (changed_max) l->change_max(index); + if (changed_min) l->change_min(index); + l->change(index); + l->bind(index); + + 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(new_min_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(new_max_size); + } + 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..163ac1d6 --- /dev/null +++ b/dwave/optimization/src/cp/core/propagator.cpp @@ -0,0 +1,93 @@ +// 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 { + +// ------- PropagatorData -------- + +void PropagatorData::mark_index(ssize_t 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, int i) const { + assert(propagator_index_ >= 0); + assert(propagator_index_ < static_cast(state.size())); + return state[propagator_index_]->active(i); +} + +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, i); +} + +void Propagator::mark_index(CPPropagatorsState& state, ssize_t index) const { + assert(propagator_index_ >= 0); + 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 new file mode 100644 index 00000000..73700b11 --- /dev/null +++ b/dwave/optimization/src/cp/propagators/identity_propagator.cpp @@ -0,0 +1,77 @@ +// 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->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; +} + +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); + 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; +} + +} // namespace dwave::optimization::cp 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/dwave/optimization/src/cp/state/copier.cpp b/dwave/optimization/src/cp/state/copier.cpp new file mode 100644 index 00000000..3ca74a41 --- /dev/null +++ b/dwave/optimization/src/cp/state/copier.cpp @@ -0,0 +1,62 @@ +// 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" +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(ssize_t 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..705f20cf --- /dev/null +++ b/dwave/optimization/src/cp/state/copy.cpp @@ -0,0 +1,28 @@ +// 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 { + +/// 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..ec23f5f3 --- /dev/null +++ b/dwave/optimization/src/cp/state/cpvar_state.cpp @@ -0,0 +1,79 @@ +// 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 + +#include "dwave-optimization/cp/core/interval_array.hpp" + +namespace dwave::optimization::cp { + +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, min_size, max_size, lb, ub); + } else { + domains_ = std::make_unique(sm, min_size, max_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); } + +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()); +} + +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()); +} + +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/dwave/optimization/src/cp/state/state_stack.cpp b/dwave/optimization/src/cp/state/state_stack.cpp new file mode 100644 index 00000000..92194f39 --- /dev/null +++ b/dwave/optimization/src/cp/state/state_stack.cpp @@ -0,0 +1,50 @@ +// 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" +#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..78403b99 100644 --- a/meson.build +++ b/meson.build @@ -53,6 +53,22 @@ dwave_optimization_src = [ 'dwave/optimization/src/fraction.cpp', '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/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', + '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/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_copier.cpp b/tests/cpp/cp/test_copier.cpp new file mode 100644 index 00000000..c8f7216a --- /dev/null +++ b/tests/cpp/cp/test_copier.cpp @@ -0,0 +1,97 @@ +// 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 + +#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..39c8f4d0 --- /dev/null +++ b/tests/cpp/cp/test_cp_var.cpp @@ -0,0 +1,191 @@ +// 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/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("IntegerNode -> CPVar") { + 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") { + 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); + } + } + } + } + } +} + +} // 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..abf91ff2 --- /dev/null +++ b/tests/cpp/cp/test_interval.cpp @@ -0,0 +1,253 @@ +// 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 (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(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); + 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() == 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); + 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() == 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)); + } + } + } + } +} + +} // namespace dwave::optimization::cp diff --git a/tests/cpp/cp/test_propagator.cpp b/tests/cpp/cp/test_propagator.cpp new file mode 100644 index 00000000..ce0f7c03 --- /dev/null +++ b/tests/cpp/cp/test_propagator.cpp @@ -0,0 +1,112 @@ +// 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 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->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->scheduled(p_state, i)); + } else { + 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)); + } + } + } + } + } + } +} +} // namespace dwave::optimization::cp diff --git a/tests/cpp/cp/utils.hpp b/tests/cpp/cp/utils.hpp new file mode 100644 index 00000000..6f7b64d4 --- /dev/null +++ b/tests/cpp/cp/utils.hpp @@ -0,0 +1,28 @@ +// 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" + +namespace dwave::optimization::cp { +class TestListener : public DomainListener { + public: + 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 ef23f764..448fd184 100644 --- a/tests/cpp/meson.build +++ b/tests/cpp/meson.build @@ -7,6 +7,13 @@ catch2 = subproject( tests_all = executable( 'tests-all', [ + 'cp/test_interval.cpp', + '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', 'nodes/test_binaryop.cpp', @@ -29,6 +36,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',