スケジューリング問題に対する定式化とソルバー

スケジューリング問題

スケジューリング問題(scheduling problem)とは,稀少資源(機械)を諸活動(ジョブ,タスク,仕事,作業,オペレーションなどの総称)へ(時間軸を考慮して)割り振るための方法に対する理論体系である. スケジューリングの応用は,工場内での生産計画,計算機におけるジョブのコントロール,プロジェクトの遂行手順の決定など,様々である.

以下では,スケジューリングに関連する様々な定式化と専用ソルバーについて述べる.

1機械リリース時刻付き重み付き完了時刻和最小化問題

1機械リリース時刻付き重み付き完了時刻和最小化問題 $1|r_j|\sum w_j C_j$ は,以下に定義される問題である.

単一の機械で $n$ 個のジョブを処理する問題を考える. この機械は一度に1つのジョブしか処理できず,ジョブの処理を 開始したら途中では中断できないものと仮定する. ジョブの集合の添え字を $j=1,2,\ldots,n$,ジョブの集合を $J=\{1,2,\ldots,n\}$ と書く. 各ジョブ$j$ に対する処理時間 $p_j$,重要度を表す重み$w_j$, リリース時刻(ジョブの処理が開始できる最早時刻)$r_j$が与えられたとき, 各ジョブ $j$ の処理完了時刻 $C_j$ の重み付きの 和を最小にするようなジョブを機械にかける順番(スケジュール)を求めよ.

この問題は古くから多くの研究がなされているスケジューリング問題であり,$NP$-困難であることが知られている. ここでは,離接定式化(disjunctive formulation)と呼ばれる単純な定式化を示す.

ジョブ $j$ の開始時刻を表す連続変数 $s_{j}$ と,ジョブ $j$ が他のジョブ $k$ に先行する(前に処理される)とき $1$ になる $0$-$1$ 整数変数 $x_{jk}$ を用いる.

離接定式化は,非常に大きな数 $M$ を用いると,以下のように書ける.

$$ \begin{array}{l l l} minimize & \displaystyle\sum_{j=1}^n w_j s_{j} + \displaystyle\sum_{j=1}^n w_j p_j & \\ s.t. & s_j + p_j -M (1-x_{jk}) \leq s_k & \forall j \neq k \\ & x_{jk} +x _{kj}= 1 & \forall j < k \\ & s_j \geq r_j & \forall j=1,2,\ldots,n \\ & x_{jk} \in \{ 0,1 \} & \forall j \neq k \end{array} $$

目的関数は重み付き完了時刻の和 $\sum w_j (s_j+p_j)$ を展開したものであり,それを最小化している. 展開した式の第2項は定数であるので,実際には第1項のみを最小化すれば良い. 最初の制約は,$x_{jk}$ が $1$ のとき,ジョブ$j$ の完了時刻 $s_j+p_j$ より ジョブ$k$ の開始時刻 $s_k$ が後になることを表す. ($x_{jk}$ が $0$ のときには,$M$ が大きな数なので制約にはならない.) 2番目の制約は,ジョブ$j$ がジョブ$k$ に先行するか,ジョブ$k$ がジョブ$j$ に 先行するかの何れかが成立することを表す. これを離接制約(disjunctive constraint)と呼ぶ. これが離接定式化の名前の由来である. 3番目の制約は,ジョブ$j$ の開始時刻 $s_j$ がリリース時刻 $r_j$ 以上であることを表す.

大きな数 $M$ は,実際にはなるべく小さな値に設定すべきである. 制約ごとに異なる $M$ を設定するならば,$r_j +\sum s_j -s_k$, すべての制約で同じ $M$ を用いるならば $\max (r_j) +\sum s_j$ とすれば十分である.

def make_data(n):
    """
    Data generator for the one machine scheduling problem.
    """
    p, r, d, w = {}, {}, {}, {}

    J = range(1, n + 1)

    for j in J:
        p[j] = random.randint(1, 4)
        w[j] = random.randint(1, 3)

    T = sum(p)
    for j in J:
        r[j] = random.randint(0, 5)
        d[j] = r[j] + random.randint(0, 5)

    return J, p, r, d, w


def scheduling_disjunctive(J, p, r, w):
    """
    scheduling_disjunctive: model for the one machine total weighted completion time problem

    Disjunctive optimization model for the one machine total weighted
    completion time problem with release times.

    Parameters:
        - J: set of jobs
        - p[j]: processing time of job j
        - r[j]: earliest start time of job j
        - w[j]: weighted of job j; the objective is the sum of the weighted completion time

    Returns a model, ready to be solved.
    """
    model = Model("scheduling: disjunctive")
    M = max(r.values()) + sum(p.values())  # big M
    s, x = (
        {},
        {},
    )  # start time variable, x[j,k] = 1 if job j precedes job k, 0 otherwise
    for j in J:
        s[j] = model.addVar(lb=r[j], vtype="C", name="s(%s)" % j)
        for k in J:
            if j != k:
                x[j, k] = model.addVar(vtype="B", name="x(%s,%s)" % (j, k))
    model.update()

    for j in J:
        for k in J:
            if j != k:
                model.addConstr(
                    s[j] - s[k] + M * x[j, k] <= (M - p[j]), "Bound(%s,%s)" % (j, k)
                )

            if j < k:
                model.addConstr(x[j, k] + x[k, j] == 1, "Disjunctive(%s,%s)" % (j, k))

    model.setObjective(quicksum(w[j] * s[j] for j in J), GRB.MINIMIZE)

    model.update()
    model.__data = s, x
    return model
