☰
GAMS二次开发教程(17):生产计划优化实战——多期多工厂产能-库存-运输 MILP
2026/10/9 5:29:19 网站建设 项目流程

GAMS二次开发教程(17):生产计划优化实战——多期多工厂产能-库存-运输 MILP

版本与许可声明

  • 工具/软件:GAMS(版本号以官方发行说明为准,本篇以gams.com/latest/docs指向的当前分发为口径);求解器侧示例取自 CPLEX 手册(CPLEX 22.1,以官方发行说明为准)。
  • 语言/环境:GAMS 语言 + GDX;模型类型为混合整数线性规划(Mixed Integer Linear Program, MIP)。
  • 本文目标:把"三个月产量怎么排、货怎么调、库存留多少、缺货认不认"这一整类产销协同问题,翻译成一份可求解、可报表、可回归的 MIP。
  • 许可边界:求解 MIP 需要支持 MIP 的求解器许可(demo license 可用,规模上限以官方许可说明为准);本篇的相关性重跑会连续求解三次 MIP,注意许可类型是否限制实际使用次数(见第 15 篇提及的许可会话问题)。

一句话结论:多期多工厂计划的骨架只有四块——三张清单定边界(决策变量 / 目标 / 约束)、库存平衡方程定时间轴(inv(p,t) =e= inv0(p)$(ord(t)=1) + inv(p,t-1)$(ord(t)>1) + y(p,t) - sum(m$link(p,m), x(p,m,t)),两个互斥的$条件把首期与后续期一次写对)、binary+ 最紧 big-M 定开停(capMaxLim(p,t) .. y(p,t) =l= capMax(p) * run(p,t) ;,这里的M就直接取该装置的产能上界,永远不要用一个"够大就行"的数)、稀疏运输变量定网络(运输变量与求和都带同一个$link(p,m)),而缺货必须用重惩罚项而不是硬约束,否则需求高峰会让整个 MIP 变成不可行。

〇、本篇要解决的认知问题

  • Q1:一个业务问题(产销协同)怎么变成模型?"决策变量 / 目标 / 约束"三张清单各自怎么列?
  • Q2:多期模型的库存平衡方程怎么写才不会在首期越界、又能在末期正确结转?
  • Q3:"开工与否"为什么必须引入binary变量?big-M 该取多大?"开了就必须产够"的最小运行量怎么加?
  • Q4:运输/调拨的网络流结构怎么表达?为什么运输变量的声明与求和要带同一个$link(p,m)?
  • Q5:目标函数的成本项该怎么构成?缺货为什么用惩罚项?"需求 ±10%"的敏感性重跑怎么组织、结果怎么收集?

一、机制解析

1.1 从业务到模型:先写三张清单

业务侧的说法通常是"这三个月各厂该产多少、货往哪调、库存留多少、实在供不上就缺货"。要落成模型,必须先把它拆成三张清单,这三张清单必须在动笔写 GAMS 之前先用中文写出来:

清单本问题的答案落到 GAMS 的语句
决策变量(要决定什么)各厂各期产量、各连线各期运量、各厂各期末库存、各市场各期缺货量、各厂各期是否开工Variable+Positive Variable+Binary Variable
目标(要最小化/最大化什么)总成本最小:固定开工成本 + 可变生产成本 + 运输成本 + 库存持有成本 + 缺货惩罚cost方程 +Solve ... using mip minimizing z
约束(什么不能被违反)产能上界与最小运行量、库存平衡、需求满足(可缺货)capMaxLim/capMinLim/invBal/demSat

这张表的价值在于:没有被列进"约束"清单的业务规则,就不该出现在方程里;反过来,凡是业务上"绝对不允许"的,就必须列进来。所有"我们没有约束它,它怎么自己跑偏了"的困惑,答案都在这里。

1.2 库存平衡方程:多期模型的时间轴

多期模型的时间耦合完全由库存平衡方程承担。它是本期与上期之间唯一的桥:

期末库存(t) = 期初库存(t) + 本期产量 - 本期发货量

其中"期初库存"在首期是给定的初始库存inv0(p),在之后各期就是上一期的期末库存inv(p,t-1)。在 GAMS 里这两种情形用两个互斥的$条件一次写完:

