Skip to content
Draft
24 changes: 12 additions & 12 deletions dwave/optimization/generators.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
concatenate,
exp,
expit,
is_disjoint_cover,
logical_or,
maximum,
minimum,
Expand Down Expand Up @@ -377,15 +378,16 @@ class as the decision variable being optimized, with permutations of its
The :meth:`~dwave.optimization.model.Model.iter_decisions` method
obtains the decision variables of the generated model.

>>> routes = next(model.iter_decisions())
>>> routes = list(model.iter_decisions())

To test the solution above, set it in the model as the state of the
decision variable. **Skip these next lines** if you have submitted your
model to the `Leap <https://cloud.dwavesys.com/leap/>`_ :term:`hybrid`
nonlinear :term:`solver`.

>>> model.states.resize(1)
>>> routes.set_state(0, [[2., 7., 1., 5.], [4., 3., 8., 6., 0.]])
>>> routes[0].set_state(0, [2., 7., 1., 5.])
>>> routes[1].set_state(0, [4., 3., 8., 6., 0.])

You can use the :meth:`~dwave.optimization.model.Model.iter_constraints`
method to check feasibility of constructed or returned solutions. Here,
Expand All @@ -398,7 +400,7 @@ class as the decision variable being optimized, with permutations of its
... for i in range(model.states.size()):
... if capacity_constraint.state(i): # Filter on feasibility
... print((f"Objective value #{i} is {model.objective.state(i).round(2)} for routes\n"
... f" {[r.state(i).tolist() for r in routes.iter_successors()]}"))
... f" {[r.state(i).tolist() for r in routes]}"))
Objective value #0 is 423.8 for routes
[[2.0, 7.0, 1.0, 5.0], [4.0, 3.0, 8.0, 6.0, 0.0]]
"""
Expand Down Expand Up @@ -511,10 +513,8 @@ class as the decision variable being optimized, with permutations of its
demand = model.constant(customer_demand)
capacity = model.constant(vehicle_capacity)

# Add the decision variable
routes = model.disjoint_lists_symbol(
primary_set_size=num_customers,
num_disjoint_lists=number_of_vehicles)
routes = [model.list(num_customers, min_size=0) for _ in range(number_of_vehicles)]
model.add_constraint(is_disjoint_cover(routes))

# The objective is to minimize the distance traveled.
# This is calculated by adding the distance from the depot to the 1st customer
Expand Down Expand Up @@ -631,15 +631,16 @@ class as the decision variable being optimized, with permutations of its
The :meth:`~dwave.optimization.model.Model.iter_decisions` method
obtains the decision variables of the generated model.

>>> routes = next(model.iter_decisions())
>>> routes = list(model.iter_decisions())

To test the solution above, set it in the model as the state of the
decision variable. **Skip these next lines** if you have submitted your
model to the `Leap <https://cloud.dwavesys.com/leap/>`_ :term:`hybrid`
nonlinear :term:`solver`.

>>> model.states.resize(1)
>>> routes.set_state(0, [[0, 2], [1]])
>>> routes[0].set_state(0, [0, 2])
>>> routes[1].set_state(0, [1])

You can use the :meth:`~dwave.optimization.model.Model.iter_constraints`
method to check feasibility of constructed or returned solutions. Here,
Expand Down Expand Up @@ -757,9 +758,8 @@ class as the decision variable being optimized, with permutations of its
one = model.constant(1)

# Add the decision variable
routes = model.disjoint_lists_symbol(
primary_set_size=num_customers,
num_disjoint_lists=number_of_vehicles)
routes = [model.list(num_customers, min_size=0) for _ in range(number_of_vehicles)]
model.add_constraint(is_disjoint_cover(routes))

# Capacity constraint
capacity_constraints = [(demand[routes[vehicle_idx]].sum() <= capacity)
Expand Down
1 change: 1 addition & 0 deletions dwave/optimization/include/dwave-optimization/nodes.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,6 @@
#include "dwave-optimization/nodes/numbers.hpp"
#include "dwave-optimization/nodes/quadratic_model.hpp"
#include "dwave-optimization/nodes/reduce.hpp"
#include "dwave-optimization/nodes/set_routines.hpp"
#include "dwave-optimization/nodes/testing.hpp"
#include "dwave-optimization/nodes/unaryop.hpp"
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,45 @@

namespace dwave::optimization {

class IsDisjointCoverNode : public ScalarOutputMixin<ArrayNode, false> {
public:
IsDisjointCoverNode(std::span<ArrayNode*> node_ptrs, ssize_t primary_set_size);

/// @copydoc Array::buff()
double const* buff(const State& state) const override;

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

/// @copydoc Array::diff()
std::span<const Update> diff(const State& state) const override;

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

/// @copydoc Array::integral()
bool integral() const override;

/// @copydoc Array::max()
double max() const override;

/// @copydoc Array::min()
double min() const override;

/// The size of the primary set to be covered
ssize_t primary_set_size() const { return primary_set_size_; };

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

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

private:
ssize_t primary_set_size_;
std::vector<Array*> operands_;
};

class IsInNode : public ArrayOutputMixin<ArrayNode> {
public:
IsInNode(ArrayNode* element_ptr, ArrayNode* test_elements_ptr);
Expand Down
3 changes: 3 additions & 0 deletions dwave/optimization/libcpp/nodes/set_routines.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,8 @@ from dwave.optimization.libcpp.graph cimport ArrayNode


cdef extern from "dwave-optimization/nodes/set_routines.hpp" namespace "dwave::optimization" nogil:
cdef cppclass IsDisjointCoverNode(ArrayNode):
Py_ssize_t primary_set_size() const

cdef cppclass IsInNode(ArrayNode):
pass
48 changes: 48 additions & 0 deletions dwave/optimization/mathematical.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
Exp,
Expit,
Extract,
IsDisjointCover,
IsIn,
LessEqual,
LinearProgram,
Expand Down Expand Up @@ -95,6 +96,7 @@
"expit",
"extract",
"hstack",
"is_disjoint_cover",
"isin",
"less_equal",
"linprog",
Expand Down Expand Up @@ -1110,6 +1112,52 @@ def hstack(arrays: collections.abc.Sequence[ArraySymbol]) -> ArraySymbol:
return concatenate(arrays, 1)


def is_disjoint_cover(subsets: list[ArraySymbol], *, primary_set_size: int|None = None) -> IsDisjointCover:
"""Return whether the symbols are disjoint, set-like, and cover a set of integers.