n = 5  # number of jobs
J, p, r, d, w = make_data(n)

model = scheduling_disjunctive(J, p, r, w)
model.optimize()
s, x = model.__data
z = model.ObjVal + sum([w[j] * p[j] for j in J])
print("Opt.value by Disjunctive Formulation:", z)
seq = [j for (t, j) in sorted([(int(s[j].X + 0.5), j) for j in s])]
print("solition=", seq)
Academic license - for non-commercial use only - expires 2022-04-03
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 30 rows, 25 columns and 80 nonzeros
Model fingerprint: 0xd604dc6f
Variable types: 5 continuous, 20 integer (20 binary)
Coefficient statistics:
  Matrix range     [1e+00, 2e+01]
  Objective range  [1e+00, 3e+00]
  Bounds range     [1e+00, 5e+00]
  RHS range        [1e+00, 2e+01]
Found heuristic solution: objective 64.0000000
Presolve removed 10 rows and 10 columns
Presolve time: 0.00s
Presolved: 20 rows, 15 columns, 60 nonzeros
Variable types: 5 continuous, 10 integer (10 binary)

Root relaxation: objective 4.200000e+01, 8 iterations, 0.00 seconds

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

     0     0   43.00000    0    5   64.00000   43.00000  32.8%     -    0s
H    0     0                      59.0000000   43.00000  27.1%     -    0s
H    0     0                      57.0000000   43.00000  24.6%     -    0s
     0     0 infeasible    0        57.00000   57.00000  0.00%     -    0s

Cutting planes:
  MIR: 2

Explored 1 nodes (15 simplex iterations) in 0.04 seconds
Thread count was 16 (of 16 available processors)

Solution count 3: 57 59 64 

Optimal solution found (tolerance 1.00e-04)
Best objective 5.700000000000e+01, best bound 5.700000000000e+01, gap 0.0000%
Opt.value by Disjunctive Formulation: 83.99999999999994
solition= [3, 4, 5, 1, 2]

1機械総納期遅れ最小化問題

1機械総納期遅れ最小化問題 $1||\sum T_j$ は,以下に定義される問題である.

単一の機械で $n$ 個のジョブを処理する問題を考える. この機械は一度に1つのジョブしか処理できず,ジョブの処理を 開始したら途中では中断できないものと仮定する. ジョブの添え字を $1,2,\ldots,n$ と書く. 各ジョブ$j$ に対する 処理時間$p_j$,納期(ジョブが完了しなければならない時刻)$d_j$, が与えられたとき, 各ジョブ $j$ の納期からの遅れの和(総納期遅れ)を 最小にするようなジョブを機械にかける順番(スケジュール)を求めよ.

この問題は,$NP$-困難であることが知られている. ここでは,大きな数 $M$ を用いない定式化として線形順序付け定式化(linear ordering formulation)を考える.

ジョブ$j$ の納期遅れを表す連続変数 $T_{j}$ と,ジョブ$j$ が他のジョブ$k$ に先行する(前に処理される)とき $1$ になる $0$-$1$ 整数変数 $x_{jk}$ を用いる.

上の記号を用いると,線形順序付け定式化は,以下のように書ける.

$$ \begin{array}{l l l} minimize & \displaystyle\sum_{j} w_j T_j & \\ s.t. & x_{jk} +x _{kj}= 1 & \forall j < k \\ & x_{jk}+x_{k \ell} +x_{\ell j} \leq 2 & \forall j \neq k \neq \ell \\ & \displaystyle\sum_{j=1}^n \displaystyle\sum_{k \neq j} p_k x_{kj} + p_j \leq d_j +T_j & \forall j=1,2,\ldots,n \\ & x_{jk} \in \{ 0,1 \} & \forall j \neq k \end{array} $$

上の定式化において, 目的関数は,各ジョブ $j$ に対する納期遅れ $T_j$ を重み $w_j$ で乗じたものの和を 最小化することを表す.  最初の制約は,ジョブ$j$ がジョブ $k$ の前に処理されるか, その逆にジョブ$k$ がジョブ$j$ の前に処理されるかのいずれかが成立することを表す. (ここまでは離接定式化と同じである.) 2番目の制約は,異なるジョブ$j,k,\ell$ に対して, ジョブ$j$ がジョブ$k$ に先行, ジョブ$k$ がジョブ$\ell$ に先行, ジョブ$\ell$ がジョブ$j$ に先行の3つが同時に成り立つことがないことを表す. この2つの制約によって,ジョブ上に線形順序(ジョブが一列に並べられること)が規定される. これが線形順序付け定式化の名前の由来である. 3番目の制約は,変数 $x$ で規定される順序付けと納期遅れを表す変数 $T$ を繋ぐための 制約であり,ジョブ$j$ の完了時刻が納期 $d_j$ より超えている時間が納期遅れ $T_j$ であることを規定する.

