配棍 · 排程 · 两个真实项目

山鹰与立邦的排产内核

两个 APS 项目,解的其实是两类不同的数学题:山鹰是在空间里切(一张 5450 mm 的母卷怎么下刀),立邦是在时间上排(几百张工单谁先谁后)。这一页把两边的问题、算法和界面,从零讲一遍。

山鹰国际 · 浙江基地 6 台纸机 立邦泉州 · 2 条产线 案例数据 819 件 / 709 件 写给第一次接触排产的人
01

一句话分清两个项目

APS(Advanced Planning and Scheduling,高级计划排程)不是一种东西,是一类问题的统称。落到这两个客户身上,题面差得很远:

山鹰 · 在空间里切 一条幅宽 = 一次下刀的全部宽度 1950 1800 1550 边料 幅宽 5450 mm(宽度,不是时间) 要决定的是:哪几个门幅拼一组、这组切几套 学名:一维下料 / Cutting Stock 立邦 · 在时间上排 一条时间轴 = 一条产线的一天 线 A 线 B 时间 → 红块 = 换色洗机 要决定的是:每张单去哪条线、排第几位
同样叫"排产",山鹰切的是宽度,立邦排的是顺序。两边的红色都是浪费——山鹰浪费的是边料,立邦浪费的是洗机时间。
山鹰要的是"配棍"把一台纸机一天的门幅需求,组合成若干"排刀组合 + 套数",再公平地拆回给客户。不是通用 APS。
立邦要的是"序列"几百张工单排进班次,减少换色洗机、少迟交。核心是顺序,不是切割。
02

先把纸厂看懂

造纸厂的物理过程很短,但每个名词都会在后面反复出现,先认一遍:

纸机(抄纸) 一天基本只做一个纸种克重 连续出纸 母卷 直径≈3 m,整幅宽 复卷机(分切) 一组刀切 N 套 换刀要停 成品卷(门幅) 宽度各不相同,按件计 发货 ↓ 配棍决定的就是这一步
纸机那头是连续的,客户要的是一件件不同宽度的卷。中间这台复卷机上的一组刀,就是全部矛盾的所在。
门幅
客户要的成品卷宽度,单位 mm,比如 1900、2500。不是机器的宽度。
幅宽
纸机能出的总宽度,比如 5450 mm、8650 mm。一台机一个档。
排刀组合
一次下刀里并排的那几个门幅,例如 1700+1800+1950。总宽必须落在允许区间内。
这组刀切一遍出一套。1700+1800+1950 × 21 套 = 这三个门幅各出 21 件。
边料
一组刀没占满的那点宽度,直接是废纸。
配棍 / 配码
计划员把一天的门幅需求凑成若干组合 + 套数的这件事。人工做一次 15 分钟到半天。
关键的量纲组合是按宽度约束的(mm 必须加起来落进窗口),交付是按件数结算的(客户要 66 件 2500)。这两个量纲对不上,就是这个问题难的根。
03

一组刀合不合格,只看一条尺

以 PM15 这台 5450 机为例:一组刀里最多 3 个门幅,总宽必须落在 5400–5450 之间——50 mm 的窗口。多了切不下,少了浪费太多,客户不认。

合格线 5400 ≤ 总宽 ≤ 5450 mm 每组 ≤ 3 刀 每组 ≥ 2 套 5450 A 2600 2800 = 5400 剩 50 mm 边料 合格 但一套只出 2 件 B 1700 1800 1950 = 5450 零边料 合格 一套出 3 件 C 1500 1500 1500 差 950
C 这一组三片全是小门幅,加起来只有 4500,够不到下限——所以小门幅也不能自己跟自己凑,它必须找个大一点的搭档。3 刀 × 5400 意味着:平均每片至少 1800 mm
≤3 幅5450 机每组门幅数上限;8650 机是 ≤7 幅。这个数字决定了一切。
≥2 套切完一套就换刀不允许——换刀要停机,换 20 次和换 10 次,当天差 100 吨。
±1~2 件每个门幅允许多做少做一两件,几十件不行。
04

为什么大门幅反而排不出来

直觉是:门幅越大,一组里放的片数越少,选择越少,应该越好算才对。真实情况正相反,而且原因不止一个。把两台机分开看,就清楚了。

在 5450 这种窄机上:大门幅的"搭档"几乎不存在

把案例里 25 个门幅逐个数一遍——它能出现在多少个合法组合里:

0 5 10 15 20 0 20 40 60 80 1300 1500 1650 1750 1850 1950 2050 2150 2250 2350 2450 2600 2800 可用组合数 需排件数 门幅(mm)
灰柱 = 这个门幅可用的合法组合数(共 102 个组合)。从 1300 mm 的 22 个一路掉到 2800 mm 的 2 个。橙线 = 客户的需求件数,偏偏在 2100–2500 那一段最高。组合最少的地方,需求最多

