Skip to content

Enhancement/Sum constraints on NumberNode#471

Open
fastbodin wants to merge 33 commits intodwavesystems:mainfrom
fastbodin:enhancement/axis_wise_bounds_numbernode
Open

Enhancement/Sum constraints on NumberNode#471
fastbodin wants to merge 33 commits intodwavesystems:mainfrom
fastbodin:enhancement/axis_wise_bounds_numbernode

Conversation

@fastbodin
Copy link
Copy Markdown
Contributor

@fastbodin fastbodin commented Jan 30, 2026

Do not merge. Draft PR. Only C++ has been implemented. Simply checking CircleCi.

@fastbodin fastbodin force-pushed the enhancement/axis_wise_bounds_numbernode branch 2 times, most recently from bebd145 to 15f7b67 Compare February 3, 2026 23:11
@fastbodin
Copy link
Copy Markdown
Contributor Author

Feature is fully implemented.

@fastbodin fastbodin force-pushed the enhancement/axis_wise_bounds_numbernode branch from e3b0255 to 1c177b4 Compare February 4, 2026 00:02
@fastbodin fastbodin marked this pull request as ready for review February 4, 2026 20:24
@fastbodin
Copy link
Copy Markdown
Contributor Author

fastbodin commented Feb 4, 2026

Closes #216.

Comment thread dwave/optimization/include/dwave-optimization/nodes/numbers.hpp Outdated
@arcondello arcondello requested a review from wbernoudy February 4, 2026 22:49
@arcondello arcondello added the enhancement New feature or request label Feb 4, 2026
Comment thread dwave/optimization/include/dwave-optimization/nodes/numbers.hpp Outdated
Comment thread dwave/optimization/libcpp/nodes/numbers.pxd Outdated
Comment thread dwave/optimization/libcpp/nodes/numbers.pxd Outdated
Comment thread dwave/optimization/symbols/numbers.pyx Outdated
Comment thread dwave/optimization/symbols/numbers.pyx Outdated
Comment thread dwave/optimization/model.py Outdated
Comment thread dwave/optimization/src/nodes/numbers.cpp Outdated
Comment thread dwave/optimization/src/nodes/numbers.cpp Outdated
Comment thread dwave/optimization/src/nodes/numbers.cpp Outdated
Comment thread dwave/optimization/include/dwave-optimization/nodes/numbers.hpp Outdated
@fastbodin
Copy link
Copy Markdown
Contributor Author

First round of comments addressed.

@fastbodin fastbodin force-pushed the enhancement/axis_wise_bounds_numbernode branch from 4b2205c to c6a93d5 Compare February 5, 2026 06:37
Comment thread dwave/optimization/symbols/numbers.pyx Outdated
Comment thread dwave/optimization/symbols/numbers.pyx Outdated
@wbernoudy
Copy link
Copy Markdown
Member

I do see "hyperslice" used in a lot of comments. Would prefer that is changed to just "slice" as well, or at least a small comment maybe in the header numbers.hpp on what that term means here.

Comment thread dwave/optimization/src/nodes/numbers.cpp Outdated
Comment thread dwave/optimization/src/nodes/numbers.cpp Outdated
Comment thread dwave/optimization/src/nodes/numbers.cpp Outdated
Comment thread tests/test_symbols.py
@fastbodin fastbodin force-pushed the enhancement/axis_wise_bounds_numbernode branch from c6a93d5 to 72c6d96 Compare February 6, 2026 02:05
@fastbodin
Copy link
Copy Markdown
Contributor Author

Second round of comments addressed.

Copy link
Copy Markdown
Member

@arcondello arcondello left a comment

Choose a reason for hiding this comment

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

Looks great! Just two aesthetic comments

Comment thread dwave/optimization/include/dwave-optimization/nodes/numbers.hpp Outdated
Comment thread dwave/optimization/src/nodes/numbers.cpp
@fastbodin
Copy link
Copy Markdown
Contributor Author

Third round of comments addressed.

Comment thread dwave/optimization/src/nodes/numbers.cpp Outdated
@fastbodin fastbodin force-pushed the enhancement/axis_wise_bounds_numbernode branch from a92e2bd to 116d03b Compare February 11, 2026 20:36
Comment thread dwave/optimization/symbols/numbers.pyx Outdated
@fastbodin fastbodin force-pushed the enhancement/axis_wise_bounds_numbernode branch from f2b1cc4 to 3b10b41 Compare February 12, 2026 23:12
@fastbodin
Copy link
Copy Markdown
Contributor Author

Previously, users could not define a bound for the sum of the values over an entire NumberNode array. This lead to the undesired behaviour that users could not define a bound on the sum of the values in a NumberNode vector.

Previously accepted list of tuples and list of lists.
Now simply list of tuples for consistency.
`BoundAxisInfo` -> `AxisBound` and `BoundAxisOperator` -> `Operator`.
`Operator` is now a nested enum classs of `AxisBound`.
Added indicator variable that all bound axis operators are `==`
to reduce redundancy in `NumberNode::exchange()` method.
Made axis, operators, and bounds private members. Added axis(),
num_bounds(), and num_operators() methods. Updated C++ code/tests,
Python, and Cython to reflect this.
Added a `const` to `State& state`. Fixed typos.
Previously, users could not define a bound for the sum of the values
over an entire `NumberNode` array. This lead to the undesired behaviour
that users could not define a bound on the sum of the values in a
`NumberNode` vector.
Users may now define a bound on the sum of the values in
the entire `NumberNode` array.
Reflect version bump.
497 removed unset() and set() method. Removed them from
sum constraint C++ tests.
@fastbodin fastbodin force-pushed the enhancement/axis_wise_bounds_numbernode branch 2 times, most recently from a2ece7f to 5e4ae36 Compare April 14, 2026 20:23
Simplify logic in `NumberNode` methods. In particular, methods that
handle sum constraints where `axis` is optional.

