复现EI论文里的风-水电联合优化运行模型,听起来是件很正经的事——把发表在EI期刊上的算法思路,用Matlab一行行写出来,跑通,再跟原文结果对比。真正做一遍就会发现,这件事的难点从来不在“会不会敲代码”,而在于论文里默认你懂的、省略掉的东西,比它写出来的东西多得多。
这篇文章我按自己完整跑通一遍该项目的过程来写:从数学模型怎么拆、EI论文怎么筛、求解器怎么选,到Matlab代码怎么搭、结果怎么验、坑怎么排。内容主要面向正在做风电/水电联合调度方向的学生、刚接触电力系统优化且准备用Matlab复现论文的工程师,以及想系统理解“联合优化运行”建模逻辑的读者。看完你至少能搭出一套能跑的确定性模型,再往随机优化、鲁棒优化方向扩展也有个清晰的起点。
1. 这个项目到底在复现什么:风-水电联合优化的数学模型
1.1 目标函数的选择:经济性与消纳怎么权衡
风-水电联合优化运行,本质上是多时段、多电源的机组组合与经济调度问题。风电和水电之所以要放到同一个框架里优化,核心原因是两者在时间尺度上互补:风电出力波动大、可控性差,水电(尤其带库容的常规水电和抽水蓄能)启停快、调节能力强,用水的“惯性”去平抑风的“随机性”,是当前解决风电并网消纳最直接的手段。
绝大多数EI文献把目标函数写成系统总运行成本最小,通式是:
min F = 火电燃料成本 + 机组启停成本 + 弃风惩罚 + 弃水惩罚 + 水电运行维护成本
火电燃料成本一般是出力的二次函数,比如第i台火电机组的成本为 Ci = ai * P_i² + bi * P_i + ci,其中 P_i 是该机组出力。在Matlab里这个二次项可以直接丢给YALMIP处理,如果你的求解器不支持二次目标,也可以对煤耗曲线做分段线性化,把问题降成纯线性规划。复现时优先保留二次项,因为精度高、代码短;等模型太大跑不动再退化成线性。
这里要专门说下弃风惩罚系数。学术论文里写“弃风惩罚设为XX元/MWh”只是一笔带过,但这个系数的量级会直接改变优化结果。我的经验是:惩罚系数至少要显著高于火电边际成本,才能让风电“优先上网”的意图真正落到调度结果里。比如火电边际成本在300元/MWh左右,弃风惩罚系数可以设到500~800元/MWh。但也不是越大越好,我见过有人填了10万元/MWh,结果求解器数值直接出问题,对偶变量和灵敏度分析全部失真。
1.2 约束条件里最容易出问题的三个环节
模型约束一般分四类:系统功率平衡、火电运行约束、水电运行约束、风电运行约束。复现时最常出问题的是下面三处。
第一处,功率平衡约束:
∑P_(i,t) + P_(w,t) + P_(h,t) = L_t
P_(w,t)是风电场t时段出力,P_(h,t)是水电站t时段出力,L_t是系统负荷。看起来是简单的等式约束,但你要确认单位。很多论文喜欢用“标幺值”或“p.u.”表示,复现代码里却混入实际MW数值,等式里的变量必须统一成同一个量纲体系,否则一上来就无解。
第二处,水电转换关系:
P_(h,t) = η * ρ * g * Q_(h,t) * H_net
这里Q_(h,t)是发电流量,H_net是净水头。问题在于这个式子是非线性的,如果水库水位变化明显,净水头也不是常数,模型就变成非线性规划。复现刚上手时,建议把出力直接建模成流量的分段线性函数,即:
P_(h,t) = K1 * Q_(h,t) (流量在区间1) P_(h,t) = K2 * Q_(h,t) + b2 (流量在区间2)
这样做牺牲了一点精度,但模型类型保持不变,能大幅降低调试成本。等线性版本跑通,再考虑要不要用非线性求解器。
第三处,风电出力约束:
0 ≤ P_(w,t) ≤ P_(w,t)^forecast
短周期调度中,更合理的写法是考虑预测误差,让风电出力的场景集落在某个区间内。确定性模型里,只保留上限约束就够了,但要注意风电预测出力数据本身带不带波动性。如果用的是平滑后的曲线,开出来的结果会过于乐观,后面第4章我会讲怎么用敏感性分析暴露这个问题。
1.3 模型假设:复现之前先把这些“默认省略”补出来
论文公式里看到的往往是高度抽象的模型,代码复现时必须把隐含假设补全。以我复现的一个典型系统为例,包含两台火电机组、一座常规水电站、一个风电场,调度周期24小时、步长1小时。
我复盘整理出的关键假设如下,这些在原文里可能只有半句话:
- 忽略网损,电网拓扑简化为单母线模型,所有机组在同一节点;
- 水电站入库流量按已知序列给出,不考虑预报误差;
- 水库水位与库容的关系简化为线性,净水头在调度期内视为常数;
- 火电机组不建模最小启停时间,只建模出力上下限和爬坡约束;
- 风电只考虑出力上限约束,不建模场内风机的详细功率曲线。
有了这套假设,你才能在代码里明确哪些变量该建、哪些该省。实际复现中很多人卡住,就是因为想把论文里所有的细节都实现,结果模型复杂度失控,连可解性都保证不了。EI复现的目的不是“超越原文”,而是“还原原文的逻辑并验证其结论”,所以简化假设只要不偏离原文的核心建模框架,就是可以接受的。
2. 复现前必须解决的三个前置问题
2.1 原始EI论文怎么找、怎么筛
很多人拿到“EI复现”四个字,第一步就去下载文献,但下载的质量参差不齐。以我的经验,EI Compendex检索页里设定检索表达式,例如“wind AND hydro AND optimal AND scheduling”,限定期刊或会议,时间范围近五年。同时把关键词限定在“unit commitment”“economic dispatch”“pumped storage”等子领域,可以缩小范围。
筛查时把握三条标准,能少走很多弯路。
第一条,模型完整度要高。论文里必须给出目标函数、所有约束条件的数学表达式,不能只给一个思路和一个结果图。完全没有公式的文献,除非你只想借鉴思路,否则不建议作为复现对象。
第二条,数据要可得。至少要有负荷曲线、风电出力曲线、水电参数这三类。很多高水平论文用某省电网真实运行数据,但数据不放附录,复现就只能靠猜,猜出来的结果不具备可比性。
第三条,求解方法要主流。优先选用了YALMIP、CPLEX、Gurobi等工具求解的论文,或者明确写了“本文采用混合整数线性规划”的文献。那些用了自研智能算法(遗传算法、粒子群之类)的论文,复现成本高、结果可能还不稳定,作为新手项目并不划算。
2.2 风电不确定性的建模:场景法还是鲁棒优化
风电最大的特征就是不确定性。同样是风-水电联合优化,不同论文采用的不确定性建模方法差别很大,这一步会在很大程度上影响你的代码结构。
场景法是复现时最自然的选择。思路是先用蒙特卡洛抽样生成大量风电出力的可能曲线,再用同步回代削减法把这些场景聚合成若干个典型场景,每个场景带一个概率权重,最后把概率放入目标函数求期望值。好处是物理含义清楚、可以直接用线性规划求解;坏处是场景数量多了以后计算量陡增。
鲁棒优化则是用盒式不确定性集合把风电出力描述在一个区间内,求解最坏情况下的最优方案。好处是不需要概率分布,结果偏保守;坏处是模型里多了一堆对偶变量和max-min结构,新手容易绕晕。
我的建议很直白:首次复现一定先做确定性模型,把风电预测曲线当成确定值输入,整个求解流程跑通后,再考虑把不确定性加进去。我见过太多人一上来就做随机优化,结果出了病态问题根本分不清是场景削减的锅、求解器设置的锅,还是模型本身的锅。一步一步来,反而是最快的方式。
2.3 用哪个求解器组合最省事
Matlab环境下复现这类优化模型,性价比最高的组合是YALMIP + CPLEX,或者YALMIP + Gurobi。YALMIP负责建模,把变量、目标、约束组织成标准形式;CPLEX/Gurobi负责求解。两者都有学术免费许可,个人复现完全可以支撑。
如果你的问题规模不大、纯线性模型,直接用MATLAB自带的linprog或intlinprog也能解决。但一旦涉及二次目标、整数变量、场景展开,自带求解器的算法选择不如CPLEX灵活。下面这个表格是我实际使用时的选型参考。
| 工具组合 | 适用问题 | 许可证 | 复现难度 |
|---|---|---|---|
| linprog/intlinprog | 小规模线性/混合整数规划 | MATLAB自带 | 容易 |
| YALMIP + CPLEX | 中大规模Lp/MILP/QP | 学术免费 | 中等 |
| YALMIP + Gurobi | 大规模LP/MILP/QP | 学术免费 | 中等 |
| MATLAB fmincon | 非线性规划 | MATLAB自带 | 较难 |
实际复现时还要注意求解器版本和Matlab版本的匹配问题。比如Gurobi每年更新版本,老版Matlab可能找不到对应接口;CPLEX在某些操作系统上的许可证路径也会出幺蛾子。这些场景性问题跑一次就知道,但不属于建模核心,遇到了去查询官方文档即可。
3. Matlab代码实现的分步拆解
3.1 数据准备:参数表与曲线怎么组织
我复现时习惯把所有数据集中到一个data_struct里,不散落在工作区。一个调度周期24小时需要准备的数据主要有四块:
- 负荷曲线:24个时段的系统负荷,单位MW;
- 风电预测出力曲线:24个时段的风电场上网预测功率,单位MW;
- 水电站参数:最大发电流量、最小技术发电流量、水库库容上下限、初末库容、入库流量序列;
- 火电参数:每台机组的出力上下限、爬坡速率、煤耗系数。
示例参数表如下,这组参数参考自多篇EI论文附录的典型测试系统,非特定原文:
| 参数 | 数值 |
|---|---|
| 火电1额定功率 | 300 MW |
| 火电1最小出力 | 50 MW |
| 火电1爬坡速率 | 60 MW/h |
| 火电2额定功率 | 200 MW |
| 水电站最大出力 | 180 MW |
| 水库最大库容 | 5000 万m³ |
| 水库最小库容 | 1000 万m³ |
| 风电场装机容量 | 200 MW |
注意库容单位“万m³”和发电流量单位“m³/s”之间的关系。在24小时模型中,一个时段的发电水量等于流量乘以3600秒,如果你把立方米和小时混在一起算,状态转移约束必然失衡。我一般先把所有流量统一折算成“每时段的水量增量”,再放入水量平衡方程,这样最不容易出错。
3.2 用YALMIP搭模型:变量、约束、求解
YALMIP建模的基本思路非常清晰:定义sdpvar变量、写目标、写约束、调用optimize。下面是核心代码框架,做了简化但结构完整。
%% 定义变量 P_f = sdpvar(2, 24); % 两台火电出力,2x24 P_h = sdpvar(1, 24); % 水电出力,1x24 Q_h = sdpvar(1, 24); % 发电流量,1x24 V = sdpvar(1, 24); % 水库库容,1x24 P_w = sdpvar(1, 24); % 风电上网出力,1x24 P_w_curtail = sdpvar(1, 24); % 弃风量 %% 目标函数 fuel_cost = 0; for i = 1:2 fuel_cost = fuel_cost + sum(a(i) * P_f(i,:).^2 + b(i) * P_f(i,:) + c(i)); end curtail_penalty = 500 * sum(P_w_curtail); Objective = fuel_cost + curtail_penalty; %% 约束 Constraints = []; % 功率平衡 for t = 1:24 Constraints = [Constraints, sum(P_f(:,t)) + P_h(t) + P_w(t) == L(t)]; end % 火电上下限与爬坡 for i = 1:2 Constraints = [Constraints, P_f(i,:) >= P_f_min(i)]; Constraints = [Constraints, P_f(i,:) <= P_f_max(i)]; Constraints = [Constraints, abs(P_f(i,2:24) - P_f(i,1:23)) <= ramp_rate(i)]; end % 水量平衡 Constraints = [Constraints, V(1) == V_initial]; for t = 1:24 Constraints = [Constraints, V(t+1) == V(t) + Inflow(t)*dt - Q_h(t) - Spill(t)]; end % 水电出力与流量关系(简化为线性) P_h = k_convert * Q_h; %% 求解 ops = sdpsettings('solver', 'gurobi', 'verbose', 1); sol = optimize(Constraints, Objective, ops);这里面最容易漏的是水量平衡里V(t+1)的定义。如果你把时段数设为24,V也应该有24个或者25个值,索引错位会让约束数量少一条或错位一条。YALMIP不会报错,但结果会异常,这种问题不调试根本看不出来。
3.3 非线性环节的线性化处理
水电出力-流量关系、火电煤耗曲线,是复现时最常见的两个非线性源。前面提到火电煤耗可以直接保留二次项,Gurobi和CPLEX都支持二次目标,所以未必需要线性化。但水电的出力-流量一般是分段函数或包含水头乘积,建议统一转成分段线性约束。
分段线性化的Matlab实现不需要手写大M法,YALMIP内置了binvar和implies等逻辑约束工具,但更高效的方式是直接使用fmincon里的非线性约束?不,为了保持模型线性,推荐用define方法或者手写“特殊有序集”。YALMIP提供了value()和addvar等操作,不过对新手来说,最快的方式是针对每个流量区间单独定义辅助变量和0-1变量。
举一个简单的两段分段函数例子:发电流量Q在[0, 200]之间,出力K1Q;在[200, 400]之间,出力K2Q + b2。代码可以用:
x = binvar(1, 1); % 选择段 Q1 = sdpvar(1, 1); Q2 = sdpvar(1, 1); Constraints = [Q == Q1 + Q2]; Constraints = [Constraints, 0 <= Q1 <= 200 * x]; Constraints = [Constraints, 0 <= Q2 <= 200 * (1-x)]; P_h = k1*Q1 + k2*Q2 + b2*(1-x);这是一个非常经典的建模技巧,把非线性问题变成了混合整数线性规划。建议复现时把所有分段函数都用这个模式写,后续增加分段数也只需扩展同一结构。
3.4 结果输出:曲线、报表和图像
模型求解成功后,第一时间要做的不是写结论,而是把结果可视化,用眼睛审查合理性。我一般输出三类图:
- 第一类,系统出力堆叠图:横轴是24个时段,纵轴是出力,用面积图表示火电、水电、风电各自承担的负荷份额;
- 第二类,水库库容和发电流量变化曲线:看库容是否越界、流量是否平滑;
- 第三类,弃风量柱状图:结合风电预测曲线共同展示,判断弃风发生在哪些时段。
Matlab里用area函数画堆叠图,bar画柱状图,再配合plot叠加预测值曲线,一张图能说明很多问题。我在实测中遇到最典型的情况是:弃风全部集中在凌晨负荷低谷时段,但风功率预测恰恰在傍晚达到峰值,这说明模型可能把风电预测数据排反了时段,或者时区没对齐。如果你不在第一步就画图,这种低级错误可能要排查两三天。
4. 复现结果验证与敏感性分析
4.1 模型正确性校验:退化测试与约束松弛实验
我建议在正式分析和对比论文结果之前,先做三轮验证。
第一轮是退化测试:把风电出力强制设为0,模型退化为纯水电-火电调度,此时的目标函数值应该和手算的近似值一致。比如夜间负荷低谷时火电要么压低出力、要么水电弃水,结果应该符合物理直觉。如果退化解不合理,说明模型里有基础性错误。
第二轮是约束松弛实验:逐一去掉约束(比如去掉爬坡约束或库容约束),观察目标函数值是否逐次下降。去掉一个约束后成本应该下降或持平,如果上升了,说明约束方向可能写反了。比如“V >= V_min”如果误写成“V <= V_min”,去掉它后成本反而上升,这是很好的报警信号。
第三轮拿预测曲线和一个经典案例对比。很多EI论文的测试系统取自IEEE RTS或类似公开数据,你可以找几篇结果图来对照同一负荷时段下的机组出力比例。虽然参数不同,但大的规律一致:负荷高峰火电满发、水电快速顶峰;负荷低谷风电占比大、水电压低出力。
4.2 风电渗透率对调度结果的影响
当模型跑通,我建议立刻做一组风电渗透率敏感性分析。所谓渗透率,就是风电场装机容量占系统最大负荷的比例。把风电容量从10%、20%…逐步调到50%,分别重新求解,记录每个场景下的弃风率、火电平均出力、水电平均调节深度。
下表是我在某次复现测试中得到的一组典型变化趋势:
| 风电渗透率 | 弃风率 | 火电利用小时下降幅度 | 水电调节频次 |
|---|---|---|---|
| 10% | 0% | 基准 | 低 |
| 20% | 2.5% | 约7% | 中 |
| 30% | 8.1% | 约15% | 高 |
| 50% | 19.6% | 约29% | 极高 |
逐时段观察,可以非常直观地看到:风电渗透率超过某一临界值后,弃风率开始加速上升,火电深度调峰压力增大,水电库容频繁在上下限之间摆动。这个“临界点”其实就是风-水电联合优化运行分析这篇文章最想传达的工程结论之一,也是你验证自己复现代码是否正确的重要参考。
4.3 来水变化下的水电调节特征
水电的调节能力从来不是无限的,它的上限由库容和入库流量决定。复现时要特别设计一组“来水变化”实验:把入库流量序列缩放0.5倍、1.0倍、1.5倍,对比调度结果。
实测下来,枯水期(来水少)时,水电站更倾向于把水量留到负荷高峰用,低谷时段甚至会出现所谓的“空闲弃风”现象——明明风很大,但因为水电调峰能力不足,系统不得不弃掉风电。丰水期(来水多)时,水电接近满发,水库长时间在高水位运行,此时的联合优化重点会转向“如何避免弃水”和“如何让火电深度调峰”。
这个分析结果对工程实际很有参考价值:风-水电联合优化的收益,很大程度上取决于水电站的库容调节能力,而不是风电装机本身。你的代码如果可以复现出“来水越少、弃风越严重”的趋势,基本就能说明模型逻辑是对的。
5. 排错记录:从模型不可行到求解超时的完整排查链路
5.1 模型不可行的定位三板斧
“problem infeasible”是复现中最常见也最让人头疼的报错。排查链路我总结成三板斧,按顺序执行。
第一板斧,冻结目标函数,只求可行解。把optimize的第一个参数换成Constraints,目标函数留空或设成零,看求解器是否能找到可行点。如果连可行解都没有,问题出在约束本身;如果可行但有目标时不可行,问题出在目标函数定义(比如把成本写成了负数)。
第二板斧,逐条松弛约束。重点检查功率平衡、库容初末值、爬坡这三类硬约束。实际操作是用一个大正数M乘以某个约束的松弛变量,比如把功率平衡改成:
sum(P_f(:,t)) + P_h(t) + P_w(t) + slack(t) == L(t)
然后看slack往哪个方向偏大,哪个方向出现问题就查哪部分数据。
第三板斧,检查数据单位。我踩过最经典的坑是库容用“万m³”,流量用“m³/s”,水量平衡里没有乘以3600,最终结果所有时段库容都超上限。这种问题用第一板斧和第二板斧都很难发现,因为约束本身自洽,只是物理量纲错了。我的建议是代码里所有输入数据统一注释单位,并且在水量平衡之后立即计算校核变量,用disp打印出来看一眼。
5.2 求解超时与内存占用问题
模型跑不动,大多数情况不是电脑太差,而是模型结构不够高效。场景法里如果你生成500个场景再展开,决策变量数量会膨胀到几万甚至几十万,加上整数变量,求解器自然卡死。
实际处理办法有三个。
第一个,削减场景数量,采用同步回代从500降到15个。一般情况下15个典型场景已经能覆盖95%以上的不确定性信息,目标函数值差异基本在2%以内。
第二个,设置求解器的停止条件。Gurobi和CPLEX都支持MIPGap参数,比如不大于2%就停止。实际运行中,某些分支定界节点后期收敛非常慢,为了那0.5%的精度多跑几个小时,完全没必要。
第三个,把可以聚合的约束向量化写。YALMIP里尽量用矩阵运算而非for循环,比如爬坡约束可以一次写成矩阵形式,代码可读性和求解效率都会提升。
5.3 数值病态:量纲不一致引发的灵异现象
最后一个坑比较隐蔽,但它解释了很多“明明模型没问题,结果却不稳定”的怪象。问题出在变量量纲跨度过大:比如目标函数里火电成本动辄上万元,而库容变量数值在几千万立方米量级,约束矩阵的系数横跨1e-6到1e6。
这种病态数值会让求解器内部的对偶缩放算法出问题,典型现象是:求解两次相同的模型,结果却在小数点后第四位波动,或者加了一条无关紧要的约束后,最优解大幅改变。
标准解法是对大规模变量做标幺化:把所有有功功率都除以系统基准容量,把所有水量除以一个参考体积,再统一用p.u.计算。虽然多了一步转换,但求解稳定性提升非常明显。我在复现几个大规模案例时,标幺化之后求解时间通常能缩短30%~50%,性能数值也稳定得多。
最后再分享一个实操小技巧:复现这类论文,我建议从一开始就建立一个“结果日志”文件夹,每跑完一个算例就保存一份Matlab的.mat结果文件和一张关键图,并附上参数版本说明。这不会直接提升代码质量,但当你面对几十组敏感性分析数据时,它会帮你快速定位“这一组奇怪结果到底是哪组参数试出来的”,也能在后续写论文、做对比时省掉大量重新跑数的时间。