連続施設配置問題とその変形

準備

import random
import math
from gurobipy import Model, quicksum, GRB
import networkx as nx
from scipy.optimize import minimize
from numpy import array

Weber問題

平面上のどこにでも施設を配置できるタイプの立地問題は,Weber問題(Weber problem)とよばれる.

各顧客 $i$ は平面上に分布しているものとし,その座標を $(x_i,y_i)$ とする. 顧客は需要量 $w_i$ をもち,目的関数は施設と顧客の間の距離に 需要量を乗じたものの和とする. 顧客と施設間の距離は直線距離とする.

顧客の集合 $I$ に対して,単一の施設の配置地点 $(X,Y)$ を決定する問題は, $$ f(X,Y) =\sum_{i \in I} w_i \sqrt{ (X-x_i)^2 +(Y-y_i)^2 } $$ を最小にする $(X,Y)$ を求める問題になる. これは,不動点アルゴリズムを用いることによって容易に求解できるが,ここでは数理最適化ソルバーで解いてみよう.

顧客 $i$ と施設の間の直線距離を表す変数 $z_i$ を導入すると, Weber問題は次のように定式化することができる.

$$ \begin{array}{ll} minimize & \sum_{i\in I} w_i z_i & \\ s.t. & (x_i-X)^2 + (y_i-Y)^2 \leq z_i^2 & \forall i \in I \\ \end{array} $$

上の問題の制約は,2次錐制約(second-order cone constraint)と呼ばれ,モダンな数理最適化ソルバー(たとえばGurobi)を使うと効率良く処理することができる.

def weber(I, x, y, w):
    """weber: model for solving the single source weber problem using soco.
    Parameters:
        - I: set of customers
        - x[i]: x position of customer i
        - y[i]: y position of customer i
        - w[i]: weight of customer i
    Returns a model, ready to be solved.
    """

    model = Model("weber")
    X, Y, z, xaux, yaux = {}, {}, {}, {}, {}
    X = model.addVar(lb=-GRB.INFINITY, vtype="C", name="X")
    Y = model.addVar(lb=-GRB.INFINITY, vtype="C", name="Y")
    for i in I:
        z[i] = model.addVar(vtype="C", name="z(%s)" % (i))
        xaux[i] = model.addVar(lb=-GRB.INFINITY, vtype="C", name="xaux(%s)" % (i))
        yaux[i] = model.addVar(lb=-GRB.INFINITY, vtype="C", name="yaux(%s)" % (i))
    model.update()

    for i in I:
        model.addConstr(
            xaux[i] * xaux[i] + yaux[i] * yaux[i] <= z[i] * z[i], "MinDist(%s)" % (i)
        )
        model.addConstr(xaux[i] == (x[i] - X), "xAux(%s)" % (i))
        model.addConstr(yaux[i] == (y[i] - Y), "yAux(%s)" % (i))

    model.setObjective(quicksum(w[i] * z[i] for i in I), GRB.MINIMIZE)

    model.update()
    model.__data = X, Y, z
    return model

def make_data(n, m):
    I = range(1, n + 1)
    J = range(1, m + 1)
    x, y, w = {}, {}, {}
    for i in I:
        x[i] = random.randint(0, 100)
        y[i] = random.randint(0, 100)
        w[i] = random.randint(1, 5)
    return I, J, x, y, w
random.seed(3)
n = 7
m = 1

I, J, x, y, w = make_data(n, m)

model = weber(I, x, y, w)
model.optimize()
X, Y, z = model.__data

G = nx.Graph()

G.add_nodes_from(I)
G.add_nodes_from(["D"])

position = {}
for i in I:
    position[i] = (x[i], y[i])
position["D"] = (round(X.X), round(Y.X))

nx.draw(G, pos=position, node_size=200, node_color="g", nodelist=I)
nx.draw(G, pos=position, node_size=400, node_color="red", nodelist=["D"], alpha=0.5)
Academic license - for non-commercial use only - expires 2023-06-24
Using license file /Users/mikiokubo/gurobi.lic
Gurobi Optimizer version 9.1.1 build v9.1.1rc0 (mac64)
Thread count: 8 physical cores, 16 logical processors, using up to 16 threads
Optimize a model with 14 rows, 23 columns and 28 nonzeros
Model fingerprint: 0xc13ed828
Model has 7 quadratic constraints
Coefficient statistics:
  Matrix range     [1e+00, 1e+00]
  QMatrix range    [1e+00, 1e+00]
  Objective range  [1e+00, 5e+00]
  Bounds range     [0e+00, 0e+00]
  RHS range        [8e+00, 8e+01]
Presolve removed 2 rows and 2 columns
Presolve time: 0.01s
Presolved: 12 rows, 21 columns, 24 nonzeros
Presolved model has 7 second-order cone constraints
Ordering time: 0.00s

