Skip to content

Commit

Permalink
jsp framework: pulp solver
Browse files Browse the repository at this point in the history
  • Loading branch information
dothinking committed Aug 29, 2021
1 parent 82d2945 commit 82a8775
Show file tree
Hide file tree
Showing 2 changed files with 306 additions and 0 deletions.
2 changes: 2 additions & 0 deletions docs/2021-08-08-作业车间调度问题求解框架.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ https://github.com/dothinking/jsp_framework

- [OR-Tools 约束求解器](2021-08-22-作业车间调度问题求解框架:OR-Tools约束求解器.md)

- [PuLP求解器](2021-08-29-作业车间调度问题求解框架:PuLP求解器.md)

- [基于规则指派](2021-08-28-作业车间调度问题求解框架:基于规则指派求解器.md)


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,304 @@
---
categories: [optimization, mathematics]
tags: [job shop schedule]
---

# 作业车间调度问题求解框架:PuLP 求解器

---


本文根据作业车间调度问题的数学描述,利用 Python 整数规划包 PuLP 进行建模和求解,并集成到 `jsp_framework` 框架中。

## PuLP

PuLP 是开源基金会 [COIN-OR](https://github.com/coin-or) (Computational Infrastructure for Operations Research) 维护的一个线性规划建模工具(LP modeler),内置了 CBC(COIN-OR Branch-and-Cut)求解器,也支持集成各种开源(如GLPK) 或者商业(CPLEX、GUROBI等)的线性规划求解器。

> https://github.com/coin-or/pulp
作为 Python 库,PuLP 支持 pip 一键安装。

```python
pip install pulp
```

更多入门材料参考:

- [官方文档](https://coin-or.github.io/pulp/index.html)
- [PuLP简介](http://fancyerii.github.io/2020/04/18/pulp/)

## 数学模型

[前文](2021-08-08-作业车间调度问题求解框架:问题描述.md) 给出了作业车间调度问题的数学模型。为了方便接下来的建模描述,改为以工序 **开始时间** 为决策变量的描述方式:

\begin{align\*}
&\min\,\, c_{m} \\\\
\\\\
&s.t.\quad
\begin{cases}
s_{ij} \geq 0 & (i,j) \in \mathbb{O} \\\\
\\\\
c_m-s_{ij} \geq p_{ij} & (i,j) \in \mathbb{O} \\\\
\\\\
s_{ij}-s_{kj} \geq p_{kj} & if \,\, (k,j) \rightarrow (i,j), \,\, i, k \in \mathbb{M}^j \\\\
\\\\
s_{ij} - s_{ik} \geq p_{ik} \,\, or \,\, s_{ik}-s_{ij} \geq p_{ij} & (i,j), (i,k) \in \mathbb{O}, \,\, j \neq k
\end{cases}
\end{align\*}

其中两个基本变量:

- $s_{ij}$ 表示工序 $(i,j)$ 即作业 $j$ 的某个在机器 $i$ 上进行的工序的开始时间
- $p_{ij}$ 表示作业 $j$ 的某个在机器 $i$ 上进行的工序所需的加工时间

四个基本约束:

- 第一个表明工序的开始时间非负
- 第二个定义了最大完工时间 $c_m$
- 第三个是工序序列约束,即同一个作业的相邻工序必须满足先后顺序
- 第四个是资源约束,即每一台机器在某个时刻只能加工一道工序

我们需要特别关注一下第四式,因为它出现了一个 **** 的约束关系,但线性规划问题的约束式通常都是 **** 也就是且的关系。为了正确实施这个约束,我们需要引入额外的变量 $b_{jk}$,表示分配到机器 $m_i$ 的所有工序中任意两者例如工序 $(i,j)$ 和工序 $(i,k)$ 的先后关系:

\begin{align\*}
b_{jk} =
\begin{cases}
0 & if \,\, (i,j) \rightarrow (i,k) \\\\
\\\\
1 & if \,\, (i,k) \rightarrow (i,j)
\end{cases}
\end{align\*}

即,$b_{jk}$ 是一个 $0-1$ 变量,当工序 $(i,j)$ 排在工序 $(i,k)$ 之前取值 0,否则 1。

于是,原来写法中逻辑 **** 的两种情况被等效为 $0-1$ 变量 $b_{jk}$ 的两种可能。进而,改写约束式的思路是合并/统一原来两个分支,一旦确定 $b_{jk}$ 则退化为其中一个分支。这一步还需要一点小技巧,最终约束四被改写为以下两个式子:

\begin{align\*}
\begin{cases}
s_{ij}-s_{ik} \geq p_{ik} - C*(1-b_{jk}) & (i,j), (i,k) \in \mathbb{O}, \,\, j \neq k \\\\
\\\\
s_{ik}-s_{ij} \geq p_{ij} - C*b_{jk} & (i,j), (i,k) \in \mathbb{O}, \,\, j \neq k
\end{cases}
\end{align\*}

其中,$C$ 是一个足够大例如满足 $C>\Sigma \, p_{ij}$ 的常数。

验证一下,假设工序 $(i,j)$ 排在工序 $(i,k)$ 之前即 $b_{jk}=0$,则第二式反映正确的约束关系,而第一式显然成立即不反应任何有效信息;相反,$b_{jk}=1$ 时,第一式反映目标约束而第二式显然成立。



## PuLP 模型


使用 PuLP 建模和求解的步骤与 Google OR-Tools 极为类似,但 PuLP 并未提供如 OR-Tools 一般便捷的建立约束的 API。例如,OR-Tools 的区间类型变量配合 `NoOverlap` 约束可以很方便地实现机器上工序不重叠的约束(即上一节描述的约束四),但 PuLP 需要更基础的数学操作,例如上一节进行的约束转换即为此服务。

PuLP 建模常用的API:

- LpProblem 类
```python
LpProblem(name='NoName', sense=LpMinimize)
```

其中,`sense` 参数目标函数是求极大值(LpMaximize)还是极小值(LpMinimize)。

建立约束后调用 `solve(solver=None, **kwargs)` 求解,默认为 CBC 求解器。


- LpVariable 类

```python
LpVariable(name, lowBound=None, upBound=None, cat='Continuous', e=None)
```

其中,

- `name` 表示变量名
- `lowBound``upBound` 表示变量范围下界和上界,默认负无穷到正无穷
- `cat` 指定变量是离散类型(Integer或Binary),还是连续类型(Continuous)

特别地,当变量个数特别多且满足批量化处理时,可以使用如下方法建立字典格式的变量集合。

```python
LpVariable.dicts(name, indexs, lowBound=None, upBound=None, cat='Continuous', indexStart=[])
```

其中,

- `name` 指定所有变量的前缀
- `indexs` 是一个列表,将会为其中每一个元素创建一个变量,并建立对应关系


## 基于 `jsp_framework` 实现

本文参考 [案例](https://github.com/KevinLu43/JSP-by-using-Mathematical-Programming-in-Python/),基于 `jsp_framework` 进行实现和集成,完整代码参考:

> https://github.com/dothinking/jsp_framework/blob/dev/jsp_fwk/solver/pulp.py


### 基本流程

参考 [Python建模](2021-08-14-作业车间调度问题求解框架:Python建模.md),自定义一个继承自 JSSolver 的 PuLPSolver 类,然后重点实现 `do_solve()` 方法。

```python
class PuLPSolver(JSSolver):

...

def do_solve(self, problem:JSProblem):
'''Solve JSP with PuLP default CBC solver.'''
# Initialize an empty solution from problem
solution = JSSolution(problem)

# create model
model, variables = self.__create_model(solution)

# The problem is solved using PuLP's choice of Solver
model.solve(pulp.PULP_CBC_CMD(maxSeconds=self.__max_time, msg=1, fracGap=0))
if model.status!=pulp.LpStatusOptimal:
raise JSPException('No feasible solution found.')

# assign pulp solution back to JSPSolution
for op, var in variables.items():
op.update_start_time(var.varValue)
problem.update_solution(solution) # update solution
```

!!! warning "注意"

当前版本采用了默认的 CBC (COIN-OR Branch-and-Cut)求解器。


### PuLP 建模

即上一代码片段 `self.__create_model(solution)` 的具体展开。

- 创建问题

```python
# create the model
model = pulp.LpProblem("min_makespan", pulp.LpMinimize)
```

- 创建变量

- 基本变量:每一道工序的开始时间

```python
# (1) start time of each operation
max_time = sum(op.source.duration for op in solution.ops) # upper bound of variables
variables = pulp.LpVariable.dicts(name='start_time', \
indexs=solution.ops, \
lowBound=0, \
upBound=max_time, \
cat='Integer')
```

其中,`max_time`是所有工序用时之和,即不重叠任何工序的调度结果。显然,这是任意工序开始时间的上限。并且,这个值可以作为问题描述中提及的 **足够大的数**

- 0-1变量:表征同一机器上任意两道工序的前后顺序

```python
# (2) binary variable, i.e. 0 or 1, indicating the sequence of every two
# operations assigned in same machine
combinations = []
for _, ops in solution.machine_ops.items():
combinations.extend(pulp.combination(ops, 2))
bin_vars = pulp.LpVariable.dicts(name='binary_var', \
indexs=combinations, \
lowBound=0, \
cat='Binary')
```

其中,`pulp.combination`得到了机器队列工序的两两组合。

- 创建目标函数:总的加工周期

```python
# objective: makespan / flowtime
s_max = pulp.LpVariable(name='max_start_time', \
lowBound=0, \
upBound=max_time, \
cat='Integer')
model += s_max
```

注意:此处目标函数仅仅是一个普通的变量,后续添加约束保证其为最大的加工周期


- 添加约束

- 最大加工周期约束

```python
# (1) the max start time
for _, ops in solution.job_ops.items():
last_op = ops[-1]
model += (s_max-variables[last_op]) >= last_op.source.duration
```

- 作业视角的工序顺序约束

```python
# (2) operation sequence inside a job
pre = None
for op in solution.ops:
if pre and pre.source.job==op.source.job:
model += (variables[op]-variables[pre]) >= pre.source.duration
pre = op
```

- 机器视角的工序顺序约束

```python
# (3) no overlap for operations assigned to same machine
for op_a, op_b in combinations:
model += (variables[op_a]-variables[op_b]) >= (op_b.source.duration - max_time*(1-bin_vars[op_a, op_b]))
model += (variables[op_b]-variables[op_a]) >= (op_a.source.duration - max_time*bin_vars[op_a, op_b])
```



## 计算实例

最后,求解几个标准问题。与Google OR-Tools一样,求解时间上限设定为300秒。

```python
# benchmark.py
from jsp_fwk import (JSProblem, BenchMark)
from jsp_fwk.solver import PuLPSolver

# problems
names = ['ft06', 'la01', 'ft10', 'swv01', 'la38', \
'ta31', 'swv12', 'ta42', 'ta54', 'ta70']
problems = [JSProblem(benchmark=name) for name in names]

# solver
solvers = [PuLPSolver(max_time=300)]

# solve
benchmark = BenchMark(problems=problems, solvers=solvers, num_threads=5)
benchmark.run(show_info=True)
```

结果如下表所示:

```
+----+---------+--------+---------------+--------------+----------+---------+-------+
| ID | Problem | Solver | job x machine | Optimum | Solution | Error % | Time |
+----+---------+--------+---------------+--------------+----------+---------+-------+
| 1 | ft06 | pulp | 6 x 6 | 55 | 55.0 | 0.0 | 3.5 |
| 2 | la01 | pulp | 10 x 5 | 666 | 666.0 | 0.0 | 209.0 |
| 3 | ft10 | pulp | 10 x 10 | 930 | 1034.0 | 11.2 | 301.2 |
| 4 | swv01 | pulp | 20 x 10 | 1407 | 2941.0 | 109.0 | 301.2 |
| 5 | la38 | pulp | 15 x 15 | 1196 | 1769.0 | 47.9 | 301.2 |
| 6 | ta31 | pulp | 30 x 15 | 1764 | 13294.0 | 653.6 | 301.1 |
| 7 | swv12 | pulp | 50 x 10 | (2972, 3003) | 9527.0 | 218.9 | 309.9 |
| 8 | ta42 | pulp | 30 x 20 | (1867, 1956) | 23202.0 | 1113.8 | 507.1 |
| 9 | ta54 | pulp | 50 x 15 | 2839 | 26229.0 | 823.9 | 608.0 |
| 10 | ta70 | pulp | 50 x 20 | 2995 | 36851.0 | 1130.4 | 625.6 |
+----+---------+--------+---------------+--------------+----------+---------+-------+
```

计算效果并不理想,只有前两个问题即工序总数 50 以内得到了最优解,其余案例误差较大。由此表明,**PuLP 默认的 CBC 求解器在作业车间调度问题上的性能不如 Google OR-Tools 的约束求解器 CpSolver**

PuLP 支持其他求解器例如商业求解器 CPLEX 和 GUROBI,有机会可以对比一下结果。

0 comments on commit 82a8775

Please sign in to comment.