所以"大门幅组合太多"这句话,在这台机上是反的:不是太多,是只剩两三个。看一眼 2500 的处境就明白:

2500 还得填 2900 – 2950 mm 一片 2500 放进 5450 之后: ① 找一片搭档?最宽的门幅才 2800,不存在 2900 以上的单片 ② 那就得用两片小的,而且两片合计必须正好落在 2900–2950: 1300+1600 1300+1650 1400+1500 ——全部可能只有 3 种 而 1300 / 1400 / 1500 / 1600 / 1650 这些小门幅,当天的需求总共只有 88 件。
每交付 1 件 2500,就要消耗两片稀缺的小门幅当填充。1750 mm 以下的小门幅当天总共 141 件,而 2050–2500 的大门幅需求是 404 件——就算把小门幅全拿来填,也只够配出 70 件左右。这是算术上的天花板,不是算法不够聪明。
白话版大门幅不是"组合多到算不完",是"能跟它拼的东西不够了"。就像 2 米长的木板要拼进 5.45 米的框里,剩下的 2.9 米口子,你手上只有几根短料能填。

在 8650 这种宽机上:反过来,组合是真的爆炸

PM17 是 8650 mm、每组最多 7 刀。这时候"组合太多了"就是字面意思:

机台每组刀数盲枚举所有搭配加上幅宽窗口过滤后实际求解
PM15 · 5450≤ 33,2751020.6 秒 最优
PM17 · 8650≤ 738,320,5675,84760 秒 全部交付

3800 万 vs 5847,差了四个数量级。如果用人的思路(或者一段贪心脚本)在 3800 万种搭配里试,试一辈子也试不完;先用幅宽窗口把不可能的剪掉、再把剩下的交给整数规划求解器,问题一下子就小了。反过来说,凡是走"逐个门幅找搭档、一路试凑"这条路的做法,碰上 7 刀都会卡住。

第三个坑,也是最致命的:目标函数选错

就算算得动,还有一个陷阱。如果直接让求解器"交付件数最多",它会得出一个数字更漂亮、但客户当场翻脸的方案:

只求交付件数最多 485 件 0 0 0 0 0 0 0 1300 1500 1650 1750 1850 1950 2050 2150 2250 2350 2450 2600 2800 公平优先 442 件 1300 1500 1650 1750 1850 1950 2050 2150 2250 2350 2450 2600 2800 100% 100%
上排:只求件数最多——总数 485 件(比人工的 366 高),但 2200 到 2500 有 7 个门幅一件不给。下排:把"最低门幅满足率"提成第一目标——总数降到 442 件,每个门幅至少交 11%,小门幅仍然 100%。
用 43 件,换掉 7 个"零交付"。
这不是假设山鹰上一家供应商的系统就是这么被否掉的:规则设计成"先排完一类",结果一个门幅全交、另一个一件不给,客户不接受。"雨露均沾"在这个项目里是硬需求,不是加分项。所以"跑出来只有小门幅有货"未必是没解出来,更可能是解出了上排那种解。

为什么最大化件数天然歧视大门幅?因为一套刀里放三片小的能交 3 件,放两片大的只交 2 件;而且每塞进一片大门幅,就要吃掉两片稀缺的小料。求解器忠实地执行了"件数最多",就一定会把大门幅饿死。错的不是求解器,是没人告诉它公平也算目标。

05

山鹰 APS 是怎么解的

整条链路就一句话:把 Excel 需求读进来,枚举出所有合法组合,用两阶段求解定套数,再公平拆回客户,最后导回客户现有的上传模板。

需求 Excel CRM 导出 解析 + 规则 机台配规校验 枚举组合 102 / 5847 个 两阶段求解(CP-SAT) 阶段 A 抬最低满足率 阶段 B 再冲总件数 拆回客户 按需求占比 LP 导出模板 排产单 / SAP / CRM 计划员工作台:锁组 · 改套数 · 单客户单独配 每改一手,当场报代价(交付 ± / 组合 ± / 边料 ±) 改完即时重算 评审台账 每版:采纳 / 改了什么 / 为什么
两阶段是整个方案的心脏:阶段 A 只问"垫底的那个门幅能到多少"(案例里是 16%),阶段 B 放宽 5 个点当下限,再去最大化总件数。字典序,不是加权求和——公平不能被总量买走。

两阶段到底在权衡什么

阶段 A 算出来的 16%,是这批需求在物理上能做到的公平极限——再高就无解了。但如果直接拿 16% 当硬下限去配,总交付只剩 382 件;把下限松到 11%,总交付回到 442 件5 个百分点的公平,换 60 件的交付。