Barrier statistics:
 Dense cols : 1
 AA' NZ     : 4.800e+01
 Factor NZ  : 9.100e+01
 Factor Ops : 8.190e+02 (less than 1 second per iteration)
 Threads    : 1

                  Objective                Residual
Iter       Primal          Dual         Primal    Dual     Compl     Time
   0   2.12303274e+03  0.00000000e+00  7.11e-15 1.16e+00  1.05e+02     0s
   1   8.95508717e+02  6.16991647e+02  2.84e-14 1.28e-06  1.33e+01     0s
   2   8.52355326e+02  8.40568408e+02  3.20e-14 1.62e-08  5.61e-01     0s
   3   8.44024357e+02  8.43976412e+02  3.03e-12 3.52e-11  2.28e-03     0s
   4   8.43988119e+02  8.43986994e+02  4.68e-10 2.67e-13  5.36e-05     0s
   5   8.43987153e+02  8.43987119e+02  1.53e-08 3.49e-12  1.61e-06     0s
   6   8.43987143e+02  8.43987142e+02  3.52e-10 3.51e-12  6.09e-08     0s

Barrier solved model in 6 iterations and 0.02 seconds
Optimal objective 8.43987143e+02

SciPyを用いて最適化

SciPyの最適化モジュール optimize の minimize(f,x0) を用いて非線形最適化を行うこともできる. ここでfは最小化する関数であり,x0は初期点である.

引数methodで探索のためのアルゴリズムを設定することができる.以下のアルゴリズムが準備されている. ここでは 'SLSQP'を用いる.

  • 'Nelder-Mead':Nelder–Mead法(単体法)

  • 'Powell':Powellの共役方向法

  • 'CG':共役勾配法(conjugate gradient)

  • 'BFGS':Broyden–Fletcher–Goldfarb–Shanno(BFGS)法

  • 'Newton-CG':Newton共役勾配法

  • 'L-BFGS-B':記憶制限付きBFGS法

  • 'TNC':打ち切りNewton共約勾配法

  • 'COBYLA':Constrained Optimization BY Linear Approximation

  • 'SLSQP':Sequential Least SQuares Programming

  • 'dogleg':ドッグレッグ信頼領域法

  • 'trust-ncg':信頼領域Newton共約勾配法

random.seed(3)
n = 7
m = 1
I, J, x, y, w = make_data(n, m)

X = array(1)

def f(X):
    total_cost = 0.0
    for i in I:
        total_cost += w[i] * math.sqrt(((x[i] - X[0]) ** 2 + (y[i] - X[1]) ** 2))
    return total_cost

res = minimize(f, (80, 10), method="SLSQP", constraints=())

print("Result=",res)
G = nx.Graph()

G.add_nodes_from(I)
G.add_nodes_from(
    "%s" % j for j in J
)  

position = {}
for i in I:
    position[i] = (x[i], y[i])
for j in J:
    position["%s" % j] = (res.x[0], res.x[1])

nx.draw(G, position, node_color="g", nodelist=I, with_labels=False)
nx.draw(G, position, node_color="r", nodelist=["%s" % j for j in J], alpha=0.5)
Result=      fun: 843.9871426661498
     jac: array([2.28881836e-05, 1.52587891e-05])
 message: 'Optimization terminated successfully'
    nfev: 28
     nit: 9
    njev: 9
  status: 0
 success: True
       x: array([41.95347752, 58.63938272])

複数施設Weber問題

Weber問題の拡張として,複数の施設を平面上に立地する場合を考える.

各顧客は,複数ある施設のうち,最も近い地点に割り振られるものとする. 施設の集合 $J$ を与えたとき, 各施設 $j (\in J$の配置地点 $(X_j,Y_j))$ を決定する問題は, $$ f(X,Y) =\sum_{i \in I} w_i \min_{j \in J} \sqrt{ (X_j-x_i)^2 +(Y_j-y_i)^2 } $$ を最小にする $(X_j,Y_j)$ を求める問題になる.

この問題の変数は,施設の座標 $(X_j, Y_j) (j \in J)$ の他に,以下の変数を用いる.

  • $v_{ij}:$ 顧客 $i$ と施設 $j$ の間の直線距離
  • $u_{ij}:$ 顧客 $i$ が施設 $j$ に割り当てられるとき $1$ になる $0$-$1$ 変数

これらの変数を用いると, 複数施設Weber問題は次のように定式化することができる.

$$ \begin{array}{ll} minimize & \sum_{i\in I} w_i u_{ij} v_{ij} & \\ s.t. & (x_i-X_j)^2 + (y_i-Y_j)^2 \leq v_{ij}^2 & \forall i \in I, j \in J \\ & \sum_{j \in J} u_{ij} = 1 & \forall i \in I \\ & u_{ij} \in \{0,1 \} & \forall i \in I, j \in J \end{array} $$

