多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

微电网经济调度Matlab代码实战:风光火储与电动汽车V2G建模

微电网经济调度Matlab代码实战:风光火储与电动汽车V2G建模 前阵子有个刚接触微电网调度方向的同学问我风光火储加上电动汽车经济调度到底怎么写Matlab代码我当时给他的回复是这题真正的难点不是Matlab语法而是建模的时候怎么把“储能SOC递推”“火电启停”“V2G出行需求”这些约束拧在一起还能让求解器在一个合理时间内跑出来。这篇文章就把我这几年做微电网经济调度项目的完整思路写下来从目标函数、约束条件、求解器选型到代码架构、调试心得一次性讲清楚。适合正在写毕业论文、准备竞赛或者做项目仿真、需要一套可复现代码框架的朋友。1. 微电网经济调度到底在算什么——先理清模型骨架经济调度说白了就是“如何安排明天的发电计划让总成本最低同时不违反任何运行限制”。传统电力系统里这个问题叫Unit Commitment进入微电网场景后多了新能源、储能和电动汽车问题就变成了多资源协调优化。Matlab代码只是一个载体真正要搞清楚的是变量、目标、约束这三样东西。1.1 风光火储与电动汽车耦合后调度边界发生了什么变化如果不加风光调度只有火电模型很简单给定负荷曲线求出火电出力使燃料成本最小顶多加个机组爬坡约束。加上风、光以后事情就复杂了因为风光出力不可完全控制你只能决定“用多少弃多少”这就引入了弃风弃光惩罚项。加上储能以后系统有了“时间搬移能力”——白天光伏多、负荷低的时候把电存起来晚上负荷高峰再放出来。真正让问题上升一个难度等级的是电动汽车。EV和普通负荷最本质的区别是它的充电时间、充电功率可以被调度甚至可以通过V2G把电池里的电反向送回微电网。也就是说EV在模型里既是负荷又是电源还是移动的储能装置。这样一来调度边界从“发输配用”单向流动变成了“源网荷储”双向互动。模型里必须有EV的电池SOC状态、出行时段约束、充放电功率限制这些额外边界否则你算出来的方案可能在数字上很好看现实中根本没法执行。1.2 优化目标怎么写才能兼顾经济性与清洁消纳微电网经济调度的目标一般是“系统总运行成本最小”但“成本”这个词要拆开看。它至少包含四部分火电的燃料和启停成本、储能充放电导致的寿命损耗成本、弃风弃光的惩罚成本以及EV参与V2G的充放电费用或补偿。目标函数可以写成如下形式[ \min \sum_{t1}^{T}\left[ C_{g}^{fuel}(P_{g,t}) C_{g}^{su}\cdot v_{on,t} C_{g}^{sd}\cdot v_{off,t}K_{ess}\cdot(P_{ch,t}P_{dis,t})\lambda_w\cdot(P_{w,t}^{avail}-P_{w,t}) \lambda_{pv}\cdot(P_{pv,t}^{avail}-P_{pv,t})C_{ev}^{buy}\cdot P_{ev,c,t} - C_{ev}^{sell}\cdot P_{ev,d,t} \right] ]这里有几个关键点需要特别注意第一火电燃料成本通常写成二次函数 (aP_g^2bP_gc)。如果直接用这个二次项模型就成了MIQP混合整数二次规划Gurobi或者CPLEX都能解但如果想用开源的CBC求解器就得把二次函数做分段线性化转成标准MILP。第二弃风弃光惩罚系数 (\lambda_w,\lambda_{pv}) 不能随便设。如果设置太低优化器会觉得“弃一点风光让火电少停机”更省钱导致明明有清洁能源却不用。一般建议惩罚系数高于火电边际成本比如几百元/MWh到上千元/MWh这样系统才会优先消纳风光。第三EV放电不能是无偿的。车主把电池里的电卖给微电网系统必须给补偿否则V2G永远不可能被调度。这个补偿价格会直接影响V2G是否被启用通常设置为电网购电价格附近再扣掉一小部分电池损耗成本。1.3 约束条件拆解功率平衡、机组爬坡、储能SOC、EV出行需求优化模型的约束条件才是代码的主体也是大多数新手容易搞崩的地方。先列一个最简约束清单功率平衡约束每个时刻风电光伏火电储能放电EV放电 负荷储能充电EV充电火电出力上下限约束、爬坡约束、启停逻辑约束储能充放电功率上限、SOC递推方程、SOC上下限、始末SOC相等约束EV充放电功率上限、SOC递推方程、出行最低电量约束功率平衡约束很容易理解。把储能充电和EV充电放到等式右边是因为它们本质上是需求端消耗电能把储能放电和EV放电放到等式左边是因为它们供给电能。这个位置千万别放反否则结果会诡异的离谱。储能SOC的递推公式是[ SOC_{t1} SOC_t \frac{\eta_{ch}P_{ch,t}\Delta t}{E_{ess}} - \frac{P_{dis,t}\Delta t}{\eta_{dis}E_{ess}} ]这个公式的意思很简单充电时电量增加放电时电量减少充放电效率不能都取1。很多论文里会要求一天调度结束后储能SOC回到初始值这是为了模拟“日循环运行”模式避免储能为了省成本而在最后一小时把电全放干净。EV的约束要更细。假设采用聚合模型把整个车队看作一个大电池那么EV同样有充放电功率上限、SOC递推方程和SOC上下限。但还有一个经常被忽略的约束——出行需求约束。比如车主早上7点要开车上班调度模型就必须保证7点时刻EV的SOC不低于某个值比如90%。如果不加这个约束优化器一定会把EV的电池在夜间放空因为V2G放电是能省钱的。这个约束是EV建模和储能建模最大的区别也是论文评审老师最爱问的问题。2. 方案选型为什么我推荐YalmipGurobi求解MILP模型搭起来了下一步就是用什么工具求解。Matlab本身自带intlinprog可以直接解混合整数线性规划但问题是建模过程极其痛苦。你要手写A矩阵、b向量、lb、ub把所有约束一条条装配进系数矩阵稍微错一个位置就全盘崩掉。所以我的建议是不要直接用intlinprog去搭复杂模型而是用Yalmip作为建模层让Gurobi或CBC去做底层求解。2.1 为什么我选MILP而不是粒子群算法这个点我每次都要强调。现在有很多人一看到经济调度就想到遗传算法、粒子群、灰狼算法但实际工程项目里我几乎不会用启发式算法解决这类问题。原因有三个一是经济调度本质上是带整数变量的优化问题。火电的启停状态是0/1变量储能是否在充电、是否在放电严格来说也是0/1状态。这种问题天然适合MILP/MIQP框架它能保证全局最优解。而粒子群或遗传算法本质是随机搜索你跑十次可能得到十个不同的解而且无法证明哪个是全局最优。二是约束处理很麻烦。启发式算法处理等式约束和不等式约束通常靠罚函数罚函数系数调起来很费劲调不好的结果是约束被轻微违反论文里会被审稿人追着问。三是MILP求解器非常成熟。Gurobi在中小规模问题上比如24时段几十个机组几百个约束通常几秒到几十秒就能收敛到最优。对做研究和写论文来说这足够了。当然启发式算法也不是一无是处。如果未来的场景涉及非常复杂的非线性约束或者要用强化学习处理不确定性那可以另说。但作为微电网经济调度的入门框架MILP是最稳的选择。2.2 Matlab侧环境配置与求解器封装简单说一下环境配置。Yalmip是一个Matlab优化建模工具箱它本身不求解只负责把优化模型翻译成求解器能懂的格式。安装步骤两条命令addpath(genpath(D:\yalmip-master)); savepath;Gurobi需要去官网申请学术license然后安装到系统里并在Matlab里执行gurobi_setup添加到路径。装好之后在Yalmip里设置求解器ops sdpsettings(solver, gurobi, verbose, 1);如果不想用商业求解器也可以用开源的CBC。CBC在Yalmip里的支持非常好小型MILP问题完全够用。但它不支持二次目标函数所以如果你的目标函数里有火电二次成本项要么换成Gurobi要么把二次函数做分段线性化。2.3 风光出力与负荷预测数据怎么变成代码输入经济调度代码的第一步不是建模而是准备数据。项目里我一般把数据分成四类负荷预测曲线、风电预测曲线、光伏预测曲线、设备参数。其中风光预测曲线可以来自历史数据、典型日数据或者用概率分布模拟然后取期望值。为了方便复现我通常用归一化曲线乘以装机容量来生成算例T 24; dt 1; % 归一化风光/负荷曲线实际项目可以从Excel或CSV读入 wind_profile [0.15 0.12 0.10 0.10 0.12 0.15 0.30 0.45 0.55 0.50 0.45 ... 0.40 0.42 0.38 0.35 0.30 0.35 0.50 0.60 0.55 0.45 0.30 ... 0.20 0.15]; pv_profile [0 0 0 0 0 0 0.05 0.20 0.45 0.70 0.85 0.95 ... 0.90 0.80 0.60 0.40 0.15 0.02 0 0 0 0 0 0]; load_profile [0.45 0.42 0.40 0.39 0.38 0.40 0.48 0.60 0.68 0.70 0.72 ... 0.75 0.73 0.70 0.68 0.66 0.80 0.90 0.92 0.85 0.75 0.65 ... 0.55 0.48]; P_w_avail 300 * wind_profile; % 风电装机300 kW P_pv_avail 200 * pv_profile; % 光伏装机200 kW P_load 600 * load_profile; % 负荷峰值600 kW单位这里我统一用kW和kWh成本单位用元。所有输入数据建议单独放到data_init.m函数或者Excel文件里不要在求解主程序里散落着写死的数据。后面要做敏感性分析、换场景的时候会省非常多的精力。3. 代码架构与关键实现从约束构建到结果分析这一章直接讲核心实现。我的代码习惯是一个主脚本解决所有问题函数拆成数据初始化、模型构建、后处理三块这样方便调试也方便把代码贴到论文或报告里。3.1 代码模块划分与运行主流程一套典型文件结构如下main_schedule.m % 主程序建模型、求解、输出 init_case.m % 参数和数据初始化 plot_results.m % 结果可视化 data/ % 存放负荷和新能源预测曲线主程序流程按顺序是参数初始化 - 生成决策变量 - 构建目标函数 - 构建约束 - 调用求解器 - 提取并保存结果 - 绘制图表。如果模型规模变大可以把约束构建拆成build_constraints.m但初学者不用过度设计先保证能跑通。3.2 决策变量定义与目标函数实现细节决策变量是模型的核心。我用Yalmip定义变量代码长这样% 时间参数 T 24; dt 1; % 火电 Pg sdpvar(1, T, full); % 出力 ug binvar(1, T); % 开停机状态 vOn binvar(1, T); % 启动事件 vOff binvar(1, T); % 停机事件 % 新能源 Pw sdpvar(1, T, full); % 实际上网风电 Ppv sdpvar(1, T, full); % 实际上网光伏 % 储能 Pch sdpvar(1, T, full); % 充电功率 Pdis sdpvar(1, T, full); % 放电功率 SOC_ess sdpvar(1, T 1, full); % 电动汽车聚合模型 Pev_c sdpvar(1, T, full); % 聚合充电功率 Pev_d sdpvar(1, T, full); % 聚合放电功率 SOC_ev sdpvar(1, T 1, full);这里我故意把火电启停分成三个变量ug表示运行状态vOn表示“从停机变成运行”的事件vOff表示“从运行变成停机”的事件。启动成本和停机成本要分别作用在这两个事件变量上不能简单地对ug取差分再算最大值因为max函数会引入非光滑逻辑导致问题变复杂。用事件变量的标准做法是Constraints []; ug0 0; % 初始时刻为停机 Constraints [Constraints, vOn(1) ug(1) - ug0]; Constraints [Constraints, vOff(1) ug0 - ug(1)]; for t 2:T Constraints [Constraints, vOn(t) ug(t) - ug(t-1)]; Constraints [Constraints, vOff(t) ug(t-1) - ug(t)]; end因为vOn和vOff是0/1变量而目标函数里启动成本和停机成本都是正的所以优化器不会无故置1。这个写法在Yalmip里很干净也容易扩展到最小开停机时间约束。目标函数我建议先写成线性成本形式把问题框定为MILP更容易调试% 火电线性成本b*Pg c*ug fuel_cost sum(b_g * Pg c_g * ug); start_cost sum(start_cost_per * vOn); stop_cost sum(stop_cost_per * vOff); % 弃风弃光惩罚 curtail_w lambda_w * sum(P_w_avail - Pw); curtail_pv lambda_pv * sum(P_pv_avail - Ppv); % 储能损耗成本简单等效为与充放电电量成正比 ess_cost K_ess * sum(Pch Pdis); % EV充放电费用买电为正放电补偿为负 ev_cost C_ev_buy * sum(Pev_c) - C_ev_sell * sum(Pev_d); Objective fuel_cost start_cost stop_cost curtail_w curtail_pv ess_cost ev_cost;如果你一定要用二次燃料成本可以升级成fuel_cost sum(a_g * Pg.^2 b_g * Pg c_g * ug);这样模型会变成MIQPGurobi照样能解但需要注意后续所有约束不变。若你手头只有CBC就得用分段线性近似代替二次项。两者结果差异通常不大。3.3 关键约束怎么写才不会引入非线性项约束构建是整个代码里最需要耐心的部分。核心约束我建议按“资源类型”分包写每个包单独调试。功率平衡Constraints [Constraints, Pw Ppv Pg Pdis Pev_d ... P_load Pch Pev_c];这个约束没什么悬念关键是确保单位一致不要把kW和MW混着放。火电约束% 出力上下限 Constraints [Constraints, Pmin * ug Pg Pmax * ug]; % 爬坡约束 for t 2:T Constraints [Constraints, Pg(t) - Pg(t-1) ramp_up]; Constraints [Constraints, Pg(t-1) - Pg(t) ramp_down]; end储能约束% 充放电功率上限 Constraints [Constraints, 0 Pch Pch_max]; Constraints [Constraints, 0 Pdis Pdis_max]; % SOC递推 Constraints [Constraints, SOC_ess(2:T1) SOC_ess(1:T) ... (eta_ch * Pch - Pdis / eta_dis) * dt / Ess_cap]; % SOC上下限 Constraints [Constraints, SOC_ess_min SOC_ess SOC_ess_max]; % 初末SOC Constraints [Constraints, SOC_ess(1) SOC_ess_init]; Constraints [Constraints, SOC_ess(T1) SOC_ess_init];这里有一个细节如果担心储能出现“同时充电又放电”的物理不可能状态严格做法是再加两个0/1变量互斥u_ch binvar(1, T); u_dis binvar(1, T); Constraints [Constraints, Pch Pch_max * u_ch]; Constraints [Constraints, Pdis Pdis_max * u_dis]; Constraints [Constraints, u_ch u_dis 1];但在很多场景下如果目标函数里储能损耗成本为正优化器不会傻到同时充放电去白白浪费效率损失。所以这个互斥约束可以在模型规模大、求解慢的时候暂时去掉等确认不出现同时充放电问题后再决定加不加。3.4 电动汽车V2G调度的建模细节EV建模有两种风格单辆建模和聚合建模。单辆建模最直观但如果有100辆车、96个时段就是9600个充放电变量加9600个SOC变量求解规模直接爆炸。所以我一般推荐聚合模型把整个车队看作一个虚拟储能。聚合模型的约束如下% 充放电功率上限可随时间变化表示不同时段在网EV数量不同 Constraints [Constraints, 0 Pev_c Pev_c_max]; % 1xT向量 Constraints [Constraints, 0 Pev_d Pev_d_max]; % 聚合SOC递推 Constraints [Constraints, SOC_ev(2:T1) SOC_ev(1:T) ... (eta_c * Pev_c - Pev_d / eta_d) * dt / EV_cap]; % SOC上下限 Constraints [Constraints, SOC_ev_min SOC_ev SOC_ev_max]; Constraints [Constraints, SOC_ev(1) SOC_ev_init];重点来了如果EV只是普通有序充电没有V2G那你只需要允许Pev_c可变把Pev_d全部置0就行。只有在V2G场景下才允许Pev_d。而且一旦允许V2G就要加上“出行电量保障约束”。最简单的处理方式是在离开时刻约束SOC不低于某一数值% 早上7点对应的时段索引 depart_idx 7; Constraints [Constraints, SOC_ev(depart_idx 1) 0.9 * EV_cap];这里1是因为SOC_ev的长度是T1时刻t对应的状态索引是t1。如果我的聚合SOC代表所有EV总电量那么在车主集中离开的时段之前总电量必须达到期望值否则算法会让车队“集体迟到”。更精细的做法是把EV按离开时间分组。比如7点离开的算一组18点离开的算一组每组都有自己的SOC和充放电变量。这样能避免“平均电量看起来够但某批车实际电量不足”的问题。分组越多模型越精确但变量也越多自己掌握平衡即可。3.5 结果输出与经济指标计算求解调用ops sdpsettings(solver, gurobi, verbose, 1); sol optimize(Constraints, Objective, ops); if sol.problem 0 % 提取结果 Pg_val value(Pg); Pw_val value(Pw); Ppv_val value(Ppv); Pch_val value(Pch); Pdis_val value(Pdis); Pev_c_val value(Pev_c); Pev_d_val value(Pev_d); SOC_ess_val value(SOC_ess); SOC_ev_val value(SOC_ev); TotalCost value(Objective); else disp(sol.info); end拿到结果以后至少要计算三类指标总运行成本直接读TotalCost新能源利用率1 - (sum(P_w_avail-Pw_val)sum(P_pv_avail-Ppv_val))/(sum(P_w_avail)sum(P_pv_avail))EV日充电/放电电量sum(Pev_c_val)和sum(Pev_d_val)用于分析EV参与调度的程度。画图的时候我一般画一张“功率平衡堆叠图”和一张“储能/EV SOC曲线图”。堆叠图用area函数t 1:T; figure; bar(t, [Pw_val; Ppv_val; Pg_val; Pdis_val; Pev_d_val], stacked); hold on; plot(t, P_load Pch_val Pev_c_val, r-o, LineWidth, 1.5); legend(风电,光伏,火电,储能放电,EV放电,负荷充电);这张图能直观看到一天里各资源的出力分配。SOC图则把储能的电池状态和EV的电池状态画在子图里一眼看出有没有超限。4. 从能跑到可信调试经验与常见问题排查代码能跑通只是第一步结果可信才是最终目标。这一章全是实战中踩过的坑。4.1 遇到Infeasible problem先按这个顺序排查“Infeasible problem”是新手最常遇见的错误意思是模型没有可行解。遇到这个报错不要慌按下面顺序查先看求解器返回信息确认问题编号。然后打开Yalmip的约束检查check(Constraints);这个命令会计算每条约束的残差。如果某条约束残差特别大基本上就是问题所在。常见原因依次是功率平衡的单位不统一、储能初始SOC和终值SOC约束冲突、火电爬坡约束写成了绝对值导致同一时刻被反向限制、EV离开时刻SOC约束与充放电功率上限冲突。我自己调试时有一个习惯先从最简单的模型开始加约束。第一步只保留功率平衡和火电上下限确定模型可解第二步加储能第三步加EV第四步再加启停成本和爬坡。每加一块就跑一次能精确知道是哪类约束把模型搞崩的。4.2 如何验证经济调度结果不是“看起来合理”很多同学拿到优化结果看到“火电出力平滑”“储能充放电合理”就觉得成功了。但这远远不够。我检查结果一般用“总量守恒”和“边界校验”两个方法。总量守恒把所有时刻的功率平衡残差打印出来正常情况下每时刻应该是0。如果某几个时刻出现偏差就去追那几条约束。边界校验检查min(Pg_val)是否低于Pmin、max(SOC_ess_val)是否超过上限、EV离开时刻SOC是否达到期望值等。这些可以写一个自动检查脚本跑完直接disp输出越限量。在99%的情况下结果异常都是边界约束被轻微违反或数据单位问题导致的系统性偏移肉眼看图根本看不出来。4.3 计算时间失控降低求解难度的4个实用招如果你把时间粒度从1小时改成15分钟T从24变成96求解时间可能指数级上升。这时候按下面四招降规模第一把非必要整数变量删掉。储能同时充放电互斥约束里的u_ch和u_dis在很多模型里是可以不加的。先不加看结果里有没有同时充放电没有就不管了。这个操作能显著减少分支定界数。第二EV聚合。把100辆车聚合成1个虚拟储能变量数少两个数量级且结果趋势与单辆建模基本一致。论文里只要说明聚合方式合理就可以。第三火电成本用线性或分段线性替代二次项。把MIQP变成MILP以后开源求解器也能跑求解速度会快很多。第四设置MIPGap容差。工程上不需要每次都精确到1e-6的全局最优设一个千分之一的相对gap能大幅加速收敛ops sdpsettings(solver, gurobi, gurobi.MIPGap, 0.001);4.4 常见问题速查表现象主要原因处理思路求解器报Infeasible约束互相矛盾或单位不一致用check(Constraints)定位残差最大的约束逐块放开验证火电频繁启停曲线锯齿缺少最小开停机时间约束增加最小开机/停机时间约束或提高启动成本储能出现同时充放电没有互斥0/1变量且成本项不阻止加入u_ch u_dis 1EV在离开时段SOC过低没有设置出行电量约束在离开时刻加SOC下限约束新能源利用率始终100%惩罚系数不够或模型设置导致不弃风检查可发数据与惩罚系数调整 (\lambda_w, \lambda_{pv})求解时间过长T太大、整数变量太多减小时间颗粒度聚合EV删非必要0/1变量结果对初值敏感优化模型本身没有唯一最优解检查是否忘了终值SOC约束或添加正则化项最后再分享一个实操体会。我做了这么多微电网调度的案例最深的感受是Matlab代码本身只占三分之一的工作量剩下三分之二都在数据整理和边界校核上。每当我拿到一个“结果不对”的模型第一件事不是改代码而是把一个资源逐项关掉跑基准对比。先把系统调成“只有火电负荷”再逐步加入储能、EV、风光每加一个就看总成本怎么变结果是否符合直觉。尤其是EV我建议先跑“仅有序充电”的方案确认没问题了再放开V2G。这样双向功率带来的不可行问题会少很多。按这个流程走你的微电网经济调度代码不仅能跑通还能经得起追问。
返回列表