如何在OR-Tools中基于机器任务顺序设置任务时长?
柔性作业车间调度中动态设置任务时长的OR-Tools解决方案
问题描述
需为柔性作业车间调度设置任务时长规则:
- 当任务是机器上的首个任务、同机器上前序任务ID不同、或任务开始前机器有空闲时,任务时长设为
max_duration(基础时长+惩罚值) - 其余情况时长设为
min_duration(基础时长)
原有实现的核心问题:
- 依据输入数据中的任务顺序而非机器实际调度顺序判断前序任务
- 直接用Python等式判断
previous_end == start,而非OR-Tools约束逻辑,导致条件不生效
解决方案思路
使用OR-Tools的Sequence约束跟踪每台机器上的任务调度顺序,通过该约束获取任务间的相邻关系,进而判断触发max_duration的三个条件:
- 任务是机器的首个任务
- 与机器上的前序任务ID不同
- 当前任务开始时间晚于前序任务结束时间(机器有空闲)
修改后的完整代码
import collections from ortools.sat.python import cp_model class SolutionPrinter(cp_model.CpSolverSolutionCallback): def __init__(self): cp_model.CpSolverSolutionCallback.__init__(self) self.solution_count = 0 def on_solution_callback(self): self.solution_count += 1 print(f"Solution {self.solution_count}:") print(f" Makespan: {self.Value(self.model().GetObjectiveVar())}") def flexible_jobshop(): """Solve a flexible jobshop problem with dynamic duration rules.""" # Data part jobs = [ # task = (id, time per single item, priority, machine id options, start time min, start time max) [ # Job 0 ["gga5", 3, 1, ["a", "c"], 10], ["gga6", 2, 2, ["b", "c"]], ["gga6", 2, 2, ["b", "c"]], ["gga6", 2, 2, ["b", "c"]], ["gga6", 2, 2, ["b", "c"]], ["gga6", 2, 2, ["b", "c"]], ["gga6", 2, 2, ["b", "c"]], ["gga6", 2, 2, ["b", "c"]], ["gga6", 2, 2, ["b", "c"]] ], [ # Job 1 ["h4jk", 4, 1, ["a", "b", "c"], 8, 12], ["h4jl", 1, 1, ["a"]], ["h4jl", 1, 1, ["a"]], ["h4jl", 1, 1, ["a"]], ["h4jl", 1, 1, ["a"]], ["h4jl", 1, 1, ["a"]], ["h4jl", 1, 1, ["a"]], ["h4jl", 1, 1, ["a"]], ["h4jl", 1, 1, ["a"]] ], [ # Job 2 ["p0o1", 2, 1, ["a", "b"], 0, 10], ["p0o2", 3, -1, ["a", "b", "c"]], ["p0o2", 3, -1, ["a", "b", "c"]], ["p0o2", 3, -1, ["a", "b", "c"]], ["p0o2", 3, -1, ["a", "b", "c"]], ["p0o2", 3, -1, ["a", "b", "c"]], ["p0o2", 3, -1, ["a", "b", "c"]], ["p0o2", 3, -1, ["a", "b", "c"]], ["p0o2", 3, -1, ["a", "b", "c"]], ["p0o2", 3, -1, ["a", "b", "c"]] ] ] ID_ROBOT = 0 PROCESSING_TIME = 1 PRIORITA = 2 MACHINE_ID = 3 START_TIME_MIN = 4 START_TIME_MAX = 5 TIME_PENALTY = 1 num_jobs = len(jobs) all_jobs = range(num_jobs) list_machines = set([alt for job in jobs for task in job for alt in task[MACHINE_ID]]) num_machines = len(list_machines) # Model initialization model = cp_model.CpModel() # Calculate horizon horizon = 0 for job in jobs: for task in job: max_task_duration = task[PROCESSING_TIME] + TIME_PENALTY horizon += max_task_duration print('Horizon = %i' % horizon) # Global storage of variables intervals_per_resources = collections.defaultdict(list) task_info_per_machine = collections.defaultdict(list) starts = {} # (job_id, task_id) -> start var durations = {} # (job_id, task_id) -> duration var presences = {} # (job_id, task_id, alt_id) -> presence var robot_ids = {} # (job_id, task_id) -> robot id job_ends = [] # Scan jobs and create variables/intervals for job_id in all_jobs: job = jobs[job_id] num_tasks = len(job) previous_end = None for task_id in range(num_tasks): task = job[task_id] robot_id = task[ID_ROBOT] min_duration = task[PROCESSING_TIME] max_duration = min_duration + TIME_PENALTY num_alternatives = len(task[MACHINE_ID]) all_alternatives = range(num_alternatives) # Create main task variables suffix_name = '_j%i_t%i' % (job_id, task_id) start = model.NewIntVar(0, horizon, 'start' + suffix_name) duration = model.NewIntVar(min_duration, max_duration, 'duration' + suffix_name) end = model.NewIntVar(0, horizon, 'end' + suffix_name) interval = model.NewIntervalVar(start, duration, end, 'interval' + suffix_name) # Store variables starts[(job_id, task_id)] = start durations[(job_id, task_id)] = duration robot_ids[(job_id, task_id)] = robot_id # Job precedence constraint (same job tasks must be sequential) if previous_end is not None: model.Add(start >= previous_end) previous_end = end # Start/End time constraints try: start_limit = task[START_TIME_MIN] model.Add(start >= start_limit) except IndexError: print(f"For {robot_id} there is no minimum start time") try: end_limit = task[START_TIME_MAX] model.Add(start <= end_limit) except IndexError: print(f"For {robot_id} there is no maximum start time") # Handle alternative machines if num_alternatives > 1: l_presences = [] for alt_id in all_alternatives: alt_suffix = '_j%i_t%i_a%i' % (job_id, task_id, alt_id) l_presence = model.NewBoolVar('presence' + alt_suffix) l_start = model.NewIntVar(0, horizon, 'start' + alt_suffix) l_duration = model.NewIntVar(min_duration, max_duration, 'duration' + alt_suffix) l_end = model.NewIntVar(0, horizon, 'end' + alt_suffix) l_interval = model.NewOptionalIntervalVar( l_start, l_duration, l_end, l_presence, 'interval' + alt_suffix) l_presences.append(l_presence) # Link global variables to local ones model.Add(start == l_start).OnlyEnforceIf(l_presence) model.Add(duration == l_duration).OnlyEnforceIf(l_presence) model.Add(end == l_end).OnlyEnforceIf(l_presence) # Add to machine's interval list and task info intervals_per_resources[task[MACHINE_ID][alt_id]].append(l_interval) task_info_per_machine[task[MACHINE_ID][alt_id]].append( (l_start, l_duration, l_end, job_id, task_id, l_presence) ) presences[(job_id, task_id, alt_id)] = l_presence # Select exactly one alternative model.AddExactlyOne(l_presences) else: machine = task[MACHINE_ID][0] intervals_per_resources[machine].append(interval) task_info_per_machine[machine].append( (start, duration, end, job_id, task_id, model.NewConstant(1)) ) presences[(job_id, task_id, 0)] = model.NewConstant(1) job_ends.append(previous_end) # -------------------------- # Add dynamic duration constraints using Sequence constraints # -------------------------- for machine in list_machines: intervals = intervals_per_resources[machine] task_info_list = task_info_per_machine[machine] num_tasks_on_machine = len(intervals) if num_tasks_on_machine <= 1: # If only one task, it must use max_duration if num_tasks_on_machine == 1: start_i, duration_i, end_i, job_id_i, task_id_i, presence_i = task_info_list[0] min_dur = jobs[job_id_i][task_id_i][PROCESSING_TIME] max_dur = min_dur + TIME_PENALTY model.Add(duration_i == max_dur) continue # Create sequence constraint with position variables positions = [model.NewIntVar(0, num_tasks_on_machine-1, f'{machine}_pos_{i}') for i in range(num_tasks_on_machine)] sequence = model.NewSequence(intervals, positions) # For each task i on the machine for i in range(num_tasks_on_machine): start_i, duration_i, end_i, job_id_i, task_id_i, presence_i = task_info_list[i] robot_i = robot_ids[(job_id_i, task_id_i)] min_dur = jobs[job_id_i][task_id_i][PROCESSING_TIME] max_dur = min_dur + TIME_PENALTY # Variable to track if we need max duration use_max = model.NewBoolVar(f'{machine}_j{job_id_i}_t{task_id_i}_use_max') # Condition 1: Task is first on the machine is_first = model.NewBoolVar(f'{machine}_j{job_id_i}_t{task_id_i}_is_first') model.AddIsFirst(sequence, intervals[i]).OnlyEnforceIf(is_first) model.AddIsNotFirst(sequence, intervals[i]).OnlyEnforceIf(is_first.Not()) # If first, use max duration model.Add(use_max == 1).OnlyEnforceIf(is_first) # Check all other tasks j to see if they are immediately before i for j in range(num_tasks_on_machine): if i == j: continue start_j, duration_j, end_j, job_id_j, task_id_j, presence_j = task_info_list[j] robot_j = robot_ids[(job_id_j, task_id_j)] # Bool var: j is immediately before i j_before_i = model.NewBoolVar(f'{machine}_j{job_id_j}_t{task_id_j}_before_j{job_id_i}_t{task_id_i}') model.AddSequenceIsNext(sequence, intervals[j], intervals[i]).OnlyEnforceIf(j_before_i) # Condition 2: Robot IDs are different robot_diff = model.NewBoolVar(f'{machine}_j{job_id_j}_t{task_id_j}_diff_j{job_id_i}_t{task_id_i}') model.Add(robot_j != robot_i).OnlyEnforceIf(j_before_i).OnlyEnforceIf(robot_diff) model.Add(robot_j == robot_i).OnlyEnforceIf(j_before_i).OnlyEnforceIf(robot_diff.Not()) # Condition 3: There is idle time between j and i (start_i > end_j) has_idle = model.NewBoolVar(f'{machine}_j{job_id_j}_t{task_id_j}_idle_before_j{job_id_i}_t{task_id_i}') model.Add(start_i > end_j).OnlyEnforceIf(j_before_i).OnlyEnforceIf(has_idle) model.Add(start_i == end_j).OnlyEnforceIf(j_before_i).OnlyEnforceIf(has_idle.Not()) # If j is before i and either condition is true, use max duration model.Add(use_max == 1).OnlyEnforceIf(j_before_i, robot_diff) model.Add(use_max == 1).OnlyEnforceIf(j_before_i, has_idle) # Set duration based on use_max model.Add(duration_i == max_dur).OnlyEnforceIf(use_max) model.Add(duration_i == min_dur).OnlyEnforceIf(use_max.Not()) # Add no-overlap constraint for each machine for machine_id in list_machines: intervals = intervals_per_resources[machine_id] if len(intervals) > 1: model.AddNoOverlap(intervals) # Makespan objective makespan = model.NewIntVar(0, horizon, 'makespan') model.AddMaxEquality(makespan, job_ends) model.Minimize(makespan) # Solve model solver = cp_model.CpSolver() solution_printer = SolutionPrinter() status = solver.Solve(model, solution_printer) print('\nFinal Solution:') for job_id in all_jobs: print(f'Job {job_id}:') for task_id in range(len(jobs[job_id])): start_value = solver.Value(starts[(job_id, task_id)]) duration_value = solver.Value(durations[(job_id, task_id)]) end_value = start_value + duration_value robot = robot_ids[(job_id, task_id)] machine = None selected_alt = -1 for alt_id in range(len(jobs[job_id][task_id][MACHINE_ID])): if solver.Value(presences[(job_id, task_id, alt_id)]): machine = jobs[job_id][task_id][MACHINE_ID][alt_id] selected_alt = alt_id print( f' Task {task_id} (Robot: {robot}) starts at {start_value}, duration {duration_value}, ends at {end_value} (Alt {selected_alt}, Machine {machine})' ) print(f'\nSolve status: {solver.StatusName(status)}') print(f'Optimal objective value: {solver.ObjectiveValue()}') print('Statistics') print(f' - conflicts : {solver.NumConflicts()}') print(f' - branches : {solver.NumBranches()}') print(f' - wall time : {solver.WallTime()} s') flexible_jobshop()
关键逻辑说明
- Sequence约束的使用:为每台机器创建
Sequence约束,跟踪任务在机器上的实际调度顺序,通过AddSequenceIsNext判断任务间的相邻关系。 - 条件判断变量:为每个任务定义布尔变量:
is_first:标记是否为机器上的首个任务robot_diff:标记与前序任务的机器人ID是否不同has_idle:标记前序任务结束后机器是否有空闲
- 动态时长约束:通过
OnlyEnforceIf关联布尔变量与时长取值:- 只要任一触发条件满足,时长设为
max_duration - 所有条件都不满足时,时长设为
min_duration
- 只要任一触发条件满足,时长设为
- 任务信息跟踪:确保每个机器的任务信息包含对应的作业ID、任务ID,以便准确获取机器人ID进行对比。
内容的提问来源于stack exchange,提问作者Karolína Štollová
相关产品推荐
相关产品推荐

