Skip to content

Commit

Permalink
update post due to jssp framework upgrading
Browse files Browse the repository at this point in the history
  • Loading branch information
dothinking committed Mar 30, 2024
1 parent 59ec192 commit 0cbabee
Show file tree
Hide file tree
Showing 6 changed files with 119 additions and 96 deletions.
154 changes: 96 additions & 58 deletions docs/2021-08-14-作业车间调度问题求解框架:Python建模.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ tags: [job shop schedule]

整个求解框架基于Python面向对象编程实现,主要结构参考下图。

![class-diagram](images/2021-08-14-01.png)
![class-diagram](https://raw.githubusercontent.com/dothinking/jsp_framework/master/doc/images/class.drawio.png)


其中,所有对象按用途可以归为三类:
Expand All @@ -27,85 +27,100 @@ tags: [job shop schedule]
- `Job` 作业实体
- `Machine` 机器实体
- `Operation` 工序实体,包含所属作业、分配的机器、加工时长等属性
- `JSProblem` 是所有工序实体 `Operation` 的封装


### (2)求解变量

`OperationStep`是工序实体`Operation` 的封装,同时加上待求解的参数 `start_time`。根据前文关于作业车间问题的两种不同的描述方式,相应有两种不同的求解思路:

- 对于以`start_time`为变量描述的数学模型,直接求解`start_time`即可
- 对于以 **析取图** 描述的模型,需要先求解工序的顺序,然后递推出`start_time`
- 对于以`start_time`为变量描述的数学模型,直接求解`start_time`即可
- 对于以 **析取图** 描述的模型,需要先求解工序的顺序,然后递推出`start_time`

因此,对于析取图描述的模型,还提供了以下中间属性:

- 继承自 `JobStep``pre_job_op``next_job_op`,分别表示当前工序在所属作业实体上的顺序:前一道工序和下一道工序;并且,它们是已知的。
- `pre_job_op``next_job_op`,分别表示当前工序在所属作业实体上的顺序:前一道工序和下一道工序;并且,它们是已知的。

- 继承自 `MachineStep``pre_machine_op``next_machine_op`,分别表示当前工序在分配机器上的加工顺序:前一道工序和下一道工序;注意这个顺序即为需要求解的变量。
- `pre_machine_op``next_machine_op`,分别表示当前工序在分配机器上的加工顺序:前一道工序和下一道工序;注意这个顺序即为需要求解的变量。


### (3)求解流程

`JSProblem` 是所有工序实体 `Operation` 的封装:
- `JSSolver` 是作业车间调度问题求解器的基类,便于继承此基类后实施新算法。

- 它的解为一个`JSSolution`实例
- 每当获得一个更好的解,**需要使用`update_solution()`方法显式更新**
- 它的解为一个`JSSolution`实例
- 每当获得一个更好的解,**需要使用`update_solution(sol)`方法显式更新**

`JSSolution` 是所有变量 `OperationStep` 的封装:
- `JSSolution` 是所有 `OperationStep` 的封装:

- 对于析取图求解模型,需要显式地调用 `evaluate()` 来基于求解的顺序计算最终变量 `start_time`;基于数学模型则无需这一步。
- `is_feasible`属性判断当前解是否满足所有约束;如果是一个可行解,`makespan`属性得到最大加工周期长度。
- 此外,还有一些后处理作图方法,例如甘特图和析取图。

- `is_feasible()` 判断一个解是否满足所有约束

- 如果是一个可行解,`makespan`属性得到最大加工周期长度


`JSSolver` 是作业车间调度问题求解器的基类,便于继承此基类后实施新算法。


## 实施新算法

以上的设计可以避免重复工作,从而专注于算法本身的实现和测试。基于此框架,实施新算法的只需创建自定义求解器类,然后继承 `JSSolver` 并实现 `do_solver()` 方法。`do_solver()` 方法内部主要分为三大步骤:
以上的设计可以避免重复工作,从而专注于算法本身的实现和测试。基于此框架,实施新算法只需创建自定义求解器类,然后继承 `JSSolver` 并实现 `do_solver()` 方法。`do_solver()` 方法内部主要分为三大步骤:

- 基于问题创建初始状态的解(注意并非可行的 **初始解**
- 基于问题创建初始状态的解(注意并非可行的 **初始解**

```python
solution = JSSolution(problem)
# direct_mode 指明是直接求解 start_time(True),还是以析取图模型间接求解(False)
solution = JSSolution(problem, direct_mode=False)
```

- 实施算法,计算或者优化这个解
- 实施算法,计算或者优化这个解。

- 对于以`start_time`为变量描述的数学模型,直接求解并更新每个工序即可。

- 对于以`start_time`为变量描述的数学模型,直接求解每个工序的`start_time`即可
- **对于以析取图描述的模型,需要先求解工序的顺序,然后显式地调用 `solution.evaluate()` 递推出`start_time`**
```python
OperationStep.update_start_time(t)
```
- 对于以析取图描述的模型,需要先求解工序的顺序,然后依次调度工序即可。其中,`update_time`参数指明是否及时更新工序的`start_time`

```python
JSSolution.dispatch(op_or_op_list, update_time=True)
```

- 每次迭代得到更好的解后,显式更新问题的解

- 每次迭代得到更好的解后,显式更新给求解器,以便触发动态甘特图的更新、自定义回调函数的调用。

```python
problem.update_solution(solution)
JSSolver.update_solution(solution)
```

关键代码参考:

```python
# user defined path, e.g. path/to/UserSolver.py
from jsp_fwk import (JSProblem, JSSolution, JSSolver)
from jsp import (JSProblem, JSSolution, JSSolver)

class UserSolver(JSSolver):

def do_solve(self, problem:JSProblem):
'''User defined solving process.'''
"""User defined solving process."""

# (1) Initialize an empty solution and specify solving mode.
# * direct_mode=True, solve start time directly;
# * direct_mode=False, solve operations sequence first and deduce start time
solution = JSSolution(problem, direct_mode=False)

# (1) Initialize an empty solution from problem
solution = JSSolution(problem)
# (2) Solve or optimize the solution.
for op in solution.ops:
# option 1: solve and set start time directly
...
op.update_start_time(solved_start_time)

# (2) Solve or optimize the solution,
# i.e. determine the start_time of OperationStep instances.
# Note to evaluate solution explicitly if disjunctive graph model.
...
# solution.evaluate()
# option 2: solve sequence
...
self.dispatch(op)

# (3) Update the solution for problem iteratively
problem.update_solution(solution)
# optional: update solution per iteration,
# triggering dynamic Gantt and callback
self.update_solution(solution)

# (3) Update solution finally
self.update_solution(solution)
```


Expand All @@ -122,23 +137,46 @@ class UserSolver(JSSolver):

- `callback` 在每次获得更好的解后执行自定义的动作,例如打印这个解

以下示例调用上一节自定义的求解器 `UserSolver` 求解 `ft10` 问题。
以下示例调用内置的规则指派类求解器 `PriorityDispatchSolver` 求解 `ft06` 问题。因为设置了`interval`参数,优化过程中会以2秒的频率动态更新甘特图


```python
# run.py
from jsp_fwk import JSProblem
from path/to/UserSolver import UserSolver
from jsp import JSProblem
from jsp.solver import PriorityDispatchSolver

# load benchmark problem
problem = JSProblem(benchmark='ft10')
problem = JSProblem(benchmark='ft06')

# solve problem with user defined solver
s = UserSolver()
fun = lambda solution: print(f'makespan: {solution.makespan}')
s = PriorityDispatchSolver(rule='HH')
fun = lambda solution: print(f'current makespan: {solution.makespan}')
s.solve(problem=problem, interval=2000, callback=fun)
```

![](./images/2021-08-14-01.png)


**注意**`solve()`是在子线程中进行的异步方法,所以如果需要获取优化结果,需要通过`JSSolver.wait()`方法来等待计算完成。下面示例获取最终结果,并绘制析取图。

```python
# start solving process
s.solve(...)

# waiting
s.wait()
print('Solving time ', s.user_time)
print('Makespan ', s.solution.makespan)

# explore solution
solution = s.solution
print('feasible solution:', solution.is_feasible)
solution.plot_disjunctive_graph()
```

![](./images/2021-08-14-02.png)


### (2)多个算法多个问题

当算法调试稳定后,我们需要测试它在不同规模问题上的表现,或者对比不同算法对相同问题的求解效率。针对此类场景,本框架内置了 `Benchmark` 类:排列组合输入的求解器和问题,然后进行多线程异步求解,最后对比结果。显然,`Benchmark` 类也适用于单个算法单个问题的场景。
Expand All @@ -148,8 +186,8 @@ s.solve(problem=problem, interval=2000, callback=fun)
```python
# benchmark.py
import logging
from jsp_fwk import (JSProblem, BenchMark)
from jsp_fwk.solver import (GoogleORCPSolver, PriorityDispatchSolver)
from jsp import (JSProblem, BenchMark)
from jsp.solver import (GoogleORCPSolver, PriorityDispatchSolver)

# ----------------------------------------
# create problem from benchmark
Expand All @@ -164,7 +202,7 @@ problems = [JSProblem(benchmark=name) for name in names]
s1 = GoogleORCPSolver(max_time=300, name='or-tools')

# priority dispatching
s2 = PriorityDispatchSolver(rule='t', name='pd-t-rule')
s2 = PriorityDispatchSolver(rule='HH', name='pd-HH')

solvers = [s1, s2]

Expand All @@ -178,18 +216,18 @@ benchmark.run(show_info=True)
结果示例:

```
+---------+-----------+-------+---------+----------+---------+-------+
| Problem | Solver | Scale | Optimum | Solution | Error % | Time |
+---------+-----------+-------+---------+----------+---------+-------+
| ft06 | pd-t-rule | 6x6 | 55 | 60.0 | 9.1 | 0.0 |
| la01 | pd-t-rule | 10x5 | 666 | 666.0 | 0.0 | 0.0 |
| ft10 | pd-t-rule | 10x10 | 930 | 1082.0 | 16.3 | 0.1 |
| ft06 | or-tools | 6x6 | 55 | 55 | 0.0 | 0.1 |
| swv01 | pd-t-rule | 20x10 | 1407 | 1839.0 | 30.7 | 0.2 |
| la01 | or-tools | 10x5 | 666 | 666 | 0.0 | 0.4 |
| la38 | pd-t-rule | 15x15 | 1196 | 1387.0 | 16.0 | 0.6 |
| ft10 | or-tools | 10x10 | 930 | 930 | 0.0 | 4.2 |
| la38 | or-tools | 15x15 | 1196 | 1196 | 0.0 | 141.1 |
| swv01 | or-tools | 20x10 | 1407 | 1414 | 0.5 | 300.1 |
+---------+-----------+-------+---------+----------+---------+-------+
+----+---------+----------+---------------+---------+----------+---------+-------+
| ID | Problem | Solver | job x machine | Optimum | Solution | Error % | Time |
+----+---------+----------+---------------+---------+----------+---------+-------+
| 1 | ft06 | or-tools | 6 x 6 | 55 | 55 | 0.0 | 0.1 |
| 2 | ft06 | pd-HH | 6 x 6 | 55 | 60.0 | 9.1 | 0.0 |
| 3 | la01 | or-tools | 10 x 5 | 666 | 666 | 0.0 | 0.1 |
| 4 | la01 | pd-HH | 10 x 5 | 666 | 666.0 | 0.0 | 0.0 |
| 5 | ft10 | or-tools | 10 x 10 | 930 | 930 | 0.0 | 14.9 |
| 6 | ft10 | pd-HH | 10 x 10 | 930 | 1082.0 | 16.3 | 0.0 |
| 7 | swv01 | or-tools | 20 x 10 | 1407 | 1432 | 1.8 | 300.3 |
| 8 | swv01 | pd-HH | 20 x 10 | 1407 | 1839.0 | 30.7 | 0.2 |
| 9 | la38 | or-tools | 15 x 15 | 1196 | 1196 | 0.0 | 300.3 |
| 10 | la38 | pd-HH | 15 x 15 | 1196 | 1387.0 | 16.0 | 0.2 |
+----+---------+----------+---------------+---------+----------+---------+-------+
```
Original file line number Diff line number Diff line change
Expand Up @@ -81,19 +81,9 @@ def NewIntervalVar(self, start, size, end, name): pass

## 基于 `jsp_framework` 实现

`OR-Tools` 的官方文档提供了一个求解作业车间调度问题的 [完整案例](https://developers.google.cn/optimization/scheduling/job_shop),本文将基于本系列的 `jsp_framework` 实现。
`OR-Tools` 的官方文档提供了一个求解作业车间调度问题的 [完整案例](https://developers.google.cn/optimization/scheduling/job_shop),本文将基于本系列的 `jsp_framework` 实现。参考 [Python建模](2021-08-14-作业车间调度问题求解框架:Python建模.md) 部分,基本流程为:

- 基本流程(参考 [Python建模](2021-08-14-作业车间调度问题求解框架:Python建模.md) 部分):

自定义一个继承自 `JSSolver``GoogleORCPSolver` 类,然后重点实现 `do_solve()` 方法。


- 完整代码参考:

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

其中,
自定义一个继承自 `JSSolver``GoogleORCPSolver` 类,然后重点实现 `do_solve()` 方法。完整代码参考[ortools.py](https://github.com/dothinking/jsp_framework/blob/master/jsp/solver/ortools.py)。其中,

- 为了避免求解规模太大而失去响应,人为设定运行时间的上限:

Expand All @@ -105,14 +95,13 @@ def NewIntervalVar(self, start, size, end, name): pass
- 为了实时输出当前最优解,定义如下执行回调函数的类:

```python

class VarArraySolutionPrinter(cp_model.CpSolverSolutionCallback):
'''Output intermediate solutions.'''
def __init__(self, variables:dict, problem:JSProblem, solution:JSSolution):
def __init__(self, variables:dict, solver:JSSolver, solution:JSSolution):
'''Initialize with variable map: operation step -> OR-Tools variable.'''
cp_model.CpSolverSolutionCallback.__init__(self)
self.__variables = variables
self.__problem = problem
self.__solver = solver
self.__solution = solution

def on_solution_callback(self):
Expand All @@ -122,7 +111,7 @@ def NewIntervalVar(self, start, size, end, name): pass
op.update_start_time(self.Value(var.start))

# update solution
self.__problem.update_solution(self.__solution)
self.__solver.update_solution(self.__solution)
```

相应地,将普通的求解方式:
Expand All @@ -144,8 +133,8 @@ def NewIntervalVar(self, start, size, end, name): pass

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

# problems
names = ['ft06', 'la01', 'ft10', 'swv01', 'la38', \
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ class PriorityDispatchSolver(JSSolver):
'''General Priority Dispatching Solver.'''

def do_solve(self, problem: JSProblem):
solution = JSSolution(problem=problem)
solution = JSSolution(problem=problem, direct_mode=False)
self.solving_iteration(solution=solution)
problem.update_solution(solution=solution)
self.update_solution(solution=solution)


def solving_iteration(self, solution:JSSolution):
Expand All @@ -50,19 +50,17 @@ class PriorityDispatchSolver(JSSolver):

# dispatch operation by priority
while head_ops:
# sort by priority
head_ops.sort(key=lambda op: self.__dispatching_rule(op, solution))

# dispatch operation with the first priority
op = head_ops[0]
op = min(head_ops, key=lambda op: self.__dispatching_rule(op, solution))
solution.dispatch(op)

# update imminent operations
pos = head_ops.index(op)
next_job_op = op.next_job_op
if next_job_op is None:
head_ops = head_ops[1:]
head_ops = head_ops[0:pos] + head_ops[pos+1:]
else:
head_ops[0] = next_job_op
head_ops[pos] = next_job_op
```

其中,定义规则的函数签名为:
Expand Down Expand Up @@ -143,8 +141,8 @@ $$z_{i,j} = \left (t_{i,j}+w_{i,j} \uparrow, \,\, r_{i,j}-1.5\,p_{i,j} \downarro


```python
from jsp_fwk import (JSProblem, BenchMark)
from jsp_fwk.solver import PriorityDispatchSolver
from jsp import (JSProblem, BenchMark)
from jsp.solver import PriorityDispatchSolver

# create problem from benchmark
names = ['ft06', 'la01', 'ft10', 'swv01', 'la38', 'ta24', \
Expand Down
Loading

0 comments on commit 0cbabee

Please sign in to comment.