invBal(p,t) .. inv(p,t) =e= inv0(p)$(ord(t) = 1) + inv(p,t-1)$(ord(t) > 1) + y(p,t) - sum(m$link(p,m), x(p,m,t)) ;

两条必须记住的细节:inv(p,t-1)在首期(ord(t)=1)是越界的,靠$(ord(t) > 1)把它关掉,GAMS 不会去访问不存在的t0;=e=是等式(注意不是==,那属于 Python/GAMSpy 侧写法)。末期结转则是免费的——只要让inv(p,t12)留在模型里并进入目标函数(holdCost * inv(p,t)),模型自己就会权衡"提前生产备货"与"当期生产"的成本,不需要额外的终端条件。

1.3binary+ big-M + 最小运行量:开停逻辑的标准三件套

"产能上限"这句话在业务上有两种截然不同的含义,必须分清:

  • 连续上限(装置在运行状态下能产多少):y(p,t) =l= capMax(p);
  • 开停上限(装置不开工就一点也不能产):必须引入Binary Variable run(p,t),并用 big-M 把它与产能挂钩。

GAMS 里有一个直接支持"带条件的上界"的写法:y.up(p,t)$(...) = ...,但它只能表达静态上界,表达不了"由变量run决定"的耦合。因此开停耦合的标准写法就是:

capMaxLim(p,t) .. y(p,t) =l= capMax(p) * run(p,t) ; capMinLim(p,t) .. y(p,t) =g= capMin(p) * run(p,t) ;

两条一起看才完整:第一条说"不开工产量为零",第二条说"开了工产量必须达到最小经济运行负荷"。没有第二条,模型的"最优解"会变成"每期都开工、每期产一点点"——因为固定成本被分摊到极小批量上,就会出现业务上根本做不到的排产方案。

big-M 取值的铁律(经验法则):big-M只能取该约束下最紧的合法上界。本问题里就是capMax(p)——它是y(p,t)自身允许的最大值,再大就是纯粹的松弛,会让分支定界更慢、线性松弛更弱(这一条不构成性能承诺,只是建模的正确性要求)。永远不要写100000 * run(p,t)这种"够大就行"的数。

1.4 运输/调拨:稀疏网络流

运输变量天然是一个网络流结构:x(p,m,t)从工厂p流向市场m。它有两条必须同时满足的纪律:

  1. 声明稀疏:只声明实际存在的连线(Set link(p,m)),而不是p × m的全笛卡尔积(第 16 篇 1.3 节);
  2. 使用端也带同一条件:方程里写sum((p,m,t)$link(p,m), shipCost(p,m) * x(p,m,t))与sum(p$link(p,m), x(p,m,t))。

"声明与使用两端带同一个条件"是稀疏建模最容易漏的一条。如果只在方程标号处带条件,但求和里引入了一个不属于link的(p,m)组合,那个组合的x会因为"出现在方程里"而被自动隐含扩展进定义域——稀疏声明就白做了。

1.5 目标函数:五项成本与"缺货惩罚"

本篇的目标函数由五项构成,每一项都对应一条业务口径:

成本项表达式业务含义
固定开工成本sum((p,t), fixCost(p) * run(p,t))只要开工就发生,与产量无关
可变生产成本sum((p,t), varCost(p) * y(p,t))与产量成正比
运输成本sum((p,m,t)$link(p,m), shipCost(p,m) * x(p,m,t))按连线、按量计费
库存持有成本sum((p,t), holdCost * inv(p,t))占用期末库存,推动"按需生产"
缺货惩罚sum((m,t), shortCost * short(m,t))惩罚未满足的需求

缺货为什么用惩罚项而不是硬约束(sum(p$link(p,m), x(p,m,t)) =g= dem(m,t))?因为硬约束意味着"需求必须被满足",一旦产能不足(需求高峰、检修期),整个 MIP 直接不可行,你得到的是"没有方案"而不是"代价最小的方案"。改成"需求满足方程带一个缺口变量short(m,t)、缺口在目标里被重罚"以后,模型永远有可行解,且在产能充裕时short会被压到零。业务上这叫"软约束 + 惩罚",是产能规划类模型的标配。

