Quadratic Assignment Problem¶
Quadratic assignment problem (QAP) is the following problem.
Let \(N\) be a positive integer. Consider \(N\) factories to be built on \(N\) candidate sites. Each factory can be built on any of the candidate sites. Every two factories have trucks traveling to and from them, and their transportation volumes are known in advance. How can we minimize the sum of the amount transported x the distance traveled?
An application could be to determine the seating chart for a meeting so that people close to each other have seats closely.
Formulation¶
Let \(N\) potential factory locations be denoted by land \(0\), land \(1\), … , and \(N\) factories are denoted as factories \(0\), factories \(1\), …, factories \(N-1\). Also let \(D_{i, j}\) denote the distance between land \(i\) and land \(j\), and \(F_{k, l}\) denote the transport volume between factory \(k\) and factory \(l\).
Variables¶
With \(N \times N\) binary variables \(q\), let \(q_{i, k}\) represent whether factory \(k\) is to be built on land \(i\).
For example, factory \(3\) will be built on land \(0\) if \(q\) has the following value.
factory 0 |
factory 1 |
factory 2 |
factory 3 |
factory 4 |
|
|---|---|---|---|---|---|
land 0 |
0 |
0 |
0 |
1 |
0 |
land 1 |
0 |
1 |
0 |
0 |
0 |
land 2 |
0 |
0 |
0 |
0 |
1 |
land 3 |
1 |
0 |
0 |
0 |
0 |
land 4 |
0 |
0 |
1 |
0 |
0 |
Constraints¶
Each row and column of the binary variable table must have exactly one variable that is 1, so we place a one-hot constraint on each row and column. Conversely, if these are satisfied, then there is only one way to determine which factory to build on which land.
Objective function¶
The objective function is the sum of transport volume x distance between factories. This can be expressed in the equation using \(q\) as follows.
Formulation¶
The above formulation, with \(N\times N\) binary variables \(q\), can be written as follows.
Problem setting¶
Before formulating with the Amplify SDK, we will create a problem. For simplicity, let the number of factories \(N=10\).
import numpy as np
N = 10
Next, we create a distance matrix \(D\) representing the distances between lands. The lands are randomly generated on the Euclidean plane. The distance matrix distance is created as a two-dimensional numpy.ndarray.
rng = np.random.default_rng()
x = rng.integers(0, 100, size=(N,))
y = rng.integers(0, 100, size=(N,))
distance = (
(x[:, np.newaxis] - x[np.newaxis, :]) ** 2
+ (y[:, np.newaxis] - y[np.newaxis, :]) ** 2
) ** 0.5
print(distance)
[[ 0. 22.023 64.008 25.318 7.071 36.62 68.264 42.544 60.44 34.132]
[ 22.023 0. 45.343 35.384 29. 53.009 50.21 54.452 44.654 48.826]
[ 64.008 45.343 0. 60.299 69.778 98.234 5.385 73.437 11.402 74.027]
[ 25.318 35.384 60.299 0. 24.187 57.567 62.968 19.105 52.631 14.56 ]
[ 7.071 29. 69.778 24.187 0. 34.015 73.824 39.294 65.46 30.083]
[ 36.62 53.009 98.234 57.567 34.015 0. 102.942 69.116 96.167 59.076]
[ 68.264 50.21 5.385 62.968 73.824 102.942 0. 75.06 11.18 76.322]
[ 42.544 54.452 73.437 19.105 39.294 69.116 75.06 0. 64.008 10.05 ]
[ 60.44 44.654 11.402 52.631 65.46 96.167 11.18 64.008 0. 65.62 ]
[ 34.132 48.826 74.027 14.56 30.083 59.076 76.322 10.05 65.62 0. ]]
Also, we create a matrix \(F\) representing the amount of transport between factories, a random symmetric matrix of dimension 2, named flow.
flow = np.zeros((N, N), dtype=int)
for i in range(N):
for j in range(i + 1, N):
flow[i, j] = flow[j, i] = rng.integers(0, 100)
print(flow)
[[ 0 14 15 39 90 12 20 27 29 44]
[14 0 47 83 80 47 80 69 7 86]
[15 47 0 47 46 73 15 95 52 13]
[39 83 47 0 84 14 34 64 75 50]
[90 80 46 84 0 75 37 78 31 18]
[12 47 73 14 75 0 34 23 22 40]
[20 80 15 34 37 34 0 72 55 95]
[27 69 95 64 78 23 72 0 59 92]
[29 7 52 75 31 22 55 59 0 78]
[44 86 13 50 18 40 95 92 78 0]]
Formulation with the Amplify SDK¶
In the formulation, we can use the Matrix class for efficient formulation, since a quadratic term consisting of any two binary variables can appear in the objective function.
Creating variables¶
To formulate using the Matrix class, VariableGenerator’s matrix() method to issue variables.
from amplify import VariableGenerator
gen = VariableGenerator()
matrix = gen.matrix("Binary", N, N) # coefficient matrix
q = matrix.variable_array # variables
q
Creating the objective function¶
The matrix created above is an instance of the class Matrix, which has the following three properties.
quadratic is numpy.ndarray representing the coefficients of the second order terms, and its shape is (N, N, N, N) this time. quadratic[i, k, j, l] corresponds to the coefficients of q[i, k] * q[j, l]. That is, quadratic must be set to a 4-dimensional NumPy array such that quadratic[i, k, j, l] = distance[i, j] * flow[k, l]
linear and constant represent the coefficient and constant terms of the linear term, respectively, but since the objective function used in this problem contains only second order terms, we will not set them.
np.einsum("ij,kl->ikjl", distance, flow, out=matrix.quadratic)
Creating constraints¶
Impose a one-hot constraint on each row and column of the variable array q created in Creating variables.
from amplify import one_hot
constraints = one_hot(q, axis=1) + one_hot(q, axis=0)
Creating a combinatorial optimization model¶
Let’s combine the objective function and constraints to create a model.
model = matrix + constraints
You add the objective function and the constraints together, and you give no weight to the constraints. Amplify AE, the solver of this example, adjusts the weight of each constraint by itself. It does not receive a weight that you set, and it ignores that weight. A solver that uses a penalty function for a constraint needs the weight. See Constraints and Penalty Functions for details.
Creating a solver client¶
Now, we will create a solver client to perform combinatorial optimization using Amplify AE. The solver client class corresponding to Amplify AE is AmplifyAEClient class.
from amplify import AmplifyAEClient
client = AmplifyAEClient()
We also need to set the API token required to run Amplify AE.
Tip
After user registration, you can obtain a free API token that can be used for evaluation and validation purposes.
client.token = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
We will set the solver’s timeout.
import datetime
client.parameters.time_limit_ms = datetime.timedelta(seconds=1)
Executing the solver¶
Finally, we will execute the solver using the created combinatorial optimization model and the solver client to find the solution to the quadratic programming problem.
from amplify import solve
result = solve(model, client)
The objective function value based on the best solution is shown below.
result.best.objective
188444.2758883677
The values of the variables in the optimal solution can be obtained in the form of a NumPy multidimensional array as follows.
q_values = q.evaluate(result.best.values)
print(q_values)
[[0. 0. 0. 1. 0. 0. 0. 0. 0. 0.]
[0. 1. 0. 0. 0. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 0. 0. 0. 0. 1.]
[0. 0. 0. 0. 0. 0. 0. 1. 0. 0.]
[0. 0. 0. 0. 1. 0. 0. 0. 0. 0.]
[1. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 0. 1. 0. 0. 0.]
[0. 0. 0. 0. 0. 1. 0. 0. 0. 0.]
[0. 0. 0. 0. 0. 0. 0. 0. 1. 0.]
[0. 0. 1. 0. 0. 0. 0. 0. 0. 0.]]
Checking the results¶
We will visualize the results using matplotlib.
import itertools
import matplotlib.pyplot as plt
plt.scatter(x, y)
factory_indices = (q_values @ np.arange(N)).astype(int)
for i, j in itertools.combinations(range(N), 2):
plt.plot(
[x[i], x[j]],
[y[i], y[j]],
c="b",
alpha=flow[factory_indices[i], factory_indices[j]] / 100,
)