下面这条曲线是把公平下限从 0% 逐格扫到 16%、每一格都完整求一次解得到的,它就是这个问题的全部可选项:

阶段 B 松开的 5 个点 380 400 420 440 460 480 500 0% 2% 4% 6% 8% 10% 12% 14% 16% 485 件 7 个门幅一件不给 442 件 我们交出去的默认方案 382 件 阶段 A 的极限:最公平 交付件数 公平下限 = 最低门幅满足率(%)
越往右越公平,越往左交付越多,没有两全。曲线不是平滑下滑的:0% 到 1% 一下掉 11 件(大门幅一旦要求"至少给一点",就得拆掉几组高效组合),13% 之后又开始陡降。两阶段做的事,就是先找到这条曲线的右端点,再沿着它往左退 5 格。
阶段 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 边料。所以后三项其实只决定"交付件数打平时选哪个方案",永远不会为了省边料去少交一件货。这正是客户的原话:重要的是生产完那一刻到底交多少。

还有几个必须处理的现实

  • 算不出来怎么办。阶段 A 要是连一个可行解都没拿到,就退化成"不设公平下限"的纯交付模式,至少给出一版能用的方案,并在解释里写明降级了;阶段 B 要是超时没解,返回锁定的组合 + 一句为什么。永远返回方案,不返回报错。
  • 人锁了组合之后。计划员锁定的组合先从需求里扣掉(它已经交付的那部分算进满足率的分子),剩下的余量才进模型。所以重算不会推翻人的决定,也不会把人已经给出去的货重复计算。
  • 时限要给够。5450 机 4 秒就能拿到最优。8650 机交付量很快就满了,但组合数对时限敏感:阶段 B 给 6 秒是 33 个组合,10 秒 24 个,≥14 秒才稳定在 22 个——差的那 11 次换刀是实打实的停机时间。所以宽幅机台的时限不设低于 20 秒。
  • 下限 5 个点是参数,不是定论。fairness_slack_pct 摆在界面上,计划员可以当场拉动看曲线上的另一个点。真正该由客户决定的事,不该焊死在代码里。
442 件5450 机公平模式,对比人工的 366 件,+21%,用时 0.6 秒
17 组组合数比人工的 20 组还少 3 组 —— 少换 3 次刀
709/7098650 机全部交付,零边料,组合数比人工少 8 个
≥11%最低门幅满足率,不再有任何一个门幅是 0

拆回客户这一步,同样是公平题

配出来的是"这个门幅能出 8 件",但这 8 件背后可能站着 5 个客户。规则是客户自己给的:不是绝对平均,是按需求占比——需求 9:6:1 就尽量按 9:6:1 缩放;小门幅占比高的客户可以多匀一点;全是大门幅的客户,先单独给它配一版再跟大表合并。这一层是个毫秒级的线性规划,目标是"各客户满足率的最小值最大"。

输出不是一张算法报表,而是一张销售可以直接拿去跟客户谈的表:客户 × 门幅 × (要多少 / 能给多少 / 缺多少)。

四个页面

页面给谁看关键设计
需求台账计划员照搬 CRM 导出表的阅读习惯:客户 × 门幅矩阵,汇总 / 库存 / 需排
配码工作台计划员左边组合列表,中间幅宽条形图(边料标红),右边余量面板实时跳;顶部 KPI 都带"人工上一版"对照
客户分配表计划员 + 销售能交多少 / 缺口多少,可导出沟通表
评审台账双方每版记结论和原因码,逐轮看"直接采纳率"有没有升

技术栈:Python FastAPI + Google OR-Tools CP-SAT + Vue 3,求解跑在独立 worker 里(免得长任务卡住页面),方案按版本存 PostgreSQL。一期只做配棍 + 分配 + Excel 进出,纸机的月/周品种计划不碰——那是多部门开会定的事。

06

立邦 APS 是怎么解的

立邦泉州是涂料厂,题面完全不同:几百张工单,两条产线,每张单该去哪条线、排第几位。难点在"换色洗机"——上一单做白色、下一单做深色,中间要洗机,洗多久取决于从哪个颜色切到哪个颜色(一张切换代价矩阵)。所以顺序本身就是钱。

按交期顺排(基线) 白 A 深 B 白 C 深 D 白 E 洗机 4 次 同类聚簇(求解器) 白 A 白 C 白 E 深 B 深 D 洗机 1 次,整体提前收工 时间 → 但不能一味聚簇:D 的交期可能就在今天,聚簇省下的洗机会被迟交罚分吃掉——这就是求解器要权衡的东西。
排序问题的全部乐趣:省洗机不迟交是一对矛盾。人工凭经验,求解器靠一个分数把两者放在同一把尺子上比。

怎么评分:硬 / 中 / 软,三层字典序