上の問題の目的関数は,非凸の2次関数であるが,モダンな数理最適化ソルバー(たとえばGurobi)だと,自動的に変形して求解してくれる.

ただし,大規模問題例に対しては不向きである.実際には,Weber問題を解くための反復解法 (Weiszfeld法) を用いて,近似的に最適な施設を求める方法がある. それらを組み込んだ連続型施設最適化のためのシステムとしてMELOS-GF (MEta Logistic network Optimization System Green Field: https://www.logopt.com/melos/ ) が開発されている.

def weber_MS(I, J, x, y, w):
    """weber -- model for solving the weber problem using soco (multiple source version).
    Parameters:
        - I: set of customers
        - J: set of potential facilities
        - x[i]: x position of customer i
        - y[i]: y position of customer i
        - w[i]: weight of customer i
    Returns a model, ready to be solved.
    """
    M = max([((x[i] - x[j]) ** 2 + (y[i] - y[j]) ** 2) for i in I for j in I])
    model = Model("weber - multiple source")
    X, Y, v, u = {}, {}, {}, {}
    xaux, yaux, uaux = {}, {}, {}
    for j in J:
        X[j] = model.addVar(lb=-GRB.INFINITY, vtype="C", name="X(%s)" % j)
        Y[j] = model.addVar(lb=-GRB.INFINITY, vtype="C", name="Y(%s)" % j)
        for i in I:
            v[i, j] = model.addVar(vtype="C", name="v(%s,%s)" % (i, j))
            u[i, j] = model.addVar(vtype="B", name="u(%s,%s)" % (i, j))
            xaux[i, j] = model.addVar(
                lb=-GRB.INFINITY, vtype="C", name="xaux(%s,%s)" % (i, j)
            )
            yaux[i, j] = model.addVar(
                lb=-GRB.INFINITY, vtype="C", name="yaux(%s,%s)" % (i, j)
            )

    model.update()

    for i in I:
        model.addConstr(quicksum(u[i, j] for j in J) == 1, "Assign(%s)" % i)
        for j in J:
            model.addConstr(
                xaux[i, j] * xaux[i, j] + yaux[i, j] * yaux[i, j] <= v[i, j] * v[i, j],
                "MinDist(%s,%s)" % (i, j),
            )
            model.addConstr(xaux[i, j] == (x[i] - X[j]), "xAux(%s,%s)" % (i, j))
            model.addConstr(yaux[i, j] == (y[i] - Y[j]), "yAux(%s,%s)" % (i, j))

    model.setObjective(
        quicksum(w[i] * v[i, j] * u[i, j] for i in I for j in J), GRB.MINIMIZE
    )

    model.update()
    model.__data = X, Y, v, u
    return model
random.seed(3)
n = 15
m = 3

I, J, x, y, w = make_data(n, m)

model = weber_MS(I, J, x, y, w)
model.optimize()
X, Y, v, u = model.__data

G = nx.Graph()

G.add_nodes_from(I)
J_list = []
for j in J:
    G.add_node(f"D{j}")
    J_list.append(f"D{j}")

for i in I:
    for j, n in enumerate(J_list):
        if u[i, j + 1].X > 0.001:
            G.add_edge(i, n)

position = {}
for i in I:
    position[i] = (x[i], y[i])
for j, n in enumerate(J_list):
    position[n] = (X[j + 1].X, Y[j + 1].X)

nx.draw(G, pos=position, node_size=200, with_labels=True, node_color="y", nodelist=I)
nx.draw(
    G,
    pos=position,
    node_size=400,
    with_labels=True,
    node_color="red",
    nodelist=J_list,
    alpha=0.5,
)
Gurobi Optimizer version 9.1.1 build v9.1.1rc0 (mac64)
Thread count: 8 physical cores, 16 logical processors, using up to 16 threads
Optimize a model with 105 rows, 186 columns and 225 nonzeros
Model fingerprint: 0x870a1b13
Model has 45 quadratic objective terms
Model has 45 quadratic constraints
Variable types: 141 continuous, 45 integer (45 binary)
Coefficient statistics:
  Matrix range     [1e+00, 1e+00]
  QMatrix range    [1e+00, 1e+00]
  Objective range  [0e+00, 0e+00]
  QObjective range [2e+00, 1e+01]
  Bounds range     [1e+00, 1e+00]
  RHS range        [1e+00, 1e+02]
Presolve removed 6 rows and 6 columns
Presolve time: 0.00s
Presolved: 189 rows, 315 columns, 438 nonzeros
Presolved model has 90 SOS constraint(s)
Presolved model has 45 quadratic constraint(s)
Variable types: 225 continuous, 90 integer (90 binary)

Root relaxation: objective 0.000000e+00, 45 iterations, 0.00 seconds

    Nodes    |    Current Node    |     Objective Bounds      |     Work
 Expl Unexpl |  Obj  Depth IntInf | Incumbent    BestBd   Gap | It/Node Time

     0     0    0.00000    0    4          -    0.00000      -     -    0s
     0     0    0.00000    0   12          -    0.00000      -     -    0s
     0     0    0.00000    0   12          -    0.00000      -     -    0s
     0     0    0.00000    0   14          -    0.00000      -     -    0s
     0     0    0.00000    0   14          -    0.00000      -     -    0s
     0     0    0.00000    0   14          -    0.00000      -     -    0s
H    0     0                    1508.3672235    0.00000   100%     -    0s
H    0     0                    1508.3671957    0.00000   100%     -    0s
H    0     2                    1508.3671934    0.00000   100%     -    0s
     0     2    0.00000    0   14 1508.36719    0.00000   100%     -    0s
H   31    40                    1508.3671526    0.00000   100%  23.0    0s
H   32    40                    1472.1274716    0.00000   100%  22.3    0s
H   38    40                    1320.8558298    0.00000   100%  26.9    0s
H   48    64                    1238.0322353    0.00000   100%  23.7    0s
H   50    64                    1131.3675347    0.00000   100%  22.8    0s
H   66    80                    1131.3675201    0.00000   100%  22.4    0s
H   70    80                    1131.3675115    0.00000   100%  21.9    0s
H   78    80                    1131.3675065    0.00000   100%  22.0    0s
H  119   144                    1131.3674800    0.00000   100%  18.6    0s
H  423   409                    1131.3674754    0.00000   100%  13.2    0s
H  443   410                    1131.3674742    0.00000   100%  13.3    0s
H  657   582                    1079.9213524    0.00000   100%  15.1    0s
H  756   619                    1060.0898516    0.00000   100%  17.4    0s
H  806   639                    1049.5172736    0.00000   100%  18.2    0s
H 1094   735                    1047.9804288    0.00000   100%  18.8    0s
H 1108   784                     982.3971440    0.00000   100%  18.7    0s
H 1114   758                     982.3970897    0.00000   100%  18.7    0s
H 1281   770                     972.1236091    0.00000   100%  19.8    0s
H 1668   919                     884.2843019    0.00000   100%  20.6    0s
* 1668   919              37     884.2843019    0.00000   100%  20.6    0s
* 1865   930              37     884.2842204    0.00000   100%  20.1    0s
* 1866   930              37     873.7116425    0.00000   100%  20.1    0s
H 2186  1040                     846.8243000    0.00000   100%  19.3    0s
H 2277  1036                     842.0160527    0.00000   100%  19.2    0s
* 2277  1036              38     842.0160527    0.00000   100%  19.2    0s
H 2979  1431                     839.8674238   64.40497  92.3%  18.4    0s
* 2979  1431              38     839.8674238   66.48308  92.1%  18.4    0s
H 3647  1649                     839.0838374   66.48308  92.1%  17.9    0s
* 3647  1649              38     839.0838374   66.48308  92.1%  17.9    0s
H 5164  2214                     828.3928504   93.60021  88.7%  17.8    0s
H 5655  2347                     828.3928235  100.60317  87.9%  18.0    0s
H 5941  2347                     828.3928233  100.60324  87.9%  18.1    0s
H 6230  2473                     828.3928103  100.60324  87.9%  18.4    0s
H 7419  2988                     828.3928097  115.47219  86.1%  18.9    0s
H 8045  3067                     828.3927951  127.63771  84.6%  19.1    0s
H 8154  3065                     827.8135053  136.44022  83.5%  19.1    0s
H 8281  3218                     827.8134938  136.44022  83.5%  19.1    0s
H11894  4173                     827.8134930  177.50110  78.6%  20.2    1s
H11989  4173                     827.8134870  177.50110  78.6%  20.2    1s
H15910  5394                     827.8134868  217.31314  73.7%  20.6    1s
 101789 10961  722.27619   26    6  827.81349  573.43112  30.7%  20.1    5s
H114262 10106                     827.8132787  614.89883  25.7%  19.9    5s
*114262 10106              36     827.8132787  614.89883  25.7%  19.9    5s

Explored 156195 nodes (2871349 simplex iterations) in 7.74 seconds
Thread count was 16 (of 16 available processors)

Solution count 10: 827.813 827.813 839.084 ... 972.124

Optimal solution found (tolerance 1.00e-04)
Warning: max constraint violation (8.3599e-05) exceeds tolerance
Best objective 8.278132877248e+02, best bound 8.278132786678e+02, gap 0.0000%