跳转至
OptAgent 1.2.0

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.taskstask.alternativesresource.alternatives() 都是只读 tuple;结构发生变化时应重建模型。

3. 物化设备互斥与共享容量

创建 Alternative 只说明“工序可以使用某项资源”,尚未声明同一资源上的并发规则。完成资源 分配后,需要显式物化资源约束:

machine_a.no_overlap()
machine_b.no_overlap()
crew.cumulative()
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 因而区分:

heat_treatment.due_at(600)       # 用于迟交或早完目标
heat_treatment.deadline_at(720)  # 必须在该时刻前结束
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) 汇总各任务的正提前量。
builder.minimize(
    schedule.total_tardiness(tasks=[heat_treatment]),
    name="delivery_performance",
)

默认 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.beforeaftersegment_indexsegment_totalis_segment_startis_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。完成构造后应统一检查:

schedule.validate()
diagnostics = schedule.diagnostics()
接口 作用
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。