def scheduling_linear_ordering(J, p, d, w):
    """
    scheduling_linear_ordering: model for the one machine total weighted tardiness problem

    Model for the one machine total weighted tardiness problem
    using the linear ordering formulation

    Parameters:
        - J: set of jobs
        - p[j]: processing time of job j
        - d[j]: latest non-tardy time for job j
        - w[j]: weighted of job j; the objective is the sum of the weighted completion time

    Returns a model, ready to be solved.
    """
    model = Model("scheduling: linear ordering")

    T, x = {}, {}  # tardiness variable; x[j,k] =1 if job j precedes job k, =0 otherwise
    for j in J:
        T[j] = model.addVar(vtype="C", name="T(%s)" % (j))
        for k in J:
            if j != k:
                x[j, k] = model.addVar(vtype="B", name="x(%s,%s)" % (j, k))
    model.update()

    for j in J:
        model.addConstr(
            quicksum(p[k] * x[k, j] for k in J if k != j) - T[j] <= d[j] - p[j],
            "Tardiness(%r)" % (j),
        )

        for k in J:
            if k <= j:
                continue
            model.addConstr(x[j, k] + x[k, j] == 1, "Disjunctive(%s,%s)" % (j, k))

            for ell in J:
                if ell > k:
                    model.addConstr(
                        x[j, k] + x[k, ell] + x[ell, j] <= 2,
                        "Triangle(%s,%s,%s)" % (j, k, ell),
                    )

    model.setObjective(quicksum(w[j] * T[j] for j in J), GRB.MINIMIZE)

    model.update()
    model.__data = x, T
    return model
n = 5  # number of jobs
J, p, r, d, w = make_data(n)

model = scheduling_linear_ordering(J, p, d, w)
model.optimize()
z = model.ObjVal
x, T = model.__data
for (i, j) in x:
    if x[i, j].X > 0.5:
        print("x(%s) = %s" % ((i, j), int(x[i, j].X + 0.5)))
for i in T:
    print("T(%s) = %s" % (i, int(T[i].X + 0.5)))
print("Opt.value by the linear ordering formulation=", z)
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 25 rows, 25 columns and 75 nonzeros
Model fingerprint: 0x1c2492f9
Variable types: 5 continuous, 20 integer (20 binary)
Coefficient statistics:
  Matrix range     [1e+00, 4e+00]
  Objective range  [1e+00, 3e+00]
  Bounds range     [1e+00, 1e+00]
  RHS range        [1e+00, 3e+00]
Found heuristic solution: objective 27.0000000
Presolve removed 12 rows and 12 columns
Presolve time: 0.00s
Presolved: 13 rows, 13 columns, 45 nonzeros
Variable types: 0 continuous, 13 integer (10 binary)

Root relaxation: objective 2.233333e+01, 9 iterations, 0.00 seconds

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

     0     0   22.33333    0    3   27.00000   22.33333  17.3%     -    0s
H    0     0                      24.0000000   22.33333  6.94%     -    0s
H    0     0                      23.0000000   22.33333  2.90%     -    0s
     0     0   22.33333    0    3   23.00000   22.33333  2.90%     -    0s

Explored 1 nodes (9 simplex iterations) in 0.03 seconds
Thread count was 16 (of 16 available processors)

Solution count 3: 23 24 27 

Optimal solution found (tolerance 1.00e-04)
Best objective 2.300000000000e+01, best bound 2.300000000000e+01, gap 0.0000%
x((1, 4)) = 1
x((2, 1)) = 1
x((2, 4)) = 1
x((2, 5)) = 1
x((3, 1)) = 1
x((3, 2)) = 1
x((4, 3)) = 1
x((5, 1)) = 1
x((5, 3)) = 1
x((5, 4)) = 1
T(1) = 11
T(2) = 0
T(3) = 4
T(4) = 7
T(5) = 1
Opt.value by the linear ordering formulation= 23.0

順列フローショップ問題

順列フローショップ問題(permutation flow shop problem) $F|prmu|C_{\max}$は,以下に定義される問題である.

$n$ 個のジョブを$m$台の機械で順番に処理することを考える. この機械は一度に1つのジョブしか処理できず,ジョブの処理を 開始したら途中では中断できないものと仮定する. いま,各機械におけるジョブの処理順序が同一であるとしたとき, 最後の機械 $m$ で最後に処理されるジョブの完了時刻を最小にする処理順を求めよ.

ここでは,この問題に対する位置データ定式化(positional data formulation) と呼ばれる定式化を示す.

いま,$n$個のジョブ(添え字は $j$)と $m$台の機械(添え字は $i$)があり, 各ジョブは,機械 $1,2,\ldots,m$ の順番で処理されるものとする. (これがフローショップと呼ばれる所以である.) ジョブ$j$ の機械$i$ 上での処理時間を $p_{ij}$ とする. ジョブの追い越しがないので,解はジョブの投入順序,いいかえれば順列で表現できる. (これが順列フローショップと呼ばれる所以である.) ジョブを並べたときの順番を $\kappa$ で表すことにし,順番を表す変数 $x_{j \kappa}$ を用いる.

$x_{j \kappa}$は,ジョブ $j$ が $\kappa$ 番目に投入されるとき $1$ になる $0$-$1$ 変数であり, $s_{i \kappa}$は,機械 $i$ の $\kappa$ 番目に並べられてるジョブの開始時刻を表す実数変数であり, $f_{i \kappa}$は,機械 $i$ の $\kappa$ 番目に並べられてるジョブの完了(終了)時刻を表す実数変数である.

この定式化は,ジョブ自身の開始時刻ではなく, 機械にかかるジョブの順番(位置)に対する開始時刻や終了時刻のデータを用いるので, 位置データ定式化と呼ばれる.

定式化は以下のようになる.

