aps-agent/docs/algorithm/scheduling-v1.md

166 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 排产算法 v1(已落地 · RULE)
> 代码:`server/engines/rule_engine.py`
> 测试:`tests/golden/test_rule_engine.py`
> 定位:构造启发式(策略排序 + 顺排占槽),毫秒级;**不是**最优解求解器。
---
## 1. 输入 / 输出
| | 内容 |
| --- | --- |
| 输入 | 世界状态 `world` + `EngineParams`(策略、展望期、起算日、订单子集、引擎类型名;可选 `deliveryBufferRatio` / `freezeWindowHours`) |
| 写出 | DRAFT `scheduleVersion`、生产订单 PO、工单 WO、`conflicts`、版本 KPI 字段 |
| 权力 | 由工作流以 P1 调用;本引擎只改传入的内存 `world` |
请求的 `engineType`:`RULE`→规则引擎;`CP`→OR-Tools CP-SAT(见 [scheduling-cp-v1.md](./scheduling-cp-v1.md));`GA`→遗传算法搜索订单顺序与可行产线;`HYBRID`→RULE 热启动 CP-SAT。三类高级引擎最终都复用本引擎的班次占槽与硬约束物化。
---
## 2. 主流程
```text
① 收集待排订单项(过滤 CANCELLED/COMPLETED 订单与 COMPLETED 明细)
② 按 strategyTemplate 排序(OR-02:综合/均衡以 `-customerLevelWeight` 为第一键;交期优先仍以交期为主)
③ 创建 DRAFT 版本(版本链 parentVersionId)
④ 逐项:选产线 → 按工艺步骤拆 WO → 在班次窗内找槽(可跨天)→ 齐套检查
⑤ 版本内冲突扫描(产能/维保等只扫本版新 WO,避免历史污染)
⑥ 汇总 KPI(延期、利用率等)
```
与 legacy `aps-frontend` 的刻意偏差(已测固化):
1. 容量/维保冲突只扫**本版**新工单
2. 利用率只按本版工单统计
---
## 3. 策略模板(排序键)
| strategyTemplate | 排序逻辑(实现) |
| --- | --- |
| `DELIVERY_FIRST` | `(deliveryDate, -levelWeight, priority)` ≈ EDD 为主 |
| `FIFO` | `(orderDate, -levelWeight, priority)` |
| `CAPACITY_BALANCE` | `(-levelWeight, priority, deliveryDate)`;选线时偏好累计占用少的线 |
| `CHANGEOVER_MIN` | 贪心最近邻:按换型矩阵选下一单;选线偏好同族续排(SC-07) |
| `CAMPAIGN` | 同产品交期窗口合并为战役 PO,再走换型最小化序(SC-08) |
| `COST_FIRST` | 同 `CHANGEOVER_MIN`(以换型分钟为成本代理) |
| 其他 / `COMPREHENSIVE` | `(-levelWeight, priority, deliveryDate)`(OR-02 客户等级权重) |
`levelWeight` 来自 `scheduleParams.customerLevelWeights`(默认 VIP=3/A=2/B=1/C=1)。
**纠偏**:`COST_FIRST` 仍**不是**财务成本最优;当前以 MD-06 换型矩阵分钟为代理。真成本目标待后续切片。
---
## 4. 占槽与资源
- 产线:由产品-产线关系 `find_product_lines`;无可用线 → `NO_LINE`
- 工位:工序资格 `find_workstation_for_operation`;无 → `NO_WORKSTATION`
- 时间:班次日历内找连续可用分钟;长工单可跨天拼接
- 展望期:默认 `planningHorizonDays`(种子参数 14)
- 齐套:BOM vs 库存/在途逻辑;不足 → `MATERIAL_SHORTAGE`(可推迟或标冲突,见代码分支)
---
## 5. 冲突类型(引擎实际产出)
| conflictType | 含义 |
| --- | --- |
| `NO_LINE` | 产品无可用产线 |
| `NO_WORKSTATION` | 工序无合格工位 |
| `MATERIAL_SHORTAGE` | 齐套不足 |
| `DELAY` | 相对交期延误 |
| `CAPACITY` | 产线日产能/占用冲突 |
| `EQUIPMENT` | 维保等设备不可用 |
前端可高亮超期/冲突;**一键修复工作流未落地**。
---
## 6. 明确未做(勿在对外材料写成已有)
- 工序级 CP Interval 全模型 / NSGA-II/Pareto / LNS(产线级 CP、GA 与 HYBRID 首切片已落地)
- 换型全局最优排序(当前为贪心最近邻)
- 战役可配窗口/最大批量与拆批回退
- 插单 LNS 局部修复 / 全量重排决策表(plan §9.11)
- 蒙特卡洛鲁棒性 / 参数推荐(Tornado one-at-a-time 已落地,见 SC-06)
见 [roadmap.md](./roadmap.md)。
## Closed-loop Scheduling Kernel v1 / Contract V2
- 统一输入:Requirement、SupplyEvent、OperationActivity、Resource、日历/维保、目标与硬约束策略。
- 统一输出:ScheduledActivity、SupplyDecision、Pegging、UnscheduledRequirement、HardViolation、Assumption与 Provenance。
- 必须先通过独立 Validator 才可物化;检查工序顺序、资源重叠/能力、日历/维保、物料时间、pegging 守恒、activity identity 和源哈希漂移。
- RULE/Pool/外部 Skill 保留 V1 兼容层,但真实主路径必须映射 V2 并失败关闭。
### Round 65 审计补强:多资源与确定性
- `OperationActivity.resourceRequirements` 使用 `ActivityResourceRequirement` 同时表达 EQUIPMENT / TEAM / TOOLING 候选、能力、容量单位和模具寿命消耗。
- `ScheduledActivity.resourceAllocations` 记录每类资源的实际分配;缺少必需 TEAM/TOOLING、种类不匹配、能力不匹配、容量不足或模具寿命不足均为硬违规。
- 设备分配沿 WORKSTATION -> LINE -> WORKSHOP -> FACTORY 父链汇总负荷;班组和模具分别计算累计容量与寿命,避免重复计费。
- 活动身份哈希纳入多资源要求;planning sourceHash 对自动生成 DRAFT 建议的技术 ID/时间去噪,但保留物料、数量、日期、供应类型和状态等业务语义。
### Round 66 求解器执行隔离与失败关闭
CP/HYBRID 的优化调用采用独立进程协议 `aps.solver-process.v2`。父进程仍负责业务问题构造、门禁与结果物化;child 只接收可序列化的 world/entries/params,执行优化并返回带 requestId/digest、运行时身份和 success/error marker 的版本化 JSON。v2 将生产 `optimize_line_assignment`、整约束 `diagnose_constraint_baseline/removal` 与 RHS 参数 `diagnose_rhs_baseline/perturbation` 三类 operation 严格分离,并绑定 invocationId、relaxed constraint 或单个 RHS 扰动、assumption 实例集合和 RHS 参数状态。
RHS 诊断只支持正向 one-at-a-time 有限差分:`C8_due_date_allowance` 给全部 fixed 待排条目的延期目标 due RHS 增加分钟,`C12_team_capacity` / `C12_tooling_capacity` 给一个真实激活的 Cumulative 资源增加 capacity;Round 82 再扩展具体 line/day 的 C7 分钟实例。C8 不改变可行域;C12 只接受 `intervalCount>=2` 的资源。OPTIMAL 才给精确差;FEASIBLE 只给 incumbent/bound 区间,且所有结果均不是对偶、跨参数不可相加。
Round 82 起,C7 已进入 CP-SAT 可行模型:具体产线上的候选工序完整 `setup+run` 分钟、激活的顺序换型分钟和既有已发布/冻结工单固定负荷,按开工自然日整笔记入 `line/day`,总和不超过该日 `shiftCalendar` 有效分钟。长工序跨午夜仍归开工日;该口径为 `start-day-full-duration.v1`,不是自然日 overlap 分摊。Round 84 虽已直接物化 CP timing/segments,C7 仍保持按逻辑工序开工日整笔记账,不改为按 segment 自然日分摊。效率已作用在工序时长,容量不重复乘效率;件/日 `capacityPerDay` 不参与分钟 RHS。
`C7_line_day_capacity_minutes` 只允许显式 `line-day:{lineId}:{date}` 实例正向增加 1~1440 分钟。非工作日不能通过该参数启用,必须修改日历。物化后 exact C7 复核写入 `materializedC7Validation`;超载仍以 CAPACITY 冲突阻断发布,CP solve status 保留用于区分模型求解与物化偏差。
父进程必须同时验证:
1. 进程在 OS 级时限内完成,且 stdout/stderr 不含 fatal marker;即使 exit code 为 0,fatal marker 仍判失败。
2. response 协议、requestId、摘要和 schema 与请求匹配。
3. Python executable/base executable、`site.ENABLE_USER_SITE`、OR-Tools/NumPy/Pandas/Protobuf 来源位于受信任 runtime prefix。
4. 只有校验通过的 ordered entries 与 operationSlots 才能绑定为 `_cpOperationTiming` 并进入父进程 `materialize_schedule()`;身份、路线、资源、segments、前后序或容量任一不一致都在写入前拒绝。
失败语义固定为 fail-closed:CP 与 HYBRID 均返回 `UNAVAILABLE`,不创建 PO/WO,不产生可发布版本;HYBRID 不静默退回 RULE。timeout 必须终止完整进程树,父进程与 API 服务继续存活,业务 world 保持原子。
真实验收:复制 `world.json` 的 CP/HYBRID 均为 `OPTIMAL`、1 PO / 10 WO / 10 operationSlots、`runtimeSafe=true`;fatal 与 timeout 注入均满足上述失败语义。复制 MOM 数据虽然求解器为 `OPTIMAL`,但现有业务主数据阻断仍为 0 PO/WO、1 slot/1 conflict,这不是现场主数据完成证明。
源码 Sidecar 在 Python `-I` 下不会信任任意 `PYTHONPATH`。真实冒烟发现隔离模式会移除源码 checkout import root 后,只允许 bootstrap 已解析的 trusted source root;`test_source_solver_child_entry_runs_under_isolated_python` 固化该边界。现有冻结 EXE 尚未重建,因此新 `--solver-child` 的打包验证仍是构建后验收,不能由源码冒烟替代。
- 父进程对 success response 强制重算 `responseDigest`,并校验请求/响应条目完整排列、关键业务字段、pipeline/status、objective/gap 与 operationSlots 覆盖;不一致统一 `SOLVER_RESPONSE_INVALID`,禁止物化。
### Round 83 C3 班次日历分段模型
CP 时间模型使用 `cp-calendar-segmented.v1`。每条产线的 shiftCalendar 先扣除休息段并归一化为有效加工窗口;正常求解的 `selectedMode=calendar-boundary-only`,工序可以跨多个窗口,但暂停和续作只能发生在窗口边界。非工作日与休息段不能加工,日历覆盖或结构非法时失败关闭。
逻辑工序与加工段分开建模:C1 约束逻辑工序的首段开始到末段结束包络;C2 工位独占和 C12 班组/工装 Cumulative 只消费实际 processing segments;C2 实例身份按逻辑工序对计数,不能把同一工序的多个段当作互相冲突的工序。C7 仍按逻辑工序开工日记完整 setup+run 分钟,保持 `start-day-full-duration.v1`,不在本轮改成自然日 overlap 分摊。
每个 `operationSlot` 输出:
- `segments[]`:实际加工段的 start/end 分钟;
- `processingMinutes`:加工段分钟总和;
- `elapsedSpanMinutes`:首段开始到末段结束的跨度;
- `pauseMinutes`:跨度减加工分钟;
- `segmentCount`、`calendarCompliant`、`calendarMode`。
整约束 C3 removal 使用独立的 `continuous` 模式,恢复单一连续 interval;其 calendar digest、anchor/horizon/coverage、bucket/window/segment 计数和 model identity 必须与 baseline 完全一致,只有 `selectedMode` 与 `active` 可按诊断语义变化。模型 segment selector 估算超过 50,000 时直接拒绝。
主 CP 求解永不把启发式 hint 固定为硬值。第一次求解返回 UNKNOWN 后,可用已经验证可行的固定 hint 进行一次 fallback,且触发状态、首轮/fallback 时间都写入 `solverMeta`。
Round 83 当时最终物化仍复用 Rule 占槽器,`materializedC3Validation.cpTimingApplied=false`,以 C3 硬冲突关闭偏差;该历史边界已由 Round 84 的直接物化契约替代。
### Round 84 经父进程验证的 CP 时间直接物化
普通 CP/HYBRID 仅在 status 为 `OPTIMAL` 或 `FEASIBLE` 时进入直接 timing 路径。父进程首先复用 `_validate_operation_slots` 校验稳定订单项身份、默认路线步骤双射、产线/工位/team/tooling、segment 汇总、C1 前后序、C2/C12 容量和 C3 日历拓扑;随后再次绑定 normalized calendar digest 和窗口身份,把每个 slot 转为绝对 plannedStart/End 与 `plannedSegments`。任一漂移在版本、PO、WO 创建前抛出 `SOLVER_RESPONSE_INVALID`。
通过预检后,每个 ordered entry 附带 `_cpOperationTiming`。RuleEngine 仍承担版本、PO、WO 和审计对象的原子持久化,但不再调用自身占槽逻辑重新计算可行 CP 时间;每个 WO 原样写入:
- `plannedStartTime` / `plannedEndTime` 与每段 `plannedSegments`;
- `processingMinutes` / `elapsedSpanMinutes` / `pauseMinutes` / `segmentCount`;
- `routingStepId` / `logicalOperationKey` / `cpOrderIndex` / `cpTimingSource=operationSlots`;
- CP 选定的 line/workstation/team/tooling 与 setup/changeover。
版本元数据固定为 `placement=cp-calendar-segmented-direct`、`materializedBy=RuleEngine.validated-cp-timing`、`directlyConsumedByMaterializer=true`、`operationTimingValidation.passed=true`。物化后 C3 必须 `cpTimingApplied=true` 并逐工序与 operationSlots 精确对齐;C7 继续使用 `start-day-full-duration.v1` 复核。非可行状态保持启发式排序回退边界,不把诊断 operation 物化。
诊断响应校验按请求语义区分生产与反事实:移除 C2 时允许工位重叠,移除 C12 时允许对应累计资源超基线容量;C12 RHS 扰动按“父进程世界基础容量 + 已绑定正增量”验证。未授权松弛、资源容量伪造或累计元数据漂移继续失败关闭。