1.6 求解、报表与敏感性重跑

  • 模型类型:由于存在run(p,t)这样的binary变量,模型类型是mip。求解语句为Solve plan using mip minimizing z ;,求解器用option MIP = CPLEX ;指定;
  • MIP 的容差:plan.optCR是相对最优性容差(第 11 篇),它决定"搜索到多大的相对 gap 就可以停"。教学示例取0.01;它的语义与各求解器的实现差异以官方文档为准;
  • 状态卡口:MIP 的状态码与 LP 不同,modelStat可能出现"整数解"这类取值(完整码表以官方文档为准)。工程上的做法是卡住plan.solveStat <> 1,再加一条plan.modelStat的检查;
  • 敏感性重跑:用第 9 篇的loop范式,把需求整体缩放 0.9 / 1.0 / 1.1,每个场景重求解一次,把 KPI 收进一个二维参数kpiS(s,*)。注意binary变量在多次solve之间会带着上一次的解(第 13 篇的"多 solve 自动传递初始点"),这对 MIP 来说是冷却起点还是热起点取决于求解器是否使用(铁律 6),因此重跑结论只能写成"本算例上如此",不做性能承诺(铁律 7)。

二、完整代码与逐行剖析

代码 17-1:planning_core.gms—— 数据层 + 模型层(不含Solve)

$title planning_core - multi period multi plant capacity-inventory-transport MILP core * 适用分发口径:Variable/Equation/Model 语法以官方 Model and Solve 章节为准 * 求解许可:本文件不含 SOLVE,无需求解器许可 * 平台:GAMS 语句通用 * 数值口径:以下全部为教学示例值,真实项目必须替换为数据源并标注来源(铁律 10) Set t "planning periods (months)" / t1*t12 / p "production plants" / p1*p3 / m "markets" / m1*m4 / ; * 稀疏网络:只声明实际存在的 plant-market 连线,而不是 p x m 的全笛卡尔积 Set link(p,m) "existing plant-market lanes" / p1.(m1,m2) p2.(m2,m3) p3.(m3,m4) / ; Parameter dem(m,t) "demand at market m in period t (cases)" capMax(p) "max production rate at plant p per period (cases)" capMin(p) "min production rate at plant p per period when running (cases)" fixCost(p) "fixed cost at plant p per period when running (USD)" varCost(p) "variable production cost at plant p (USD per case)" shipCost(p,m) "shipping cost on lane (p,m) (USD per case)" inv0(p) "initial inventory at plant p (cases)" holdCost "inventory holding cost (USD per case per period)" / 0.8 / shortCost "shortage penalty (USD per case)" / 25.0 / ; dem(m,t) = 120 + 12 * ord(t) + 10 * ord(m) ; capMax(p) = 900 ; capMin(p) = 100 ; fixCost(p) = 4000 ; varCost(p) = 2.0 + 0.4 * ord(p) ; shipCost(p,m)$link(p,m) = 1.2 + 0.3 * ord(p) + 0.2 * ord(m) ; inv0(p) = 50 ; Variable y(p,t) "production quantity at plant p in period t (cases)" x(p,m,t) "shipment from plant p to market m in period t (cases)" inv(p,t) "end-of-period inventory at plant p in period t (cases)" short(m,t) "unmet demand at market m in period t (cases)" run(p,t) "binary: plant p is running in period t" z "total cost (USD)" ; Positive Variable y, x, inv, short ; Binary Variable run ; Equation cost "total fixed, variable, shipping, holding and shortage cost" capMaxLim(p,t) "max production rate, coupled to the run binary (big-M)" capMinLim(p,t) "min production rate when running" invBal(p,t) "inventory balance at plant p in period t" demSat(m,t) "demand satisfaction at market m in period t" ; cost .. z =e= sum((p,t), fixCost(p) * run(p,t)) + sum((p,t), varCost(p) * y(p,t)) + sum((p,m,t)$link(p,m), shipCost(p,m) * x(p,m,t)) + sum((p,t), holdCost * inv(p,t)) + sum((m,t), shortCost * short(m,t)) ; * big-M 取该装置产能上界本身,这是最紧的合法取值,不用"够大就行"的数 capMaxLim(p,t) .. y(p,t) =l= capMax(p) * run(p,t) ; * 最小运行量:不开工则产量为零,开工则必须达到最小经济运行负荷 capMinLim(p,t) .. y(p,t) =g= capMin(p) * run(p,t) ; * 首期接初始库存,之后各期接上期期末库存:两个互斥的 $ 条件 invBal(p,t) .. inv(p,t) =e= inv0(p)$(ord(t) = 1) + inv(p,t-1)$(ord(t) > 1) + y(p,t) - sum(m$link(p,m), x(p,m,t)) ; * 软约束:需求满足方程带缺口变量,缺口在目标里被重罚 demSat(m,t) .. sum(p$link(p,m), x(p,m,t)) + short(m,t) =e= dem(m,t) ; Model plan / all / ;