$$ \begin{array}{l l l} minimize & f_{m n} & \\ s.t. & \displaystyle\sum_{\kappa} x_{j\kappa} = 1 & \forall j=1,2,\ldots,n \\ & \displaystyle\sum_{j} x_{j\kappa} = 1 & \forall \kappa=1,2,\ldots,n \\ & f_{i \kappa} \leq s_{i,\kappa +1} & \forall i=1,2,\ldots,m; \kappa=1,2,\ldots,n-1 \\ & s_{i \kappa}+\displaystyle\sum_{j} p_{ij} x_{j \kappa} \leq f_{i \kappa} & \forall i=1,2,\ldots,m; \kappa=1,2,\ldots,n \\ & f_{i \kappa} \leq s_{i+1,\kappa} & \forall i=1,2,\ldots,m-1; \kappa=1,2,\ldots,n \\ & x_{j \kappa} \in \{ 0,1 \} & \forall j=1,2,\ldots,n; \kappa=1,2,\ldots,n \\ & s_{i \kappa} \geq 0 & \forall i=1,2,\ldots,m; \kappa=1,2,\ldots,n \\ & f_{i \kappa} \geq 0 & \forall i=1,2,\ldots,m; \kappa=1,2,\ldots,n \end{array} $$

上の定式化において, 目的関数は最後の機械($m$)の最後の($n$番目の)ジョブの完了時刻 を最小化している. 最初の制約は,各ジョブがいずれかの位置(順番)に割り当てられることを表す. 2番目の制約は,各位置(場所)にいずれかのジョブが割り当てられることを表す. 3番目の制約は,$\kappa$番目のジョブの完了時刻より $\kappa+1$番目のジョブの開始時刻が 遅いことを表す. 4番目の制約は,機械$i$ の $\kappa$番目のジョブの開始時刻と完了時刻の関係を 規定する制約であり, ジョブの位置を表す変数 $x$ と開始(終了)時刻を表す変数 $s$ ($f$) を繋ぐ制約である. これは,$x_{j\kappa}$ が $1$ のときに処理時間が $p_{ij}$ になる ことから導かれる. 5番目の制約は,機械$i$ 上で $\kappa$番目に割り当てられたジョブの完了時刻より, 機械$i+1$ 上で $\kappa$番目の割り当てられたジョブの開始時刻が遅いことを表す.

def permutation_flow_shop(n, m, p):
    """gpp -- model for the graph partitioning problem
    Parameters:
        - n: number of jobs
        - m: number of machines
        - p[i,j]: processing time of job i on machine j
    Returns a model, ready to be solved.
    """
    model = Model("permutation flow shop")
    x, s, f = {}, {}, {}
    for j in range(1, n + 1):
        for k in range(1, n + 1):
            x[j, k] = model.addVar(vtype="B", name="x(%s,%s)" % (j, k))

    for i in range(1, m + 1):
        for k in range(1, n + 1):
            s[i, k] = model.addVar(vtype="C", name="start(%s,%s)" % (i, k))
            f[i, k] = model.addVar(vtype="C", name="finish(%s,%s)" % (i, k))
    model.update()

    for j in range(1, n + 1):
        model.addConstr(
            quicksum(x[j, k] for k in range(1, n + 1)) == 1, "Assign1(%s)" % (j)
        )
        model.addConstr(
            quicksum(x[k, j] for k in range(1, n + 1)) == 1, "Assign2(%s)" % (j)
        )

    for i in range(1, m + 1):
        for k in range(1, n + 1):
            if k != n:
                model.addConstr(f[i, k] <= s[i, k + 1], "FinishStart(%s,%s)" % (i, k))
            if i != m:
                model.addConstr(f[i, k] <= s[i + 1, k], "Machine(%s,%s)" % (i, k))

            model.addConstr(
                s[i, k] + quicksum(p[i, j] * x[j, k] for j in range(1, n + 1))
                <= f[i, k],
                "StartFinish(%s,%s)" % (i, k),
            )

    model.setObjective(f[m, n], GRB.MINIMIZE)

    model.update()
    model.__data = x, s, f
    return model


def make_data_permutation_flow_shop(n, m):
    """make_data: prepare matrix of m times n random processing times"""
    p = {}
    for i in range(1, m + 1):
        for j in range(1, n + 1):
            p[i, j] = random.randint(1, 10)
    return p
n = 15
m = 10
p = make_data_permutation_flow_shop(n, m)

model = permutation_flow_shop(n, m, p)
model.optimize()
x, s, f = model.__data
print("Opt.value=", model.ObjVal)
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 455 rows, 525 columns and 3550 nonzeros
Model fingerprint: 0xc50336c5
Variable types: 300 continuous, 225 integer (225 binary)
Coefficient statistics:
  Matrix range     [1e+00, 1e+01]
  Objective range  [1e+00, 1e+00]
  Bounds range     [1e+00, 1e+00]
  RHS range        [1e+00, 1e+00]
Presolve removed 48 rows and 49 columns
Presolve time: 0.02s
Presolved: 407 rows, 476 columns, 3157 nonzeros
Variable types: 229 continuous, 247 integer (225 binary)

Root relaxation: objective 1.369651e+02, 1175 iterations, 0.05 seconds

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

     0     0  136.96514    0  110          -  136.96514      -     -    0s
H    0     0                     172.0000000  136.96514  20.4%     -    0s
H    0     0                     171.0000000  136.96514  19.9%     -    0s
H    0     0                     161.0000000  136.96514  14.9%     -    0s
     0     0  137.12491    0  116  161.00000  137.12491  14.8%     -    0s
     0     0  137.12513    0  116  161.00000  137.12513  14.8%     -    0s
     0     0  137.48839    0  118  161.00000  137.48839  14.6%     -    0s
     0     2  137.65026    0  118  161.00000  137.65026  14.5%     -    0s
