问题背景
一个常见的调度问题是job shop,即在几台机器上处理多个作业。 每个作业都包含一系列任务,这些任务必须以给定的顺序执行,并且每个任务都必须在特定的机器上进行处理。 例如,作业可以是制造单个消费品,例如汽车。 问题为如何在机器上安排任务,以最大程度地减少调度时间(完成所有作业所花费的时间)。
作业车间问题有几个约束条件:
- 在完成该作业的上一项任务之前,无法启动该作业的下一项任务。
- 一台机器一次只能执行一项任务。
- 任务一旦开始,就必须一直执行直到完成。
问题示例
下面是一个简单的问题示例,其中每个任务用一对数字对(m,p)标记,其中m是处理对应任务的机器的编号,p是任务的处理时间 (作业和机器的编号从0开始)。
在该示例中,作业0具有三个任务。 第一项任务(0,3)指必须在机器0上以3个时间单位进行处理。 第二项任务(1,2)必须在机器1上以2个时间单位处理,依此类推。 总共3个作业8项任务。
问题的一个解决方案
解决job shop问题的一种方法是在满足上述约束的条件下为每个任务分配开始时间。 下图显示了该问题的一种可能解决方案。(3个作业共需要12个时间单位完成,当然这并不是最优的)
定义问题的变量和约束
下面介绍如何设置问题的变量和约束。 首先,让task(i,j)表示作业i的顺序中的第j个任务。 例如,task(0,2)表示作业0的第2个任务,它对应于问题描述中的数字对(1,2)。
接下来,将
t
i
,
j
t_{i,j}
ti,j定义为task(i,j)的开始时间.
t
i
,
j
t_{i,j}
ti,j是作业车间问题中的变量。 解决方案需要确定满足约束的这些变量的值。
该问题涉及两个类型的约束:
- 优先关系约束
具体来说,对于同一作业中的任何两个连续任务,必须先完成第一个任务,然后才能启动第二个任务。 例如,task(0,2)和task(0,3)是作业0的连续任务。由于任务task(0,2)的处理时间为2,因此task(0,3)的开始时间必须在任务2的开始时间之后至少2个时间单位后。得到以下约束:
t 0 , 2 + 2 ≤ t 0 , 3 t_{0,2}+2\leq t_{0,3} t0,2+2≤t0,3 - 资源可用性约束
该限制是由于机器不能同时执行两个任务的限制而引起的。 例如,task(0,2)和task(2,1)都在机器1上处理。由于它们的处理时间分别为2和4,因此必须满足以下约束之一:
t 0 , 2 + 2 ≤ t 2 , 1 ( 如 果 t a s k ( 0 , 2 ) 先 执 行 ) t_{0,2}+2\leq t_{2,1} (如果task(0,2)先执行) t0,2+2≤t2,1(如果task(0,2)先执行) t 2 , 1 + 4 ≤ t 0 , 2 ( 如 果 t a s k ( 2 , 1 ) 先 执 行 ) t_{2,1}+4\leq t_{0,2} (如果task(2,1)先执行) t2,1+4≤t0,2(如果task(2,1)先执行)
问题的目标
job shop问题的目标是最大程度地减少完工时间:从作业的最早开始时间到所有作业结束时间的时间长度。
一个Python求解方法
下面介绍用于求解的几个python模块。
定义模型
# 导入or-tools CP-SAT求解器
from ortools.sat.python import cp_model
def MinimalJobshopSat():
"""Minimal jobshop problem."""
# 创建模型
model = cp_model.CpModel()
定义数据
jobs_data = [ # task = (机器编号, 活动持续时间).
[(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)#机器数量(3台)
all_machines = range(machines_count)#机器编号(0,1,2)
定义变量
# 命名元组用于存储有关已创建变量的信息
task_type = collections.namedtuple('task_type', 'start end interval')
# 命名元组以操纵解决方案信息
assigned_task_type = collections.namedtuple('assigned_task_type',
'start job index duration')
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)#以task_type格式存储到all_task字典里
machine_to_intervals[machine].append(interval_var)#向对应的机器添加时间间隔
对于每个作业和任务,程序使用求解器的NewIntVar方法创建变量:
- start_var: 任务的开始时间
- end_var: 任务的结束时间
start_var和end_var的上界为horizon,即所有任务时间的累加。
接下来,程序使用NewIntervalVar方法为任务创建一个间隔变量(其值为可变时间间隔)。
NewIntervalVar的输入是:
start_var:任务开始时间
duration:任务持续时间
end_var:任务结束时间
‘interval_%i_%i’%(job,task_id)):时间间隔变量的名称
在任何解决方案中,end_var减去start_var必须等于任务持续时间。
定义约束
#机器最大量约束
for machine in all_machines:
model.AddNoOverlap(machine_to_intervals[machine])
#任务先后执行约束
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)
该程序使用求解器的AddNoOverlap方法创建无重叠约束,以防止同一台机器的任务在时间上重叠。接下来,程序添加了优先级约束,以防止同一作业的连续任务在时间上重叠。
定义目标函数
# 完工时间目标。obj_var,其值为所有作业的结束时间的最大值-即总时间。
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)
定义求解器
# 模型求解
solver = cp_model.CpSolver()
status = solver.Solve(model)
结果展示
# 为每台机器创建一个任务分配列表
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]))
# 创建每台机器的输出行
output = ''
for machine in all_machines:
# 按照开始时间排序
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)
# 在输出中添加空格保持列对齐
sol_line_tasks += '%-10s' % name
start = assigned_task.start
duration = assigned_task.duration
sol_tmp = '[%i,%i]' % (start, start + duration)
# 在输出中添加空格保持列对齐
sol_line += '%-10s' % sol_tmp
sol_line += '\n'
sol_line_tasks += '\n'
output += sol_line_tasks
output += sol_line
# 打印求解结果
print('Optimal Schedule Length: %i' % solver.ObjectiveValue())
print(output)
结果为:
Optimal Schedule Length: 11
Optimal Schedule
Machine 0: job_0_0 job_1_0
Machine 1: job_2_0 job_0_1 job_1_2
Machine 2: job_1_1 job_0_2 job_2_1
Task Time Intervals
Machine 0: [0,3] [3,5]
Machine 1: [0,4] [4,6] [7,11]
Machine 2: [5,6] [6,8] [8,11]
完整项目代码
from __future__ import print_function
import collections
# Import Python wrapper for or-tools CP-SAT solver.
from ortools.sat.python import cp_model
def MinimalJobshopSat():
"""Minimal jobshop problem."""
# Create the model.
model = cp_model.CpModel()
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)
MinimalJobshopSat()
英文地址:ortools求解器—调度问题