逐行剖析:Set link(p,m)用p1.(m1,m2)圆括号简写声明一条工厂的全部下线,把网络结构写成数据而不是维度积;shipCost(p,m)$link(p,m) = ...只在连线上赋成本,其余格保持 0 且不会被引用;Binary Variable run ;声明开停指示变量——注意它是单独的Binary语句,与Positive Variable y, x, inv, short ;分开写,因为二值变量不具备"非负连续"的语义;cost方程用五行sum把五项成本逐项列出,顺序与 1.5 节的表完全一致,这样"目标里有哪几块钱"一眼可查;capMaxLim(p,t) .. y(p,t) =l= capMax(p) * run(p,t) ;是 big-M 耦合,M就是capMax(p);capMinLim与它成对,缺了这条就会出现"每期都开工、每期产一点点"的假最优;invBal里inv0(p)$(ord(t) = 1)与inv(p,t-1)$(ord(t) > 1)是两个互斥条件,首期只接初始库存、其余期只接上期期末,inv(p,t-1)在首期不会越界;demSat用=e=把"发货 + 缺口 = 需求"写成恒等式,于是short(m,t)的含义精确等于缺口量,缺货 KPI 可以直接用它的level;Model plan / all / ;把五条方程一次装配。

代码 17-2:planning_run.gms—— 单次求解 + 期间 × 工厂报表 + GDX 导出

$title planning_run - single MIP solve plus period by plant report and gdx export * 适用分发口径:option / 模型属性(optCR / resLim / iterLim)以官方 Option Statement 页为准 * 求解许可:本文件执行 SOLVE,需要支持 MIP 的求解器许可(以官方许可说明为准) * 平台:GDX 跨平台通用 * 前置条件:与 planning_core.gms 放在同一目录 $include planning_core.gms option MIP = CPLEX ; * 求解器选择:MIP 需要支持整数规划的求解器 plan.optCR = 0.01 ; * 相对最优性容差:搜索到 1% 相对 gap 即可停 plan.resLim = 300 ; * 秒级资源上限:先排除"被时间截断"这一可能 plan.iterLim = 2000000 ; * 迭代上限 Solve plan using mip minimizing z ; * ---- 状态卡口:MIP 的 modelStat 码表以官方文档为准 ---- abort$(plan.solveStat <> 1) 'SOLVE did not terminate normally: check solvestat' ; abort$(plan.modelStat <> 1) 'MODEL is not optimal: check modelstat' ; * ---- 报表参数:期间 x 工厂的产量与期末库存,以及按连线的运量 ---- Parameter prod(p,t) "production by plant and period (cases)" invEnd(p,t) "end-of-period inventory by plant and period (cases)" shipLane(p,m,t) "shipped quantity by lane and period (cases)" kpi(*) "key performance indicators" ; prod(p,t) = y.l(p,t) ; invEnd(p,t) = inv.l(p,t) ; shipLane(p,m,t)$link(p,m) = x.l(p,m,t) ; kpi('totalCost') = z.l ; kpi('fixCostPart') = sum((p,t), fixCost(p) * run.l(p,t)) ; kpi('varCostPart') = sum((p,t), varCost(p) * y.l(p,t)) ; kpi('shipCostPart') = sum((p,m,t)$link(p,m), shipCost(p,m) * x.l(p,m,t)) ; kpi('holdCostPart') = sum((p,t), holdCost * inv.l(p,t)) ; kpi('shortCostPart') = sum((m,t), shortCost * short.l(m,t)) ; kpi('totalShort') = sum((m,t), short.l(m,t)) ; kpi('plantPeriods') = sum((p,t), run.l(p,t)) ; option decimals = 2 ; option dispWidth = 14 ; Display kpi, prod, invEnd, plan.modelStat, plan.solveStat ; * ---- 落一份文本报表,便于归档、diff 与人工抽查 ---- file rep / planning_report.txt / ; put rep ; put 'period plant production inventory' / ; loop((p,t), put t.tl:8, p.tl:8, y.l(p,t):14:1, inv.l(p,t):14:1 /) ; putclose rep ; * ---- 层间契约:把解与 KPI 写成 GDX,供 Python / Engine 侧消费 ---- execute_unload 'planning_solution.gdx', prod, invEnd, shipLane, kpi, run ;