H   31    40                     160.0000000  137.76481  13.9%   126    0s
H   88    94                     156.0000000  137.76481  11.7%  99.4    0s
H  154   142                     153.0000000  137.76481  10.0%  91.1    1s
  1941  1113  140.06571   25   95  153.00000  140.06571  8.45%  61.5    5s
H 2319  1133                     152.0000000  140.63636  7.48%  59.6    6s
  8831  3375     cutoff   36       152.00000  144.72003  4.79%  61.8   10s
 18444  5790     cutoff   38       152.00000  147.00000  3.29%  60.5   15s
 33721 10415     cutoff   29       152.00000  148.71429  2.16%  53.1   20s
 41559 13816  151.63348   43   35  152.00000  149.00000  1.97%  50.1   32s
 56453 17766  149.00000   49   42  152.00000  149.00000  1.97%  47.2   35s
 63240 21894  149.61487   46   46  152.00000  149.00000  1.97%  46.5   40s
 83467 24988  150.14505   44   37  152.00000  149.14286  1.88%  45.6   45s
 97670 26792     cutoff   40       152.00000  149.39959  1.71%  44.3   50s
 111483 28130  151.21240   41   41  152.00000  149.62842  1.56%  43.1   55s
 123676 28597  151.84483   33   36  152.00000  149.84084  1.42%  42.1   60s
 135521 29523  150.42275   38   44  152.00000  150.00000  1.32%  41.5   65s
 149962 31493     cutoff   42       152.00000  150.00000  1.32%  41.2   70s
 164393 33299     cutoff   51       152.00000  150.00000  1.32%  41.0   75s
*164415 18475              49     151.0000000  150.00000  0.66%  41.0   75s
 168828 18033  150.00000   41   33  151.00000  150.00000  0.66%  41.1   80s
 182105 17354  150.10104   43   30  151.00000  150.00000  0.66%  41.9   85s
 200251 12211  150.91634   40   50  151.00000  150.28769  0.47%  41.1   90s
 221565  1196     cutoff   61       151.00000  150.81906  0.12%  39.4   95s

Cutting planes:
  MIR: 28
  Flow cover: 21

Explored 225023 nodes (8796364 simplex iterations) in 95.46 seconds
Thread count was 16 (of 16 available processors)

Solution count 8: 151 152 153 ... 172

Optimal solution found (tolerance 1.00e-04)
Best objective 1.510000000000e+02, best bound 1.510000000000e+02, gap 0.0000%
Opt.value= 151.0

ジョブショップスケジューリング問題

ジョブショップスケジューリング問題 $J| | C_{\max}$ は,以下のように定義される古典的なスケジューリング問題である.

ジョブを $J_1, J_2,\cdots, J_n$, ジョブ $J_j$ に属するオペレーションを $O_{1j},O_{2j},\cdots,O_{m_j j}$, 機械を $M_1,M_2, \cdots, M_m$ とする. オペレーションは $O_{1j},O_{2j},\cdots,O_{m_j j}$ の順で処理されなければならず, オペレーション $O_{ij}$ を処理するには機械 $\mu_{ij}$ を作業時間 $p_{ij}$ だけ占有する. オペレーションが途中で中断できないという仮定の下で, 最後のオペレーションが完了する時刻を最小化する各機械上でのオペレーションの処理順序を 求める問題.

