Skip to content

Commit 3ab3ec5

Browse files
authored
An Ising Optimization algorithm for solving the 0-1 Knapsack Problem (qiskit-community/qiskit-aqua#878)
* Add the 0-1 Knapsack Problem
1 parent dadb44b commit 3ab3ec5

File tree

3 files changed

+302
-1
lines changed

3 files changed

+302
-1
lines changed

qiskit/optimization/ising/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
# This code is part of Qiskit.
44
#
5-
# (C) Copyright IBM 2019.
5+
# (C) Copyright IBM 2019, 2020.
66
#
77
# This code is licensed under the Apache License, Version 2.0. You may
88
# obtain a copy of this license in the LICENSE.txt file in the root directory
@@ -36,6 +36,7 @@
3636
tsp
3737
vehicle_routing
3838
vertex_cover
39+
knapsack
3940
4041
Automatic Ising Model Generator from DoCPLEX Model
4142
==================================================

qiskit/optimization/ising/knapsack.py

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
# -*- coding: utf-8 -*-
2+
3+
# This code is part of Qiskit.
4+
#
5+
# (C) Copyright IBM 2020.
6+
#
7+
# This code is licensed under the Apache License, Version 2.0. You may
8+
# obtain a copy of this license in the LICENSE.txt file in the root directory
9+
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
10+
#
11+
# Any modifications or derivative works of this code must retain this
12+
# copyright notice, and modified files need to carry a notice indicating
13+
# that they have been altered from the originals.
14+
15+
"""
16+
Convert knapsack parameters instances into Pauli list
17+
The parameters are a list of values a list of weights and a maximum weight of the knapsack.
18+
19+
In the Knapsack Problem we are given a list of objects that each has a weight and a value.
20+
We are also given a maximum weight we can carry. We need to pick a subset of the objects
21+
so as to maximize the total value without going over the maximum weight.
22+
23+
If we have the weights w[i], the values v[i] and the maximum weight W_max.
24+
We express the solution as a binary array x[i]
25+
where we have a 1 for the items we take in the solution set.
26+
We need to maximize sum(x[i]*v[i]) while respecting W_max >= sum(x[i]*w[i])
27+
28+
"""
29+
30+
import logging
31+
import math
32+
import numpy as np
33+
34+
from qiskit.quantum_info import Pauli
35+
from qiskit.aqua.operators import WeightedPauliOperator
36+
37+
38+
logger = logging.getLogger(__name__)
39+
40+
41+
def get_operator(values, weights, max_weight):
42+
"""
43+
Generate Hamiltonian for the knapsack problem.
44+
45+
Notes:
46+
To build the cost function for the Hamiltonian we add a term S
47+
that will vary with our solution. In order to make it change wit the solution
48+
we enhance X with a number of additional bits X' = [x_0,..x_{n-1},y_{n}..y_{n+m-1}].
49+
The bytes y[i] will be the binary representation of S.
50+
In this way the optimizer will be able to optimize S as well as X.
51+
52+
The cost function is
53+
$$C(X') = M(W_{max} - \\sum_{i=0}^{n-1} x_{i}w_{i} - S)^2 - \\sum_{i}^{n-1} x_{i}v_{i}$$
54+
where S = sum(2**j * y[j]), j goes from n to n+log(W_max).
55+
M is a number large enough to dominate the sum of values.
56+
57+
Because S can only be positive, when W_max >= sum(x[i]*w[i])
58+
the optimizer can find an S (or better the y[j] that compose S)
59+
so that it will take the first term to 0.
60+
This way the function is dominated by the sum of values.
61+
If W_max < sum(x[i]*w[i]) then the first term can never be 0
62+
and, multiplied by a large M, will always dominate the function.
63+
64+
The minimum value of the function will be that where the constraint is respected
65+
and the sum of values is maximized.
66+
67+
Args:
68+
values (list of non-negative integers) : a list of values
69+
weights (list of non-negative integers) : a list of weights
70+
max_weight (non negative integer) : the maximum weight the knapsack can carry
71+
72+
Returns:
73+
WeightedPauliOperator: operator for the Hamiltonian
74+
float: a constant shift for the obj function.
75+
76+
Raises:
77+
ValueError: values and weights have different lengths
78+
ValueError: A value or a weight is negative
79+
ValueError: All values are zero
80+
ValueError: max_weight is negative
81+
82+
"""
83+
if len(values) != len(weights):
84+
raise ValueError("The values and weights must have the same length")
85+
86+
if any(v < 0 for v in values) or any(w < 0 for w in weights):
87+
raise ValueError("The values and weights cannot be negative")
88+
89+
if all(v == 0 for v in values):
90+
raise ValueError("The values cannot all be 0")
91+
92+
if max_weight < 0:
93+
raise ValueError("max_weight cannot be negative")
94+
95+
y_size = int(math.log(max_weight, 2)) + 1 if max_weight > 0 else 1
96+
n = len(values)
97+
num_values = n + y_size
98+
pauli_list = []
99+
shift = 0
100+
101+
# pylint: disable=invalid-name
102+
M = 10 * np.sum(values)
103+
104+
# term for sum(x_i*w_i)**2
105+
for i in range(n):
106+
for j in range(n):
107+
coefficient = -1 * 0.25 * weights[i] * weights[j] * M
108+
pauli_op = _get_pauli_op(num_values, [j])
109+
pauli_list.append([coefficient, pauli_op])
110+
shift -= coefficient
111+
112+
pauli_op = _get_pauli_op(num_values, [i])
113+
pauli_list.append([coefficient, pauli_op])
114+
shift -= coefficient
115+
116+
coefficient = -1 * coefficient
117+
pauli_op = _get_pauli_op(num_values, [i, j])
118+
pauli_list.append([coefficient, pauli_op])
119+
shift -= coefficient
120+
121+
# term for sum(2**j*y_j)**2
122+
for i in range(y_size):
123+
for j in range(y_size):
124+
coefficient = -1 * 0.25 * (2 ** i) * (2 ** j) * M
125+
126+
pauli_op = _get_pauli_op(num_values, [n + j])
127+
pauli_list.append([coefficient, pauli_op])
128+
shift -= coefficient
129+
130+
pauli_op = _get_pauli_op(num_values, [n + i])
131+
pauli_list.append([coefficient, pauli_op])
132+
shift -= coefficient
133+
134+
coefficient = -1 * coefficient
135+
pauli_op = _get_pauli_op(num_values, [n + i, n + j])
136+
pauli_list.append([coefficient, pauli_op])
137+
shift -= coefficient
138+
139+
# term for -2*W_max*sum(x_i*w_i)
140+
for i in range(n):
141+
coefficient = max_weight * weights[i] * M
142+
143+
pauli_op = _get_pauli_op(num_values, [i])
144+
pauli_list.append([coefficient, pauli_op])
145+
shift -= coefficient
146+
147+
# term for -2*W_max*sum(2**j*y_j)
148+
for j in range(y_size):
149+
coefficient = max_weight * (2 ** j) * M
150+
151+
pauli_op = _get_pauli_op(num_values, [n + j])
152+
pauli_list.append([coefficient, pauli_op])
153+
shift -= coefficient
154+
155+
for i in range(n):
156+
for j in range(y_size):
157+
coefficient = -1 * 0.5 * weights[i] * (2 ** j) * M
158+
159+
pauli_op = _get_pauli_op(num_values, [n + j])
160+
pauli_list.append([coefficient, pauli_op])
161+
shift -= coefficient
162+
163+
pauli_op = _get_pauli_op(num_values, [i])
164+
pauli_list.append([coefficient, pauli_op])
165+
shift -= coefficient
166+
167+
coefficient = -1 * coefficient
168+
pauli_op = _get_pauli_op(num_values, [i, n + j])
169+
pauli_list.append([coefficient, pauli_op])
170+
shift -= coefficient
171+
172+
# term for sum(x_i*v_i)
173+
for i in range(n):
174+
coefficient = 0.5 * values[i]
175+
176+
pauli_op = _get_pauli_op(num_values, [i])
177+
pauli_list.append([coefficient, pauli_op])
178+
shift -= coefficient
179+
180+
return WeightedPauliOperator(paulis=pauli_list), shift
181+
182+
183+
def get_solution(x, values):
184+
"""
185+
Get the solution to the knapsack problem
186+
from the bitstring that represents
187+
to the ground state of the Hamiltonian
188+
189+
Args:
190+
x (numpy.ndarray): the ground state of the Hamiltonian.
191+
values (numpy.ndarray): the list of values
192+
193+
Returns:
194+
numpy.ndarray: a bit string that has a '1' at the indexes
195+
corresponding to values that have been taken in the knapsack.
196+
i.e. if the solution has a '1' at index i then
197+
the value values[i] has been taken in the knapsack
198+
"""
199+
return x[:len(values)]
200+
201+
202+
def knapsack_value_weight(solution, values, weights):
203+
"""
204+
Get the total wight and value of the items taken in the knapsack.
205+
206+
Args:
207+
solution (numpy.ndarray) : binary string that represents the solution to the problem.
208+
values (numpy.ndarray) : the list of values
209+
weights (numpy.ndarray) : the list of weights
210+
211+
Returns:
212+
tuple: the total value and weight of the items in the knapsack
213+
"""
214+
value = np.sum(solution * values)
215+
weight = np.sum(solution * weights)
216+
return value, weight
217+
218+
219+
def _get_pauli_op(num_values, indexes):
220+
pauli_x = np.zeros(num_values, dtype=np.bool)
221+
pauli_z = np.zeros(num_values, dtype=np.bool)
222+
for i in indexes:
223+
pauli_z[i] = not pauli_z[i]
224+
225+
return Pauli(pauli_z, pauli_x)

test/optimization/test_knapsack.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# -*- coding: utf-8 -*-
2+
3+
# This code is part of Qiskit.
4+
#
5+
# (C) Copyright IBM 2020.
6+
#
7+
# This code is licensed under the Apache License, Version 2.0. You may
8+
# obtain a copy of this license in the LICENSE.txt file in the root directory
9+
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
10+
#
11+
# Any modifications or derivative works of this code must retain this
12+
# copyright notice, and modified files need to carry a notice indicating
13+
# that they have been altered from the originals.
14+
15+
""" Test Knapsack Problem """
16+
17+
from test.optimization import QiskitOptimizationTestCase
18+
import numpy as np
19+
20+
from qiskit.optimization.ising import knapsack
21+
from qiskit.optimization.ising.common import sample_most_likely
22+
from qiskit.aqua.algorithms import NumPyMinimumEigensolver
23+
24+
25+
class TestTSP(QiskitOptimizationTestCase):
26+
"""Knapsack Ising tests."""
27+
28+
@staticmethod
29+
def _run_knapsack(values, weights, max_weight):
30+
qubit_op, _ = knapsack.get_operator(values, weights, max_weight)
31+
32+
algo = NumPyMinimumEigensolver(qubit_op)
33+
result = algo.run()
34+
x = sample_most_likely(result.eigenstate)
35+
36+
solution = knapsack.get_solution(x, values)
37+
value, weight = knapsack.knapsack_value_weight(solution, values, weights)
38+
39+
return solution, value, weight
40+
41+
def test_knapsack(self):
42+
""" Knapsack test """
43+
values = [10, 40, 50, 75]
44+
weights = [5, 10, 3, 12]
45+
max_weight = 20
46+
47+
solution, value, weight = self._run_knapsack(values, weights, max_weight)
48+
49+
np.testing.assert_array_equal(solution, [1, 0, 1, 1])
50+
np.testing.assert_equal(value, 135)
51+
np.testing.assert_equal(weight, 20)
52+
53+
def test_knapsack_zero_max_weight(self):
54+
""" Knapsack zero max weight test """
55+
values = [10, 40, 50, 75]
56+
weights = [5, 10, 3, 12]
57+
max_weight = 0
58+
59+
solution, value, weight = self._run_knapsack(values, weights, max_weight)
60+
61+
np.testing.assert_array_equal(solution, [0, 0, 0, 0])
62+
np.testing.assert_equal(value, 0)
63+
np.testing.assert_equal(weight, 0)
64+
65+
def test_knapsack_large_max_weight(self):
66+
""" Knapsack large max weight test """
67+
values = [10, 40, 50, 75]
68+
weights = [5, 10, 3, 12]
69+
max_weight = 1000
70+
71+
solution, value, weight = self._run_knapsack(values, weights, max_weight)
72+
73+
np.testing.assert_array_equal(solution, [1, 1, 1, 1])
74+
np.testing.assert_equal(value, 175)
75+
np.testing.assert_equal(weight, 30)

0 commit comments

Comments
 (0)