Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
3f5e37f
Add stateless axis-wise bound info to NumberNode
fastbodin Jan 6, 2026
363192c
Add axis-wise bound state dependant data to NumberNode
fastbodin Jan 6, 2026
8e87082
Add NumberNode axis-wise bound methods
fastbodin Jan 12, 2026
80048a2
Simplify NumberNodeStateData
fastbodin Jan 28, 2026
eaf979c
NumberNode: Construct state given exactly one axis-wise bound.
fastbodin Jan 29, 2026
bf0b648
Improve NumberNode bound axes
fastbodin Jan 29, 2026
1cc5c57
Clean up axis-wise bound NumberNode C++ code
fastbodin Jan 30, 2026
f97bc07
Fixed issue in `NumberNode::initialize()`
fastbodin Feb 2, 2026
15b047c
BoundAxisOperator is now an enum class
fastbodin Feb 2, 2026
265c775
NumberNode bound_axis arg. is no longer optional
fastbodin Feb 2, 2026
ca17501
NumberNode checks feasibility of axis-wise bounds at construction.
fastbodin Feb 3, 2026
73f2bf0
Correct BoundAxisInfo get_bound and get_operator
fastbodin Feb 3, 2026
479375c
Expose NumberNode axis-wise bounds to Python
fastbodin Feb 3, 2026
5ce44c6
Enabled zip/unzip of axis-wise bounds on NumberNode
fastbodin Feb 3, 2026
9c813b1
Fixed integer and binary python docs
fastbodin Feb 3, 2026
50950ce
Added release note for axis-wise bounds
fastbodin Feb 3, 2026
70e42ac
Cleaning NumberNode axis-wise bounds
fastbodin Feb 3, 2026
09690d1
Restrict NumberNode _from_zip return type
fastbodin Feb 4, 2026
0663e9c
Cleaned up C++ code, comments, and tests for NumberNode
fastbodin Feb 4, 2026
cfd2216
Cleaned up Python and Cython code for NumberNode
fastbodin Feb 4, 2026
4b81d02
New names for NumberNode bound axis data
fastbodin Feb 5, 2026
e2b34dc
Address 1st rnd. comments NumberNode axis-wise bounds
fastbodin Feb 5, 2026
7936406
Address 2nd rnd. comments NumberNode axis-wise bounds
fastbodin Feb 6, 2026
0ec4bbb
Reformat AxisBound struct on NumberNode
fastbodin Feb 6, 2026
156481a
Reformat NumberNode mutate methods
fastbodin Feb 6, 2026
ebe6091
Modified arg types for `NumberNode::bound_axis_sums()
fastbodin Feb 11, 2026
3b10b41
Override copy method to NumberNodeStateData
fastbodin Feb 12, 2026
e44833f
Allow bounds over entire `NumberNode` array at C++ level.
fastbodin Feb 27, 2026
0553d55
Allow bounds over entire `NumberNode` array at Python level.
fastbodin Feb 27, 2026
c7d3b1b
Simplify Integer and Binary symbols `from_zip()`
fastbodin Mar 6, 2026
51662b0
Changed `NumberNode` bound axis naming convention at C++ level
fastbodin Mar 6, 2026
8380096
Changed `NumberNode` bound axis naming convention at Python level
fastbodin Mar 6, 2026
531d2ee
`NumberNode::sum_constraint()` -> `NumberNode::sum_constraints()`
fastbodin Mar 8, 2026
30bb86c
`NumberNode::sum_constraint_sum` -> `NumberNode::sum_constraints_lhs`
fastbodin Mar 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 133 additions & 31 deletions dwave/optimization/include/dwave-optimization/nodes/numbers.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,55 @@ namespace dwave::optimization {
/// A contiguous block of numbers.
class NumberNode : public ArrayOutputMixin<ArrayNode>, public DecisionNode {
public:
/// Stateless sum constraint information.
///
/// A sum constraint constrains the sum of values within slices of the array.
/// The slices are defined along `axis` when `axis` has a value. If
/// `axis == std::nullopt`, the constraint is applied to the entire array,
/// which is treated as a flat array with a single slice.
///
/// Constraints may be defined either:
/// - for ALL slices (the `operators` and `bounds` vectors have length 1), or
/// - PER slice (their lengths equal the number of slices along `axis`).
///
/// Each slice sum is constrained by an `Operator` and a corresponding `bound`.
struct SumConstraint {
public:
/// Allowable operators.
enum class Operator { Equal, LessEqual, GreaterEqual };

/// To reduce the # of `IntegerNode` and `BinaryNode` constructors, we
/// allow only one constructor.
SumConstraint(std::optional<ssize_t> axis, std::vector<Operator> operators,
std::vector<double> bounds);

/// Return the axis along which slices are defined.
/// If `std::nullopt`, the sum constraint applies to the entire array.
std::optional<ssize_t> axis() const { return axis_; };

/// Obtain the bound associated with a given slice.
double get_bound(const ssize_t slice) const;

/// Obtain the operator associated with a given slice.
Operator get_operator(const ssize_t slice) const;

/// The number of bounds.
ssize_t num_bounds() const { return bounds_.size(); };

/// The number of operators.
ssize_t num_operators() const { return operators_.size(); };

private:
/// Axis along which slices are defined (`std::nullopt` = whole array).
std::optional<ssize_t> axis_ = std::nullopt;
/// Operator for ALL slices (vector has length one) or operators PER
/// slice (length of vector is equal to the number of slices).
std::vector<Operator> operators_;
/// Bound for ALL slices (vector has length one) or bounds PER slice
/// (length of vector is equal to the number of slices).
std::vector<double> bounds_;
};

NumberNode() = delete;

// Overloads needed by the Array ABC **************************************
Expand Down Expand Up @@ -68,6 +117,11 @@ class NumberNode : public ArrayOutputMixin<ArrayNode>, public DecisionNode {
// Initialize the state of the node randomly
template <std::uniform_random_bit_generator Generator>
void initialize_state(State& state, Generator& rng) const {
// Currently do not support random node initialization with sum constraints.
if (sum_constraints_.size() > 0) {
throw std::invalid_argument("Cannot randomly initialize_state with sum constraints.");
}

std::vector<double> values;
const ssize_t size = this->size();
values.reserve(size);
Expand All @@ -86,6 +140,9 @@ class NumberNode : public ArrayOutputMixin<ArrayNode>, public DecisionNode {
return initialize_state(state, std::move(values));
}

/// @copydoc Node::propagate()
void propagate(State& state) const override;

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

// In the given state, swap the value of index i with the value of index j.
Expand All @@ -106,21 +163,43 @@ class NumberNode : public ArrayOutputMixin<ArrayNode>, public DecisionNode {
// in a given index.
void clip_and_set_value(State& state, ssize_t index, double value) const;

/// Return the stateless sum constraints.
const std::vector<SumConstraint>& sum_constraints() const;

/// If the node is subject to sum constraints, we track the state
/// dependent sum of the values within each slice per constraint. The
/// returned vector is indexed in the same ordering as the constraints
/// given by `sum_constraints()`.
const std::vector<std::vector<double>>& sum_constraints_lhs(const State& state) const;

protected:
explicit NumberNode(std::span<const ssize_t> shape, std::vector<double> lower_bound,
std::vector<double> upper_bound);
std::vector<double> upper_bound,
std::vector<SumConstraint> sum_constraints = {});

// Return truth statement: 'value is valid in a given index'.
virtual bool is_valid(ssize_t index, double value) const = 0;

// Default value in a given index.
virtual double default_value(ssize_t index) const = 0;

/// Update the relevant sum constraints running sums (`lhs`) given that the
/// value stored at `index` is changed by `value_change` in a given state.
void update_sum_constraints_lhs(State& state, const ssize_t index,
const double value_change) const;

/// Statelss global minimum and maximum of the values stored in NumberNode.
double min_;
double max_;

/// Stateless index-wise upper and lower bounds.
std::vector<double> lower_bounds_;
std::vector<double> upper_bounds_;

/// Stateless sum constraints.
std::vector<SumConstraint> sum_constraints_;
/// Indicator variable that all sum constraint operators are "==".
bool sum_constraints_all_equals_;
};

/// A contiguous block of integer numbers.
Expand All @@ -134,33 +213,45 @@ class IntegerNode : public NumberNode {
// Default to a single scalar integer with default bounds
IntegerNode() : IntegerNode({}) {}

// Create an integer array with the user-defined bounds.
// Defaulting to the specified default bounds.
// Create an integer array with the user-defined index-wise bounds and sum
// constraints. Index-wise bounds default to the specified default bounds.
// By default, there are no sum constraints.
IntegerNode(std::span<const ssize_t> shape,
std::optional<std::vector<double>> lower_bound = std::nullopt,
std::optional<std::vector<double>> upper_bound = std::nullopt);
std::optional<std::vector<double>> upper_bound = std::nullopt,
std::vector<SumConstraint> sum_constraints = {});
IntegerNode(std::initializer_list<ssize_t> shape,
std::optional<std::vector<double>> lower_bound = std::nullopt,
std::optional<std::vector<double>> upper_bound = std::nullopt);
std::optional<std::vector<double>> upper_bound = std::nullopt,
std::vector<SumConstraint> sum_constraints = {});
IntegerNode(ssize_t size, std::optional<std::vector<double>> lower_bound = std::nullopt,
std::optional<std::vector<double>> upper_bound = std::nullopt);
std::optional<std::vector<double>> upper_bound = std::nullopt,
std::vector<SumConstraint> sum_constraints = {});

IntegerNode(std::span<const ssize_t> shape, double lower_bound,
std::optional<std::vector<double>> upper_bound = std::nullopt);
std::optional<std::vector<double>> upper_bound = std::nullopt,
std::vector<SumConstraint> sum_constraints = {});
IntegerNode(std::initializer_list<ssize_t> shape, double lower_bound,
std::optional<std::vector<double>> upper_bound = std::nullopt);
std::optional<std::vector<double>> upper_bound = std::nullopt,
std::vector<SumConstraint> sum_constraints = {});
IntegerNode(ssize_t size, double lower_bound,
std::optional<std::vector<double>> upper_bound = std::nullopt);
std::optional<std::vector<double>> upper_bound = std::nullopt,
std::vector<SumConstraint> sum_constraints = {});

IntegerNode(std::span<const ssize_t> shape, std::optional<std::vector<double>> lower_bound,
double upper_bound);
double upper_bound, std::vector<SumConstraint> sum_constraints = {});
IntegerNode(std::initializer_list<ssize_t> shape,
std::optional<std::vector<double>> lower_bound, double upper_bound);
IntegerNode(ssize_t size, std::optional<std::vector<double>> lower_bound, double upper_bound);

IntegerNode(std::span<const ssize_t> shape, double lower_bound, double upper_bound);
IntegerNode(std::initializer_list<ssize_t> shape, double lower_bound, double upper_bound);
IntegerNode(ssize_t size, double lower_bound, double upper_bound);
std::optional<std::vector<double>> lower_bound, double upper_bound,
std::vector<SumConstraint> sum_constraints = {});
IntegerNode(ssize_t size, std::optional<std::vector<double>> lower_bound, double upper_bound,
std::vector<SumConstraint> sum_constraints = {});

IntegerNode(std::span<const ssize_t> shape, double lower_bound, double upper_bound,
std::vector<SumConstraint> sum_constraints = {});
IntegerNode(std::initializer_list<ssize_t> shape, double lower_bound, double upper_bound,
std::vector<SumConstraint> sum_constraints = {});
IntegerNode(ssize_t size, double lower_bound, double upper_bound,
std::vector<SumConstraint> sum_constraints = {});

// Overloads needed by the Node ABC ***************************************

Expand Down Expand Up @@ -190,33 +281,44 @@ class BinaryNode : public IntegerNode {
/// A binary scalar variable with lower_bound = 0.0 and upper_bound = 1.0
BinaryNode() : BinaryNode({}) {}

// Create a binary array with the user-defined bounds.
// Defaulting to lower_bound = 0.0 and upper_bound = 1.0
// Create a binary array with the user-defined index-wise bounds and sum
// constraints. Index-wise bounds default to lower_bound = 0.0 and
// upper_bound = 1.0. By default, there are no sum constraints.
BinaryNode(std::span<const ssize_t> shape,
std::optional<std::vector<double>> lower_bound = std::nullopt,
std::optional<std::vector<double>> upper_bound = std::nullopt);
std::optional<std::vector<double>> upper_bound = std::nullopt,
std::vector<SumConstraint> sum_constraints = {});
BinaryNode(std::initializer_list<ssize_t> shape,
std::optional<std::vector<double>> lower_bound = std::nullopt,
std::optional<std::vector<double>> upper_bound = std::nullopt);
std::optional<std::vector<double>> upper_bound = std::nullopt,
std::vector<SumConstraint> sum_constraints = {});
BinaryNode(ssize_t size, std::optional<std::vector<double>> lower_bound = std::nullopt,
std::optional<std::vector<double>> upper_bound = std::nullopt);
std::optional<std::vector<double>> upper_bound = std::nullopt,
std::vector<SumConstraint> sum_constraints = {});

BinaryNode(std::span<const ssize_t> shape, double lower_bound,
std::optional<std::vector<double>> upper_bound = std::nullopt);
std::optional<std::vector<double>> upper_bound = std::nullopt,
std::vector<SumConstraint> sum_constraints = {});
BinaryNode(std::initializer_list<ssize_t> shape, double lower_bound,
std::optional<std::vector<double>> upper_bound = std::nullopt);
std::optional<std::vector<double>> upper_bound = std::nullopt,
std::vector<SumConstraint> sum_constraints = {});
BinaryNode(ssize_t size, double lower_bound,
std::optional<std::vector<double>> upper_bound = std::nullopt);
std::optional<std::vector<double>> upper_bound = std::nullopt,
std::vector<SumConstraint> sum_constraints = {});

BinaryNode(std::span<const ssize_t> shape, std::optional<std::vector<double>> lower_bound,
double upper_bound);
double upper_bound, std::vector<SumConstraint> sum_constraints = {});
BinaryNode(std::initializer_list<ssize_t> shape, std::optional<std::vector<double>> lower_bound,
double upper_bound);
BinaryNode(ssize_t size, std::optional<std::vector<double>> lower_bound, double upper_bound);

BinaryNode(std::span<const ssize_t> shape, double lower_bound, double upper_bound);
BinaryNode(std::initializer_list<ssize_t> shape, double lower_bound, double upper_bound);
BinaryNode(ssize_t size, double lower_bound, double upper_bound);
double upper_bound, std::vector<SumConstraint> sum_constraints = {});
BinaryNode(ssize_t size, std::optional<std::vector<double>> lower_bound, double upper_bound,
std::vector<SumConstraint> sum_constraints = {});

BinaryNode(std::span<const ssize_t> shape, double lower_bound, double upper_bound,
std::vector<SumConstraint> sum_constraints = {});
BinaryNode(std::initializer_list<ssize_t> shape, double lower_bound, double upper_bound,
std::vector<SumConstraint> sum_constraints = {});
BinaryNode(ssize_t size, double lower_bound, double upper_bound,
std::vector<SumConstraint> sum_constraints = {});

// Flip the value (0 -> 1 or 1 -> 0) at index i in the given state.
void flip(State& state, ssize_t i) const;
Expand Down
33 changes: 26 additions & 7 deletions dwave/optimization/libcpp/nodes/numbers.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,42 @@
# See the License for the specific language governing permissions and
# limitations under the License.

from libcpp.optional cimport optional
from libcpp.vector cimport vector

from dwave.optimization.libcpp.graph cimport ArrayNode
from dwave.optimization.libcpp.state cimport State


cdef extern from "dwave-optimization/nodes/numbers.hpp" namespace "dwave::optimization" nogil:
cdef cppclass IntegerNode(ArrayNode):
void initialize_state(State&, vector[double]) except+
double lower_bound(Py_ssize_t index)
double upper_bound(Py_ssize_t index)
double lower_bound() except+
double upper_bound() except+

cdef cppclass BinaryNode(ArrayNode):
cdef cppclass NumberNode(ArrayNode):
struct SumConstraint:
# It appears Cython automatically assumes all (standard) enums are "public".
# Because of this, we use this very explict override.
enum class Operator "dwave::optimization::NumberNode::SumConstraint::Operator":
Equal
LessEqual
GreaterEqual

SumConstraint(optional[Py_ssize_t] axis, vector[Operator] operators,
vector[double] bounds)

optional[Py_ssize_t] axis()
double get_bound(Py_ssize_t slice)
Operator get_operator(Py_ssize_t slice)
Py_ssize_t num_bounds()
Py_ssize_t num_operators()

void initialize_state(State&, vector[double]) except+
double lower_bound(Py_ssize_t index)
double upper_bound(Py_ssize_t index)
double lower_bound() except+
double upper_bound() except+
const vector[SumConstraint] sum_constraints()

cdef cppclass IntegerNode(NumberNode):
pass

cdef cppclass BinaryNode(IntegerNode):
pass
Loading