含义典型项
hard不可违反,违反了方案就是废的设备不匹配、锁定的单没落在锁定班次、班次超产能
medium尽量满足,比软目标重要一个数量级交期延误、计划期内排不进去
soft越优越好洗机工时、切换次数、非首选设备、尽早完工

三层是字典序比较:先比硬的,硬的一样才比中的。跟山鹰的"先公平再总量"是同一个思路——有些东西不能被另一些东西买走

怎么搜:Late Acceptance

解空间大到没法穷举,于是用局部搜索:随机挪一张单、换两张单、把一段倒过来、把同类的一整块搬走,每动一次算分。关键在"接不接受这一步"——只跟 50 步以前的自己比(Late Acceptance),比那时候好就接受。这样它敢走下坡路,翻得过小山头,又不会像模拟退火那样要调一堆参数。连续一万步没进步,就回到历史最优、砸掉 12 张单重排(ILS 重启)。两条线 190 张单,3 秒收敛。

真正值钱的不是算法,是让人敢用

立邦两轮验收踩出来的这套设计,在山鹰项目里被原样搬了过来:

  • 先证明底账没算错,再给建议。系统敢改开单量之前,先把现有口径逐行对到 102/102。到山鹰就是:先复现计划员现在的配棍结果,再谈优化。
  • 任何数字都能问"为什么"。规则触发链路、约束分解到单张工单、还有反事实解释("这张单挪到别的班会怎样")。验收时评价最高的就是这条。
  • KPI 卡自带对照,而且是真对照。曾经有个"班次超产能"指标恒为 0,被验收抓出来是假校验——一个永远显示 0 的指标,会让人误以为已经校验过了。也别臆造对照组:叫"现状模拟"结果里面没有现状逻辑,被追问一次,全部对比数字都会被怀疑。
  • 人机分工要明确。默认只让人钉"哪台机、哪天、哪个班",班内顺序交给求解器——人掌握系统不知道的信息(客户在催、今天缺人),系统强在组合优化;让人去拖分钟级的时刻,是在跟求解器抢它擅长的事。
  • 每一手当场报代价。改完弹一句"洗机 +1.0 h · 迟交 +2 张"。山鹰版就是"交付 −3 件 · 边料 +200 mm"。
  • 改数据和改排法要分账。补录主数据、订正产线状态改的是输入,不能算进"人工调整的净影响";否则"洗机少了 1 小时"说不清是数据终于填对了,还是排法真的变好了——这两件事该找谁改,完全不同。
07

两边到底能共用什么

山鹰的配棍内核是从零写的(立邦仓库里没有任何一维下料的代码),但外面那一圈——规则引擎、人工调整语义、评审台账、回测方法——直接搬,省掉约 40% 的工作量。

立邦的东西复用程度到山鹰变成什么
规则引擎(85 行,零依赖)直接复用机台配规校验,能展示"这条规则读了什么、算出什么"
评审台账 / 工作日历直接复用每版方案的采纳记录与逐轮收敛
人工调整六入口一套语义抽象复用锁组 / 改套数 / 单客户单独配 / 撤销 / 流水 / 报代价
回测方法论抽象复用拿客户 8 月的历史排产单跑一遍,证明"不比人工差"
Late Acceptance 求解内核留到三期纸机品种月周计划才用得上;配棍用 CP-SAT 更合适
标准库 HTTP 服务 / 单文件前端重写FastAPI + Vue 3:2390 行单文件已到维护上限

选求解器的逻辑也不一样。配棍是"选哪些组合、各切几套"的整数规划,变量清清楚楚,用 CP-SAT 能证明最优;排序是"几百张单的排列",没法证明最优,只能用局部搜索找一个足够好的。同一个项目组,两种问题,两把工具。

08

记住这几句就够了

  • 难度不看门幅大小,看"一组能放几刀"和"窗口有多窄"。3 刀 + 50 mm 窗口,是这个问题真正的两个旋钮。
  • 窄机上,大门幅难在没搭档——组合数从 22 掉到 2,而需求恰恰堆在那一头。
  • 宽机上,大幅宽难在组合爆炸——7 刀盲枚举 3800 万种;先剪枝再求解,只剩 5847 种。
  • 目标函数比算法更容易出事。"件数最多"会把大门幅饿死到 0;上一家供应商就是这么被否的。
  • 公平要写进目标,不能事后补。两阶段字典序:先抬垫底的那个,再冲总量。
  • 能不能落地,最后拼的是解释能力——每个数字都能问为什么,每一手都当场报代价,每一版都留痕。

页内所有数字来自山鹰《案例.xlsx》两台纸机的真实需求(5450 机 819 件 / 8650 机 709 件)与本项目的可行性验证脚本实测结果;立邦部分来自 nippon-aps 仓库的代码级评估与两轮验收记录。客户配规文件尚未到位时,机台参数走默认配置。