1. 项目概述
这个项目是电力系统方向很典型的“硬骨头”:在一个包含光伏、储能、柴油机(或微型燃气轮机)、空调负荷和电动汽车的微网/虚拟电厂里,做一天的日前优化调度,让总运行成本最低,同时还得保证母线电压、功率平衡、设备出力范围全部满足约束。MATLAB代码是主要载体,优化部分用的是Yalmip工具箱加求解器,模型里同时塞进了需求响应、电动汽车充放电(甚至V2G)、储能SOC递推和分时电价这几个模块。
先说清楚这个代码能解决什么问题。如果只是搭一个微网模型,把各台设备的功率算出来,那用仿真的思路就够了,不需要扯优化。但这里要的是“给定明天24小时的光伏预测、负荷预测、电价曲线,提前把每台机组每小时出多少电、储能充多少放多少、电动汽车什么时候充什么时候放、空调负荷能压多少,全部安排出来”,这就是典型的日前调度问题。它的最终产物不是一组曲线,而是一组“决策变量取值”,而且是全年下来成本最优的那一组。
这篇内容适合谁看?一类是做微电网、虚拟电厂、综合能源系统方向的研究生,需要快速搭出调度模型跑出对比算例;另一类是准备电气类竞赛(比如电工杯A题这种含光伏、储能、分时电价背景的赛题)的学生,需要一个能改能扩的代码底子。如果你完全没接触过Yalmip和优化建模,也能看,但建议先把线性规划、混合整数线性规划的概念过一遍,否则后面读约束条件会有点吃力。
我自己最初做这个项目时走了不少弯路,最大的感受是:这个模型难的不是数学公式,而是怎么把“现实中零零散散的需求响应规则”翻译成“计算机能解的线性约束”。从建模到出结果,每一步都有细节坑。下面把整个思路、代码结构和实操经验完整拆一遍。
2. 整体设计思路与模块拆分
2.1 为什么选择“日前调度+多种响应资源”的组合
微网调度的核心矛盾很简单:明天负荷怎么变、光伏发多少,今天不可能完全知道,但功率平衡又必须实时满足。传统的做法是“固定出力+储能平抑”,调度员当天盯着曲线手动调整。但这种方式碰到高比例新能源接入后越来越吃力,特别是负荷峰谷差拉大、电动汽车充电负荷又叠加上来,单纯靠储能削峰填谷,要么储能容量不够用,要么电池频繁充放损耗太大。
所以现在的做法是把“可调负荷”也变成调度资源。空调、电动汽车这些负荷,本质上都有一定的灵活性:空调房间有热惯性,温度在舒适区间内波动一两度用户感知不强;电动汽车的充电时间可以提前或推迟,甚至可以在电价高峰时段反向放电。把这些资源纳入日前优化,相当于把原来固定不变的“负荷曲线”变成“可调整的决策变量”,系统调节能力一下子就上来了。这背后其实就是需求响应的基本逻辑:用价格信号或激励契约,引导用户调整用电行为,实现削峰填谷。
这套代码把两类需求响应都做了进去:一类是价格型需求响应,即用户会根据分时电价调整用电量;另一类是激励型需求响应,比如空调负荷在高峰时段被直接削减或提前预冷。加上电动汽车的有序充放电,三种灵活性资源放在同一个优化框架下,对不同电价场景、不同渗透率场景的对比分析就非常方便。
2.2 微网与虚拟电厂的建模差异
有一点容易被新手忽略:微网和虚拟电厂在物理模型上不完全是一回事。微网通常有明确的物理边界,有自己的配电线路、控制中心,可以离网运行,母线电压和线路潮流是必须算的物理量。而虚拟电厂强调的是“聚合”,它不一定有物理边界,可能只是把分散在不同位置的分布式电源、储能、可调负荷通过通信和调度中心聚合成一个整体,对外像一座电厂一样参与市场。
但在这套代码里,两者用的是同一套日前优化框架,只是一个把网络约束考虑得多一些,另一个偏向于“聚合功率平衡”。做模型时不必死抠定义差异,关键是明确你要解决什么问题。如果是做微网,建议把网络约束(比如节点电压上下限、线路容量)加进去;如果只是做虚拟电厂的日前申报,那做单母线功率平衡就够了,代码复杂度能降不少。
2.3 代码文件结构与参数配置建议
整套代码如果用模块化的思路来组织,效率会高很多。我习惯把代码分成五个模块:数据输入脚本、模型参数文件、优化模型搭建脚本、求解与结果输出脚本、绘图脚本。
数据输入脚本负责读入光伏出力预测、负荷预测数据,可以是Excel或.mat文件,也可以直接写成矩阵。参数文件单独放一个脚本或者结构体变量,比如para.ESS_capacity、para.PV_power、para.price,这样后面对比不同场景时只改参数文件,不用翻找主程序。优化模型搭建脚本是核心,负责定义决策变量、目标函数和约束,用Yalmip语法把数学公式一行行翻译过来。求解与结果输出脚本负责调用求解器、整理结果、计算各项成本。
参数配置这块,分时电价我建议至少设置峰、平、谷三段,如果想做更细腻的对比,可以细化成八段或二十四段。空调负荷的舒适温度范围和电动汽车的到达/离开时间,这些参数直接影响优化结果,一定要结合场景数据设置,不要随手填。代码里我还会留一个scenario_switch变量,用来切换是否启用需求响应、是否允许V2G,方便快速生成对比算例。
3. 核心模型解析与关键公式
3.1 目标函数:运行成本到底包括哪些项
日前调度的目标函数一般是最小化系统总运行成本。在这个项目里,成本构成主要有四块:
第一是购电成本,即从上级电网买电的费用,用分时电价乘以购买功率再累加一天24小时。这部分在电价高的时段要尽量少买,但负荷必须平衡,所以需要储能和需求响应配合。
第二是柴油机或微型燃气轮机的发电成本,通常用二次函数近似,为了用线性规划求解,会分段线性化或直接用一次项系数近似。我在代码里用的是线性成本系数,加上启动成本(如果模型考虑机组启停)。
第三是储能充放电的损耗成本,或者叫运行维护成本。这个值一般很小,但不能设成零,否则优化会倾向于让储能频繁动作,结果虽然满足约束,但实际工程上不合理。
第四是需求响应成本,即调用空调负荷削减或电动汽车放电后支付给用户的补偿费用。这个价格一般设置在电价阈值附近,既不能太高(增加系统成本),也不能太低(用户没动力参与)。
目标函数写出来就是: min ∑(t=1:24) [ C_pur(t)·P_pur(t) + C_dg·P_dg(t) + C_ess·(P_ch(t)+P_dis(t)) + C_dr·P_shed(t) + C_ev·(P_ev_ch(t)+P_ev_dis(t)) ]
其中C_ev这项在V2G模式下,如果放电补偿小于充电成本,优化自然会选择少放或不放,这个经济信号很有意思,跑出来能看到规律。
3.2 需求响应的数学建模:价格型和激励型
需求响应建模是整个项目里最需要花心思的部分。价格型需求响应,核心是建立“电价和负荷需求”之间的弹性关系。常用的是价格弹性系数矩阵法,即 P_load_res(t) = P_load_base(t) · [1 + ∑(s) E(t,s) · (price(s) - price_base(s)) / price_base(s)]
这里的E(t,s)是自弹性和交叉弹性系数,表示t时段的负荷对s时段电价变动的敏感程度。自弹性为负,交叉弹性为正,具体数值可以从文献里找参考,一般自弹系数在-0.2到-0.5之间,交叉弹性在0.1到0.3之间。这个公式翻译成MATLAB代码并不复杂,但要注意:需求响应后的负荷不能无限偏移,要给定上下限,否则优化会把负荷调到不合理的位置。
激励型需求响应在代码里我主要用空调负荷体现。空调负荷的建模常用等效热参数模型(ETP),核心思想是房间温度变化满足一阶热动力学方程。在实际调度里不需要模拟逐秒温度动态,而是简化成:空调在某个时段被削减功率后,室内温度会在一定时间内上升到上限;通过提前预冷,可以在削减时段内维持舒适温度。
具体建模时,要定义空调的额定功率、热容、热阻、初始温度和目标温度范围。削减量是所有空调可削减功率的聚合量,用线性约束限制削减持续时间,例如同一台空调一天最多削减4小时,或者每削减1小时后必须有1小时恢复期。这些约束没有统一标准,但要对应实际需求响应合同中的条款,代码里做成参数,方便改。
3.3 电动汽车建模:无序充电、有序充电与V2G
电动汽车的建模分三个层次。最简单的是无序充电模型,即电动汽车一回到家就以额定功率充电直到充满,不可调度,这相当于一个固定负荷。这样做基线负荷对比时很有用。
第二层是有序充电模型,充电功率变成决策变量,只要在用户设定的离场时间前充到目标SOC就行。比如用户晚上18点到家,第二天早上8点离开,电池容量60kWh,初始SOC 20%,目标SOC 90%,充电功率上限7kW。模型只需要加两个约束:全天充电电量等于需求电量;充电功率不超过上限;离场前SOC达到目标。充电时间具体在凌晨还是晚上,就完全由优化算法根据电价决定。
第三层是V2G模型,即电动汽车不仅能充电,还能在高峰时段向电网放电。这需要加放电功率决策变量,同时为了提高电池寿命,通常会限制一日内的充放电切换次数。这个用二进制变量加辅助约束实现,会增加求解难度。代码样例里我加了每辆EV一天最多放电两次的限制,既能体现V2G削峰价值,又不会让电池寿命被过度牺牲。
3.4 储能与光伏的处理方式
储能模型用SOC递推方程: SOC(t+1) = SOC(t) + [ P_ch(t)·η_ch - P_dis(t)/η_dis ] · Δt / E_rated
同时要加SOC上下限约束、充放电功率上下限约束、同一时刻只能充电或放电的互斥约束。互斥约束用两个二进制变量加一个sum≤1的约束实现。这个约束判断是很多新手写代码容易漏的地方,一旦漏掉,求解器可能同时给出又充又放的方案,这在物理上是不成立的。
光伏模型的标准化处理相对简单:日前调度用的是预测出力曲线,直接作为已知参数。需要注意的是,弃光约束要不要加。如果允许弃光,就给一个弃光惩罚成本,优化可能会在某些时段主动切掉一部分光伏;如果硬性要求全额消纳,就把光伏出力设为恒定值,作为一个负的负荷直接叠加到功率平衡约束里。两种做法对应不同政策场景,代码里建议做成开关量。
3.5 功率平衡约束与母线潮流约束
功率平衡是日前调度最刚性的约束。对单母线微网来说,就是每个时段所有电源出力加上购电功率,等于所有负荷消耗加上储能充电功率、电动汽车充电功率: P_pur(t) + P_pv(t) + P_dg(t) + P_dis(t) + P_ev_dis(t) = P_load_base(t) - P_dr_curtail(t) + P_ch(t) + P_ev_ch(t)
这里把需求响应削减量放在负荷侧,以负号形式参与平衡,含义是负荷被削减了。要注意单位统一,全部用kW。所有变量的下标对应的都是小时级数据,Δt=1小时,所以功率数值等于对应的电量数值,计算时不会出现积分项。
如果要做带网架的微网,需要加DistFlow潮流方程,节点电压约束用二阶锥松弛。但这套代码以单母线为主,更复杂的网络扩展我会在第五章说明。
4. 实操过程与MATLAB代码实现细节
4.1 环境准备与工具箱安装
运行代码前需要准备MATLAB环境,以及Yalmip工具箱和求解器。建议的版本组合是MATLAB R2020b以上版本搭配Yalmip最新版,求解器用CPLEX或Gurobi(学术版免费)。如果装的是Gurobi,需要额外安装Gurobi的MATLAB接口,并确认gurobi_setup能成功运行。CPLEX的MATLAB接口相对老一些,但稳定性不错。
在命令行里测试一下Yalmip是否正常:
yalmiptest这个命令会列出可用的求解器状态。如果显示Solvernot found,需要检查求解器安装路径是否加入MATLAB路径。很多报错问题都出在这一步,建议先把这个跑通再继续。
安装过程中最容易踩的坑是版本不匹配,比如Gurobi 11只支持MATLAB R2022b以上版本,强行在旧版MATLAB上装会出现接口报错。建议装之前去官方文档查一下支持矩阵。另外,Yalmip可以通过GitHub下载最新的压缩包,解压后把整个文件夹加入MATLAB路径即可。
4.2 核心代码:决策变量定义
在Yalmip中,决策变量的定义用sdpvar、binvar和intvar三种类型。在这个项目里,大部分功率量是连续变量,只有储能充放电互斥开关、电动汽车充放电状态切换需要用到二进制变量。
% 时间尺度 T = 24; % 连续变量:储能充电/放电功率 P_ch = sdpvar(1, T); P_dis = sdpvar(1, T); % 连续变量:从电网购电功率 P_pur = sdpvar(1, T); % 连续变量:柴油机出力(如果考虑) P_dg = sdpvar(1, T); % 连续变量:电动汽车总充电/放电功率 P_ev_ch = sdpvar(1, T); P_ev_dis = sdpvar(1, T); % 连续变量:空调需求响应削减功率 P_dr = sdpvar(1, T); % 连续变量:储能SOC SOC = sdpvar(1, T+1); % 二进制变量:储能充放电状态,1表示充电 u_ch = binvar(1, T); u_dis = binvar(1, T);这里有两个细节要注意。第一,SOC的维度定义成1×(T+1)是方便处理初值和末值约束:SOC(1)是初始荷电状态,是一个已知值;SOC(25)是一天结束时的荷电状态,通常约束它等于初始值,这样储能可以进入第二天的循环调度。第二,二进制变量越多求解越慢,所以能合并的约束尽量合并。比如电动汽车如果只考虑充电不考虑V2G,就不需要定义放电状态对应的二进制变量,直接让充放电互斥约束用P_ev_ch + P_ev_dis == P_ev_total就好。
4.3 约束条件逐条编写与注释
约束条件的编写是整个代码的重头戏。建议采用“先写功率平衡,再写各设备约束,最后写需求响应约束”的顺序。
功率平衡约束写成:
Constraints = []; Constraints = [Constraints, P_pur + P_pv + P_dg + P_dis + P_ev_dis == ... P_load_base - P_dr + P_ch + P_ev_ch];这里P_pv和P_load_base是已知参数向量,直接用+拼接等式约束即可。如果用了sdpvar写的约束集合,之后添加约束都用[Constraints, Constraints, new_constraint]的形式把它串起来,这样到求解前统一交给optimize函数处理。
储能约束包括上下限、递推关系和互斥关系:
% SOC上下限 Constraints = [Constraints, SOC_min <= SOC <= SOC_max]; % SOC末端等于初始值 Constraints = [Constraints, SOC(T+1) == SOC_init]; % 充放电功率上限 Constraints = [Constraints, 0 <= P_ch <= P_ch_max .* u_ch]; Constraints = [Constraints, 0 <= P_dis <= P_dis_max .* u_dis]; Constraints = [Constraints, u_ch + u_dis <= 1]; % SOC递推 for t = 1:T Constraints = [Constraints, SOC(t+1) == SOC(t) + ... (P_ch(t)*eta_ch - P_dis(t)/eta_dis) / E_rated]; end注意充放电功率上限里的.*运算符,表示“逐点相乘”,这是把二进制变量当作开关使用。当u_ch(t)=1时,P_ch(t)才允许大于0;当u_ch(t)=0时,P_ch(t)被强制为0。这种写法比用implies函数快很多,推荐优先使用。
电动汽车约束要区分是聚合模型还是单体模型。聚合模型用一个总功率变量描述所有电动汽车,只限制总功率上限;单车模型则用三维变量P_ev(i,t)表示第i辆车在t时段的充电功率。代码里先用聚合模型,后面扩展成单车模型时,只需把变量维度改一下,其余逻辑完全一样:
% 电动汽车总充电功率不超过所有车的总功率上限 Constraints = [Constraints, 0 <= P_ev_ch <= total_ev_ch_max]; Constraints = [Constraints, 0 <= P_ev_dis <= total_ev_dis_max]; % 考虑V2G时,一天放电量不能超过充电量+初始电量的部分,防止"无中生有" Constraints = [Constraints, sum(P_ev_dis) - sum(P_ev_ch) <= total_ev_battery_capacity * 0.2];第二个约束是我在实际建模中觉得比较关键的:如果不约束电动汽车“总放电电量不能超过总充电电量过多”,优化可能会通过大量放电套利,导致结果不真实。具体放多少电量取决于电池容量和用户设置的最大放电深度,0.2是保守值,可以调。
需求响应约束方面,空调削减功率上下限、累计削减时长限制写成:
% 空调削减功率上下限 Constraints = [Constraints, 0 <= P_dr <= P_dr_max]; % 每时段削减量不能超过该时段空调基础负荷的一定比例 Constraints = [Constraints, P_dr <= 0.3 * P_load_ac_base]; % 累计削减量限制(全天总削减量不超过总负荷的一定比例) Constraints = [Constraints, sum(P_dr) <= 0.15 * sum(P_load_ac_base)];这里的比例参数要根据场景设,参考值是空调负荷弹性系数和合同约定的可中断比例。建议多跑几组不同比例的结果做灵敏度分析,输出表格对比总成本变化。
4.4 求解设置与结果输出
Yalmip调用求解器的语句非常简单:
ops = sdpsettings('solver', 'gurobi', 'verbose', 2, 'debug', 1); optimize(Constraints, Objective, ops);'debug', 1这个选项建议打开。如果模型冗余约束导致无解,Yalmip会输出具体是哪一行约束导致的问题,非常有助于排查。求解完可以用value()提取变量的数值结果:
P_ch_opt = value(P_ch); P_dis_opt = value(P_dis); P_pur_opt = value(P_pur);结果输出建议用一个结构体统一收纳,然后分别计算购电成本、发电成本、需求响应成本、总成本等指标,再绘图和导出Excel表格。绘图部分用stairs画24小时阶梯曲线非常直观,比用plot好看很多。如果能画出“负荷曲线+光伏+储能+购电”的堆叠面积图,一眼就能看出电价高峰时段的调度策略。
4.5 完整运行流程示例
以典型夏日的场景举例。光伏出力峰值设在400kW(中午12点),基础负荷峰值500kW(晚上19点),电价峰值出现在10:00-12:00和18:00-21:00两段,储能容量500kWh,最大充放电功率100kW,电动汽车总共50辆,每辆电池容量60kWh,18:00统一到家,次日8:00离开。
第一步,运行参数设置脚本,把上述数据全部写入para结构体。第二步,运行模型搭建脚本,先定义变量,再写约束。第三步,求解,通常在几秒到几分钟内完成(取决于二进制变量数量)。第四步,输出结果,对比“无需求响应+无序充电”和“有需求响应+有序充电+V2G”两种场景的总成本。
跑完之后你大概率会看到:有序充电+需求响应场景下,购电成本明显下降,因为电动汽车充电被优化到了凌晨低谷时段;空调削减集中在两个电价高峰;V2G是否放电,取决于放电补偿价格和电池损耗成本之间的权衡。这些结论都是可以定量展示出来的,非常适合写进论文或竞赛报告。
5. 常见问题与排查技巧实录
5.1 模型无解或求解器报Infeasible
这是出现频率最高的报错。有几个排查方向和手段:
最常用的手段是让Yalmip告诉你哪个约束不可满足,设置'debug', 1重新运行。Yalmip会用红色高亮标注冲突约束。如果还是看不出问题,就采用“约束逐条注释法”:先把所有约束注释掉,然后一段一段加回来,每加一段就求解一次。这样能快速锁定是储能约束、电动汽车约束还是功率平衡约束导致的不可行。
还有一个隐藏比较深的坑是SOC初始值设置和功率平衡约束不一致。比如初始SOC是0.2,但你在递推约束里算出来的SOC(2)变成了负值,说明放电功率过大。这类问题要靠检查SOC序列是否越界来排查,可以先用value(SOC)打印出来逐小时检查。
另外注意,如果分时电价曲线和负荷曲线写反了单位(千瓦时写成了兆瓦时),功率平衡约束也会出现数量级离谱的情况。统一单位非常关键。
5.2 求解结果出现同时充放电
如果看到储能“边充边放”、电动汽车“边充边放”,大概率是漏了互斥约束,或者二进制变量与连续变量没绑定。需要检查有没有写这两行:
u_ch + u_dis <= 1; 0 <= P_ch <= P_ch_max .* u_ch; 0 <= P_dis <= P_dis_max .* u_dis;如果写了还是出现,检查一下u_ch和u_dis的名称有没有拼写错误,变量替换导致约束没真正加上。这类问题用value(u_ch)和value(P_ch)对比就能发现。
还有一种情况是充放电功率上限值设成了标量而不是向量,MATLAB在做矩阵逐点乘法时会自动扩展,但如果两个变量维度不一致,Yalmip可能不会报错,而是产生一个错误维度的约束。建议把所有参数都预先扩展成1×24的向量,再参与约束运算。
5.3 求解时间过长的优化技巧
随着电动汽车数量增加和V2G普及,二进制变量数量会成倍增长。求解时间从秒级跳到分钟级,甚至内存溢出。常用改进措施有三条。
一是设置求解器的MIP gap容忍度。以Gurobi为例,在sdpsettings里加上'gurobi.MIPGap', 0.01,表示允许1%的最优性差距。电力系统日前调度对最优性要求并不苛刻,0.1%到1%的差距完全可接受,但求解时间能大幅缩短。
二是去掉不必要的二进制变量。如果只是做有序充电(不允许V2G),电动汽车完全不需要二进制变量,充放电互斥的约束也不用加。实际上,有序充电只需要限制充电功率上限和总充电电量即可,这是一个纯线性规划问题,求解速度快一个数量级。
三是用聚合模型替代单体模型。50辆电动汽车如果每辆都是独立变量,就是50×24个连续变量加50×24个二进制变量;如果采用聚合模型,只有24个连续变量加少量二进制变量。如果论文不需要展示每辆车的SOC轨迹,用聚合模型已经足够说明问题。
5.4 结果曲线的“锯齿跳变”原因分析
有读者跑完代码后反馈,储能的充放电曲线呈高频抖动,一会儿充电一会儿放电。这种情况通常是目标函数里缺少对储能动作次数的惩罚。如果不加这个惩罚,储能为了利用每一段微小的电价差,会频繁切换状态,虽然总成本最低,但实际工程中电池寿命损耗很大,决策结果不具备可实施性。
解决方案是给储能充放电功率加上一个小惩罚项,比如每千瓦时0.01元;或者对切换次数加限制,但这就引入了另一个二进制变量集合,求解时间会上升。我建议先加成本惩罚项,这个思路简单且对线性模型友好,实测效果已经足够平滑。同理,电动汽车V2G的频繁充放也可以通过惩罚项抑制。
5.5 参数灵敏度分析的小技巧
好的调度结果不能只看单一场景,还要做参数灵敏度分析。最常用的分析维度是:需求响应比例从10%逐步增加到30%,观察总成本和削峰率的变化;电动汽车渗透率从0到50台逐步增加,观察负荷曲线形状和新增成本。
做这个分析时,不要每次手动改参数重跑代码,建议写一个外层的for循环,批量修改para结构体并保存每组结果。每组结果存成result(i).total_cost这样的结构体数组,最后一次性绘出多条对比曲线。这样论文里需要的对比表格和曲线都能自动生成,比手动重复跑几十次省力得多。
6. 扩展方向与个人实操心得
目前这套模型的扩展空间很大。如果后续要往深度走,建议优先考虑三个方向:一是把单母线扩展成IEEE 33节点配电网模型,加入DistFlow潮流约束,做考虑网络阻塞的日前调度;二是在需求响应模型中加入空调的详细ETP模型,温度变化逐时段仿真,可以更精确地评估舒适度约束对调度结果的影响;三是加进旋转备用约束,应对光伏预测误差,把确定性优化扩展成两阶段鲁棒优化或随机优化。
我在实际使用中还有一个体会:这类代码最终的“护城河”不是目标函数多复杂,而是对实际约束的理解够不够细。以电动汽车为例,如果不考虑用户在出行前的SOC需求,优化出来的调度曲线就是让车一直在低谷时段充电,看起来很完美,但用户第二天早上根本开不了车。这类约束必须在模型里体现,否则结果没有实际价值。
最后分享一个小技巧:调试阶段把时间尺度从24小时缩成3小时,所有功率参数缩小一个数量级,运行速度快非常多,逻辑验证通过后再放大回24小时。我靠这个方法省了大量等待时间,也更容易看清每个约束是否真正生效。如果参数设计得合理,这个缩放过程不需要改动任何建模逻辑,只改输入数据即可。