この問題は $NP$-困難であり, 中規模な問題例でさえ数理最適化ソルバーで解くことは難しい. 実務的な付加条件のほとんどを考慮したスケジューリング最適化システムとしてOptSeq ( https://www.logopt.com/optseq/ )が開発されている.

ジョブショップスケジューリングのベンチマーク問題例も,OptSeqを使用すると比較的容易に良い解を得ることができる.

例として, OR-Lib.(http://people.brunel.ac.uk/~mastjjb/jeb/orlib/jobshopinfo.html) にあるベンチマーク問題例を読み込んで解く.

例としてft06.txtを用いる.

 6 6
 2  1  0  3  1  6  3  7  5  3  4  6
 1  8  2  5  4 10  5 10  0 10  3  4
 2  5  3  4  5  8  0  9  1  1  4  7
 1  5  0  5  2  5  3  3  4  8  5  9 
 2  9  1  3  4  5  5  4  0  3  3  1
 1  3  3  3  5  9  0 10  4  4  2  1

得られた最大完了時刻 $55$ は最適値である.

def jssp(fname="../data/jssp/ft06.txt"):
    """
    ジョブショップスケジューリング問題のベンチマーク問題例
    """
    f = open(fname, "r")
    lines = f.readlines()
    f.close()
    n, m = map(int, lines[0].split())
    print("n,m=", n, m)

    model = Model()
    act, mode, res = {}, {}, {}
    for j in range(m):
        res[j] = model.addResource(f"machine[{j}]", capacity=1)

    # prepare data as dic
    machine, proc_time = {}, {}
    for i in range(n):
        L = list(map(int, lines[i + 1].split()))
        for j in range(m):
            machine[i, j] = L[2 * j]
            proc_time[i, j] = (L[2 * j], L[2 * j + 1])

    for i, j in proc_time:
        act[i, j] = model.addActivity(f"Act[{i}{j}]")
        mode[i, j] = Mode(f"Mode[{i}{j}]", proc_time[i, j][1])
        mode[i, j].addResource(res[proc_time[i, j][0]], 1)
        act[i, j].addModes(mode[i, j])

    for i in range(n):
        for j in range(m - 1):
            model.addTemporal(act[i, j], act[i, j + 1])

    model.Params.TimeLimit = 1
    model.Params.Makespan = True
    return model
from optseq import *

model = jssp("../data/jssp/ft06.txt")
model.Params.TimeLimit = 1
model.optimize()
n,m= 6 6

 ================ Now solving the problem ================ 


Solutions:

OR-toolsを用いた求解

fname = "../data/jssp/ft06.txt"
# fname = "../data/jssp/ft20.txt"
# fname = "../data/jssp/ft10.txt"
f = open(fname, "r")
lines = f.readlines()
f.close()
n, m = map(int, lines[0].split())
print("n,m=", n, m)

# prepare data
machine, proc_time = {}, {}
for i in range(n):
    L = list(map(int, lines[i + 1].split()))
    for j in range(m):
        machine[i, j] = L[2 * j]
        proc_time[i, j] = (L[2 * j], L[2 * j + 1])
jobs_data = []
for i in range(n):
    row = []
    for j in range(m):
        row.append((machine[i, j], proc_time[i, j][1]))
    jobs_data.append(row)
# jobs_data
n,m= 6 6
import collections
from ortools.sat.python import cp_model

model = cp_model.CpModel()

# # original example
# jobs_data = [  # task = (machine_id, processing_time).
#     [(0, 3), (1, 2), (2, 2)],  # Job0
#     [(0, 2), (2, 1), (1, 4)],  # Job1
#     [(1, 4), (2, 3)]  # Job2
# ]

machines_count = 1 + max(task[0] for job in jobs_data for task in job)
all_machines = range(machines_count)

# Computes horizon dynamically as the sum of all durations.
horizon = sum(task[1] for job in jobs_data for task in job)

# Named tuple to store information about created variables.
task_type = collections.namedtuple("task_type", "start end interval")
# Named tuple to manipulate solution information.
assigned_task_type = collections.namedtuple(
    "assigned_task_type", "start job index duration"
)

# Creates job intervals and add to the corresponding machine lists.
all_tasks = {}
machine_to_intervals = collections.defaultdict(list)

for job_id, job in enumerate(jobs_data):
    for task_id, task in enumerate(job):
        machine = task[0]
        duration = task[1]
        suffix = "_%i_%i" % (job_id, task_id)
        start_var = model.NewIntVar(0, horizon, "start" + suffix)
        end_var = model.NewIntVar(0, horizon, "end" + suffix)
        interval_var = model.NewIntervalVar(
            start_var, duration, end_var, "interval" + suffix
        )
        all_tasks[job_id, task_id] = task_type(
            start=start_var, end=end_var, interval=interval_var
        )
        machine_to_intervals[machine].append(interval_var)

# Create and add disjunctive constraints.
for machine in all_machines:
    model.AddNoOverlap(machine_to_intervals[machine])

# Precedences inside a job.
for job_id, job in enumerate(jobs_data):
    for task_id in range(len(job) - 1):
        model.Add(
            all_tasks[job_id, task_id + 1].start >= all_tasks[job_id, task_id].end
        )

# Makespan objective.
obj_var = model.NewIntVar(0, horizon, "makespan")
model.AddMaxEquality(
    obj_var,
    [all_tasks[job_id, len(job) - 1].end for job_id, job in enumerate(jobs_data)],
)
model.Minimize(obj_var)

# Solve model.
solver = cp_model.CpSolver()
status = solver.Solve(model)

if status == cp_model.OPTIMAL:
    # Create one list of assigned tasks per machine.
    assigned_jobs = collections.defaultdict(list)
    for job_id, job in enumerate(jobs_data):
        for task_id, task in enumerate(job):
            machine = task[0]
            assigned_jobs[machine].append(
                assigned_task_type(
                    start=solver.Value(all_tasks[job_id, task_id].start),
                    job=job_id,
                    index=task_id,
                    duration=task[1],
                )
            )

    # Create per machine output lines.
    output = ""
    for machine in all_machines:
        # Sort by starting time.
        assigned_jobs[machine].sort()
        sol_line_tasks = "Machine " + str(machine) + ": "
        sol_line = "           "

        for assigned_task in assigned_jobs[machine]:
            name = "job_%i_%i" % (assigned_task.job, assigned_task.index)
            # Add spaces to output to align columns.
            sol_line_tasks += "%-10s" % name

            start = assigned_task.start
            duration = assigned_task.duration
            sol_tmp = "[%i,%i]" % (start, start + duration)
            # Add spaces to output to align columns.
            sol_line += "%-10s" % sol_tmp

        sol_line += "\n"
        sol_line_tasks += "\n"
        output += sol_line_tasks
        output += sol_line

    # Finally print the solution found.
    print("Optimal Schedule Length: %i" % solver.ObjectiveValue())
    print(output)
Optimal Schedule Length: 55
Machine 0: job_0_1   job_3_1   job_2_3   job_5_3   job_1_4   job_4_4   
           [1,4]     [13,18]   [19,28]   [28,38]   [38,48]   [48,51]   
Machine 1: job_1_0   job_3_0   job_5_0   job_0_2   job_4_1   job_2_4   
           [0,8]     [8,13]    [13,16]   [16,22]   [22,25]   [41,42]   
Machine 2: job_0_0   job_2_0   job_1_1   job_4_0   job_3_2   job_5_5   
           [0,1]     [2,7]     [8,13]    [13,22]   [22,27]   [42,43]   
Machine 3: job_2_1   job_5_1   job_3_3   job_0_3   job_1_5   job_4_5   
           [7,11]    [16,19]   [27,30]   [30,37]   [48,52]   [52,53]   
Machine 4: job_1_2   job_4_2   job_3_4   job_5_4   job_0_5   job_2_5   
           [13,23]   [25,30]   [30,38]   [38,42]   [42,48]   [48,55]   
Machine 5: job_2_2   job_5_2   job_1_3   job_0_4   job_4_3   job_3_5   
           [11,19]   [19,28]   [28,38]   [38,41]   [41,45]   [45,54]   

資源制約付きプロジェクトスケジューリング問題

様々なスケジューリング問題を一般化した資源制約付き(プロジェクト)スケジューリング問題を考える.

資源制約付きスケジューリング問題

ジョブの集合 $Job$ , 各時間ごとの使用可能量の上限をもつ資源 $Res$ が与えられている. 資源は,ジョブごとに決められた作業時間の間はジョブによって使用されるが, 作業完了後は,再び使用可能になる. ジョブ間に与えられた時間制約を満たした上で, ジョブの作業開始時刻に対する任意の関数の和を最小化するような, 資源のジョブへの割り振りおよびジョブの作業開始時刻を求める.

時刻を離散化した期の概念を用いた定式化を示す.

まず,定式化で用いる集合,入力データ,変数を示す.

集合

  • $Job$: ジョブの集合;添え字は $j,k$.
  • $Res$: 資源の集合;添え字は $r$.
  • $Prec$: ジョブ間の時間制約を表す集合. $Job \times Job$ の部分集合で, $(j,k) \in Prec$ のとき, ジョブ $j$ とジョブ $k$ の開始(完了)時刻間に何らかの制約が定義されるものとする..

入力データ

  • $T$: 最大の期数;期の添え字は $t=1,2,\ldots,T$. 連続時間との対応づけは,時刻 $t-1$ から $t$ までを期 $t$ と定義する. なお,期を表す添え字は $t$ の他に $s$ も用いる.
  • $p_j$: ジョブ $j$ の処理時間.非負の整数を仮定する.
  • $Cost_{jt}$: ジョブ $j$ を期$t$ に開始したときの費用.
  • $a_{jrt}$: ジョブ $j$ の開始後 $t(=0,1,\ldots,p_j-1)$期経過時の処理に要する資源$r$ の量.
  • $RUB_{rt}$: 時刻 $t$ における資源 $r$ の使用可能量の上限.

変数

  • $x_{jt}$: ジョブ $j$ を時刻 $t$ に開始するとき $1$,それ以外のとき $0$ を表す $0$-$1$変数.

以下に資源制約付きスケジューリング問題の定式化を示す.

目的関数: $$ minimize \sum_{j \in Job} \sum_{t=1}^{T-p_j+1} Cost_{jt} x_{jt} $$

  • ジョブ遂行制約: $$ \sum_{t=1}^{T-p_j+1} x_{jt} =1  \forall j \in Job $$ すべてのジョブは必ず1度処理されなければならないことを表す.
  • 資源制約:
$$ \sum_{j \in Job} \sum_{s =\max\{t-p_j+1,1\}}^{\min\{t,T-p_j+1\}} a_{jr,t-s} x_{js} \leq RUB_{rt} \ \ \ \forall r \in Res , t \in \{1,2,\ldots,T \} $$

ある時刻 $t$ に作業中であるJob の資源使用量の合計 が,資源使用量の上限を超えないことを規定する. 時刻 $t$ に作業中であるJob は, 時刻 $t-p_j+1$ から $t$ の間に作業を開始したジョブであるので,上式を得る.

  • 時間制約:

ジョブ $j$ の開始時刻 $S_j$ は $\sum_{t=2}^{T-p_j+1} (t-1)x_{jt}$ であるので, 完了時刻 $C_j$ はそれに $p_j$ を加えたものになる. ジョブの対 $(j,k) \in Prec$ に対しては,ジョブ $j,k$ の開始(完了)時刻間に 何らかの制約が付加される.たとえば,ジョブ $j$ の処理が終了するまで,ジョブ $k$ の処理が開始できないことを表す先行制約は,

$$ \sum_{t=2}^{T-p_j+1} (t-1)x_{jt} +p_j \leq \sum_{t=2}^{T-p_k+1} (t-1) x_{kt}  \ \ \ \forall (j,k) \in Prec $$

と書くことができる. ここで,左辺の式は,ジョブ $j$の完了時刻を表し,右辺の式はジョブ $k$ の開始時刻を表す.

以下に,数理最適化ソルバー Gurobiを用いたモデルを示す. 実際には,Gurobiでは中規模問題例までしか解くことができない. ジョブショップスケジューリング問題と同様に, スケジューリング最適化システムとしてOptSeq ( https://www.logopt.com/optseq/ )を用いることによって,容易に求解できる.

def rcs(J, P, R, T, p, c, a, RUB):
    """rcs -- model for the resource constrained scheduling problem
    Parameters:
        - J: set of jobs
        - P: set of precedence constraints between jobs
        - R: set of resources
        - T: number of periods
        - p[j]: processing time of job j
        - c[j,t]: cost incurred when job j starts processing on period t.
        - a[j,r,t]: resource r usage for job j on period t (after job starts)
        - RUB[r,t]: upper bound for resource r on period t
    Returns a model, ready to be solved.
    """
    model = Model("resource constrained scheduling")
    s, x = {}, {}  # s - start time variable;  x=1 if job j starts on period t
    for j in J:
        s[j] = model.addVar(vtype="C", name="s(%s)" % j)
        for t in range(1, T - p[j] + 2):
            x[j, t] = model.addVar(vtype="B", name="x(%s,%s)" % (j, t))
    model.update()

    for j in J:
        # job execution constraints
        model.addConstr(
            quicksum(x[j, t] for t in range(1, T - p[j] + 2)) == 1,
            "ConstrJob(%s,%s)" % (j, t),
        )

        # start time constraints
        model.addConstr(
            quicksum((t - 1) * x[j, t] for t in range(2, T - p[j] + 2)) == s[j],
            "ConstrJob(%s,%s)" % (j, t),
        )

    # resource upper bound constraints
    for t in range(1, T - p[j] + 2):
        for r in R:
            model.addConstr(
                quicksum(
                    a[j, r, t - t_] * x[j, t_]
                    for j in J
                    for t_ in range(max(t - p[j] + 1, 1), min(t + 1, T - p[j] + 2))
                )
                <= RUB[r, t],
                "ResourceUB(%s)" % t,
            )

    # time (precedence) constraints, i.e., s[k]-s[j] >= p[j]
    for (j, k) in P:
        model.addConstr(s[k] - s[j] >= p[j], "Precedence(%s,%s)" % (j, k))

    model.setObjective(quicksum(c[j, t] * x[j, t] for (j, t) in x), GRB.MINIMIZE)

    model.update()
    model.__data = x, s
    return model


def make_1r():
    J, p = multidict(
        {  # jobs, processing times
            1: 1,
            2: 3,
            3: 2,
            4: 2,
        }
    )
    P = [(1, 2), (1, 3), (2, 4)]
    R = [1]
    T = 6
    c = {}
    for j in J:
        for t in range(1, T - p[j] + 2):
            c[j, t] = 1 * (t - 1 + p[j])
    a = {
        (1, 1, 0): 2,
        (2, 1, 0): 2,
        (2, 1, 1): 1,
        (2, 1, 2): 1,
        (3, 1, 0): 1,
        (3, 1, 1): 1,
        (4, 1, 0): 1,
        (4, 1, 1): 2,
    }
    RUB = {(1, 1): 2, (1, 2): 2, (1, 3): 1, (1, 4): 2, (1, 5): 2, (1, 6): 2}
    return (J, P, R, T, p, c, a, RUB)


def make_2r():
    J, p = multidict(
        {  # jobs, processing times
            1: 2,
            2: 2,
            3: 3,
            4: 2,
            5: 5,
        }
    )
    P = [(1, 2), (1, 3), (2, 4)]
    R = [1, 2]
    T = 6
    c = {}
    for j in J:
        for t in range(1, T - p[j] + 2):
            c[j, t] = 1 * (t - 1 + p[j])
    a = {
        # resource 1:
        (1, 1, 0): 2,
        (1, 1, 1): 2,
        (2, 1, 0): 1,
        (2, 1, 1): 1,
        (3, 1, 0): 1,
        (3, 1, 1): 1,
        (3, 1, 2): 1,
        (4, 1, 0): 1,
        (4, 1, 1): 1,
        (5, 1, 0): 0,
        (5, 1, 1): 0,
        (5, 1, 2): 1,
        (5, 1, 3): 0,
        (5, 1, 4): 0,
        # resource 2:
        (1, 2, 0): 1,
        (1, 2, 1): 0,
        (2, 2, 0): 1,
        (2, 2, 1): 1,
        (3, 2, 0): 0,
        (3, 2, 1): 0,
        (3, 2, 2): 0,
        (4, 2, 0): 1,
        (4, 2, 1): 2,
        (5, 2, 0): 1,
        (5, 2, 1): 2,
        (5, 2, 2): 1,
        (5, 2, 3): 1,
        (5, 2, 4): 1,
    }
    RUB = {
        (1, 1): 2,
        (1, 2): 2,
        (1, 3): 2,
        (1, 4): 2,
        (1, 5): 2,
        (1, 6): 2,
        (1, 7): 2,
        (2, 1): 2,
        (2, 2): 2,
        (2, 3): 2,
        (2, 4): 2,
        (2, 5): 2,
        (2, 6): 2,
        (2, 7): 2,
    }
    return (J, P, R, T, p, c, a, RUB)
(J,P,R,T,p,c,a,RUB) = make_2r()
model = rcs(J,P,R,T,p,c,a,RUB)
model.optimize()
x,s = model.__data

print ("Opt.value=",model.ObjVal)
for (j,t) in x:
    if x[j,t].X > 0.5:
        print (x[j,t].VarName,x[j,t].X) 

for j in s:
    if s[j].X > 0.:
        print (s[j].VarName,s[j].X )
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-15-6847624bf375> in <module>
      1 (J,P,R,T,p,c,a,RUB) = make_2r()
----> 2 model = rcs(J,P,R,T,p,c,a,RUB)
      3 model.optimize()
      4 x,s = model.__data
      5 

<ipython-input-14-26f8c11c7657> in rcs(J, P, R, T, p, c, a, RUB)
     15     s,x = {},{}   # s - start time variable;  x=1 if job j starts on period t
     16     for j in J:
---> 17         s[j] = model.addVar(vtype="C", name="s(%s)"%j)
     18         for t in range(1,T-p[j]+2):
     19             x[j,t] = model.addVar(vtype="B", name="x(%s,%s)"%(j,t))

AttributeError: 'Model' object has no attribute 'addVar'