逐行剖析:$include planning_core.gms让本文件只负责"求解 + 输出"两件事,这正是第 16 篇的四层分离;option MIP = CPLEX ;指定求解器(第 12 篇的选型矩阵),MIP 与 LP 的求解器选项是分开的,不要用option LP去指定 MIP 求解器;plan.optCR = 0.01是模型属性层的设置(优先级高于option语句),含义是"相对最优性容差";abort$(plan.solveStat <> 1)卡"是否正常终止",abort$(plan.modelStat <> 1)卡"是否拿到最优",两道闸门缺一不可——MIP 最危险的失败模式是"被resLim截断却拿了一个漂亮的整数解";prod(p,t) = y.l(p,t)一行就把"期间 × 工厂"的产量矩阵整理好,.l取变量的 level;KPI 里把成本按项拆开(固定 / 可变 / 运输 / 库存 / 缺货),这是产能规划类项目最常被追问的一张表——“这块钱花在哪了”;put rep ;与loop((p,t), put ...)生成一份可归档的文本报表,t.tl/p.tl取集合元素的文本标签左对齐输出;最后execute_unload 'planning_solution.gdx', ...把解与 KPI 一起写成 GDX,成为下游的接口。

代码 17-3:planning_sens.gms—— 需求 ±10% 的敏感性重跑与回归闸门

$title planning_sens - demand plus or minus ten percent sensitivity rerun * 适用分发口径:loop 与 Set/Parameter 语法以官方文档为准 * 求解许可:本文件对每个场景执行一次 SOLVE,共三次,均需要 MIP 求解器许可 * 平台:GAMS 语句通用 * 前置条件:与 planning_core.gms 放在同一目录 $include planning_core.gms Set s "demand scenarios" / low, base, high / ; Parameter scale(s) "demand multiplier of each scenario" kpiS(s,*) "key performance indicators by scenario" demBase(m,t) "baseline demand kept for the reruns" ; scale('low') = 0.9 ; scale('base') = 1.0 ; scale('high') = 1.1 ; demBase(m,t) = dem(m,t) ; option MIP = CPLEX ; plan.optCR = 0.01 ; plan.resLim = 300 ; loop(s, dem(m,t) = demBase(m,t) * scale(s) ; Solve plan using mip minimizing z ; abort$(plan.solveStat <> 1) 'a scenario did not terminate normally' ; kpiS(s,'totalDemand') = sum((m,t), dem(m,t)) ; kpiS(s,'totalCost') = z.l ; kpiS(s,'plantPeriods')= sum((p,t), run.l(p,t)) ; kpiS(s,'totalShort') = sum((m,t), short.l(m,t)) ; kpiS(s,'totalInv') = sum((p,t), inv.l(p,t)) ; ) ; option decimals = 2 ; option dispWidth = 16 ; Display kpiS ; * ---- 回归闸门:需求上升时总成本不应低于需求下降时的总成本 ---- abort$(kpiS('high','totalCost') < kpiS('low','totalCost')) 'REGRESSION: cost must not fall when demand rises' ; execute_unload 'planning_sensitivity.gdx', kpiS ;

逐行剖析:$include planning_core.gms让"数据 + 模型"只写一次,敏感性文件只写"驱动逻辑";demBase(m,t) = dem(m,t) ;先留一份基线需求,因为循环里dem会被反复覆盖——不先备份,第二次循环就会拿"上一次缩放后的需求"再缩放一次,这是这类脚本最经典的静默 bug;loop(s, ...)是第 9 篇的参数化重求解范式,循环体内第一件事是重算需求,第二件事就是Solve;abort$(plan.solveStat <> 1)放在循环内,任何一个场景失败都立刻中止,避免"两个场景成功、一个场景挂了"却还在往下汇总;kpiS(s,...)是二维参数,第一维是场景、第二维是自由指标(用*声明),这张表就是敏感性分析的最终交付物;末尾的abort$(kpiS('high','totalCost') < kpiS('low','totalCost'))是业务级回归闸门:需求整体上升而总成本反而更低,只能说明数据、缩放或模型被改坏了——把它写成硬门禁,比人眼扫表可靠得多;最后execute_unload把敏感性表也导出成 GDX。