Determines whether a collection of array symbols is disjoint and the union equals a fixed set.

Args:
subsets: List of array symbols to test whether they are disjoint and cover the primary set
primary_set_size: Number of elements in the primary set: {0, 1, ..., primary_set_size - 1}.
Must be non-negative. If `None`, primary set size is inferred from the common, finite,
nonnegative maximum value of the arrays.

Returns:
A scalar boolean-valued array symbol indicating whether the subsets are a disjoint cover

Examples:
>>> from dwave.optimization.model import Model
>>> from dwave.optimization.mathematical import is_disjoint_cover
...
>>> model = Model()
>>> model.states.resize(1)
>>> subsets = []
>>> subsets.append(model.constant([0, 1]))
>>> subsets.append(model.constant([2, 3, 4]))
>>> subsets.append(model.constant([]))
>>> is_disjoint = is_disjoint_cover(subsets, primary_set_size=5)
>>> with model.lock():
... print(is_disjoint.state(0))
1.0

See Also:
:class:`~dwave.optimization.symbols.IsDisjointCover`: Generated symbol

:func:`.isin`

.. versionadded:: 0.7.2
"""
if primary_set_size is None:
primary_set_size = int(subsets[0].info().max) + 1
if not np.isfinite(primary_set_size) or primary_set_size <= 0:
raise ValueError("Cannot infer primary set size from subsets")
for subset in subsets[1:]:
if int(subset.info().max) != primary_set_size - 1:
raise ValueError("Cannot infer primary set size from subsets")
return IsDisjointCover(subsets, primary_set_size)


def isin(element: ArraySymbol, test_elements: ArraySymbol) -> IsIn:
"""Return which values of one array symbol are in another.

Expand Down
9 changes: 9 additions & 0 deletions dwave/optimization/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -591,6 +591,15 @@ def disjoint_lists_symbol(

:meth:`.iter_decisions`, :meth:`.iter_successors`
"""
warnings.warn(
"The use of Model.disjoint_lists_symbol() is deprecated "
"since dwave.optimization 0.7.2. Use\n"
"from dwave.optimization.mathematical import is_disjoint_cover\n"
"lists = [model.list(primary_set_size, min_size=0) for _ in range(num_disjoint_lists)]\n"
"model.add_constraint(is_disjoint_cover(primary_set_size, lists))",
DeprecationWarning,
)

from dwave.optimization.symbols import DisjointLists, DisjointList # avoid circular import
disjoint_lists = DisjointLists(self, primary_set_size, num_disjoint_lists)

Expand Down
Loading