Skip to content

Commit c6a93d5

Browse files
committed
Address 1st rnd. comments NumberNode axis-wise bounds
1 parent ffdae9a commit c6a93d5

6 files changed

Lines changed: 57 additions & 61 deletions

File tree

dwave/optimization/include/dwave-optimization/nodes/numbers.hpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,8 @@ class NumberNode : public ArrayOutputMixin<ArrayNode>, public DecisionNode {
144144
const std::vector<AxisBound>& axis_wise_bounds() const;
145145

146146
/// Return the state-dependent sum of the values within each hyperslice
147-
/// along each bound axis.
147+
/// along each bound axis. The returned vector is indexed by the
148+
/// bound axes in the same ordering that `axis_wise_bounds()` returns.
148149
const std::vector<std::vector<double>>& bound_axis_sums(State& state) const;
149150

150151
protected:

dwave/optimization/libcpp/nodes/numbers.pxd

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,10 @@ cdef extern from "dwave-optimization/nodes/numbers.hpp" namespace "dwave::optimi
3030
GreaterEqual
3131

3232
AxisBound(Py_ssize_t axis, vector[Operator] axis_opertors,
33-
vector[double] axis_bounds)
33+
vector[double] axis_bounds)
3434
Py_ssize_t axis
35-
vector[Operator] operators;
36-
vector[double] bounds;
35+
vector[Operator] operators
36+
vector[double] bounds
3737

3838
void initialize_state(State&, vector[double]) except+
3939
double lower_bound(Py_ssize_t index)