三、常见报错与排查

排查 3-1:模型不可行(modelStat显示不可行)。现象:需求高峰月份无解。根因:把"需求必须满足"写成了硬约束sum(p$link(p,m), x(p,m,t)) =g= dem(m,t),而该期产能(含capMin/capMax与检修)根本不够。解法:改成软约束(demSat(m,t) .. sum(p$link(p,m), x(p,m,t)) + short(m,t) =e= dem(m,t) ;),把缺口交给short(m,t)并给一个足够高的shortCost;这样模型永远可行,缺货量本身就是一条业务指标。别用"把shortCost设成很小的数"来粉饰——那等于告诉模型"缺货很便宜",最优解会真的去缺货。

排查 3-2:最优解出现"每期都开工、每期只产一点点"。现象:run.l(p,t)几乎全为 1,产量却极低。根因:只写了 big-M 上界y =l= capMax * run,漏了最小运行量下界,于是固定成本被摊到极小批量上反而更便宜。解法:补上capMinLim(p,t) .. y(p,t) =g= capMin(p) * run(p,t) ;,让"开工"必须至少产到最小经济运行负荷。改完要复核kpi('plantPeriods')(开工的厂·期数)是否回到业务上可执行的量级。

排查 3-3:稀疏网络"漏了条件",模型规模暴涨。现象:明明只声明了几条连线,方程项数却按全笛卡尔积增长。根因:某处求和写了sum((p,m,t), ...)而漏了$link(p,m),或某个方程标号处漏了条件,导致不属于link的组合被自动隐含扩展进定义域。解法:“声明与使用两端带同一个条件”——所有涉及运输的项统一写成sum((p,m,t)$link(p,m), ...)与sum(p$link(p,m), ...);改完用.lst的Equation Listing复核项数(第 16 篇 1.3 节)。

排查 3-4:MIP 目标值"看起来合理",但其实是被时限截断的解。现象:z.l与预期量级相符,solveStat却不是 1(达到resLim)。根因:MIP 达到资源上限时会返回当前最好的整数解,而modelStat可能指示"整数解"而非"最优"(完整码表以官方文档为准)。解法:把abort$(plan.solveStat <> 1)与abort$(plan.modelStat <> 1)固定成求解层的尾巴;判断"算不完"还是"模型难",先看是不是被resLim/iterLim卡住(第 16 篇 1.6 节),再考虑放宽optCR或调求解器参数。

排查 3-5:敏感性循环里第二个场景的数据被"二次缩放"。现象:low场景的成本反而比重跑前更高/更低得离谱。根因:循环里直接写dem(m,t) = dem(m,t) * scale(s),用的是上一次循环已经缩放过的需求,缩放被累乘。解法:像代码 17-3 那样先备份基线(demBase(m,t) = dem(m,t) ;),循环里从demBase重新计算。另一个同源隐患是inv/short等变量的level在多个solve之间自动传递(第 13 篇)——它不改变最优解,但会让你以为"新一轮是冷启动";判断收敛行为时要显式清零或明确接受残留起点,并按铁律 6 区分"GAMS 传了"和"求解器用了"。

