Repository: dwavesystems/dimod
Pull request #1397 (2024-12-18 18:24:13)

Add Quadratic Multi Knapsack Generator
A quadratic multiple knapsack generator that allows for custom values, weights, profits and capacities.

@arcondello: @eordog and I chatted about whether it makes more sense for the diagonal of `profits` to be the `values` instead of passing `values` in as a separate array. Some of her benchmarking datasets have `values` as a separate array and some have it as the diagonal of `profits`. Curious if you have any thoughts on this. I went with separate as that's how I did it for `quadratic_knapsack`, whatever we end up going with I think these two functions should be consistent.


Patch:
diff --git a/dimod/generators/multi_knapsack.py b/dimod/generators/multi_knapsack.py
index f6756678..c32b4fe7 100644
--- a/dimod/generators/multi_knapsack.py
+++ b/dimod/generators/multi_knapsack.py
@@ -20,7 +20,7 @@ from dimod.binary_quadratic_model import BinaryQuadraticModel
 from dimod.constrained import ConstrainedQuadraticModel
 from dimod.typing import ArrayLike
 
-__all__ = ['multi_knapsack', 'random_multi_knapsack']
+__all__ = ['multi_knapsack', 'quadratic_multi_knapsack', 'random_multi_knapsack']
 
 
 def multi_knapsack(values: ArrayLike,
@@ -42,7 +42,7 @@ def multi_knapsack(values: ArrayLike,
 
         A constrained quadratic model encoding the multiple-knapsack problem.
         Variables are labelled as ``x_{i}_{j}``, where ``x_{i}_{j} == 1`` means
-        that item ``i`` is placed in bin ``j``.
+        that item ``i`` is placed in knapsack ``j``.
 
     """
     values = np.asarray(values)
@@ -76,6 +76,74 @@ def multi_knapsack(values: ArrayLike,
     return model
 
 
+def quadratic_multi_knapsack(values: ArrayLike,
+                             weights: ArrayLike,
+                             profits: ArrayLike,
+                             capacities: ArrayLike,
+                             ) -> ConstrainedQuadraticModel:
+    """Generate a constrained quadratic model encoding a quadratic multiple
+    knapsack problem.
+
+    The quadratic multiple knapsack problem seeks to fit the most value into each
+    knapsack of weight less than or equal to each knapsack's capacity and maximize
+    profits associated with adding any two items to the same knapsack.
+
+    Args:
+        values: A list of each item's value.
+        weights: A list of each item's associated weight.
+        profits: A matrix where entry (i, j) is the value of adding items i and j together.
+        capacities: A list of the maximum weights each knapsack can hold.
+
+    Returns:
+
+        A constrained quadratic model encoding the quadratic multiple knapsack problem.
+        Variables are labelled as ``x_{i}_{j}``, where ``x_{i}_{j} == 1`` means
+        that item ``i`` is placed in knapsack ``j``.
+
+    """
+    values = np.asarray(values)
+    weights = np.asarray(weights)
+    profits = np.asarray(profits)
+
+    if values.shape != weights.shape:
+        raise ValueError("`values` and `weights` must have the same shape")
+
+    if not np.array_equal(profits, profits.T):
+        raise ValueError("`profits` must be symmetric")
+
+    if values.shape[0] != profits.shape[0]:
+        raise ValueError("`profits` must have an entry for each pair of items")
+
+    model = ConstrainedQuadraticModel()
+    obj = BinaryQuadraticModel(vartype='BINARY')
+    x = {(i, j): obj.add_variable(f'x_{i}_{j}') for i in range(values.shape[0]) for j in range(len(capacities))}
+
+    for i, value in enumerate(values):
+        for j in range(len(capacities)):
+            obj.set_linear(x[(i, j)], -value)
+
+    for i, profit in np.ndenumerate(profits):
+        if i[0] < i[1]:
+            for j in range(len(capacities)):
+                obj.set_quadratic(x[i[0], j], x[i[1], j], -profit)
+
+    model.set_objective(obj)
+
+    # Each item at most goes to one bin.
+    for i in range(values.shape[0]):
+        model.add_constraint(
+            [(x[(i, j)], 1) for j in range(len(capacities))] + [(-1,)],
+            sense="<=", label='item_placing_{}'.format(i))
+
+    # Build knapsack capacity constraints
+    for j, capacity in enumerate(capacities):
+        model.add_constraint(
+            [(x[(i, j)], weight) for i, weight in enumerate(weights)] + [(-capacity,)],
+            sense="<=", label='capacity_bin_{}'.format(j))
+
+    return model
+
+
 def random_multi_knapsack(num_items: int,
                           num_bins: int,
                           seed: int = 32,
@@ -101,7 +169,7 @@ def random_multi_knapsack(num_items: int,
 
         A constrained quadratic model encoding the multiple-knapsack problem.
         Variables are labelled as ``x_{i}_{j}``, where ``x_{i}_{j} == 1`` means
-        that item ``i`` is placed in bin ``j``.
+        that item ``i`` is placed in knapsack ``j``.
 
     """
 
diff --git a/docs/reference/generators.rst b/docs/reference/generators.rst
index 331fdbd4..e89092de 100644
--- a/docs/reference/generators.rst
+++ b/docs/reference/generators.rst
@@ -51,6 +51,7 @@ Optimization
    maximum_weight_independent_set
    mimo
    multi_knapsack
+   multi_quadratic_knapsack
    quadratic_assignment
    quadratic_knapsack
    random_bin_packing
diff --git a/releasenotes/notes/quadratic-multi-knapsack-generator-6bd6daf629f11399.yaml b/releasenotes/notes/quadratic-multi-knapsack-generator-6bd6daf629f11399.yaml
new file mode 100644
index 00000000..013630bb
--- /dev/null
+++ b/releasenotes/notes/quadratic-multi-knapsack-generator-6bd6daf629f11399.yaml
@@ -0,0 +1,4 @@
+---
+features:
+  - |
+    Add ``quadratic_multi_knapsack`` generator function to ``dimod.generators``.


Test patch:
diff --git a/tests/test_generators.py b/tests/test_generators.py
index c8b450a1..94f5be40 100644
--- a/tests/test_generators.py
+++ b/tests/test_generators.py
@@ -653,6 +653,25 @@ class TestMultiKnapsack(unittest.TestCase):
         self.assertEqual(len(cqm.variables), num_items*num_bins)
         self.assertEqual(len(cqm.constraints), num_bins+num_items)
 
+    def test_quadratic_exceptions(self):
+        with self.assertRaises(ValueError):
+            dimod.generators.quadratic_multi_knapsack([1], [], [], [])
+
+        with self.assertRaises(ValueError):
+            dimod.generators.quadratic_multi_knapsack([1], [1], [[1, 1]], [])
+
+        with self.assertRaises(ValueError):
+            dimod.generators.quadratic_multi_knapsack([1], [1], [[1, 1], [1, 1]], [])
+
+    def test_quadratic_model(self):
+        values = [1, 2, 3]
+        weights = [3, 2, 1]
+        profits = [[1, 2, 3], [2, 2, 2], [3, 2, 1]]
+        capacity = [1, 2]
+        cqm = dimod.generators.quadratic_multi_knapsack(values, weights, profits, capacity)
+        self.assertEqual(len(cqm.variables), len(values)*len(capacity))
+        self.assertEqual(len(cqm.constraints), len(weights)+len(capacity))
+
 
 class TestGates(unittest.TestCase):
     def test_gates_no_aux(self):