# SchedulingModel

工业调度需要在有限时间范围内回答一组相互耦合的问题：哪些工序需要执行，工序由哪台设备
完成，何时开始和结束，共享人员或工具是否足够，工艺顺序是否允许，以及最终方案如何在交付、
产能和切换成本之间取得平衡。

`SchedulingModel` 将这些问题组织为 Task、Alternative、Resource 和 active sequence 等标准调度
结构。它是 `ModelBuilder` 上的领域化入口，而不是另一套求解模型；创建任务、执行方案或约束时，
对应节点会直接进入同一个表达式图。普通数值关系仍由 `ModelBuilder` 表达，调度接口只负责跨对象
的不变量和求解器需要识别的结构。

本文按照一个生产计划从现场事实到数学模型的形成过程介绍接口。时间统一使用调用方选择的离散
单位，例如分钟或秒；`horizon` 表示本次计划覆盖的有限时间范围。

各章节代码用于分别说明一种建模机制，沿用相同变量名以保持场景连续，但不应直接拼接执行；第 9
节给出可以整体运行的模型骨架。

## 1. 确定计划范围与生产资源

考虑一个机加工车间：订单可以在两台加工中心中的任意一台生产，但每台设备同一时刻只能加工
一个工件；设备运行还需要共享操作人员。这里存在两类不同的资源约束：

- 加工中心是 unary resource，容量语义是同一时刻至多执行一个 interval；
- 操作人员是 cumulative resource，容量语义是同一时刻所有需求量之和不能超过班组人数。

先创建共享 Builder、计划范围和资源：

```python
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)
```

| 接口 | 工业语义 |
| --- | --- |
| <span id="optagent.SchedulingModel.unary_resource"></span>`unary_resource(name, attrs=None)` | 创建不可并行占用的设备、工位或专用工具。 |
| <span id="optagent.SchedulingModel.cumulative_resource"></span>`cumulative_resource(name, capacity, attrs=None)` | 创建可按数量共享的人员、能源或工具池。 |
| <span id="optagent.SchedulingModel.resource"></span>`resource(name)` | 严格查找已登记资源；未知名称直接报错。 |

资源必须显式创建。系统不会根据字符串自动生成资源，也不会从 `attrs` 推断容量或业务规则。
Resource 名称在同一模型中唯一，`schedule.resources` 是只读 tuple。

<span id="optagent.SchedulingModel.resources"></span>

## 2. 将生产工序表示为 Task 和 Alternative

生产订单中的“加工零件 J-100”是一项业务工作，但它并不天然对应唯一 interval。若工序可以选择
不同设备，不同设备上的加工时长也可能不同。因此 DSL 将业务工作和执行方式分开：

- `Task` 表示必须被计划或允许被放弃的一项业务工作；
- `Alternative` 表示该工作在某个主资源上的一种具体执行方式，并拥有 interval、presence 和
  duration；
- `requirements` 表示执行该方案时同时消耗的 cumulative resources。

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

<span id="optagent.SchedulingModel.task"></span>
<span id="optagent.SchedulingModel.tasks"></span>

| 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：

```python
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 只说明“工序可以使用某项资源”，尚未声明同一资源上的并发规则。完成资源
分配后，需要显式物化资源约束：

```python
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：

```python
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`。 |
| <span id="optagent.SchedulingModel.precedence"></span>`precedence(before, after, lag=0)` | 后工序必须在前工序结束并经过最小间隔后开始。 |
| <span id="optagent.SchedulingModel.chain"></span>`chain(tasks, lag=0)` | 对已知工艺路线中的相邻工序批量建立 precedence。 |

`precedence(...)` 接受 Task 或 Alternative，并对 optional presence 保持一致语义。`chain(...)` 适合
线性工艺路线；分支、汇合或返工关系应分别声明 precedence，以免隐藏实际工艺图。

## 5. 区分承诺交期与不可突破的截止时间

工业计划中的日期不总是同一种约束。客户承诺日通常允许迟交但会产生代价；法规窗口、船期截点或
强制维护前的完工时间则不可违反。DSL 因而区分：

```python
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 接口 | 指标定义 |
| --- | --- |
| <span id="optagent.SchedulingModel.makespan"></span>`makespan(tasks=None)` | 指定任务集合的最大完工时间。 |
| <span id="optagent.SchedulingModel.total_tardiness"></span>`total_tardiness(tasks=None)` | 汇总各任务的正迟交量。 |
| <span id="optagent.SchedulingModel.total_earliness"></span>`total_earliness(tasks=None)` | 汇总各任务的正提前量。 |

```python
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 内持续可用。已下达且不可移动的作业会占据固定区间，预防性维护、
停电或停机窗口也必须参与资源互斥，否则计划可能落入实际上不可生产的时段。

```python
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",
)
```

| 接口 | 建模作用 |
| --- | --- |
| <span id="optagent.SchedulingModel.fixed_interval"></span>`fixed_interval(name, resource, start, duration, attrs=None)` | 创建起止位置不可移动的资源占用。 |
| <span id="optagent.SchedulingModel.calendar"></span>`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：

```python
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 相邻关系、允许路径与切换成本

```python
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：

```python
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 或类型转换重置：

```python
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，可继续进入约束、目标和结果求值。

<span id="optagent.SchedulingModel.sequence_item"></span>
<span id="optagent.SchedulingModel.reset_item"></span>

| 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 同时参与顺序和资源占用。

!!! warning "后端边界"
    Active sequence、setup 和 dimension 当前主要由 heuristic runtime 支持。Direct exact 的逐项
    状态以[精确求解器接口兼容性](exact-compatibility.md)为准；后端不能保持语义时会明确拒绝，
    不会忽略节点或生成近似结果。

## 8. 在求解前检查模型结构

调度模型往往由订单、路线、日历和资源主数据共同生成。局部对象创建成功并不意味着全局结构已经
闭合，例如多方案 Task 可能遗漏选择策略，设备可能遗漏 no-overlap。完成构造后应统一检查：

```python
schedule.validate()
diagnostics = schedule.diagnostics()
```

| 接口 | 作用 |
| --- | --- |
| <span id="optagent.SchedulingModel.validate"></span>`validate()` | 汇总检查未完成的 Task selection 和尚未物化的资源约束。 |
| <span id="optagent.SchedulingModel.diagnostics"></span>`diagnostics()` | 返回领域对象、资源约束和 Builder node IDs 的观测映射。 |

`validate()` 不编译、不创建节点，也不返回 Builder。它验证的是 facade 能负责的结构一致性，不能
替代具体后端的能力检查。重名、非法 requirements、跨模型 Resource 以及 sequence 物化后新增
item 会在对应创建调用中立即失败。`diagnostics()` 用于审计模型生成结果，不参与 solver
correctness。

## 9. 完整的柔性工序示例

下面的模型同时包含设备选择、共享人员、工艺先后、软交期和设备停机。所有约束和目标最终进入
同一个 Builder，并直接交给 `solve(...)`：

```python
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。

<!-- public-api: optagent.SchedulingModel -->
<!-- public-api: optagent.Task -->
<!-- public-api: optagent.Alternative -->
<!-- public-api: optagent.Resource -->
<!-- public-api: optagent.ResourceSequence -->
<!-- public-api: optagent.ResourceSequenceView -->
<!-- public-api: optagent.SequenceTransition -->
<!-- public-api: optagent.SequenceDimension -->
