You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在OR-Tools中基于机器任务顺序设置任务时长?

柔性作业车间调度中动态设置任务时长的OR-Tools解决方案

问题描述

需为柔性作业车间调度设置任务时长规则:

  • 当任务是机器上的首个任务、同机器上前序任务ID不同、或任务开始前机器有空闲时,任务时长设为max_duration(基础时长+惩罚值)
  • 其余情况时长设为min_duration(基础时长)

原有实现的核心问题:

  1. 依据输入数据中的任务顺序而非机器实际调度顺序判断前序任务
  2. 直接用Python等式判断previous_end == start,而非OR-Tools约束逻辑,导致条件不生效

解决方案思路

使用OR-Tools的Sequence约束跟踪每台机器上的任务调度顺序,通过该约束获取任务间的相邻关系,进而判断触发max_duration的三个条件:

  1. 任务是机器的首个任务
  2. 与机器上的前序任务ID不同
  3. 当前任务开始时间晚于前序任务结束时间(机器有空闲)

修改后的完整代码

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()

关键逻辑说明

  1. Sequence约束的使用:为每台机器创建Sequence约束,跟踪任务在机器上的实际调度顺序,通过AddSequenceIsNext判断任务间的相邻关系。
  2. 条件判断变量:为每个任务定义布尔变量:
    • is_first:标记是否为机器上的首个任务
    • robot_diff:标记与前序任务的机器人ID是否不同
    • has_idle:标记前序任务结束后机器是否有空闲
  3. 动态时长约束:通过OnlyEnforceIf关联布尔变量与时长取值:
    • 只要任一触发条件满足,时长设为max_duration
    • 所有条件都不满足时,时长设为min_duration
  4. 任务信息跟踪:确保每个机器的任务信息包含对应的作业ID、任务ID,以便准确获取机器人ID进行对比。

内容的提问来源于stack exchange,提问作者Karolína Štollová

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.12 20:55:16