简介:考虑安全约束机组组合的电力系统机组组合(含经济调度)优化资源包,面向电力系统优化研究人员与电气工程专业学生,基于IEEE 30节点测试系统,结合MATLAB与CPLEX求解含安全约束的机组组合与经济调度问题,是理解电力系统运行优化的典型范例。资源共11个文件,包括6个Excel结果表、2个Visio图表、2个MATLAB脚本和1份基本要求说明文档,压缩包仅301KB,体量小巧但覆盖完整。目前已有1960人学习下载。文件覆盖不同热备用率(如0.05、0.2)下的机组最优出力与组合结果、直流潮流下的节点导纳矩阵,以及机组组合优化与分段处理的MATLAB代码,可完整还原从模型构建到求解输出的流程,便于理解功率平衡、备用容量、最小启停时间等约束的设置,也可作为进一步改进算法或扩展至更大系统的仿真基础。
1. SCUC不是让电价最低,而是让计划“敢被执行”
SCUC(Security-Constrained Unit Commitment,安全约束机组组合)是电力系统调度计划里的核心优化问题,但它要解决的不是“电价最低”,而是在约束了线路潮流、备用容量和机组物理特性之后,仍然能找到一组“能安全执行的开停机计划”。我在几个电力系统调度项目里观察到一个反直觉现象:不少团队用经济调度(ED)算出来的方案,表面上单价更低,拉到安全校核里却频繁触发联络线越限,最后只能人工回退开机——所谓“省钱方案”根本执行不下去。SCUC就是把这个回退过程前置,让安全校核参与优化而不是事后仲裁,适合做日前计划、日内滚动计划和新能源消纳评估的从业者。这篇笔记从建模、代码、求解参数、避坑到结果验证,按一条可复现的路径讲完。
2. SCUC建模:安全约束如何把线性规划逼成MILP
2.1 从经济调度到机组组合的“质变”
先理清一条链条:经济调度(ED)是在开机组合已经确定的前提下给各个机组分配出力,变量是连续的,目标函数是凸的,本质上是个LP或QP问题,求解器几毫秒就能出结果。机组组合(UC)则要额外回答“哪台机组、在哪个时段、是开着还是关着”——这就要引入0/1整数变量,问题从LP变成MILP。SCUC在UC的基础上再叠加电网安全约束,也就是把直流潮流的线路限额放进约束里。
这个“质变”带来的不是计算量翻倍,而是数量级爆炸。3台机组24时段,启停状态组合就是$2^{3 \times 24}=2^{72}$,穷举完全不可行。哪怕用分支定界,收敛快慢也极度依赖模型的紧凑性、初始解质量、以及约束界是否紧。很多初学者把SCUC当成“带整数变量的经济调度”,用ED的思维去调平滑系数,结果要么每天早上醒来发现求解器挂在那里没解完,要么得到一个在边界上抖动、一落地就翻车的计划。
| 问题类型 | 决策变量 | 复杂度 | 典型求解时间 |
|---|---|---|---|
| 经济调度ED | 连续出力 | 多项式时间 | 毫秒~秒级 |
| 机组组合UC | 0/1启停+连续出力 | NP-hard | 秒~分钟级 |
| 安全约束机组组合SCUC | 0/1启停+连续出力+安全约束 | NP-hard且规模更大 | 分钟~小时级 |
我在实际项目里会把UC和ED拆成两个模块来测试:先做不含安全约束的UC,看整数变量和启停成本逻辑是否正确;再单独加载潮流约束,看线路限额是否把某个机组的出力压下来了。这样排查起来比一上来就解完整SCUC快得多。
整数变量的难点不在于变量个数本身,而在于它让可行域变成非凸的:连续问题的最优解一定在边界上,MILP的最优解却可能在枚举树的任何一层。商用求解器(Gurobi、CPLEX)能做的,无非是分支、割平面、启发式初始解三板斧,而这三板斧的效率跟模型的表述方式强相关。我后面会讲,同一个物理问题,用不同的大M值或约束写法,求解时间可能差十倍——这不是玄学,是数值线性代数和组合优化共同作用的结果。
2.2 直流潮流、PTDF与N-1:安全约束的建模选型
SCUC里的“S”就是安全(Security),落到工程上最常表现为线路潮流限额。交流潮流是非线性的,直接丢进MILP几乎没法求解,所以调度计划阶段普遍用直流潮流(DC Power Flow)近似:忽略无功、忽略网损、认为相角差很小。在这个假设下,线路有功潮流可以用线性公式表示。
我一般不在模型里显式建节点相角,而是用PTDF(Power Transfer Distribution Factor,功率传输分布因子)矩阵。PTDF的物理含义是:在某个参考点注入单位功率时,某条线路上会增加的潮流比例。有了它,线路潮流直接写成所有节点注入功率的线性组合:
$F_l = \sum_{i} \text{PTDF}{l,i} \cdot (P{g,i} - D_i)$
其中$P_{g,i}$是节点i上的机组出力总和,$D_i$是该节点负荷。一个3机6节点的算例,PTDF矩阵大约长这样(数字是示意,真实系统的PTDF来自潮流计算程序):
| 线路 | 节点1 | 节点2 | 节点3 |
|---|---|---|---|
| L1 | 0.4 | -0.2 | 0.3 |
| L2 | 0.1 | 0.5 | 0.2 |
| L3 | 0.3 | -0.1 | 0.6 |
注意这些系数可正可负,代表潮流方向。生产环境中PTDF通常由PSSE或BPA的潮流数据导出,工程上要确保参考节点一致,否则整列符号反了还不自知。
N-1安全校核是另一个大头:要求任意一条线路退出运行后,其余线路不越限。如果每条线路的N-1状态都用完整约束建模,变量会爆炸。常见做法是先跑一遍无安全约束的UC,把潮流最重的top-K线路找出来,对它们单独枚举N-1工况并加入约束,再迭代校验,没被枚举到的线路偶发越限再补。这类lazy constraint手法用到现在,工程上没出过可信度问题,后面第4章会展开讲闭环实现。
2.3 完整SCUC模型的数学形式
把目标函数和约束列在一起,方便你对着代码核验:
目标函数(最小化总成本):
$\min \sum_{t=1}^T \sum_{i=1}^N \left[ F_i(p_{i,t}) + SU_i \cdot v_{i,t} \right]$
其中$F_i(p)=\alpha_i p^2 + \beta_i p + \gamma_i$是燃料成本二次函数,$SU_i$是启动成本,$v_{i,t}$是启动动作0/1变量。约束包括:
- 功率平衡:$\sum_i p_{i,t} = D_t$(每时段)
- 系统备用:$\sum_i P^{\max}i \cdot u{i,t} \ge D_t + R_t$
- 出力上下限:$P^{\min}i \cdot u{i,t} \le p_{i,t} \le P^{\max}i \cdot u{i,t}$
- 爬坡约束:$-RD_i \le p_{i,t} - p_{i,t-1} \le RU_i$
- 最小启停时间:启后不得立刻停,停后不得立刻启
- 线路潮流:$F_l^{\min} \le \sum_i \text{PTDF}{l,i}(p{i,t}-D_{i,t}) \le F_l^{\max}$
其中,$u_{i,t}$是运行状态0/1变量。注意爬坡约束只约束出力变化,不能约束启停瞬间——机组从停机到开机那一下的爬坡由启动过程单独处理,很多模型翻车都是把这条混在一起了。
最小启停时间的建模,我习惯用“聚合”写法,假设最小开机时间为$T^{on}$:
$u_{i,t} - u_{i,t-1} \le \sum_{k=1}^{T^{on}} u_{i,t+k-1}$
这个约束的含义是:一旦在$t$时刻开机,未来$T^{on}$个时段都必须保持开机。末端时段的处理需要把约束范围截到T以内,否则越界。另一种常见写法是对每个可能的开机起始时刻设置状态窗口,代码更长但数值更稳。我的经验:在Gurobi上,聚合写法更快;在CPLEX上,两者差异不大,优先用可读性好的那版。
3. 用YALMIP+Gurobi跑通最小SCUC:3机6节点完整代码与参数
3.1 为什么选YALMIP而不是手写C/C++库
做SCUC的代码路径很多,常见的有:Python+Pyomo、MATLAB+YALMIP、直接调Gurobi C API。我自己的项目里MATLAB+YALMIP用得最多。原因有三:一是建模语法贴近数学表达式,写目标函数时不必手动拼系数矩阵,可以显著减少“约束符号写反”这种低级错误;二是YALMIP封装了求解器接口,同一份模型在Gurobi和CPLEX之间切换只要改一行配置,做对比实验时很有用;三是调试方便,用value()随时检查变量求解值,配合assign()就能做热启动和结果回读。
有人会担心YALMIP的封装导致性能损失。实话说,建模层开销在SCUC这种分钟级求解场景里可以忽略,真正耗时是求解器内部的MILP求解。如果你追求极致的性能,可以在模型规模确认稳定后用Gurobi的Python API重写底层热循环,把YALMIP当成原型验证工具——我一般先验证正确性,再优化性能,顺序不要颠倒。
如果你和我一样在Linux服务器上跑调度,记得安装MATLAB的时候把YALMIP的路径加到startup.m里,避免每次都要用addpath(genpath('yalmip'))临时加载。CPU核数多的机器,在sdpsettings里指定solver='gurobi'后,Gurobi会自动利用全部核数,但如果你同时跑多个算例会抢资源,可以用gurobi.threads限制。
3.2 最小SCUC完整代码
下面这个算例是3台火电机组、6个节点、24小时。负荷曲线是我捏的简化数据,PTDF矩阵按2.2节的示意值填空。代码可以直接存成scuc_example.m跑,注意先装好YALMIP和Gurobi并配置license。
% scuc_example.m —— 3机6节点SCUC最小示例 % 模型:目标函数含燃料成本+启动成本;约束含功率平衡、备用、 % 出力上下限、爬坡、最小启停时间、简化PTDF线路潮限。 % 注:负荷与PTDF为示意数据,生产环境请替换为真实系统参数。 clear; clc; yalmip('clear') %% 1. 机组与系统数据 Pmax = [100; 80; 60]; % 最大出力 MW Pmin = [20; 15; 10]; % 最小技术出力 MW a = [0.008; 0.012; 0.018]; % 二次成本系数 $/MWh^2 b = [18; 20; 24]; % 一次成本系数 $/MWh c = [200; 180; 160]; % 空载成本 $/h SU = [120; 90; 70]; % 启动成本 $/次 RU = [50; 40; 30]; % 上爬坡 MW/h RD = [50; 40; 30]; % 下爬坡 MW/h Tu = [4; 3; 2]; % 最小开机时间 h Td = [2; 2; 1]; % 最小停机时间 h T = 24; % 调度周期 % 简化日负荷曲线(尖峰段重点观察爬坡) D = 120 + 15*sin((0:T-1)*pi/12 - pi/3) + ... [zeros(1,6), 20:20:80, 80:-20:0, zeros(1,3)]; %% 2. 决策变量 u = binvar(3, T, 'full'); % 0/1 运行状态 p = sdpvar(3, T, 'full'); % 连续出力 MW v = sdpvar(3, T, 'full'); % 启动动作(连续辅助变量,取0或1) %% 3. 约束集合 Cons = []; for t = 1:T % 出力上下限(u=0时出力强制为0) Cons = [Cons, p(:,t) >= Pmin .* u(:,t)]; Cons = [Cons, p(:,t) <= Pmax .* u(:,t)]; % 启动动作:开机的那一瞬间 v=1 if t == 1 Cons = [Cons, v(:,t) >= u(:,t)]; % 假设初始全停机 else Cons = [Cons, v(:,t) >= u(:,t) - u(:,t-1)]; end Cons = [Cons, v(:,t) >= 0]; % 功率平衡与系统备用 Cons = [Cons, sum(p(:,t)) == D(t)]; Cons = [Cons, sum(Pmax .* u(:,t)) >= D(t) + 0.1*D(t)]; end % 爬坡约束(时段间出力变化限制) for t = 2:T Cons = [Cons, p(:,t) - p(:,t-1) <= RU]; Cons = [Cons, p(:,t-1) - p(:,t) <= RD]; end % 最小启停时间(以最小开机时间为例,最小停机时间对称展开) for i = 1:3 for t = Tu(i)+1:T Cons = [Cons, sum(u(i, t-Tu(i):t-1)) >= Tu(i)*(u(i,t)-u(i,t-1))]; end end % 简化线路潮限:仅约束单条瓶颈线路(用PTDF第1行) PTDF_row1 = [0.4, -0.2, 0.3]; % 示意数据 for t = 1:T % 负荷按节点均摊,真实工程应使用节点注入向量 F1 = PTDF_row1 * (p(:,t) - [D(t)/3; D(t)/3; D(t)/3]); Cons = [Cons, -80 <= F1 <= 80]; % 线路潮流限额 MW end %% 4. 目标函数与求解 Obj = 0; for t = 1:T Obj = Obj + sum(a .* p(:,t).^2 + b .* p(:,t) + c .* u(:,t)); Obj = Obj + sum(SU .* v(:,t)); end ops = sdpsettings('solver', 'gurobi', 'verbose', 0, ... 'gurobi.mipgap', 0.001, 'gurobi.timelimit', 300); sol = optimize(Cons, Obj, ops); %% 5. 结果回读与简单校验 if sol.problem == 0 u_opt = round(value(u)); p_opt = value(p); fprintf('最优成本: %.2f $\n', value(Obj)); fprintf('各机组平均出力: %.1f MW\n', mean(p_opt, 2)); else yalmiperror(sol.problem); end这段代码的核心逻辑是把SCUC拆成“变量定义—约束装配—目标函数—求解器调用”四段。变量定义里,u是真正的整数变量,p在每个时段上被Pmin.*u钳制,保证停机时段不出力;v用连续变量代替整数启动变量,因为目标函数里的启动成本会驱动它取0或1,省掉一批binvar能明显加快分支定界。
约束部分要逐条对齐2.3节的数学形式。爬坡约束我特意用了两条独立不等式,而不是绝对值不等式,原因是绝对值写法在YALMIP里会引入额外变量,且数值上不如分拆后紧。最小启停时间约束写成“如果当前时段开机,则前Tu个时段必须开机”,但要注意末端截断:当t离调度周期末端小于Tu时,约束自然少算几个时段。严格一点的工程里应该在末端补上对后续时段的强制约束,或者把优化周期拉长到超过窗口;如果只是做算例验证,这个简化可以接受。
潮限约束我偷懒只取了PTDF的第一行,模拟一条瓶颈线路。真实工程会用矩阵乘法一次把所有线路都约束进去,写成F = PTDF * (p - D_node)再逐条加限。注意D_node要按节点注入构造,不能像我示例里这样均摊——我这样写是为了让代码短一点、重点不跑偏,量产时别学。
3.3 关键参数怎么调:大M、MIPGap与求解时长
3.2节代码里最值得关注的三个参数是:
第一个是出力上下限里的Pmin .* u。这就是所谓“紧大M”,把隐式的大M值压到机组出力边界上。如果你写成p(t) <= M * u(t)且M=1e6,Gurobi的LP松弛会很松,分支定界效率断崖下跌。我的经验是:SCUC模型的M取值必须用物理极限,比如最大出力、最大爬坡、最大备用需求,不要为了“保险”用一个大很大的数。宁可多写几行、把界算精确,省下来的都是求解时间。
第二个是gurobi.mipgap。这里设0.001,意思是“找到的解与最优解的相对间隙在0.1%以内就停”。日前调度对MIPGap的要求通常1%足够,因为负荷预测本身的误差就大于1%;但如果你做的是安全性要求高的日内滚动,建议收紧到0.05%并配合时间上限。值得提醒的是,MIPGap过小会让求解器长时间挂在收敛曲线上,尤其在最后0.1%的收尾阶段,时间可能翻倍。
第三个是gurobi.timelimit=300。生产调度窗口如果把求解时间卡死在300秒,要接受它是一个软上限——Gurobi到点会返回当前最优整数解,但不会保证它满足最优性条件。我一般会在求解完成后检查sol.problem之外,再读一下Gurobi的gap状态字段,如果gap没到设定值就主动触发一轮降级策略(比如放开备用约束10%或固定机组前6小时状态),而不是把不满足质量门槛的解硬提交。
4. SCUC求解性能调优:热启动、割平面与求解器参数优化
4.1 热启动:把昨天的解变成今天的初值
SCUC在调度场站里通常是滚动执行的:今天下午3点要出明日的计划,半小时后又基于最新预测重算一次。这两轮模型之间,80%的机组状态不会变。如果每次都从零开始做分支定界,完全是在浪费求解器已经算过的信息。热启动(warm start)就是把这些已知状态喂给求解器当初始解。
YALMIP里做热启动很简单,直接给变量赋值再加assign:
u_guess = u_daily_prev; % 上轮计划的0/1矩阵 assign(u, u_guess); ops = sdpsettings('solver', 'gurobi', 'verbose', 0, ... 'gurobi.mipgap', 0.001, 'gurobi.timelimit', 180); optimize(Cons, Obj, ops);这里的原理是:Gurobi拿到u的初始值以后,会把它作为启发式整数解的一部分,用现有MIP start去指引分支的方向。实测下来,在我的项目里热启动能把MILP的首次整数解发现时间从几分钟压到几秒,总求解时长普遍缩短30%以上。前提是初始解本身可行——如果你把上一轮的解塞进来但模型条件已经变了(比如负荷预测上调了10%),求解器会先做一轮修复,反而更慢。
一个工程细节:热启动给的是u(0/1状态变量),p和v不需要给,Gurobi会用LP松弛自动推出它们的可行值。我也不建议把p的连续值写死在初始解里,因为那样会限制求解器在同一个整数组合下寻找更优出力的自由度。若你的u矩阵来自上一次的round(value(u)),记得在喂之前确认它满足最小启停时间约束,否则这个“可行解”实际不可行,热启动可能被求解器当作坏分支点而放弃。
4.2 割平面与lazy constraint:N-1校核的工程姿势
在第2章提到N-1校核如果用枚举约束会爆炸,这里展开讲具体的闭环操作。我的标准循环是四步:
- 先解一个不含N-1的SCUC(只含基本潮流约束);
- 把启停计划和出力结果回放到全系统N-1校核程序里;
- 找出越限的预想事故,把对应的线性约束加回模型;
- 重新求解,重复直到没有新的越限。
这个循环在Gurobi里可以直接用lazy constraint回调实现,也可以像我这样外层手动循环。外层循环的实现更可控,因为调度系统还要留日志、统计迭代次数,回调函数里做这些相对别扭。给一段伪代码:
// 伪代码:外层N-1迭代(语言无关) while (true) { solve_SCUC_model(); violated = n1_check(u_opt, p_opt, system); if (violated.isEmpty()) break; add_cut_constraints(violated); // 把越限事故对应的约束加进模型 }注意循环必须设置最大迭代次数,我设成5轮。如果5轮之后还有新的越限,通常说明潮流模型或基础约束本身有问题,要回去查数据而不是继续加约束。N-1校核程序里需要处理的元素是:每条线路的潮流上下限、事故后网络拓扑变化(表现为PTDF矩阵重算)、以及发电机的紧急出力上限。越限的量化值可以直接取“越限幅度×系数”作为该约束的右端项裕度,这样约束更紧,收敛更快,但小心别紧过头导致模型无解。
4.3 求解器参数优化:线程、对称性与MIPGap的“够用哲学”
MILP求解器有一堆参数看起来很有道理,实际会用到的就那几个。我的建议是先固定一套“保守但稳定”的参数,再针对你的算例逐个试验。Gurobi里我固定这么几个:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| Threads | 4~8 | 显式设置,避免license限制或资源争抢 |
| MIPGap | 0.001(日前)/0.005(滚动) | 不要一上来就0.0001 |
| TimeLimit | 300~600秒 | 软上限,超时取当前整数解 |
| PreSolve | 1(默认) | 消除冗余约束和固定变量 |
| LogFile | 开启 | 每周看一次求解日志 |
对称性是MILP里一个常被忽略的杀手。两台同型号、同燃料成本、接在同一节点的机组,在数学上完全等价,分支定界会对“谁开谁关”的对称分支反复探索,浪费时间。工程上最简单的办法是在模型层面加一个排序约束:让编号小的机组出力不小于编号大的机组(或开机优先级高),人为打破对称。这个方法只能用于机组确实可互换的场景,如果不同机组有不同的检修计划、环保约束,就不能加。
另外一个实用技巧是用松弛解定位性能瓶颈。模型第一次跑不通时,花30秒去掉整数变量、把u改成连续变量[0,1],解一个纯LP。如果LP本身都耗时很长,说明约束规模或数值状态需要先优化,不要急着在MILP层面调参。这一步的诊断顺序能帮你区分“模型问题”和“求解器问题”。
5. SCUC落地避坑:5条血泪记录
5.1 求解器报“infeasible”,但手算觉得明明可行
现象:模型加完备用约束后,Gurobi直接返回infeasible,连初始解都没有。
原因:最常见的是备用约束sum(Pmax .* u) >= D + R在某个时段与最小启停时间约束冲突。比如两台机组的最小开机时间都要求至少开机4小时,而负荷低谷时段只有一台机组能开,备用不足,于是模型找不到任何满足所有约束的组合。手算时你只看了负荷,没把启停窗口的硬约束联动起来看。
解决:先在约束里给备用松弛变量,每一项都加一个小的惩罚项:sum(Pmax .* u) + slack >= D + R,目标函数里给slack一个很高但不过大的成本系数(比如1000 $/MW)。这样模型不会直接infeasible,而是返回一个带备用缺口的解,你再看是哪个时段缺、缺口多大,就能定位是备用本身不够还是被最小启停卡住。定位到具体约束后,再用Gurobi的IIS(Irreducible Inconsistent Subsystem)接口拿到最小不可行子系统,比盲调快得多。
5.2 优化结果每天启停“抖”个不停
现象:连续三天的日前计划,某台50MW机组今天开明天停后天又开,运行人员看到计划都不敢签收。
原因:负荷曲线在临界点附近波动,而启动成本相对燃料成本的比例设得偏低,求解器发现“多用一点高成本机组、而通过启停来省空载费用”的组合比“保持状态连续”更便宜。另一个隐含因素是机组最小停机时间太短,约束没有在时间维度上“压住”启停抖动。
解决:首先检查启动成本是否从合同或实测数据中取的真实值,很多团队图省事把启动成本写成固定值,实际上冷态/温态启动成本差异很大。其次,在目标函数里加一个“启停次数惩罚项”,统计所有机组的启停动作次数并乘一个适中的惩罚系数。最后,把机组最小开机/停机时间参数从严设到与实际物理限制一致。这三板斧下来,抖动基本能被压住;如果还是抖,就要怀疑负荷曲线在临界时段出现了明显的单点毛刺,建议对负荷做一次三小时滑动平均预处理。
5.3 SCUC结果比ED还便宜,这肯定哪里不对
现象:同一个系统,SCUC加上安全约束后总成本反而低于之前纯经济调度的口径,你第一反应是代码写错了。
原因:这不是写错,而是口径不一致。ED如果只做了功率平衡和机组上下限,它没有为备用、爬坡、线路越限支付成本;SCUC显式把备用资源约束进去,会在最优解里强制让某些低效机组开机,表面总成本升高才对。如果你的SCUC比ED低,多半是目标函数里遗漏了某块成本(比如空载成本c .* u没加),或者把线路越限的惩罚从目标函数里去掉了,导致优化器“钻空子”让线路越限换低成本。
解决:把SCUC里的目标函数拆出来与ED逐一对比:燃料成本项、空载成本项、启动成本项是否都包含了。还有一个隐蔽陷阱是二次成本系数a的量纲,$/MWh^2乘以MW^2才是$,如果你在代码里把a写成了0.008/MW,整个成本会被低估一个数量级。成本口径统一之后,SCUC通常比ED高2%~5%,这个量级是合理的。
5.4 同样的模型,换一台机器就解不出来
现象:本地工作站上两分钟出解,镜像部署到客户的16核服务器上,求解时间翻了三倍,甚至解不出可行解。
原因:最常见的是求解器线程数没有显式设置。本地是8核,Gurobi自动用8线程;客户的机器核数不同或超卖严重,自动发现的线程数和实际可用资源不一致。另一种情况是客户服务器上的Gurobi license是限制线程数的基础版,而你本地用的学术license不限线程——同一份代码在不同license下走了完全不同的求解路径。
解决:在sdpsettings里显式写死gurobi.threads=4或8,不要依赖自动探测。如果客户环境是受限license,把线程数设为4往往反而比默认更快,因为MILP分支的并行效率在超过物理核数后是负收益。部署前尽量在目标机器上先跑一个30秒的小算例验证求解器版本和license生效情况,别等到大算例压上去才发现环境问题。
5.5 求解时间卡在5%的MIPGap过不去
现象:时间到了300秒,MIPGap停在5.2%下不去,解出来了但不是最优,运行部门催着要计划。
原因:典型的冷启动+模型对称。冷启动意味着求解器先用启发式花了很多时间才找到第一个可行整数解;对称问题我们已经说过,会让分支树大量探索等价分支。另一个原因是约束里存在比较松的冗余约束,导致LP松弛的边界离整数解很远,割平面切不进去。
解决:先热启动,用上一轮的计划直接喂初值。然后按4.3节的方法加对称破缺约束。最后检查求得的整数解的成本构成——如果是少数几台机组的启动成本在主导解的质量,说明最优解其实已经接近,5%的gap完全够用于日前计划;如果是线路越限约束在主导,那就去看看PTDF里是否有数量级不对的项(比如单位从MW误传成了kW)。不要盲目把MIPGap从0.001收到0.0001,先确认这5%的gap对应的实际成本是多少钱,再决定要不要烧算力。
6. 验证SCUC结果的三个土办法:场景回放、爬坡检验与成本审计
最后一个环节是验证而不是直接信任求解器输出。我在投产前至少会做下面三个检查,成本不高但常年挡住“假优化”。
第一个是场景回放。把SCUC解出的启停计划固定下来,把阶段式的负荷曲线替换成实际运行中带波动的15分钟级序列,重新只做经济调度(SCED),看每台机组在回放场景下是否触及爬坡上限或最小出力下限。如果回放里某台机组出力连续三点在同一方向逼近限值,说明SCUC的爬坡余量取小了。这个回放不需要重新解MILP,连续性LP几秒就出结果,适合作为每次计划发布的自动校验步骤。
第二个是爬坡检验。人为构造最恶劣的负荷爬坡场景,比如用“前一时段最小负荷、后一时段最大负荷”造一条阶跃曲线,检查SCUC结果里总可用上调能力是否覆盖阶跃量。这个场景在真实系统里发生的概率极低,但把它跑通了能反向暴露备用约束是否只写了“总量满足”而没细化到“关键机组的爬坡速度”。我曾在验收时用这个土办法发现某个节点上的快速机组没被调度,备用全压在一台慢机组上。
第三个是成本审计。把SCUC的目标函数值拆成燃料、空载、启动三部分,和调度员手工编制的经典计划对比。注意对比口径要一致:手工计划往往不包含线路越限惩罚,所以只看燃料+启动两块是否合理,不看总额。如果SCUC算出的启动次数比手工计划少很多,要警惕是不是启动成本被低估导致模型偏好“让高成本机组多开”;如果启动次数多,要检查抖动机理是否复发。这三项检查没有一项是数学上的完备证明,但它们结合起来,基本能确认模型没有“为了最优而最优”地脱离物理设备约束。
我现在的习惯是每交付一个SCUC项目,先让用户用这三条把过去的三个月真实运行数据回算一遍,对比“优化计划”和“实际执行计划”的成本差与启停差。第一个样例项目过了这个验收,后面再衔接调度员的工作流就容易了。希望这篇笔记能把SCUC从“一个难啃的MILP黑匣子”变成“一个可以逐步拆解、调试和验证的工程问题”,也帮你在做机组组合优化时少踩几个坑,祝顺利。
本文还有配套的精品资源,点击获取