两个 APS 项目,解的其实是两类不同的数学题:山鹰是在空间里切(一张 5450 mm 的母卷怎么下刀),立邦是在时间上排(几百张工单谁先谁后)。这一页把两边的问题、算法和界面,从零讲一遍。
APS(Advanced Planning and Scheduling,高级计划排程)不是一种东西,是一类问题的统称。落到这两个客户身上,题面差得很远:
造纸厂的物理过程很短,但每个名词都会在后面反复出现,先认一遍:
以 PM15 这台 5450 机为例:一组刀里最多 3 个门幅,总宽必须落在 5400–5450 之间——50 mm 的窗口。多了切不下,少了浪费太多,客户不认。
直觉是:门幅越大,一组里放的片数越少,选择越少,应该越好算才对。真实情况正相反,而且原因不止一个。把两台机分开看,就清楚了。
把案例里 25 个门幅逐个数一遍——它能出现在多少个合法组合里:
所以"大门幅组合太多"这句话,在这台机上是反的:不是太多,是只剩两三个。看一眼 2500 的处境就明白:
PM17 是 8650 mm、每组最多 7 刀。这时候"组合太多了"就是字面意思:
| 机台 | 每组刀数 | 盲枚举所有搭配 | 加上幅宽窗口过滤后 | 实际求解 |
|---|---|---|---|---|
| PM15 · 5450 | ≤ 3 | 3,275 | 102 | 0.6 秒 最优 |
| PM17 · 8650 | ≤ 7 | 38,320,567 | 5,847 | 60 秒 全部交付 |
3800 万 vs 5847,差了四个数量级。如果用人的思路(或者一段贪心脚本)在 3800 万种搭配里试,试一辈子也试不完;先用幅宽窗口把不可能的剪掉、再把剩下的交给整数规划求解器,问题一下子就小了。反过来说,凡是走"逐个门幅找搭档、一路试凑"这条路的做法,碰上 7 刀都会卡住。
就算算得动,还有一个陷阱。如果直接让求解器"交付件数最多",它会得出一个数字更漂亮、但客户当场翻脸的方案:
为什么最大化件数天然歧视大门幅?因为一套刀里放三片小的能交 3 件,放两片大的只交 2 件;而且每塞进一片大门幅,就要吃掉两片稀缺的小料。求解器忠实地执行了"件数最多",就一定会把大门幅饿死。错的不是求解器,是没人告诉它公平也算目标。
整条链路就一句话:把 Excel 需求读进来,枚举出所有合法组合,用两阶段求解定套数,再公平拆回客户,最后导回客户现有的上传模板。
阶段 A 算出来的 16%,是这批需求在物理上能做到的公平极限——再高就无解了。但如果直接拿 16% 当硬下限去配,总交付只剩 382 件;把下限松到 11%,总交付回到 442 件。5 个百分点的公平,换 60 件的交付。
下面这条曲线是把公平下限从 0% 逐格扫到 16%、每一格都完整求一次解得到的,它就是这个问题的全部可选项:
| 阶段 A · 公平 | 阶段 B · 交付 | |
|---|---|---|
| 要回答的问题 | 垫底的那个门幅,最高能到多少 | 在保住这个底线的前提下,最多能交多少 |
| 目标函数 | max r,r = 最低门幅满足率 | max Σ交付×10000 − 组合数×100 − 边料×1 − 分散度×10 |
| 公平约束 | 每个门幅 交付/需排 ≥ r(r 是变量) | 每个门幅 交付/需排 ≥ r* − 5(下限已是常数) |
| 案例结果 | r* = 16%,0.03 秒 | 下限 11% → 442 件 / 17 组 / 163 套 |
| 时间预算 | 最多用掉总时限的 30% | 剩下的全给它 |
最自然的想法是把公平也变成一项分数,和交付加在一起调权重。问题在于这两者需要一个"兑换率"——一件货,值几个百分点的公平?这个数在业务上根本不存在,而且调不出中间地带:公平权重稍低,求解器立刻回到 485 件那种"7 个门幅为 0"的解;稍高,交付就崩。字典序回避了这个问题:它不需要给公平定价,只需要规定谁先问。
但阶段 B 内部的三个次目标是加权的,因为它们之间的兑换率是真实存在的:每交付 1 件记 10000 分,每多用一个组合扣 100 分,每 1 mm·套边料扣 1 分。换算过来就是——多交 1 件货,值得多换 100 次刀、或者多浪费 10000 mm 边料。所以后三项其实只决定"交付件数打平时选哪个方案",永远不会为了省边料去少交一件货。这正是客户的原话:重要的是生产完那一刻到底交多少。
配出来的是"这个门幅能出 8 件",但这 8 件背后可能站着 5 个客户。规则是客户自己给的:不是绝对平均,是按需求占比——需求 9:6:1 就尽量按 9:6:1 缩放;小门幅占比高的客户可以多匀一点;全是大门幅的客户,先单独给它配一版再跟大表合并。这一层是个毫秒级的线性规划,目标是"各客户满足率的最小值最大"。
输出不是一张算法报表,而是一张销售可以直接拿去跟客户谈的表:客户 × 门幅 × (要多少 / 能给多少 / 缺多少)。
| 页面 | 给谁看 | 关键设计 |
|---|---|---|
| 需求台账 | 计划员 | 照搬 CRM 导出表的阅读习惯:客户 × 门幅矩阵,汇总 / 库存 / 需排 |
| 配码工作台 | 计划员 | 左边组合列表,中间幅宽条形图(边料标红),右边余量面板实时跳;顶部 KPI 都带"人工上一版"对照 |
| 客户分配表 | 计划员 + 销售 | 能交多少 / 缺口多少,可导出沟通表 |
| 评审台账 | 双方 | 每版记结论和原因码,逐轮看"直接采纳率"有没有升 |
技术栈:Python FastAPI + Google OR-Tools CP-SAT + Vue 3,求解跑在独立 worker 里(免得长任务卡住页面),方案按版本存 PostgreSQL。一期只做配棍 + 分配 + Excel 进出,纸机的月/周品种计划不碰——那是多部门开会定的事。
立邦泉州是涂料厂,题面完全不同:几百张工单,两条产线,每张单该去哪条线、排第几位。难点在"换色洗机"——上一单做白色、下一单做深色,中间要洗机,洗多久取决于从哪个颜色切到哪个颜色(一张切换代价矩阵)。所以顺序本身就是钱。
| 层 | 含义 | 典型项 |
|---|---|---|
| hard | 不可违反,违反了方案就是废的 | 设备不匹配、锁定的单没落在锁定班次、班次超产能 |
| medium | 尽量满足,比软目标重要一个数量级 | 交期延误、计划期内排不进去 |
| soft | 越优越好 | 洗机工时、切换次数、非首选设备、尽早完工 |
三层是字典序比较:先比硬的,硬的一样才比中的。跟山鹰的"先公平再总量"是同一个思路——有些东西不能被另一些东西买走。
解空间大到没法穷举,于是用局部搜索:随机挪一张单、换两张单、把一段倒过来、把同类的一整块搬走,每动一次算分。关键在"接不接受这一步"——只跟 50 步以前的自己比(Late Acceptance),比那时候好就接受。这样它敢走下坡路,翻得过小山头,又不会像模拟退火那样要调一堆参数。连续一万步没进步,就回到历史最优、砸掉 12 张单重排(ILS 重启)。两条线 190 张单,3 秒收敛。
立邦两轮验收踩出来的这套设计,在山鹰项目里被原样搬了过来:
山鹰的配棍内核是从零写的(立邦仓库里没有任何一维下料的代码),但外面那一圈——规则引擎、人工调整语义、评审台账、回测方法——直接搬,省掉约 40% 的工作量。
| 立邦的东西 | 复用程度 | 到山鹰变成什么 |
|---|---|---|
| 规则引擎(85 行,零依赖) | 直接复用 | 机台配规校验,能展示"这条规则读了什么、算出什么" |
| 评审台账 / 工作日历 | 直接复用 | 每版方案的采纳记录与逐轮收敛 |
| 人工调整六入口一套语义 | 抽象复用 | 锁组 / 改套数 / 单客户单独配 / 撤销 / 流水 / 报代价 |
| 回测方法论 | 抽象复用 | 拿客户 8 月的历史排产单跑一遍,证明"不比人工差" |
| Late Acceptance 求解内核 | 留到三期 | 纸机品种月周计划才用得上;配棍用 CP-SAT 更合适 |
| 标准库 HTTP 服务 / 单文件前端 | 重写 | FastAPI + Vue 3:2390 行单文件已到维护上限 |
选求解器的逻辑也不一样。配棍是"选哪些组合、各切几套"的整数规划,变量清清楚楚,用 CP-SAT 能证明最优;排序是"几百张单的排列",没法证明最优,只能用局部搜索找一个足够好的。同一个项目组,两种问题,两把工具。
页内所有数字来自山鹰《案例.xlsx》两台纸机的真实需求(5450 机 819 件 / 8650 机 709 件)与本项目的可行性验证脚本实测结果;立邦部分来自 nippon-aps 仓库的代码级评估与两轮验收记录。客户配规文件尚未到位时,机台参数走默认配置。