
电力系统低碳调度这两年确实是论文和工程项目的热门方向尤其是“源荷不确定”这几个字一出来基本就标志着问题难度上了一个台阶。很多做这块的同学卡在同一个地方目标函数怎么建不确定性怎么处理以及最现实的问题——代码写成什么样才能让求解器真的跑得动。这套MATLAByalmip框架下的风电低碳调度代码正好是把这条完整链路打通了。这篇文章我把整套思路、建模过程和实操细节一次性讲透照着捋完你应该能自己把类似的低碳调度问题复现出来也能改造成你自己的场景。1. 整体思路与设计拆解先说清楚这套代码到底在解决什么问题。传统经济调度只盯着煤耗成本或者发电成本最小化低碳调度则是在这个基础上把碳排放因素嵌进目标函数里通常是采用碳交易机制或者阶梯碳价来量化碳排放的经济代价。再加上风电并网后天然带有出力不确定性负荷侧也在波动就让问题从确定性优化变成了不确定性优化。代码要做的就是同时处理这三件事低碳目标建模、风电随机性描述、负荷波动刻画最终求解出一个让系统在成本和碳排放之间取得平衡的调度方案。1.1 为什么选择yalmip作为建模层在MATLAB里做优化建模yalmip几乎是绕不开的最佳选择。它的核心价值在于把建模和求解分离你只需要用人类的思维去描述目标函数和约束条件具体怎么调用求解器交给yalmip处理。比如你要写一个约束“机组出力上下限”用yalmip就是Pmin P Pmax语法上非常接近数学表达式本身不容易出错。对比直接写求解器接口的原始方式yalmip的优势体现在这几个方面对变量类型天然区分binvar定义0-1变量sdpvar定义连续变量intvar定义整数变量混合整数规划写起来非常顺手。约束可以批量添加循环里往约束集合里constraints [constraints, expr]最后一次性传给求解器代码干净利落。切换求解器是改一行代码的事CPLEX、Gurobi、MOSEK、CBC随便换不用动模型本身。很多新手纠结“会不会加了yalmip这层就变慢了”实际上建模层的开销和求解器内部的迭代计算相比可以忽略不计真正吃时间的是求解器在分支定界树里的搜索过程。yalmip反而是帮你节省了大量调试时间。1.2 代码整体架构与模块划分这套代码的模块划分大致是这样一个逻辑数据输入模块机组参数、负荷预测曲线、风电预测出力、碳交易参数 不确定性场景生成模块蒙特卡洛抽样生成风电场景K-means聚类缩减 模型构建模块定义决策变量写目标函数写约束条件 求解与后处理模块调用求解器提取结果绘制调度曲线每个模块之间的数据流向很清晰改参数时不需要牵一发动全身。比如你想把风电预测数据换成自己项目的实测数据只需要改数据输入模块那个文件模型和求解模块不动。这也是我在实际写代码时比较推荐的做法——模块化不是花架子是能让你少熬夜调试的关键习惯。1.3 不确定性建模的两种主流思路处理源荷不确定性学术界和工程界大致走两条路线。第一条是随机规划核心是用场景集来代表不确定性。基本做法是假设风电出力误差服从某个概率分布通常是正态分布或者拉普拉斯分布通过蒙特卡洛抽样生成大量场景再用K-means或者同步回代缩减法把场景数量缩减到一个计算上吃得消的规模每个场景赋予一个概率。第二条是鲁棒优化核心是构造一个不确定集合。风电出力在这个集合内任意取值方案都要可行相当于做最坏情况下的决策。这两种思路各有适用场景这套代码采用场景法加缩减原因是它的结果更贴近实际调度场景的期望表现且在yalmip框架下实现方便、可解释性强审稿人和评审专家也是比较认可这种处理方式的。2. 低碳调度模型核心细节解析模型是整个代码的灵魂求解器只是执行者。低碳调度模型要做到完整且逻辑自洽需要把目标函数和约束条件一步一步拆清楚。2.1 目标函数的层次与碳交易机制低碳调度的目标函数不是简单的单目标而是多目标的加权组合或者有机融合。常见框架是把三个部分叠在一起。第一部分是常规运行成本包括火电机组的煤耗成本、启停成本。煤耗成本通常用二次函数拟合a * P^2 b * P c其中P是机组出力。这里需要留意的是yalmip里直接写二次项没问题但如果机组数量多、时段多二次项会让混合整数二次规划MIQP的求解难度上升。工程上常用分段线性化来处理煤耗曲线代码里可以调用yalmip的pw_poly或者手动引入辅助变量。第二部分是碳排放成本也就是碳交易机制的核心。基础版是给每台机组分配一个碳排放配额实际排放量超出配额的部分需要去碳市场购买低于配额的部分可以出售获利用这个价差把碳排放变成真金白银的成本或者收益。进阶版是阶梯碳价超额排放越多单价越高更能反映碳价对高排放的约束力。第三部分是惩罚项通常是弃风惩罚和失负荷惩罚。风电出力大时常规机组为了安全必须压出力实在压不下去就只能弃风弃风会有经济代价。同理如果负荷过高而机组爬坡跟不上会有失负荷惩罚。这两个惩罚项的存在意义是防止优化结果出现“宁可弃风也不调机组”的偷懒行为。我见过有些代码只写煤耗成本加碳排放成本两项忽略了弃风惩罚。结果就是风电渗透率高的时段优化器会选择大量弃风来避免让煤电机组深度调峰倒不是说逻辑错了而是结果失真。所以惩罚项的设置不是可有可无它直接决定了优化结果会不会出现“看起来最优、实际不可用”的方案。2.2 关键约束条件的构成与含义约束条件方面这套代码里值得重点讲的有以下几类。功率平衡约束是最基础的每个时段内所有机组出力加上风电出力减去弃风量要刚好等于负荷需求。这个约束是相等约束在yalmip里直接写sum(P) W - Wcurtail L就行。机组出力上下限约束处理的是每台火电机组的出力范围这个简单直接不等式约束即可。但要注意冷态时的一些技术出力限制有些机组在深度调峰状态下有最小技术出力下限这个值不是固定的可能跟机组的调峰深度有关。爬坡约束是很多新手容易漏掉的。机组从上一个时段到当前时段的出力变化量要限制在爬坡速率范围内不管是升负荷还是降负荷都有速率限制。这个约束让调度结果真正具有可执行性不然优化出来的方案可能在时间轴上忽高忽低现场根本操作不了。启停约束涉及最小开机时间和最小停机时间约束这是机组物理特性的约束。一台机组刚启动后必须至少运行若干小时才能停反之亦然。这个约束会引入二进制变量也是造成求解变慢的直接原因之一。处理方式是在约束里加布尔量之间的逻辑关系用yalmip表达后交给求解器去解。旋转备用约束考虑的是风电波动带来的不确定性表述通常要求系统正备用容量不低于风电预测误差的一定比例。这个约束是把不确定性融入模型的第一处。如果采用场景法备用约束会变成各个场景下都需要满足的条件约束代码里需要在场景维度上循环添加。碳排放约束可以有两种写法。一种是总量上限约束整个调度周期内总碳排放量不能超过某个值。另一种是前面说的碳交易成本法碳排放通过配额和交易价格进入目标函数。两种方法各有侧重总量约束适合做政策层面的分析碳交易法更适合做经济调度层面的优化。代码里选的是碳交易法好处是直观成本压力直接反映在目标函数里。2.3 为什么碳交易价格选择的临界值很关键我在实际调试中踩过一个很深的坑碳价设得太低优化结果跟传统经济调度几乎没区别碳约束形同虚设碳价设得太高系统会极端地压火电出力哪怕天然气机组成本高得离谱也要上结果总成本暴涨。这个临界值需要仔细校准。这个道理跟“罚款金额决定行为”一样。骑电动车闯红灯罚20元跟罚200元大家的心态完全不一样。碳价就是系统对碳排放的“罚款单价”它决定了机组在“多发新能源”和“多用火电”之间的取舍倾向。代码里可以试几个不同的碳价水平比如30元/吨、60元/吨、90元/吨对比不同碳价下的机组出力和碳排放总量通常能找到一个拐点碳价低于拐点时碳排放不敏感高于拐点后减排效果边际递减。这个拐点就是合适的碳价区间根据项目实际背景设定。3. 实操过程与核心环节实现理论捋清楚了接下来看看代码具体怎么写。这一部分我尽量按实际写代码的顺序来展开你可以跟着过一遍流程。3.1 数据准备与参数定义第一步是把所有参数填进去这是最枯燥但出错率最高的地方。建议把所有参数集中放在一个结构体里或者写一个独立的参数初始化脚本。比如这样% 机组参数 gen.bus [1; 2; 3]; % 节点号 gen.Pmax [400; 300; 200]; % 最大出力 MW gen.Pmin [100; 75; 50]; % 最小出力 MW gen.a [0.0012; 0.0015; 0.0020]; % 煤耗二次系数 gen.b [20; 22; 25]; % 煤耗一次系数 gen.c [100; 110; 120]; % 煤耗常数项 gen.ramp [100; 80; 60]; % 爬坡速率 MW/h gen.minUp [3; 2; 1]; % 最小开机时间 h gen.minDown [2; 2; 1]; % 最小停机时间 h gen.CO2 [0.92; 0.85; 0.78]; % 碳排放强度 t/MWh这套机组参数基本是按照“一大两小”的经典三机系统设计的规模不大但各种约束都能体现。如果你想扩展到IEEE 30节点甚至118节点系统结构是一样的换个参数集就行。负荷预测曲线和风电预测场次通常是读入一个T x N的矩阵T是调度时段数N是场景数。如果只做确定性调度N1就行。如果是场景法比如生成500个场景再做聚类缩减最后保留10到20个典型场景。聚类缩减这一步我用的是kmeans函数但注意kmeans是基于欧氏距离的对时序曲线来说这个相似度度量通常够用。你要是追求效果可以用DTW距离的变体来聚类代码会复杂不少效果也确实有提升。3.2 场景生成与缩减的实现场景生成这块我用拉丁超立方抽样替代普通的蒙特卡洛抽样特别是当变量维度较高时LHS的样本分布更均匀。然后对抽样后的误差序列做Cholesky分解来引入时间相关性但代码里通常简化为直接对每个时段独立抽样。生成完大量场景后直接放进优化模型里注定算不动所以场景缩减这一步必须有。K-means聚类的逻辑是把1000个风电出力场景聚成10类用聚类中心的场景代替该类内的所有场景聚类比例作为该场景的概率。代码大致是这样% 生成500个风电场景 nScen 500; windScen zeros(T, nScen); for i 1:nScen windScen(:, i) windForecast normrnd(0, sigma, T, 1); end % K-means聚类缩减到10个场景 nCluster 10; [idx, C] kmeans(windScen, nCluster); scenProb histcounts(idx, nCluster) / nScen; windTypical C;聚类有个细节值得注意kmeans的初始中心选择用kmeans算法MATLAB默认就是但聚类结果可能受随机初始化的影响。建议多跑几次聚类选最优的那次或者在代码里固定随机种子rng(42)保证结果可复现。论文里写实验时这个固定种子很重要不然审稿人复核时你的结果每次都不同会非常尴尬。3.3 目标函数与约束条件的yalmip实现yalmip里定义决策变量需要厘清哪些是连续变量哪些是0-1变量。火电机组出力是连续变量机组启停状态是二进制变量风电出力和弃风量也是连续变量。节点相角如果是做直流潮流的话也是连续变量。P sdpvar(nGen, T, full); % 机组出力 u binvar(nGen, T, full); % 机组启停状态1表示开机 W sdpvar(nWT, T, full); % 风电实际出力 Wcurtail sdpvar(nWT, T, full); % 弃风量full这个参数很关键不加的话yalmip默认创建的是对称矩阵变量维度不对会直接报错。这个坑我估计十个新手九个踩过务必留意。目标函数的写法也很直观% 运行成本 fuelCost 0; for i 1:nGen for t 1:T fuelCost fuelCost gen.a(i) * P(i,t)^2 gen.b(i) * P(i,t) gen.c(i) * u(i,t); end end % 碳排放成本 emission 0; for i 1:nGen for t 1:T emission emission gen.CO2(i) * P(i,t); end end carbonCost carbonPrice * (emission - quotaTotal); % 弃风惩罚 curtailPenalty curtailPrice * sum(Wcurtail, all); objective fuelCost carbonCost curtailPenalty;这里没有写启停成本如果要做启停成本可以在目标函数里加上startCost * max(0, u(i,t) - u(i,t-1))但max函数在优化里是非光滑的yalmip不一定能直接处理建议用变量代换引入启停辅助变量来线性化。约束条件按前面分析的类别用循环批量添加。爬坡约束的写法是for i 1:nGen for t 2:T constraints [constraints, P(i,t) - P(i,t-1) gen.ramp(i)]; constraints [constraints, P(i,t-1) - P(i,t) gen.ramp(i)]; end end最小启停时间约束稍微复杂需要用到区间视角。最小开机时间约束的表达式是如果机组在t时刻启动那么t到tminUp-1连续时段内必须保持开机状态。用u的相邻差分来表达“启动”这个事件写成约束就是for i 1:nGen for t 2:T for tau t:min(T, t gen.minUp(i) - 1) constraints [constraints, u(i,t) - u(i,t-1) u(i,tau)]; end end end这段逻辑值得好好琢磨左侧u(i,t) - u(i,t-1)等于1时表示机组在这两个时段的交界处启动此时右侧要求后续所有时段u至少为1这就保证满足最小开机时间。同理可以实现最小停机时间的约束。很多写不好这类约束的代码都是从网上抄的自己推一遍才真正明白怎么改。3.4 求解器配置与结果输出yalmip本身不求解它只是翻译官。真正干活的求解器学术界常用的是CPLEX和Gurobi。模型里有二进制变量、有二次项属于混合整数二次规划MIQP这类问题比较考验求解器的实力。Gurobi在MIQP上的表现通常是首选CPLEX略逊但也是主流。求解器的调用方式是加一行选项设置ops sdpsettings(solver, gurobi, verbose, 2, mipgap, 0.001); sol optimize(constraints, objective, ops);solver指定求解器verbose控制日志输出mipgap设置相对最优间隙。0.001意味着求解器可以接受与理论最优值相差0.1%的解。在工程实践中这个精度通常足够了但能大幅缩短求解时间。如果你要求精确最优解把mipgap改成0但做好求解时间翻倍的准备。求解完后先判断sol.problem的值0表示成功解出最优解非0值需要查yalmiperror(sol.problem)来定位是哪种错误。这是调试的第一步很多人做完optimize就直接画图结果画出个乱图还不知道哪出了问题。结果提取用value(P)注意是value()函数不是直接用的变量名因为yalmip变量在求解前是符号对象只有value()才能取到数值。这个细节也容易踩坑。4. 常见问题与排查技巧实录代码写得再顺利调试也是一定要经历的。我把用这套框架时最容易碰到的几个问题和对应排查方法整理出来有些是我自己踩过的坑有些是帮别人看代码时的常见问题。4.1 求解失败报Infeasible问题模型无解是混合整数规划里最常见的问题常见原因有两个。第一个是约束过紧备用约束加上爬坡约束同时收紧时可能出现任何机组组合都无法满足全部约束的情况。排查方法是先注释掉备用约束看看能不能解如果能解说明备用约束给得太苛刻了适当放宽备用比例。第二个是数据错误比如机组Pmin大于Pmax、负荷数据远大于总装机容量、风电预测数据出现负值等。建议写个数据校验脚本在求解前把所有参数的取值范围检查一遍尽早暴露低级错误。如果实在排查不出来可以尝试逐个加约束的方式定位从只有功率平衡约束开始每加一组约束就求解一次看哪组约束加进去之后导致无解问题就锁定在那组约束上。4.2 求解速度慢怎么加速混合整数规划在机组数量多、时段长的时候求解慢是常态。24时段三机组的问题通常几秒到几十秒能出结果但如果扩展到几十台机组可能要几小时。提速的几个方向一是松mipgap比如从0.0001放到0.01求解时间可能缩短一个数量级代价是最优性稍微差一点二是把二次煤耗曲线分段线性化把MIQP转成MILP虽然约束数量变多但通常整体求解更快三是给求解器一个好的初始可行解yalmip里可以用assign函数预赋值变量让求解器从接近最优的起点开始搜。我个人的经验是先不做任何线性化直接用二次函数跑通整个流程验证逻辑确认模型没问题后再考虑用分段线性化加速。别一开始就给自己找加速的麻烦逻辑没对加速没有意义。4.3 yalmip报错维度不匹配出现“DMATRIX”或者“double”类型不一致的报错时检查矩阵维度特别是full有没有加。如果两个sdpvar变量一个定义成对称矩阵一个定义成普通矩阵运算时维度对不上也会报这类错误。我自己遇到过最离谱的一次是P矩阵定义成nGen x T但循环里写的是t x i而不是i x t导致所有维度都乱掉了。调试了半小时发现就是循环下标写反了。所以矩阵形状定义完先跑一遍size(P)确认一下总是不会错的。4.4 画出来的调度结果违反常识求解器报成功解出最优解但画图发现某时段总有大量弃风或者某些机组出力忽大忽小。这种情况通常不是求解错了而是目标函数权重有问题。弃风惩罚设得太低优化器觉得弃风比让机组压出力更划算爬坡约束没加出力曲线就会像锯齿一样上下跳。这种“模型正确但结果不合理”的情况比无解更隐蔽因为求解器不会报错。对策是把惩罚系数调大然后观察曲线是否恢复合理形态。我在实际项目中遇到过弃风惩罚设为50元/MWh时优化结果在风电高峰时段直接弃掉50%风电改成500元/MWh后弃风明显收敛到了合理范围。所以惩罚系数的量级很关键建议先跑一版结果根据不合理程度调整系数量级。4.5 不同求解器结果不一致CPLEX和Gurobi对同一模型算出略微不同的最优值是正常的毕竟是数值优化存在数值精度差异。但如果结果差异很大说明问题本身存在多个接近最优的解决方案这种情况在低碳调度里确实可能出现两个方案总成本非常接近但机组组合方式完全不同。对策是看目标函数值是不是几乎相等如果目标函数值差得不多就不用太纠结具体方案是哪个说明问题是近似退化的。这时候可以加一个正则化项比如在目标函数里加一个极小的项来打破对称性让结果更稳定。5. 低碳调度代码的扩展方向与个人体会这套代码跑通了之后往哪个方向继续扩展我根据自己的经验给你几个参考方向。一是引入储能系统。加储能之后约束会多一组储能SOC状态转移约束目标函数里也可以加储能充放电的成本。储能对低碳调度的意义在于它能平移时间上的电量让风电大发时段多存电负荷高峰时段多放电从机制上进一步降低碳排放。二是考虑碳捕集电厂。碳捕集设备让电厂变成了“低碳甚至负碳”的电源但这部分建模比较复杂需要处理捕集能耗与捕集率之间的权衡关系。捕集率高了电厂净出力会下降你得同时优化捕集率和出力分配。三是做多目标优化。低碳调度本身是“成本最小化”和“碳排放最小化”两个目标的折中。可以用NSGA-II之类的多目标进化算法画出Pareto前沿让决策者根据偏好选方案。不过这类算法与mip求解器的结合需要仔细设计不是简单加个权重就行的。四是日内滚动修正调度。日前调度用的是预测数据日内实际值跟预测总会有偏差。可以做一个模型预测控制MPC框架每15分钟用实时数据重跑一次短时域优化让调度方案始终贴合实际运行状态。这个方向在工程落地时非常实用但工作量比日前调度大不少。我在实际做这类项目时的体会是低碳调度代码的难点不在任何一个单独的环节而在整个链条的衔接。数据、建模、求解、结果分析环环相扣任何一环掉链子都看不到可靠的结果。最有效的做法是先把一个小规模系统完全跑通验证每一步输出都合理再去扩大规模、增加复杂度。一套能跑通的三机系统代码比一堆写了一半的百机系统代码更有价值。最后分享一个调试时的习惯每写完一个模块就打印关键中间量比如场景缩减后的典型场景曲线、求解器的返回值、各机组的出力汇总确认无误再写下一个模块。这比全部写完之后一次性排错快得多也是代码能稳定复现的保障。