Moved `commit()`, `revert()`, and `update_sum_constraint_lhs()` onto
`NumberNodeStateData`. Added a slice cache for faster `reverts()`
Deleted `prior_sum_constraint` (unnecessary due to slice cache).

Remove `NumberNode::sum_constraints_all_equals_`. Unnecessary given
slice cache. Update `NumberNodeStateData` and mutate methods to reflect
change.

Remove excessive static_casts in NumberNode C++.

Users may now provide the slices (per sum constraint) that a given
index lies on. This is relevant to `exchange()`, `set_value()`,
`clip_and_set_value()`, and `flip()`.
@fastbodin fastbodin force-pushed the enhancement/axis_wise_bounds_numbernode branch from 5e4ae36 to 95b047b Compare April 14, 2026 20:30
return std::make_unique<NumberNodeStateData>(*this);
}

/// Commit the state dependant data of NumberNode.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

dependent (also in a few other places)

assert(difference != 0); // Should not call when no change occurs.
assert(sum_constraints.size() == sum_constraints_lhs.size());

if (slices.has_value()) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Seeing that you have separate implementations for the two cases of slices being provided or not, did you consider making overloads for the these methods instead of having default values for slices? i.e. make overloads that take std::vector<ssize_t> slices as the final argument.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes. I flip-flopped on that. Happy to make the change 👍

Copy link
Copy Markdown
Contributor Author

@fastbodin fastbodin Apr 15, 2026

Choose a reason for hiding this comment

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

Fixed in dbaa0ec

Previously, used `std::optional<>` to capture both cases. Updated all
relevant `NumberNode` mutate methods to handle the overload.
@fastbodin fastbodin force-pushed the enhancement/axis_wise_bounds_numbernode branch from 5c9fd45 to 50e7b23 Compare April 15, 2026 19:42
@fastbodin fastbodin marked this pull request as ready for review April 15, 2026 20:32
@fastbodin
Copy link
Copy Markdown
Contributor Author

fastbodin commented Apr 15, 2026

Added custom BinaryNode state data to track indices (per sum constraint and per slice) whose buffer value is 1 and 0 via DisjointSparseSet

}

/// Return the `i`th index in `dense_sets_[dense_set]` whose buffer value is 0.
ssize_t get_ith_false_index(const ssize_t dense_id, const ssize_t i) const {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If I call this immediately after construction, it will return -1 correct? Do I need to call add_index() for each index before I am able to use this method?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Correct. There are assert to catch this in update_true() and update_false().

Copy link
Copy Markdown
Member

@wbernoudy wbernoudy Apr 15, 2026

Choose a reason for hiding this comment

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

Would it be possible to set up the dense sets on construction so that's not possible?

Copy link
Copy Markdown
Contributor Author

@fastbodin fastbodin Apr 15, 2026

Choose a reason for hiding this comment

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

Definitely possible. I did it this way to make BinaryNodeStateData::compute_slice_indices_() simple. I could hand buffer iterators per slice to each dense set.... It will take some thinking to do it nicely.

I just realized that I did not answer your second question. You do not need to call add_index() for each index before you are able to use get_ith_false_index(). When you add indices it emplace_back() them on their appropriate dense set.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The only methods that you could not use are update_false() and update_true(), since you cannot update something that has not been added.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Given that num_true(), num_false(), get_ith_true_index() and get_ith_false_index() all work even when all indices have not been added, I am inclined to leave it as is. An index should only ever call update_true() or update_false() when it has already been added.


/// Given a slice (w.r.t a sum constraint), return the `i`th index in in
/// the slice whose buffer value is 1.
ssize_t get_ith_true_index(const ssize_t sum_constraint_id, const ssize_t slice,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I would consider making slice_indices_ public and having BinaryNode call these methods directly on the DisjointSparseSets instead of having more pass through methods

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I went back-and-forth on that. Happy to make the change

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Was there a downside to exposing them publicly?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I don't think so. At first I thought it would be best practice to have them as pass through methods so that they handle their own reporting internally. But then, after having done it, the number of pass throughs felt excessive 🤷 . The fact that you feel the same suggests I should change it 👍

// Reverse the change applied to each slice.
for (size_t j = 0, stop = slices.size(); j < stop; ++j) {
sum_constraints_lhs[j][slices[j]] -= difference;
slice_indices_[j].update(update.index, slices[j], -difference);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The definition of update() and calling it with the difference here feels confusing to me, could you instead just pass in the value (update.value) into update() and have it take true/false? Or just

if (update.value) slice_indices_[j].update_true(update.index, slices[j]);
else slice_indices_[j].update_false(update.index, slices[j]);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I did it to mirror updates to sum_constraint_lhs. Agreed it feels awkward. Happy to change 👍

`BinaryNodeStateData::slice_indices_` is used to cache, per sum constraint,
the flat indices of each slice. For a given sum constraint and
slice, the cached indices are ordered such that the indices in the
region `[0, num_true(slice))` have buffer value 1 and the indices
in the region `[num_true(slice), end)` have buffer value 0. The data
structure used is similar to a `SparseSet` but is generalized to a
`DisjointSparseSet` where there is one `sparse` look-up table but
multiple disjoint `dense` data tables.

All relevant `BinaryNode` methods have changed to reflect updating and
reverting this data.
@fastbodin fastbodin force-pushed the enhancement/axis_wise_bounds_numbernode branch from 50e7b23 to b4a240d Compare April 16, 2026 16:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Consider adding the ability to specify one axis bound to BinaryVariable

3 participants