
本文详解如何在 OR-Tools CP-SAT 求解器中,对需跨多个班次(如早/中/夜班)或周末停机时段的长工件工序,实现不可分割、严格连续的排程——避免被其他任务插入中断,并兼顾日历约束(如班次间隙、周末停工)。
本文详解如何在 or-tools cp-sat 求解器中,对需跨多个班次(如早/中/夜班)或周末停机时段的长工件工序,实现**不可分割、严格连续**的排程——避免被其他任务插入中断,并兼顾日历约束(如班次间隙、周末停工)。
在实际制造调度中,一个典型挑战是:某道关键工序(如大型铸件热处理、精密真空镀膜)耗时长达16小时以上,必须连续运行,不能因班次交接(如22:00–06:00夜班结束)或周末停机而中断。若简单将其拆分为多个短任务并用 OnlyEnforceIf 强制首尾衔接,如提问者所遇,求解器仍可能在逻辑间隙中插入其他作业——因为 OnlyEnforceIf 是条件约束(soft),而非硬性拓扑强制;且多段拆分后,各子任务间缺乏“原子性”语义,易受日历空档干扰。
✅ 正确解法:使用 IntervalVar 的动态长度 + 日历感知建模
Google OR-Tools 官方文档明确推荐:不主动拆分长任务,而是定义一个能“跨越日历断点”的单区间变量(IntervalVar),其长度自动适配实际可用工作时间。核心在于利用 model.NewOptionalIntervalVar() 配合 model.AddNoOverlap() 和日历约束,让求解器自主计算该任务在非工作时段(如周末、班次间隔)的“自然暂停”,但逻辑上保持连续——即:任务开始后,仅在机器可用时段推进,总加工时长不变,且中途绝不允许其他任务抢占同一机器。
▶ 关键代码实现(CP-SAT Python)
from ortools.sat.python import cp_model
model = cp_model.CpModel()
# 假设:machine_id = 0 的机床采用两班制(22:00–06:00 + 周末停工)
# 定义其工作日历:以分钟为单位,例如 [ (0, 1440), (1560, 2880) ] 表示周一00:00–24:00中仅13:00–24:00可用(示例)
work_intervals = [
(0, 1440), # 周一 00:00–24:00 全天(可选)
(1560, 2880), # 周一 13:00–24:00(夜班时段)
# ... 更多班次与日期区间
]
# 创建「日历感知」的区间变量:start, end, size 均为变量,size = 总加工时长(固定)
task_duration = 960 # 16小时 = 960分钟(不可拆分的物理约束)
start_var = model.NewIntVar(0, horizon, 'task_start')
end_var = model.NewIntVar(0, horizon, 'task_end')
interval = model.NewIntervalVar(start_var, task_duration, end_var, 'long_task_0')
# 【核心】添加日历约束:任务只能在 work_intervals 内执行
# 方法:为每个不可用时段 [unavail_start, unavail_end],添加“任务不得完全落入其中”的约束
for unavail_start, unavail_end in get_unavailable_intervals(work_intervals, horizon):
# 任务要么在不可用时段前结束,要么在其后开始
model.Add(end_var <= unavail_start).OnlyEnforceIf(
model.NewBoolVar(f'before_{unavail_start}')
)
model.Add(start_var >= unavail_end).OnlyEnforceIf(
model.NewBoolVar(f'after_{unavail_end}').Not()
)
# 或更简洁地:使用 AddNoOverlap + 辅助不可用区间(推荐)
# model.AddNoOverlap([interval, model.NewIntervalVar(unavail_start, unavail_end - unavail_start, unavail_end, f'gap_{unavail_start}')])
# 强制该任务在 machine_id=0 上执行(绑定资源)
machine_intervals = machine_to_intervals[0]
model.AddNoOverlap(machine_intervals + [interval]) # 将长任务加入该机所有任务区间集合
# 目标:最小化最大完工时间(makespan)
model.Minimize(end_var)? 为什么优于拆分+OnlyEnforceIf?
- ✅ 语义正确:单个 IntervalVar 天然表达“一个不可中断的物理过程”,求解器理解其原子性;
- ✅ 日历自适应:无需手动枚举所有班次间隙,通过区间约束自动规避非工作时段;
- ✅ 求解鲁棒:避免因布尔变量爆炸导致 OnlyEnforceIf 约束松弛失效;
- ✅ 目标清晰:直接优化 end_var,而非间接最小化首尾距离 spltMin。
⚠️ 注意事项与最佳实践
- 时间粒度统一:全程使用分钟(或秒) 作为基础单位,避免浮点误差与整数截断;
- 日历预处理:将班次表(如 [22:00, 06:00])和节假日转换为绝对时间戳区间(如 1672531200 → 1672552800),再生成 unavailable_intervals;
- 避免 AddCircuit 误用:AddCircuit 适用于路径规划(如TSP),不适用于单机任务序列约束;此处应坚持 AddNoOverlap + 区间建模;
- 性能调优:对超长任务(>48h),可设置 model.Add(end_var - start_var == task_duration) 替代 NewIntervalVar(..., size, ...),提升数值稳定性;
- 验证连续性:求解后检查 solution.value(end_var) - solution.value(start_var) 是否恒等于 task_duration —— 若成立,则证明无隐式中断。
通过这一建模范式,您不再需要“把大象塞进冰箱分三步”,而是直接为大象定制一间带休息室的专属房间:它何时进门、何时出门,由调度逻辑决定;但只要进门,就必须待满16小时,期间任何人不得打扰——这正是智能制造中高价值工序调度的工程本质。

