四、动手练习

  • 练习 1(基线跑通):依次运行gams planning_run.gms。判定:(a) 生成planning_report.txt与planning_solution.gdx;(b)modelStat与solveStat都是 1;©kpi('totalCost')等于fixCostPart + varCostPart + shipCostPart + holdCostPart + shortCostPart(用Display核对到小数点后两位)。
  • 练习 2(缺货取舍):把shortCost从 25 改成 60,重跑。判定:(a)kpi('totalShort')不会上升;(b) 能解释"缺货惩罚提高后,模型是否愿意多开工、多留库存"。
  • 练习 3(最小运行量的必要性):把capMinLim方程从Model plan / all / ;里去掉,或把capMin(p)设为 0,重跑。判定:能指出kpi('plantPeriods')与逐期产量分布的变化,并说明为什么去掉下界会得到业务上不可执行的"点点开工"方案。
  • 练习 4(big-M 松紧对照):把capMaxLim里的capMax(p) * run(p,t)改成100000 * run(p,t),重跑。判定:(a) 最优目标值不变;(b).lst里求解器报告的节点数/迭代量发生变化;© 能写出结论——big-M 取最紧上界是建模正确性要求,不是性能调优。
  • 练习 5(敏感性重跑):运行gams planning_sens.gms。判定:(a) 生成planning_sensitivity.gdx;(b)kpiS('high','totalCost')不小于kpiS('low','totalCost');© 能把demBase那一行删掉,复现"二次缩放"的静默错误并说清原因。

五、小结与下一篇预告

本篇把产销协同问题落成了可交付的 MIP,核心是四件事:三张清单把业务翻译成模型(决策变量 / 目标 / 约束,没被列进约束清单的规则不该出现在方程里);库存平衡方程承担全部时间耦合(inv0(p)$(ord(t)=1)与inv(p,t-1)$(ord(t)>1)两个互斥条件一次写对首期与后续期,末期结转由目标函数免费完成);binary+ 最紧 big-M + 最小运行量是开停逻辑的标准三件套(只写上界会出现"每期都开工、每期产一点点"的假最优,big-M 永远取该约束最紧的合法上界);稀疏运输变量搭网络流(声明与使用两端都带$link(p,m));而缺货必须用重惩罚的软约束,否则需求高峰会让模型直接不可行。最后用loop+ KPI 表做 ±10% 敏感性重跑,并用一条业务级abort$回归闸门把它锁住。四条铁律在这里落地:许可先讲清(2,MIP 需要支持整数规划的求解器许可)、初值机制说明白(6,多solve之间的level传递与求解器是否采纳)、调优不承诺性能(7)、数值不臆造(10,全部教学示例值)。

第 18 篇《优化调度实战:离散/连续时间调度模型与 MIP 建模技巧》把时间从"期间"细化到"时间格":离散时间与连续时间建模的取舍(精度 vs 规模)、用binary+ big-M 或 SOS 表达顺序与重叠、启动/停机成本与最小运行/停机时间、时间窗与优先级约束,以及用mipTrace与初值给 MIP 搜索加速的工程手法,最后用put输出甘特式文本报表——那是在同一个"时间轴"上,把计划的"月"换成调度的"小时"。

本篇认知问题回显(FAQ)

Q1:一个业务问题(产销协同)怎么变成模型?"决策变量 / 目标 / 约束"三张清单各自怎么列?
A:做法是在动笔写 GAMS 之前先用中文把三张清单写出来。决策变量清单回答"要决定什么":本问题里是各厂各期产量y(p,t)、各连线各期运量x(p,m,t)、各厂各期末库存inv(p,t)、各市场各期缺货量short(m,t)、各厂各期是否开工run(p,t),分别用Variable+Positive Variable+Binary Variable落地。目标清单回答"要最小化什么":总成本最小,由固定开工成本、可变生产成本、运输成本、库存持有成本、缺货惩罚五项构成,写在cost方程里、由Solve plan using mip minimizing z驱动。约束清单回答"什么不能被违反":产能上界与最小运行量、库存平衡、需求满足(可缺货),分别落在capMaxLim/capMinLim/invBal/demSat四条方程上。这张表的关键价值是双向的:没被列进约束清单的业务规则不该出现在方程里;业务上"绝对不允许"的就必须列进来——所有"没约束它、它怎么跑偏了"的困惑,答案都在这一步。

Q2:多期模型的库存平衡方程怎么写才不会在首期越界、又能在末期正确结转?
A:多期模型的时间耦合完全由库存平衡方程承担,语义是"期末库存(t) = 期初库存(t) + 本期产量 − 本期发货量"。首期的"期初库存"是给定初始库存,之后各期的"期初库存"是上期期末库存,两种情形用两个互斥的$条件一次写完:inv(p,t) =e= inv0(p)$(ord(t) = 1) + inv(p,t-1)$(ord(t) > 1) + y(p,t) - sum(m$link(p,m), x(p,m,t)) ;。要点有三:inv(p,t-1)在首期是越界的,靠$(ord(t) > 1)关掉它,GAMS 就不会去访问不存在的t0;=e=是 GAMS 的等式运算符(不是==,后者属于 Python/GAMSpy 侧写法);末期结转不需要额外终端条件——只要让inv(p,t12)留在模型里并进入目标函数(holdCost * inv(p,t)),模型自己就会权衡"提前备货"与"当期生产"的成本。

