☰
微电网经济调度Matlab代码实战:风光火储与电动汽车V2G建模
2026/10/10 6:58:16 网站建设 项目流程

前阵子有个刚接触微电网调度方向的同学问我:风光火储加上电动汽车,经济调度到底怎么写Matlab代码?我当时给他的回复是:这题真正的难点不是Matlab语法,而是建模的时候怎么把“储能SOC递推”“火电启停”“V2G出行需求”这些约束拧在一起,还能让求解器在一个合理时间内跑出来。这篇文章就把我这几年做微电网经济调度项目的完整思路写下来,从目标函数、约束条件、求解器选型到代码架构、调试心得,一次性讲清楚。适合正在写毕业论文、准备竞赛或者做项目仿真、需要一套可复现代码框架的朋友。

1. 微电网经济调度到底在算什么?——先理清模型骨架

经济调度说白了就是“如何安排明天的发电计划,让总成本最低,同时不违反任何运行限制”。传统电力系统里这个问题叫Unit Commitment,进入微电网场景后,多了新能源、储能和电动汽车,问题就变成了多资源协调优化。Matlab代码只是一个载体,真正要搞清楚的是变量、目标、约束这三样东西。

1.1 风光火储与电动汽车耦合后,调度边界发生了什么变化

如果不加风光,调度只有火电,模型很简单:给定负荷曲线,求出火电出力使燃料成本最小,顶多加个机组爬坡约束。加上风、光以后,事情就复杂了,因为风光出力不可完全控制,你只能决定“用多少,弃多少”,这就引入了弃风弃光惩罚项。加上储能以后,系统有了“时间搬移能力”——白天光伏多、负荷低的时候把电存起来,晚上负荷高峰再放出来。

真正让问题上升一个难度等级的,是电动汽车。EV和普通负荷最本质的区别是:它的充电时间、充电功率可以被调度,甚至可以通过V2G把电池里的电反向送回微电网。也就是说,EV在模型里既是负荷,又是电源,还是移动的储能装置。这样一来,调度边界从“发输配用”单向流动变成了“源网荷储”双向互动。模型里必须有EV的电池SOC状态、出行时段约束、充放电功率限制这些额外边界,否则你算出来的方案可能在数字上很好看,现实中根本没法执行。

1.2 优化目标怎么写才能兼顾经济性与清洁消纳

微电网经济调度的目标一般是“系统总运行成本最小”,但“成本”这个词要拆开看。它至少包含四部分:火电的燃料和启停成本、储能充放电导致的寿命损耗成本、弃风弃光的惩罚成本,以及EV参与V2G的充放电费用或补偿。

目标函数可以写成如下形式;

[ \min \sum_{t=1}^{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^2+bP_g+c)。如果直接用这个二次项,模型就成了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_{t+1} = 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. 方案选型:为什么我推荐Yalmip+Gurobi求解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);

这样模型会变成MIQP,Gurobi照样能解,但需要注意后续所有约束不变。若你手头只有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:T+1) == 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(T+1) == 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:T+1) == 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的长度是T+1,时刻t对应的状态索引是t+1。如果我的聚合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 <= 1
EV在离开时段SOC过低没有设置出行电量约束在离开时刻加SOC下限约束
新能源利用率始终100%惩罚系数不够或模型设置导致不弃风检查可发数据与惩罚系数,调整 (\lambda_w, \lambda_{pv})
求解时间过长T太大、整数变量太多减小时间颗粒度,聚合EV,删非必要0/1变量
结果对初值敏感优化模型本身没有唯一最优解检查是否忘了终值SOC约束,或添加正则化项

最后再分享一个实操体会。我做了这么多微电网调度的案例,最深的感受是:Matlab代码本身只占三分之一的工作量,剩下三分之二都在数据整理和边界校核上。每当我拿到一个“结果不对”的模型,第一件事不是改代码,而是把一个资源逐项关掉跑基准对比。先把系统调成“只有火电+负荷”,再逐步加入储能、EV、风光,每加一个就看总成本怎么变,结果是否符合直觉。尤其是EV,我建议先跑“仅有序充电”的方案,确认没问题了,再放开V2G。这样双向功率带来的不可行问题会少很多。按这个流程走,你的微电网经济调度代码不仅能跑通,还能经得起追问。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询