dwave/optimization/model.py

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,8 @@ def objective(self, value: ArraySymbol):
166166
def binary(self, shape: None | _ShapeLike = None,
167167
lower_bound: None | np.typing.ArrayLike = None,
168168
upper_bound: None | np.typing.ArrayLike = None,
169-
subject_to: None | np.typing.ArrayLike = None) -> BinaryVariable:
169+
subject_to: None | list[tuple(int, str | list[str], float |
170+
list[float])] = None) -> BinaryVariable:
170171
r"""Create a binary symbol as a decision variable.
171172
172173
Args:
@@ -183,13 +184,13 @@ def binary(self, shape: None | _ShapeLike = None,
183184
array of tuples where each tuple has the form: (axis, operators, bounds)
184185
- axis (int): The axis along which the bounds are applied.
185186
- operators (str | array[str]): The operator(s) ("<=", "==", or ">=").
186-
A single operator applies to all hyperslices along the axis; an
187-
array specifies one operator per hyperslice.
187+
A single operator applies to all slices along the axis; an
188+
array specifies one operator per slice.
188189
- bounds (float | array[float]): The bound value(s). A single value
189-
applies to all hyperslices; an array specifies one bound per hyperslice.
190-
If provided, the sum of values within each hyperslice along the specified
191-
axis must satisfy the corresponding operator–bound pair.
192-
Note: At most one axis-wise bound may be provided.
190+
applies to all slices; an array specifies one bound per slice.
191+
If provided, the sum of values within each slice along the
192+
specified axis must satisfy the corresponding operator–bound
193+
pair. Note: At most one axis-wise bound may be provided.
193194
194195
Returns:
195196
A binary symbol.
@@ -230,13 +231,13 @@ def binary(self, shape: None | _ShapeLike = None,
230231
This example adds a :math:`(2x3)`-sized binary symbol with
231232
index-wise lower bounds and an axis-wise bound along axis 1. Let
232233
x_i (int i : 0 <= i <= 2) denote the sum of the values within
233-
hyperslice i along axis 1. For each state defined for this symbol:
234+
slice i along axis 1. For each state defined for this symbol:
234235
(x_0 <= 0), (x_1 == 2), and (x_2 >= 1).
235236
236237
>>> from dwave.optimization.model import Model
237238
>>> import numpy as np
238239
>>> model = Model()
239-
>>> n = model.binary([2, 3], lower_bound=[[0, 1, 1], [0, 1, 0]],
240+
>>> b = model.binary([2, 3], lower_bound=[[0, 1, 1], [0, 1, 0]],
240241
... subject_to=[(1, ["<=", "==", ">="], [0, 2, 1])])
241242
>>> np.all(n.axis_wise_bounds() == [(1, ["<=", "==", ">="], [0, 2, 1])])
242243
True
@@ -508,8 +509,8 @@ def integer(
508509
shape: None | _ShapeLike = None,
509510
lower_bound: None | numpy.typing.ArrayLike = None,
510511
upper_bound: None | numpy.typing.ArrayLike = None,
511-
subject_to: None | np.typing.ArrayLike = None
512-
) -> IntegerVariable:
512+
subject_to: None | list[tuple(int, str | list[str], float |
513+
list[float])] = None) -> IntegerVariable:
513514
r"""Create an integer symbol as a decision variable.
514515
515516
Args:
@@ -526,13 +527,13 @@ def integer(
526527
array of tuples where each tuple has the form: (axis, operators, bounds)
527528
- axis (int): The axis along which the bounds are applied.
528529
- operators (str | array[str]): The operator(s) ("<=", "==", or ">=").
529-
A single operator applies to all hyperslices along the axis; an
530-
array specifies one operator per hyperslice.
530+
A single operator applies to all slice along the axis; an array
531+
specifies one operator per slice.
531532
- bounds (float | array[float]): The bound value(s). A single value
532-
applies to all hyperslices; an array specifies one bound per hyperslice.
533-
If provided, the sum of values within each hyperslice along the specified
534-
axis must satisfy the corresponding operator–bound pair.
535-
Note: At most one axis-wise bound may be provided.
533+
applies to all slices; an array specifies one bound per slice.
534+
If provided, the sum of values within each slice along the
535+
specified axis must satisfy the corresponding operator–bound
536+
pair. Note: At most one axis-wise bound may be provided.
536537
537538
Returns:
538539
An integer symbol.
@@ -574,7 +575,7 @@ def integer(
574575
This example adds a :math:`(2x3)`-sized integer symbol with general
575576
lower and upper bounds and an axis-wise bound along axis 1. Let x_i
576577
(int i : 0 <= i <= 2) denote the sum of the values within
577-
hyperslice i along axis 1. For each state defined for this symbol:
578+
slice i along axis 1. For each state defined for this symbol:
578579
(x_0 <= 2), (x_1 <= 4), and (x_2 <= 5).
579580
580581
>>> from dwave.optimization.model import Model

dwave/optimization/src/nodes/numbers.cpp

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -177,19 +177,18 @@ void NumberNode::initialize_state(State& state, std::vector<double>&& number_dat
177177

178178
if (bound_axes_info_.size() == 0) { // No bound axes to consider.
179179
emplace_data_ptr<NumberNodeStateData>(state, std::move(number_data));
180-
return;
181-
}
180+
} else {
181+
// Given the assingnment to NumberNode `number_data`, compute the sum of the
182+
// values within each hyperslice along each bound axis.
183+
std::vector<std::vector<double>> bound_axes_sums = get_bound_axes_sums(this, number_data);
182184

183-
// Given the assingnment to NumberNode `number_data`, compute the sum of the
184-
// values within each hyperslice along each bound axis.
185-
std::vector<std::vector<double>> bound_axes_sums = get_bound_axes_sums(this, number_data);
185+
if (!satisfies_axis_wise_bounds(bound_axes_info_, bound_axes_sums)) {
186+
throw std::invalid_argument("Initialized values do not satisfy axis-wise bounds.");
187+
}
186188

187-
if (!satisfies_axis_wise_bounds(bound_axes_info_, bound_axes_sums)) {
188-
throw std::invalid_argument("Initialized values do not satisfy axis-wise bounds.");
189+
emplace_data_ptr<NumberNodeStateData>(state, std::move(number_data),
190+
std::move(bound_axes_sums));
189191
}
190-
191-
emplace_data_ptr<NumberNodeStateData>(state, std::move(number_data),
192-
std::move(bound_axes_sums));
193192
}
194193

195194
/// Given a `span` (used for strides or shape data), reorder the values
@@ -282,8 +281,8 @@ void construct_state_given_exactly_one_bound_axis(const NumberNode* node,
282281
const std::vector<ssize_t> buff_strides = shift_axis_data(node->strides(), bound_axis);
283282
// Define an iterator for `values` corresponding with the beginning of
284283
// slice 0 along the bound axis.
285-
BufferIterator<double, double, false> slice_0_it(values.data(), ndim, buff_shape.data(),
286-
buff_strides.data());
284+
const BufferIterator<double, double, false> slice_0_it(values.data(), ndim, buff_shape.data(),
285+
buff_strides.data());
287286
// Determine the size of each hyperslice along the bound axis.
288287
const ssize_t slice_size = std::accumulate(buff_shape.begin() + 1, buff_shape.end(), 1.0,
289288
std::multiplies<ssize_t>());
@@ -336,14 +335,12 @@ void NumberNode::initialize_state(State& state) const {
336335
values.push_back(default_value(i));
337336
}
338337
initialize_state(state, std::move(values));
339-
return;
340338
} else if (bound_axes_info_.size() == 1) {
341339
construct_state_given_exactly_one_bound_axis(this, values);
342340
initialize_state(state, std::move(values));
343-
return;
341+
} else {
342+
unreachable();
344343
}
345-
346-
throw std::invalid_argument("Cannot initialize state with multiple bound axes.");
347344
}
348345

349346
void NumberNode::commit(State& state) const noexcept {

dwave/optimization/symbols/numbers.pyx

Lines changed: 14 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import json
1818

19+
import collections.abc
1920
import numpy as np
2021

2122
from cython.operator cimport typeid
@@ -71,25 +72,20 @@ cdef vector[NumberNode.AxisBound] _convert_python_bound_axes(
7172
if not isinstance(axis, int):
7273
raise TypeError("Bound axis must be an int.")
7374

74-
cpp_ops.clear()
7575
if isinstance(py_ops, str):
76+
cpp_ops.resize(1)
7677
# One operator defined for all slices.
77-
cpp_ops.push_back(_parse_python_operator(py_ops))
78-
else:
78+
cpp_ops[0] = _parse_python_operator(py_ops)
79+
elif isinstance(py_ops, collections.abc.Iterable):
7980
# Operator defined per slice.
80-
ops_array = np.asarray(py_ops, order='C')
81-
if (ops_array.ndim <= 1):
82-
cpp_ops.reserve(ops_array.size)
83-
for op in ops_array:
84-
# Convert op to `str` because _parse_python_operator()
85-
# does not expect a `numpy.str_`.
86-
cpp_ops.push_back(_parse_python_operator(str(op)))
87-
else:
88-
raise TypeError("Bound axis operator(s) should be str or"
89-
" a 1D-array of str(s).")
81+
cpp_ops.reserve(len(py_ops))
82+
for op in py_ops:
83+
cpp_ops.push_back(_parse_python_operator(op))
84+
else:
85+
raise TypeError("Bound axis operator(s) should be str or a 1D-array"
86+
" of str(s).")
9087

91-
cpp_bounds.clear()
92-
bound_array = np.asarray_chkfinite(py_bounds, dtype=np.double, order='C')
88+
bound_array = np.asarray_chkfinite(py_bounds, dtype=np.double)
9389
if (bound_array.ndim <= 1):
9490
mem = bound_array.ravel()
9591
cpp_bounds.reserve(mem.shape[0])
@@ -98,7 +94,7 @@ cdef vector[NumberNode.AxisBound] _convert_python_bound_axes(
9894
else:
9995
raise TypeError("Bound axis bound(s) should be scalar or 1D-array.")
10096

101-
output.push_back(NumberNode.AxisBound(axis, cpp_ops, cpp_bounds))
97+
output.push_back(NumberNode.AxisBound(axis, move(cpp_ops), move(cpp_bounds)))
10298

10399
return output
104100

@@ -206,10 +202,9 @@ cdef class BinaryVariable(ArraySymbol):
206202
subject_to = None
207203
else:
208204
with zf.open(info, "r") as f:
209-
subject_to = json.load(f)
210205
# Note that import is a list of lists, not a list of tuples.
211206
# Hence we convert to tuple. We could also support lists.
212-
subject_to = [(axis, ops, bounds) for axis, ops, bounds in subject_to]
207+
subject_to = [(axis, ops, bounds) for axis, ops, bounds in json.load(f)]
213208

214209
return BinaryVariable(model,
215210
shape=shape_info["shape"],
@@ -415,11 +410,9 @@ cdef class IntegerVariable(ArraySymbol):
415410
subject_to = None
416411
else:
417412
with zf.open(info, "r") as f:
418-
# Note that import is a list of lists, not a list of tuples
419-
subject_to = json.load(f)
420413
# Note that import is a list of lists, not a list of tuples.
421414
# Hence we convert to tuple. We could also support lists.
422-
subject_to = [(axis, ops, bounds) for axis, ops, bounds in subject_to]
415+
subject_to = [(axis, ops, bounds) for axis, ops, bounds in json.load(f)]
423416

424417
return IntegerVariable(model,
425418
shape=shape_info["shape"],

tests/test_symbols.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -742,7 +742,7 @@ def test_axis_wise_bounds(self):
742742
self.assertEqual(x.axis_wise_bounds(), [(0, ["<=", "=="], [1])])
743743
x = model.binary((2, 3), subject_to=[(0, "<=", 1)])
744744
self.assertEqual(x.axis_wise_bounds(), [(0, ["<="], [1])])
745-
x = model.binary((2, 3), subject_to=[(0, np.asarray(["<=", "=="]), np.asarray([1, 2]))])
745+
x = model.binary((2, 3), subject_to=[(0, ["<=", "=="], np.asarray([1, 2]))])
746746
self.assertEqual(x.axis_wise_bounds(), [(0, ["<=", "=="], [1, 2])])
747747

748748
# infeasible axis-wise bounds
@@ -1924,7 +1924,7 @@ def test_axis_wise_bounds(self):
19241924
self.assertEqual(x.axis_wise_bounds(), [(0, ["<=", "=="], [1])])
19251925
x = model.integer((2, 3), subject_to=[(0, "<=", 1)])
19261926
self.assertEqual(x.axis_wise_bounds(), [(0, ["<="], [1])])
1927-
x = model.integer((2, 3), subject_to=[(0, np.asarray(["<=", "=="]), np.asarray([1, 2]))])
1927+
x = model.integer((2, 3), subject_to=[(0, ["<=", "=="], np.asarray([1, 2]))])
19281928
self.assertEqual(x.axis_wise_bounds(), [(0, ["<=", "=="], [1, 2])])
19291929

19301930
# infeasible axis-wise bounds
@@ -1953,6 +1953,10 @@ def test_axis_wise_bounds(self):
19531953
with self.assertRaises(TypeError):
19541954
model.integer((2, 3), subject_to=[(1, [["=="]], [0, 0, 0])])
19551955

1956+
# invalid number of bound axes
1957+
with self.assertRaises(ValueError):
1958+
model.integer((2, 3), subject_to=[(0, "==", 1), (1, "<=", [1, 1, 1])])
1959+
19561960
# Todo: we can generalize many of these tests for all decisions that can have
19571961
# their state set
19581962

0 commit comments

Comments
 (0)