SchedulingModel¶
工业调度需要在有限时间范围内回答一组相互耦合的问题:哪些工序需要执行,工序由哪台设备 完成,何时开始和结束,共享人员或工具是否足够,工艺顺序是否允许,以及最终方案如何在交付、 产能和切换成本之间取得平衡。
SchedulingModel 将这些问题组织为 Task、Alternative、Resource 和 active sequence 等标准调度
结构。它是 ModelBuilder 上的领域化入口,而不是另一套求解模型;创建任务、执行方案或约束时,
对应节点会直接进入同一个表达式图。普通数值关系仍由 ModelBuilder 表达,调度接口只负责跨对象
的不变量和求解器需要识别的结构。
本文按照一个生产计划从现场事实到数学模型的形成过程介绍接口。时间统一使用调用方选择的离散
单位,例如分钟或秒;horizon 表示本次计划覆盖的有限时间范围。
各章节代码用于分别说明一种建模机制,沿用相同变量名以保持场景连续,但不应直接拼接执行;第 9 节给出可以整体运行的模型骨架。
1. 确定计划范围与生产资源¶
考虑一个机加工车间:订单可以在两台加工中心中的任意一台生产,但每台设备同一时刻只能加工 一个工件;设备运行还需要共享操作人员。这里存在两类不同的资源约束:
- 加工中心是 unary resource,容量语义是同一时刻至多执行一个 interval;
- 操作人员是 cumulative resource,容量语义是同一时刻所有需求量之和不能超过班组人数。
先创建共享 Builder、计划范围和资源:
from optagent import ModelBuilder, SchedulingModel
builder = ModelBuilder(metadata={"plant": "machining-shop"})
schedule = SchedulingModel(builder, horizon=7 * 24 * 60)
machine_a = schedule.unary_resource("machine-a", attrs={"line": "north"})
machine_b = schedule.unary_resource("machine-b", attrs={"line": "south"})
crew = schedule.cumulative_resource("operators", capacity=3)
| 接口 | 工业语义 |
|---|---|
unary_resource(name, attrs=None) |
创建不可并行占用的设备、工位或专用工具。 |
cumulative_resource(name, capacity, attrs=None) |
创建可按数量共享的人员、能源或工具池。 |
resource(name) |
严格查找已登记资源;未知名称直接报错。 |
资源必须显式创建。系统不会根据字符串自动生成资源,也不会从 attrs 推断容量或业务规则。
Resource 名称在同一模型中唯一,schedule.resources 是只读 tuple。
2. 将生产工序表示为 Task 和 Alternative¶
生产订单中的“加工零件 J-100”是一项业务工作,但它并不天然对应唯一 interval。若工序可以选择 不同设备,不同设备上的加工时长也可能不同。因此 DSL 将业务工作和执行方式分开:
Task表示必须被计划或允许被放弃的一项业务工作;Alternative表示该工作在某个主资源上的一种具体执行方式,并拥有 interval、presence 和 duration;requirements表示执行该方案时同时消耗的 cumulative resources。
operation = schedule.task("J-100/machining", attrs={"order": "J-100"})
operation.alternative(
resource=machine_a,
duration=80,
requirements={crew: 2},
attrs={"routing": "A"},
)
operation.alternative(
resource=machine_b,
duration=110,
requirements={crew: 1},
attrs={"routing": "B"},
)
operation.exactly_one_alternative()
| Task 接口 | 建模作用 |
|---|---|
task(name, attrs=None) |
创建唯一命名的业务工序。 |
alternative(resource, duration, optional=True, requirements=None, attrs=None, start=0, lb_start=None, ub_start=None) |
创建一种设备、时长和附加容量需求均已确定的执行方式。 |
exactly_one_alternative(name=None) |
要求多种执行方式中恰好选择一种。 |
at_most_one_alternative(name=None) |
允许整项工作不执行;执行时至多选择一种方式。 |
start() / end() / duration() |
返回所选 Alternative 的时间投影,用于后续约束和目标。 |
单设备且必须执行的工序可以直接创建 mandatory Alternative:
inspection = schedule.task("J-100/inspection")
inspection.alternative(resource=machine_b, duration=20, optional=False)
多方案 Task 通常保留各 Alternative 的默认 optional=True,再由 Task 的选择策略决定整项工作
是否必须出现。Alternative.resource 只表示一个主 unary resource;额外需求只接受 cumulative
resources。需要同时占用多台专用设备的关系,应在业务模型中显式构造,而不是把多个主资源压缩
进一个 Alternative。
attrs 用于保存订单号、工艺路线等追踪信息,不参与约束正确性。schedule.tasks、
task.alternatives 和 resource.alternatives() 都是只读 tuple;结构发生变化时应重建模型。
3. 物化设备互斥与共享容量¶
创建 Alternative 只说明“工序可以使用某项资源”,尚未声明同一资源上的并发规则。完成资源 分配后,需要显式物化资源约束:
| Resource 接口 | 建模作用 |
|---|---|
no_overlap(name=None) |
禁止 unary resource 上 present intervals 发生时间重叠。 |
cumulative(name=None) |
约束 cumulative resource 上同时发生的需求总量不超过 capacity。 |
将两种结构分开可以保留清晰的量纲:机器互斥是 interval 关系,人员需求是容量关系。任务重量、
订单数量、设备负荷或能耗不应被解释为通用 resource demand;这些业务量需要使用
ModelBuilder 表达式显式建模。schedule.validate() 会报告遗漏的资源约束。
4. 表达工艺先后与时间窗口¶
同一订单通常包含多道工序。热处理必须在机加工完成后开始,转运可能要求固定等待时间,原料到厂 前不能投产。对这类时间关系,Task 投影会自动跟随实际选中的 Alternative:
machining = schedule.task("J-200/machining")
machining.alternative(resource=machine_a, duration=60, optional=False)
heat_treatment = schedule.task("J-200/heat-treatment")
heat_treatment.alternative(resource=machine_b, duration=90, optional=False)
machining.release_at(120)
schedule.precedence(machining, heat_treatment, lag=15)
| 接口 | 工业语义 |
|---|---|
task.release_at(release, name=None) |
原料、图纸或前置条件就绪后才能开始;生成硬约束 start >= release。 |
precedence(before, after, lag=0) |
后工序必须在前工序结束并经过最小间隔后开始。 |
chain(tasks, lag=0) |
对已知工艺路线中的相邻工序批量建立 precedence。 |
precedence(...) 接受 Task 或 Alternative,并对 optional presence 保持一致语义。chain(...) 适合
线性工艺路线;分支、汇合或返工关系应分别声明 precedence,以免隐藏实际工艺图。
5. 区分承诺交期与不可突破的截止时间¶
工业计划中的日期不总是同一种约束。客户承诺日通常允许迟交但会产生代价;法规窗口、船期截点或 强制维护前的完工时间则不可违反。DSL 因而区分:
| Task 接口 | 建模作用 |
|---|---|
due_at(due) |
保存软交期,不自动生成硬约束。 |
deadline_at(deadline, name=None) |
生成硬约束 end <= deadline。 |
tardiness(due=None) |
返回 max(end - due, 0)。 |
earliness(due=None) |
返回 max(due - end, 0)。 |
软交期只有进入目标或其他表达式后才影响方案。以下 helpers 汇总常见计划指标,但只返回普通 Expr, 不会代替调用方决定优化方向或权重:
| SchedulingModel 接口 | 指标定义 |
|---|---|
makespan(tasks=None) |
指定任务集合的最大完工时间。 |
total_tardiness(tasks=None) |
汇总各任务的正迟交量。 |
total_earliness(tasks=None) |
汇总各任务的正提前量。 |
默认 tardiness/earliness 汇总要求每个输入 Task 已调用 due_at(...)。如果交期来自局部表达式,可用
task.tardiness(due=...) 或 task.earliness(due=...) 显式覆盖。多个业务目标应在 Builder 层按
明确量纲和权重组合;SchedulingModel 不定义私有的多目标策略。
6. 纳入已知占用与设备日历¶
实际设备并非在整个 horizon 内持续可用。已下达且不可移动的作业会占据固定区间,预防性维护、 停电或停机窗口也必须参与资源互斥,否则计划可能落入实际上不可生产的时段。
schedule.fixed_interval(
"machine-a/frozen-order",
resource=machine_a,
start=0,
duration=45,
attrs={"source": "released-plan"},
)
schedule.calendar(
machine_a,
unavailable=[(480, 540), (960, 1080)],
name="machine-a-maintenance",
)
| 接口 | 建模作用 |
|---|---|
fixed_interval(name, resource, start, duration, attrs=None) |
创建起止位置不可移动的资源占用。 |
calendar(resource, unavailable, name="calendar") |
将 horizon 内有限的不可用窗口物化为 fixed intervals。 |
calendar 窗口采用 (start, end),并与普通 Alternative 一起进入资源的 no_overlap()。当前接口
不展开循环班次、强度曲线,也不表示工序在停机期间暂停后继续;这类规则应先转换为本次 horizon
内的明确窗口,或由业务层建立更具体的模型。
固定占用和 calendar item 不是业务 Task,不进入 schedule.tasks,但会在资源序列和
diagnostics() 的 sequence_items 中保留可观测身份。
7. 表达顺序相关的工艺规则¶
在涂装、炼钢、轧制、清洗和配方生产中,仅决定每项工序的开始时间还不够。相邻工件的材质、颜色 或规格会决定切换时间、切换成本,某些相邻组合甚至完全禁止。由于 optional 工序和未选 Alternative 不应出现在实际加工序列中,这些规则建立在 active sequence 上。
资源的所有 Alternative 和 sequence items 创建完成后,再取得稳定基础序列并建立 view:
lot_a = schedule.task("lot-a")
lot_a_run = lot_a.alternative(resource=machine_a, duration=40, optional=False)
lot_b = schedule.task("lot-b")
lot_b_run = lot_b.alternative(resource=machine_a, duration=55, optional=False)
cleaning = schedule.reset_item(
"machine-a/cleaning",
resource=machine_a,
duration=20,
optional=True,
)
view = machine_a.sequence().view(
items=[lot_a_run, cleaning, lot_b_run],
name="grade-sequence",
)
machine_a.no_overlap()
首次调用 resource.sequence() 后,该资源的 item identity 和顺序变量已经物化,不能再向资源添加
Alternative、fixed interval 或 sequence item。view(items=...) 是基础序列的静态投影;运行时
未 present 的 item 会从 active sequence 中消失。
| Sequence 接口 | 建模作用 |
|---|---|
resource.sequence() |
为 unary Resource 物化并返回唯一的稳定基础序列。 |
sequence.view(items=None, name=None) |
创建全量或指定 items 的静态投影;顺序规则只作用于该 view 的 active items。 |
7.1 相邻关系、允许路径与切换成本¶
item_types = {
lot_a_run: "grade-a",
cleaning: "cleaning",
lot_b_run: "grade-b",
}
transition = view.transition(
item_types=item_types,
transitions={
("grade-a", "cleaning"): 0,
("cleaning", "grade-b"): 0,
},
default_cost=10,
name="grade-change",
)
builder.constraint(transition.valid, name="allowed-grade-transitions")
| ResourceSequenceView 接口 | 建模作用 |
|---|---|
successor(before, after) |
返回两个 present items 是否在 active sequence 中直接相邻。 |
transition(item_types, transitions, ...) |
返回同一类型转换图上的 SequenceTransition(cost, valid)。 |
cost 可进入切换损失目标,valid 表示 active sequence 是否只使用已声明边。transition() 不会
隐式把 valid 登记为硬约束,因为工业模型可能只需要对未列出的转换计罚,而不是禁止它们。需要
限制路径时,应像示例一样显式调用 builder.constraint(transition.valid, ...)。
7.2 顺序相关准备时间¶
若规格切换不仅产生成本,还要求后一道工序延后开始,应使用 setup time:
view.setup_time(
item_types=item_types,
transitions={
("grade-a", "cleaning"): 5,
("cleaning", "grade-b"): 10,
},
name="grade-setup",
)
setup_time(...) 登记相邻 interval 之间的最小 setup gap,因此它是可行性语义,而不是单纯的
评分项。类型映射必须是静态数据;动态工艺状态应通过普通表达式或 sequence dimension 建模。
| ResourceSequenceView 接口 | 建模作用 |
|---|---|
setup_time(item_types, transitions, start_type=None, start_transitions=None, name=None) |
根据相邻 item 类型登记最小准备时间约束。 |
7.3 沿加工顺序累计和重置状态¶
有些资源约束取决于“自上次清洗以来”的累计加工量,例如轧辊里程、刀具寿命、炉次重量或连续
生产时长。dimension(...) 沿 active view 维护标量前缀状态,并可由 reset item 或类型转换重置:
usage = view.dimension(
contribution={lot_a_run: 40, cleaning: 0, lot_b_run: 55},
initial_value=0,
reset_items=[cleaning],
upper_bound=100,
name="tool-usage",
)
| ResourceSequenceView 接口 | 建模作用 |
|---|---|
dimension(contribution, initial_value=0, reset_items=(), reset_transitions=(), item_types=None, lower_bound=None, upper_bound=None, name=None) |
创建沿 active sequence 累计、受界并可重置的标量状态。 |
SequenceDimension.before、after、segment_index、segment_total、is_segment_start 和
is_segment_end 都是普通 DAG Expr,可继续进入约束、目标和结果求值。
| SchedulingModel 接口 | 建模作用 |
|---|---|
sequence_item(name, resource, duration=0, optional=True, release=0, ub_start=None, attrs=None) |
创建有序列位置但不属于业务 Task 的辅助 item。 |
reset_item(name, resource, duration=0, optional=True, release=0, ub_start=None, attrs=None) |
创建可供 sequence dimension 使用的显式重置 item。 |
零时长 item 影响顺序但不占据设备时间,正时长 item 同时参与顺序和资源占用。
后端边界
Active sequence、setup 和 dimension 当前主要由 heuristic runtime 支持。Direct exact 的逐项 状态以精确求解器接口兼容性为准;后端不能保持语义时会明确拒绝, 不会忽略节点或生成近似结果。
8. 在求解前检查模型结构¶
调度模型往往由订单、路线、日历和资源主数据共同生成。局部对象创建成功并不意味着全局结构已经 闭合,例如多方案 Task 可能遗漏选择策略,设备可能遗漏 no-overlap。完成构造后应统一检查:
| 接口 | 作用 |
|---|---|
validate() |
汇总检查未完成的 Task selection 和尚未物化的资源约束。 |
diagnostics() |
返回领域对象、资源约束和 Builder node IDs 的观测映射。 |
validate() 不编译、不创建节点,也不返回 Builder。它验证的是 facade 能负责的结构一致性,不能
替代具体后端的能力检查。重名、非法 requirements、跨模型 Resource 以及 sequence 物化后新增
item 会在对应创建调用中立即失败。diagnostics() 用于审计模型生成结果,不参与 solver
correctness。
9. 完整的柔性工序示例¶
下面的模型同时包含设备选择、共享人员、工艺先后、软交期和设备停机。所有约束和目标最终进入
同一个 Builder,并直接交给 solve(...):
from optagent import ModelBuilder, SchedulingModel, solve
builder = ModelBuilder(metadata={"case": "flexible-routing"})
schedule = SchedulingModel(builder, horizon=600)
machine_a = schedule.unary_resource("machine-a")
machine_b = schedule.unary_resource("machine-b")
crew = schedule.cumulative_resource("operators", capacity=2)
cut = schedule.task("order-17/cut")
cut.due_at(300)
cut.alternative(resource=machine_a, duration=60, requirements={crew: 2})
cut.alternative(resource=machine_b, duration=80, requirements={crew: 1})
cut.exactly_one_alternative()
inspect = schedule.task("order-17/inspect")
inspect.due_at(360)
inspect.alternative(resource=machine_b, duration=30, optional=False, requirements={crew: 1})
schedule.precedence(cut, inspect, lag=10)
schedule.calendar(machine_a, unavailable=[(180, 240)], name="maintenance")
machine_a.no_overlap()
machine_b.no_overlap()
crew.cumulative()
builder.minimize(schedule.total_tardiness(), name="total-tardiness")
schedule.validate()
solution = solve(builder, time_limit_s=10)
10. 对象关系与职责边界¶
前述建模过程形成以下所有权关系:
| 对象 | 职责 |
|---|---|
SchedulingModel |
绑定 Builder、有限 horizon、领域注册表和结构校验。 |
Task |
表示业务工作,拥有一个或多个 Alternative,但不直接拥有 interval。 |
Alternative |
表示具体执行方式,拥有 interval、presence、duration、主 Resource 和 cumulative requirements。 |
Resource |
表示 unary 或 cumulative 生产能力,并物化对应资源约束。 |
ResourceSequence |
保存 unary Resource 上稳定的 item identity 和基础顺序。 |
ResourceSequenceView |
在基础顺序上定义静态 item 投影和 active sequence 语义。 |
SequenceTransition |
提供同一类型转换图的 cost 与 valid 表达式。 |
SequenceDimension |
表示沿 active view 累计、受界和重置的标量状态。 |
这一区分保持了接口的组合性:业务 Task 不等于 interval,设备选择属于 Alternative,资源可行性 属于 Resource,顺序相关规则属于 ResourceSequenceView,目标登记仍属于 ModelBuilder。