Q3:"开工与否"为什么必须引入binary变量?big-M 该取多大?"开了就必须产够"的最小运行量怎么加?
A:因为"产能上限"有两种含义必须分清:连续上限(装置运行状态下能产多少,y(p,t) =l= capMax(p))与开停上限(不开工就一点也不能产)。后者是一个"由变量决定的上界",用静态的y.up(p,t)$(...)表达不了,必须引入Binary Variable run(p,t)并用 big-M 耦合:capMaxLim(p,t) .. y(p,t) =l= capMax(p) * run(p,t) ;。big-M 的取值铁律是只能取该约束下最紧的合法上界——本问题里就是capMax(p)本身(y(p,t)允许的最大值),再大就是纯粹松弛;永远不要写100000 * run(p,t)这种"够大就行"的数。最小运行量用下界形式加:capMinLim(p,t) .. y(p,t) =g= capMin(p) * run(p,t) ;。只写上界、不写下界,最优解会变成"每期都开工、每期产一点点"(固定成本被摊到极小批量上),这在业务上根本做不到,是产能模型最典型的假最优。

Q4:运输/调拨的网络流结构怎么表达?为什么运输变量的声明与求和要带同一个$link(p,m)?
A:运输变量天然是网络流结构,x(p,m,t)从工厂流向市场。两条纪律必须同时满足。第一是声明稀疏:只声明实际存在的连线(Set link(p,m) "existing plant-market lanes" / p1.(m1,m2) p2.(m2,m3) p3.(m3,m4) / ;),而不是p × m的全笛卡尔积。第二是使用端也带同一条件:目标里写sum((p,m,t)$link(p,m), shipCost(p,m) * x(p,m,t)),需求满足里写sum(p$link(p,m), x(p,m,t))。为什么两端都要带?因为 GAMS 会把"出现在方程里的索引组合"隐含扩展进符号的定义域——如果只在方程标号处带条件,求和里引入了一个不属于link的(p,m)组合,稀疏声明就白做了,模型规模会按全笛卡尔积增长。改完后用.lst的Equation Listing复核项数是验证手段。

Q5:目标函数的成本项该怎么构成?缺货为什么用惩罚项?"需求 ±10%"的敏感性重跑怎么组织、结果怎么收集?
A:目标由五项构成,每项对应一条业务口径:固定开工成本sum((p,t), fixCost(p) * run(p,t))、可变生产成本sum((p,t), varCost(p) * y(p,t))、运输成本sum((p,m,t)$link(p,m), shipCost(p,m) * x(p,m,t))、库存持有成本sum((p,t), holdCost * inv(p,t))、缺货惩罚sum((m,t), shortCost * short(m,t))。缺货用惩罚项而不用硬约束,是因为硬约束意味着"需求必须满足",一旦产能不足(需求高峰、检修),整个 MIP 直接不可行,你得到的是"没有方案"而不是"代价最小的方案";改成"需求满足方程带一个缺口变量short(m,t)、缺口在目标里被重罚"后模型永远可行,产能充裕时缺口会被压到零。敏感性重跑用loop范式:定义Set s / low, base, high /与scale(s) = 0.9 / 1.0 / 1.1,先把基线需求备份到demBase(m,t)(否则循环里会对已缩放的需求二次缩放,这是最经典的静默 bug),循环体内重算需求、Solve、把结果收进二维参数kpiS(s,*),最后可用一条业务级回归闸门(abort$(kpiS('high','totalCost') < kpiS('low','totalCost')))把"需求升而成本降"这类数据/模型损坏挡在交付之前。注意 MIP 在多次solve之间会带着上一次解的level,但求解器是否采纳为起点取决于它的选项(铁律 6),因此重跑结论只写"本算例上如此",不作性能承诺(铁律 7)。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询