基于Yalmip微网优化调度模型实战:蓄电池、风电、光伏与柴油机组协同运行的完整经验
做微网项目这几年,我接触最多的一类问题就是:一个园区或海岛微网里,既有光伏、风电这些靠天吃饭的电源,又有蓄电池这种能充能放的缓冲单元,还带着一台用来兜底的柴油机组,负荷还一天一个样,到底怎么分配各单元出力才能让运行成本最低?这个问题听起来像经验调度就能解决,但真要在电价波动、风光出力不确定性、储能寿命约束这些条件同时压上来的时候,人工定规则根本顶不住。我最后选择的技术路线是MATLAB加Yalmip工具箱,把整个微网优化调度模型写成标准数学规划,交给求解器去算。这一套做下来,方案可迁移、逻辑看得见、结果也稳,实测下来效果相当好。
这篇文章我会从模型思路、单元建模、调度策略、代码实操和坑位排查五个方面完整拆一遍。内容主要面向正在做微电网能量管理、毕业设计选题是优化调度方向,或者想从规则控制转向模型预测控制的工程师和同学。你不需要有很深的数学基础,但如果你对线性规划、混合整数规划的基本概念有点印象,读起来会顺畅很多。
1. 为什么微网调度必须上优化模型
1.1 人工经验调度的天花板
早期做微网控制,很多人习惯用查表法和规则库。比如光伏出力大的时候就多充蓄电池,负荷高了就启柴油机,蓄电池SOC低于30%就强制充电。这种思路在小系统里确实能用,但它有个致命问题:规则之间是割裂的,没法通盘考虑整个系统的经济性。
举个我实际碰到的例子。园区微网在中午光伏出力特别猛,按规则会把蓄电池充满,但当天下午电价尖峰出现在傍晚,蓄电池应该在傍晚放电去套利。如果中午把电全存进蓄电池,傍晚确实有得放,可这时候柴油机因为启动爬坡慢,反而要顶上去一部分负荷,柴油一烧就是钱。规则调度不会告诉你“中午少充一点、让光伏多限一点,傍晚放电避开柴油机出力”的整体收益更大,它只会按固定顺序机械地走。
这就是所谓“局部最优、全局次优”的问题。而优化调度模型做的事情,就是在满足功率平衡、机组运行边界、储能容量约束这些硬条件下,用一个目标函数把所有成本项(购电费、燃料费、启停费、惩罚项)统一起来,一次性算出未来24小时逐时段各单元的最优出力计划。
1.2 从“定规则”到“解优化”
用数学语言重新描述调度问题,本质上是个带约束的优化问题。决策变量是每个时段各台机组的出力、蓄电池充放电功率、柴油机启停状态,约束是系统的物理限制和运行边界,目标是最小化总运行成本。这个描述方式的好处是清晰、不依赖工程师的个人经验,改一个价格参数、加一台机组,模型结构不用大动。
当然,光把问题写出来还不够,要求解它。微网调度模型里因为有柴油机启停这种“要么开要么关”的逻辑,还会引入蓄电池充放电互斥、购售电互斥这些状态量,这些都要用0-1变量来表达。一旦引入整型变量,问题就从线性规划变成了混合整数线性规划(MILP)。MILP求解不是一个“套公式”就能完成的事,需要分支定界算法,这时候就体现出求解器的重要性了。
1.3 为什么选Yalmip这条路线
MATLAB里做优化,老办法是自己写约束矩阵然后调linprog、intlinprog。问题不大还好说,问题稍微复杂一点,约束条件上百条的时候,手工维护矩阵的下标映射简直就是灾难。你想加一个爬坡约束,就得去数清楚状态变量存在矩阵的第几列第几行,改一个维度整片下标都要跟着动,调试一次掉一层头发。
Yalmip解决的就是这个痛点。它是一个建模层工具,让你用贴近数学表达式的代码直接写变量、约束和目标函数,底层调用什么求解器由Yalmip自动调度。我常用的组合是Yalmip配Gurobi,算力要求不高的场景配Cplex或者MATLAB自带的求解器也行。做研究或者工程验证,这个组合是圈子里的主流选择,资料多,踩坑答案也好找。
我个人的体会是,Yalmip的价值不只在省事,更在于它让模型本身的逻辑透明了。约束条件一行行写在脚本里,审模型的同事一眼能看懂你要表达什么物理约束。这种可读性对项目交付来说,比一点点性能提升重要得多。
2. 四种发电单元的建模细节
2.1 蓄电池储能:SOC递推与充放电效率
蓄电池是微网里最灵活的单元,建模的核心是SOC的状态转移方程。简单说,这一时段的剩余电量等于上一时段剩余电量加上充电量、减去放电量,还要乘上对应的效率系数。充电过程有充电效率,放电过程有放电效率,而且这两个效率一般不相等,在模型里要分开写。
% SOC状态递推约束(Yalmip写法示意) % E_bat为储能容量,eta_ch和eta_dis分别为充放电效率 F = [F, SOC(2:T+1) == SOC(1:T) + ... (P_ch(1:T) * eta_ch - P_dis(1:T) / eta_dis) / E_bat];这里有个细节很多人一开始会忽略:蓄电池不能同时充电和放电,必须引入一对互斥的二进制变量来约束。同时还要给充放电功率各设一个上限,而这个上限要和对应的状态变量挂钩——只有状态为1时,功率才能大于0。
% 定义二进制充放电状态变量 x_ch = binvar(T,1); % 1表示充电 x_dis = binvar(T,1); % 1表示放电 F = [F, x_ch + x_dis <= 1]; % 互斥约束 F = [F, 0 <= P_ch <= P_ch_max * x_ch]; F = [F, 0 <= P_dis <= P_dis_max * x_dis];除了能量递推,工程上还要注意SOC的运行范围。理论上电池可以0到100%,但实际为了延长寿命,通常限制在20%到90%之间,这个上下限不是纯数学约束,而是运行策略,直接在边界约束里写死就行。另外,如果做的是多日滚动调度,还要加SOC的周期约束,也就是末尾时段的SOC要回到初始值附近,否则模型会倾向于把电“用光”,而不是为下一天留余地。
2.2 风电与光伏:出力特性与不确定性处理
风电和光伏的建模相对简单,核心是给它们定义一个出力上限曲线。实际调度中,系统的决策变量是“实际消纳多少”,而不是“能发多少”,因为受制于负荷需求和系统调节能力,风光出力有时要削减。于是功率平衡约束里会有一个弃风弃光的松弛项。
% Ppv_forecast和Pwt_forecast为预测可用出力,Ppv_cur和Pwt_cur为削减量 F = [F, Ppv_sched + Ppv_cur == Ppv_forecast]; F = [F, Pwt_sched + Pwt_cur == Pwt_forecast];不确定性是这个环节的大问题。预测曲线只是个期望值,真实出力会有偏差。处理不确定性有三种常见做法:第一种是鲁棒优化,用最恶劣场景做调度,偏保守但可靠性高;第二种是随机规划,生成多个场景,给每个场景赋予概率,模型追求期望成本最小;第三种是最常用的做法,先做确定性日前计划,再用日内滚动修正——每个控制周期刷新一次预测数据,重新求解未来数小时的计划,只执行第一个时段的结果。
我做的项目里,第三种做法落地最多,因为实现难度适中、效果也最稳。你不需要在一开始就把不确定性建模做得很复杂,先把确定性模型跑通、把滚动框架搭好,之后的精度提升都是在这个骨架上加东西。
2.3 柴油机组:成本曲线与运行约束
柴油机的建模是整个模型里最需要对“线性化”有感觉的部分。它的燃料成本不是一条直线,而是一条凸二次曲线——出力越高,边际成本上升越快。但MILP求解器不能直接处理二次目标里的变量乘积,所以通常用分段线性化去逼近那条曲线。
% 分段线性化示意:把出力区间分成N段,各段斜率为k_i % 用lambda_i表示各段的出力,柴油机总出力为各段之和 F = [F, P_diesel == sum(lambda)]; % 各段出力范围由对应二进制变量约束需要说明:Yalmip本身支持二次目标函数,如果给求解器指定的是Gurobi这种支持MIQP的,可以直接写二次项。但实际工程里我一般还是优先做线性化,因为MILP的求解稳定性比MIQP好,尤其是问题规模大了以后,MIQP的求解时间会明显拉长。
柴油机另一个复杂的约束是爬坡率。出力从10%升到80%,柴油机需要时间,不能一步到位,所以相邻两个时段的出力变化量要在限制范围内。
% 爬坡约束:P_diesel(t)与P_diesel(t-1)之差受限于爬坡速率 F = [F, -ramp_down <= P_diesel(2:T) - P_diesel(1:T-1) <= ramp_up];柴油机的启停时间也要建模。如果要求机组至少运行2小时,至少停机1小时,就需要引入额外的状态变量来描述“已经连续运行了几小时”。这个约束写起来比较繁琐,但如果你的项目包含长期运行调度,它不能省。我见过不少模型省略这个约束后,求解结果里柴油机频繁启停,实际现场根本不敢按这个计划执行。
2.4 目标函数怎么拼出经济性
目标函数是整个模型的“指挥棒”。目标函数怎么定,模型就会往哪个方向优化。我的微网模型里目标函数一般包含以下几项:
% 目标函数各项(按常见优先级排序) % 1. 向大电网购电费用(分时电价) % 2. 柴油机燃料费用 % 3. 柴油机启停费用 % 4. 蓄电池充放电惩罚项(防止频繁切换、减少循环损耗) % 5. 弃风弃光惩罚项(尽量消纳新能源) obj = sum(buy_price .* P_buy * dt ... + fuel_cost(P_diesel) ... + start_cost .* z_start ... + penalty_soc * (SOC - SOC_ref).^2 ... + penalty_cur * (Ppv_cur + Pwt_cur));看目标函数就能发现,优化调度不只是“省电费”。启停惩罚和SOC偏离惩罚虽然是软约束,但它们决定了柴油机的运行方式和蓄电池的循环深度。权重系数需要整定,调参的依据不是拍脑袋,而是看调度的行为和实际运行的需要。
举个例子,我把SOC的偏离惩罚权重从1调到10,结果蓄电池变得特别“懒”,输出功率都被SOC约束钳制住,系统的经济性反而下降。这说明权重不是越大越好,需要一个相对平衡的点。
3. 调度策略与“结果良好”的判定标准
3.1 日前计划与日内滚动
我的典型做法是两阶段调度框架。第一阶段是日前计划,用预测数据提前一天生成未来24小时的机组组合与出力计划,时间尺度为一小时。第二阶段是日内滚动,每15分钟或30分钟刷新一次信息,重新求解未来4小时内的计划,但只执行第一个时段。
这个框架看起来不复杂,但它精确对应了实际系统里的两个问题:日前阶段解决机组组合的全局优化,日内阶段解决预测误差的局部修正。如果只做日前计划,预测偏差一大,实际运行必然跑偏;如果只做日内滚动,机组启停这种需要提前准备的决策又没有足够长的视野去做全局优化。
我个人推荐的实施顺序是:先别急着搞随机规划,把确定性日前加日内滚动框架跑通,看一下跟踪效果,再考虑加场景或者鲁棒边界。因为每一步的改动都能对应到实际运行效果的变化,调试起来心里有数。
3.2 并网与孤岛模式切换
微网会有并网运行和孤岛运行两种状态。并网时,系统和大电网之间有功率交换,购电价格和售电价格可以不一致,因此要区分购电量和售电量,而且这两个量要互斥——你不能同时买电又卖电,这又是一个二进制变量组合。
孤岛模式下,联络线功率为零,系统必须完全靠内部机组和储能支撑负荷,约束会变得更紧。如果模型要同时覆盖两种模式,需要用模式变量把两种场景的约束统一表达。多数情况下,项目需求只针对并网模式,那就没必要把模型人为复杂化,把孤岛模式作为约束的一个特例处理即可。
3.3 四条评价指标看结果
做完调度,怎么判断“运行结果良好”?不能光看目标函数值低,得从多个角度衡量:
| 指标 | 计算方式 | 良好标准 |
|---|---|---|
| 经济性 | 总运行成本(购电费+燃料费+启停费) | 低于人工规则调度5%-15% |
| 新能源消纳率 | 实际消纳量/可用风光出力 | 90%以上为良好,低则需检查惩罚权重 |
| 储能SOC健康度 | SOC变化范围及循环次数 | 运行范围内波动平稳,无剧烈反复 |
| 调度可执行性 | 逐时出力是否满足爬坡/启停约束 | 计划曲线平滑,无频繁启停 |
我在项目里最看重的是最后一条。很多模型的优化结果数值很漂亮,但柴油机一会儿开一会儿关,蓄电池充放功率在相邻时段之间来回跳,现场根本不敢拿去用。目标函数里的惩罚项,很大程度上就是为了约束这些“理论上可行、实际没法操作”的结果。
系统里判“结果良好”,我一般看两条曲线:一条是功率平衡曲线的残差,理想情况下应该为零;另一条是蓄电池SOC曲线,它应该像一个带有坡度的浴缸曲线,而不是锯齿状的心电图。如果SOC曲线在相邻时段之间频繁转向,说明惩罚项的权重还不够,或者预测数据里有异常毛刺。
4. 基于Yalmip的实操落地
4.1 环境配置与求解器选择
Yalmip的安装很简单,把工具箱文件夹放到MATLAB路径里,在设置中保存路径就可以了。真正的坑在选择求解器上。常见的选择有Gurobi、Cplex、SCIP、以及MATLAB自带的intlinprog。
我自己的经验是,中小规模测试题首选Gurobi。它的免费学术许可申请很快,安装也方便,性能在同类商业求解器里属于第一梯队。如果不想装商业求解器,intlinprog也能跑,但求解速度慢一个数量级,而且对MILP问题里的数值稳定性要差一些。
设置求解器是在sdpsettings里指定参数:
options = sdpsettings('solver','gurobi', ... 'verbose', 2, ... % 输出求解过程 'gurobi.MIPGap', 0.01); % 设置MIP间隙1%MIPGap这个参数很实用。它表示当前解与最优解之间的相对差距,设置成1%意味着求解器找到的可行解与理论最优之间的偏差不超过1%,再往上追就要花很长时间。工程上用0.5%到1%的间隙,解的质量已经很高了。
提示:Gurobi的许可证配置要注意环境变量设置,装完后在MATLAB里调用前先在命令行终端里确认licence能正常读到。之前在Windows上遇到过明明安装成功但MATLAB报错找不到licence的问题,结果只是环境变量没生效,重启一下MATLAB就正常了。
4.2 模型搭建与求解的主流程
整个模型的求解流程,通俗讲就四步:定义时段、定义变量、拼约束和目标、调用求解。
第一步,定义时间尺度和系统的基准参数。比如调度周期是24小时,时间间隔是1小时,那就定义T=24。系统参数包括负荷曲线、风光预测出力、分时电价、储能容量参数、柴油机参数等等,这些数据要么从现场SCADA系统取,要么用典型的测试数据代替。
第二步,在Yalmip里定义决策变量。连续变量用sdpvar,二进制变量用binvar。需要注意的是,变量定义最好把数组维度一次性写清楚,比如P_buy = sdpvar(T,1),这样后面约束里可以直接用P_buy(t)索引,不至于维度搞混。
第三步,把所有等式和不等式约束拼到一个约束集合F里。每一条约束的写法都对应一条物理规则,建议每写一条就加一个注释,不然三天后回来看代码,根本想不起来某条约束是在限制什么。
第四步,调optimize函数,拿到求解结果后,用value()函数把变量取出来,画图看曲线。大部分情况下,模型不是一次就能调通的,画图是最重要的调试手段——变量曲线是否合理、功率平衡是否闭合,一眼就能判断。
4.3 核心代码骨架解析
这里分享一个我到目前项目里仍在用的简化版核心骨架,去掉了很多业务细节,但主体结构已经能支撑一个完整的调度求解:
%% 基本参数 T = 24; P_load = data.P_load; % 负荷曲线 24x1 Ppv_f = data.Ppv_f; % 光伏预测 24x1 Pwt_f = data.Pwt_f; % 风电预测 24x1 price_buy = data.price_buy; % 购电价 24x1 price_sell = data.price_sell; % 售电价 24x1 %% 定义决策变量 P_buy = sdpvar(T,1); % 购电功率 P_sell = sdpvar(T,1); % 售电功率 P_pv = sdpvar(T,1); % 光伏实际消纳 P_wt = sdpvar(T,1); % 风电实际消纳 P_diesel = sdpvar(T,1); % 柴油机出力 P_ch = sdpvar(T,1); % 蓄电池充电功率 P_dis = sdpvar(T,1); % 蓄电池放电功率 SOC = sdpvar(T+1,1); % 蓄电池SOC x_buy = binvar(T,1); % 购电状态 x_sell = binvar(T,1); % 售电状态 x_dg = binvar(T,1); % 柴油机启停状态 x_ch = binvar(T,1); % 充电状态 x_dis = binvar(T,1); % 放电状态 %% 约束条件 F = []; % 功率平衡约束 F = [F, P_buy + P_pv + P_wt + P_diesel + P_dis == ... P_load + P_sell + P_ch]; % 购售电互斥 F = [F, x_buy + x_sell <= 1]; F = [F, P_buy <= P_line_max * x_buy]; F = [F, P_sell <= P_line_max * x_sell]; % 风光消纳约束 F = [F, P_pv + Ppv_cur == Ppv_f]; F = [F, P_wt + Pwt_cur == Pwt_f]; % 储能约束 F = [F, SOC(2:T+1) == SOC(1:T) + ... (P_ch * eta_ch - P_dis / eta_dis) / E_bat]; F = [F, SOC_min <= SOC <= SOC_max]; F = [F, SOC(1) == SOC_init]; F = [F, x_ch + x_dis <= 1]; F = [F, P_ch >= 0, P_ch <= P_ch_max * x_ch]; F = [F, P_dis >= 0, P_dis <= P_dis_max * x_dis]; % 柴油机约束 F = [F, P_diesel >= P_diesel_min * x_dg]; F = [F, P_diesel <= P_diesel_max * x_dg]; F = [F, -ramp_down <= P_diesel(2:T)-P_diesel(1:T-1) <= ramp_up]; %% 目标函数与求解 obj = sum(price_buy .* P_buy) - sum(price_sell .* P_sell) ... + sum(a * P_diesel.^2 + b * P_diesel + c * x_dg) ... + sum(start_cost * max(0, x_dg(2:T) - x_dg(1:T-1))); options = sdpsettings('solver','gurobi','verbose',1,'gurobi.MIPGap',0.005); sol = optimize(F, obj, options); %% 结果提取 P_buy_opt = value(P_buy); P_diesel_opt = value(P_diesel); SOC_opt = value(SOC);这段代码跑通之后,你会得到一个调度计划和对应的SOC曲线。剩下的就是拿这个计划和实际运行数据去对比,看哪里出现了偏差,再回头调整模型参数。
注意:MATLAB里max(0, x_dg(2:T) - x_dg(1:T-1))这种写法在Yalmip表达式里不是合法的线性表达,直接按这个写会报错。开机的正确建模方式是引入辅助变量z_start,加约束z_start >= x_dg(t) - x_dg(t-1)且z_start >= 0,目标里再把z_start乘上启动成本。
4.4 从单日到多日的扩展
很多同学做到单日调度就觉得完工了,但实际项目里往往需要连续跑一周甚至一个月,这时候就要处理两个新问题。
第一个是SOC的周期耦合。单日模型可以从一个固定初值开始跑,到末尾不管SOC停在哪,反正任务结束了。但连续多日调度时,今天的末尾SOC就是明天的初值,而且从运行角度看,希望一天的结束SOC不要偏离初始值太远,否则系统没有“为明天预留”的余地。
处理方式是加一个末端SOC约束,比如要求SOC(T+1)在SOC_init附近的一个区间内。这个约束看着简单,但对模型求解难度影响不小,因为SOC是全部能量约束的“记忆项”,一旦首尾固定,中间的自由度就少了,求解时间可能增加。工程上常见的妥协是把末端SOC区间放宽,比如允许40%到60%之间,而不是锁定在一个固定点。
第二个是数据的滚动更新。从第二天开始,每次重新求解前都要把最新的实测数据填入,而不是沿用前一天的预测。这块要留好数据接口,避免每跑一天就要改一次脚本。
5. 常见问题排查与经验心得
5.1 常见报错与排查速查表
做微网调度模型时间长了,一些报错反复出现,我整理成了一张速查表:
| 现象 | 常见原因 | 解决方法 |
|---|---|---|
| 求解器报Infeasible | 约束过强,数据与实际矛盾 | 检查功率平衡约束;检查爬坡约束是否与实际调度需求匹配;给平衡约束预留松弛项 |
| 求解时间过长 | 二进制变量太多,MIP规模大 | 压缩时间来减少时段数量;设置合理MIPGap;考虑把部分约束改成优先级分解 |
| 结果出现NaN | 变量未赋值或求解器数值问题 | 检查约束里是否有未初始化的参数;用presolve查看问题规模 |
| 蓄电池SOC锯齿状波动 | 目标函数缺少SOC平滑惩罚 | 增加SOC偏离参考值的惩罚项 |
| 柴油机频繁启停 | 缺少最小启停时间约束或启停成本过低 | 增加最小运行/停机时间约束;调高启停成本权重 |
| 风光消纳率非常低 | 消纳惩罚权重设置过小 | 提高curtailment惩罚权重,或对消纳率设定硬约束 |
这里我想单独说一下Infeasible的排查思路。第一次遇到无解的时候,容易慌,然后开始乱改约束。有一个非常有效的排查技巧:把约束分组,先只保留核心约束(功率平衡、SOC递推),跑一次看是否有解,再把其余约束一组一组加进去,直到哪一组约束加进去之后模型变成不可行,问题就定位到那一组了。这个过程有点像二分查找,效率很高。
5.2 我踩过的几个坑
第一个坑是SOC递推约束的等号写法。在Yalmip里,等式约束直接写F = [F, SOC(2:T+1) == SOC(1:T) + ...]就行,但要特别注意两边的维度必须严格一致。我有一次把SOC写成(T+1,1),递推右边写的是(T,1),结果Yalmip报维度不匹配,还报得挺隐晦。后来习惯性地在每一条约束后面加一行注释标注维度,问题明显减少。
第二个坑是目标函数里二次项的分段线性化。Yalmip里如果直接用P_diesel.^2,Gurobi也能解MIQP,但我在一个100时段的问题里发现求解时间翻了十倍,后来规规矩矩写成MILP,求解时间直接降了一个量级。如果你的模型要反复运行做参数敏感性分析,这点时间差异很关键。
第三个坑是数据归一化。不同的量纲放在一个模型里,数值差异太大容易导致求解器数值不稳定。比如SOC是0到1的量级,而购电功率是几百千瓦的量级,如果两个变量在约束里系数差异太大,求解器在预处理时需要做大量的缩放,处理不好会出现“数值困难”警告。我的做法是,凡是功率类变量统一用MW或者统一的基准值,SOC保持在0到1之间,目标函数里的成本系数按同样的量纲折算。
第四个坑是缺少冷启动的“平滑状态”。第一次跑模型和第二次跑模型,如果初值设置不一致,结果可能有细微差异,这不是bug,但容易让人困惑。解决办法是固定随机种子,或者把初始SOC、初始柴油机状态都作为输入参数显式传给模型。
5.3 这个模型能怎么扩展
调度模型跑通之后,扩展方向很多,我列几个我做过和计划做过的:
- 考虑碳排放成本。在目标函数里把柴油机排放折算成碳成本,配合新能源消纳,模型可以往绿色低碳方向发展。
- 加氢储能或电锅炉。现在园区微网经常配氢储能系统,目标函数会增加氢相关的成本和约束,模型结构还是同一套,只是单元多了。
- 需求响应。把部分可平移负荷建模为可调度变量,让负荷侧也参与优化。
- 多微网互联。多个微网之间有联络线功率交换,变成一个更大的优化问题,Yalmip在这个场景下的优势更明显,因为你会发现手工写矩阵几乎不可能维护。
每个扩展方向都对应新的约束和新的决策变量,但底层框架不变,这也是当时选择“用统一建模工具做一切”这个策略的正确性所在。
最后分享一个小技巧
调试模型时,别急着看总成本,先画功率平衡分解图。把负荷曲线和各类电源出力画在同一张图上,用面积堆叠的方式展示,一眼就能看出哪个时段缺电、哪个时段在弃风弃光。我调了这么多个版本的模型,90%的问题都是先从这张图上发现端倪的。相比盯着目标和迭代日志没有头绪地猜,画图定位要快得多。这个习惯一直留到现在,每次改完模型都会先出图验证再收工。