ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

储能调峰调频联合优化模型及MATLAB实现

2026/10/3 18:27:44 拓冰建站 浏览量
储能调峰调频联合优化模型及MATLAB实现 储能参与调峰调频联合优化最近一年来问我的人特别多。很多人手里有一份单目标的调峰模型也见过单独的调频出清模型但真要把调峰收益、调频容量收益、里程收益、寿命损耗这些目标放进同一个优化框架里用MATLAB把代码写出来能一次跑通的人很少。这篇文章基于我在一个含风电、光伏和储能的新能源场站配套项目中反复迭代过的联合优化模型把调峰和调频放在同一个目标函数里让储能既做能量搬移又做频率支撑同时用充放电功率、SOC、调频备用容量等约束把物理极限框死最后用YALMIP调用CPLEX求解混合整数线性规划。文章会把建模思路、目标函数、约束条件、代码实现和调试技巧一次讲清楚适合电力系统方向的硕博生、储能系统工程师以及想复现算例做方案评估的学习者。1. 为什么调峰调频必须放在一起算1.1 调峰与调频在时间尺度上的互补调峰解决的是长时间尺度上的电量不平衡。午间光伏大发、负荷处于低谷时系统多电傍晚负荷爬升而光伏出力下降时系统缺电储能参与调峰本质上是把多余的电量搬移到缺电时段吸收和释放都是以小时计的。调频解决的是短时间尺度上的功率不平衡。负荷或新能源出力突然波动导致系统频率偏离50Hz储能需要在一两秒内响应AGC指令快速上调或下调有功功率提供向上或向下的动态备用。一个管能量一个管功率时间尺度差得很远但两者在储能容量上是同一个资源的竞争关系。储能装置只有一个额定功率和一个额定容量同一时刻不能既满发放电又提供向上的调频增量也不能既满充充电又吸收向下的调频减量。如果只做调峰优化模型会为了让峰谷套利收益最大化让储能在高峰时段满功率放电、在低谷时段满功率充电结果就是储能每个关键时段都没有调节裕度。真实运行中调度员根本不敢调用这样的储能去调频因为一个AGC指令下来储能已经在满发状态向上无空间可加。联合优化的核心价值就是让优化器在每一步替储能留出调节裕度并且在目标函数里把调频收入变成这种“留裕度”的报酬让调频和调峰在同一个框架下自动协商容量分配。1.2 单独优化最大的问题容量争抢看不见单独做调峰优化的目标通常是运行成本最小或新能源消纳最大储能被当成一个可平移负荷充放电计划完全由电价和净负荷决定。单独做调频优化则通常只看AGC调用成本储能被当成快速调节电源不考虑它还要承担峰谷搬移任务。两个模型单独跑出来的最优解在物理上往往对不上调峰模型给出某时段满充或满放的计划恰好把调频模型需要的备用空间全部占满。即便先后串行计算把其中一个模型的结果当作另一个模型的固定边界也容易得到不可行解因为两个优化模型不会自动协商“这个小时到底留多少MW给调频”。联合优化还能带来一个容易被忽略的收益叠加效应。储能项目的固定投资很高仅靠峰谷价差往往覆盖不了全生命周期成本加上调频容量补偿和里程收益后整个项目经济性才成立。这也是为什么现在很多项目把储能配置成“调峰加调频双功能”而不是只干一件事。当所有收益项放进同一个目标函数后优化器会自动评估同一个MW拿去峰谷套利划算还是留作调频容量拿补偿和里程收益划算从机制上避免了人为拍脑袋定储能容量分配比例的问题。2. 联合优化模型的核心数学结构2.1 决策变量怎么定义才不会出现充放电同时为正储能模型里最常见的错误是把充电功率和放电功率直接定义成两个独立连续变量然后只在目标函数里加一个很小的惩罚系数指望它们不要同时为正。这种做法在混合整数线性规划里的出bug率极高。惩罚系数设置得稍微小一点求解器就会利用同时充放来伪造功率平衡比如一边充电一边放电功率平衡照样满足但能量在储能内部白循环既浪费效率又虚增收益惩罚系数设置得太大又会影响真实最优解干扰正常的峰谷套利决策。正确做法是用二进制变量把充电和放电状态隔开。设u_ch(t)和u_dis(t)是两个0-1变量强制u_ch(t)u_dis(t)≤1然后功率约束写成0 ≤ P_ch(t) ≤ u_ch(t)·P_max 0 ≤ P_dis(t) ≤ u_dis(t)·P_max加上这个约束以后即便目标函数里没有任何惩罚也不可能出现同一时刻既充电又放电的情况。这里还有一个小细节功率变量应该定义为非负量SOC递推时充电项和放电项分开处理不要引入负号符号混乱是初版代码中最容易引发数值错误的原因。调频容量P_reg(t)也是连续变量但它受到储能剩余功率空间的限制具体形式在约束部分展开。2.2 目标函数把调峰收益和调频收益放进同一张表联合优化模型的目标函数可以写成综合运行成本最小化这等价于净收益最大化。我在这套模型里常用的目标函数是min Σ_t [ K_maint·(P_ch(t)P_dis(t))·Δt π_buy(t)·P_buy(t)·Δt λ_curt·P_cur(t)·Δt - π_sell(t)·P_dis(t)·Δt - π_cap(t)·P_reg(t)·Δt - π_mile·K_use·P_reg(t)·Δt ]各项的含义分别是第一项是储能运行维护成本按充放电电量计费起到抑制频繁动作的作用第二项是从电网购电的成本本地新能源加储能不够供应负荷时从电网补足缺额第三项是弃风弃光惩罚成本让新能源消纳优先如果必须弃电就把惩罚记入总成本第四项是放电收益储能对外供电按售电电价结算第五项是调频容量收益相当于电网为储能预留调节能力支付的容量费用第六项是调频里程收益K_use是经验折算系数表示储能实际被AGC调用的里程与预留容量之间的关系。当这些收益项和成本项放在同一个目标函数里优化器会自然地在每个时段比较把1MW功率留给调频获得的容量收益加里程收益如果大于拿这1MW去峰谷套利的收益就少放一点电、多留一点备用。这个比较完全由模型内部完成不需要人为定一个储能调频备用比例这是联合优化相对经验分配方式最大的优势。另外要注意购电电价和售电电价不一定相同现货市场或分时电价结构下两者可能存在输配电价差异输入数据时必须分开给不能想当然用同一个电价向量。2.3 约束条件SOC、功率平衡、调频备用最容易出错先说SOC递推约束。SOC是储能荷电状态递推关系写成SOC(1) SOC0 η_ch·P_ch(1)·Δt/E_total - P_dis(1)·Δt/(η_dis·E_total) SOC(t) SOC(t-1) η_ch·P_ch(t)·Δt/E_total - P_dis(t)·Δt/(η_dis·E_total) SOC_min ≤ SOC(t) ≤ SOC_max SOC(T) ≥ SOC_end其中E_total是储能额定容量Δt是单个优化时段的时长取1小时。这里有两个容易翻车的细节。第一充电时电量增加要乘充电效率放电时电量减少要除以放电效率。我见过很多初版代码把放电项也乘效率导致能量不守恒SOC越跑越偏。第二SOC上下界不要直接设成0%到100%实际电池有放电深度限制通常取0.1到0.9比较合理。SOC(T)的末端约束是为了防止模型为了套利把电量在最后时刻全部放光如果做的是滚动时域优化这个末端约束尤其重要。其次是功率平衡约束写成L(t) P_w(t) P_pv(t) - P_cur(t) P_dis(t) - P_ch(t) P_buy(t)左边是负荷需求右边是风电出力、光伏出力减去弃电功率再加上储能放电、减去储能充电、再加上从电网购买的电量。这个约束把所有能量流的进出都串在一起。实际系统中如果储能配置在新能源场站内部负荷可能为零P_buy代表上网功率方向符号要根据物理接线调整这是模型移植到不同项目时最容易改错的地方。最后是调频备用约束。储能提供调频能力意味着它在接收AGC指令时既能向上增加出力也能向下减少出力。这两个方向需要同时满足约束写成P_reg(t) ≤ P_max - P_dis(t) P_reg(t) ≤ P_max - P_ch(t)第一行表示储能还有多少空间可以立刻增加放电功率第二行表示储能还有多少空间可以继续增加充电功率P_reg被限制为左右裕度较小的一方。如果实际项目只要求单向调频可以只保留其中一行可行域会变大收益也会相应提高。这个约束的难点在于它引进了放电和充电功率对调频容量的耦合正是这种耦合关系让联合优化变得必要。3. MATLAB代码实现与求解器选择3.1 数据准备从负荷、风光曲线到价格序列写代码前先规定所有数据的时间序列格式。我一般把数据整理成一个矩阵行数等于优化时段数T列依次是时段编号、负荷预测、风电预测、光伏预测、分时购电价、分时售电价、调频容量价格、调频里程价格。单位统一用MW、MWh、元/MWh、元/MW这是整套模型能稳定求解的基础。数值尺度是一个非常值得提前统一的问题。把功率和容量控制在10的一次到三次方量级电价控制在0.1到1元量级目标函数各项量级接近求解器在预处理阶段不容易出现数值问题。如果单位搞成kW和万元目标函数里会出现1e6量级的数字CPLEX的MIP求解日志会显示异常的伪成本系数求解时间明显变长甚至出现看似收敛但实际最优性间隙很大的情况。实测下来用MW和元/MWh的组合最稳。所有时间序列建议用列向量存放YALMIP的sdpvar变量也用列向量这样后续约束可以用for循环逐时刻添加避免行列转置带来的维度错误。3.2 YALMIP建模仿真核心代码解读YALMIP是我最常用的MATLAB优化建模工具它把变量定义、约束收集、目标函数和求解器调用封装成很自然的语法。建模仿真代码按模块展开首先是基础参数和决策变量定义% 基础参数 P_max 20; % 储能额定功率 MW E_total 40; % 储能额定容量 MWh SOC0 0.3; % SOC初始值 SOC_min 0.1; % SOC下界 SOC_max 0.9; % SOC上界 SOC_end 0.3; % SOC末端约束值 eta_ch 0.95; % 充电效率 eta_dis 0.95; % 放电效率 dt 1; % 时间步长 h T 24; % 优化时段数 % 决策变量 P_ch sdpvar(T,1); % 充电功率 P_dis sdpvar(T,1); % 放电功率 P_reg sdpvar(T,1); % 调频容量 u_ch binvar(T,1); % 充电状态 0-1 u_dis binvar(T,1); % 放电状态 0-1 SOC sdpvar(T,1); % 荷电状态 P_cur sdpvar(T,1); % 弃电功率 P_buy sdpvar(T,1); % 从电网购电功率接下来是约束条件收集。储能功率约束和状态约束是模型的骨架Constraints []; % 充电放电互斥 Constraints [Constraints, u_ch u_dis 1]; % 功率上界受状态变量控制 Constraints [Constraints, 0 P_ch u_ch*P_max]; Constraints [Constraints, 0 P_dis u_dis*P_max]; % 调频容量上下界 Constraints [Constraints, 0 P_reg P_max]; % 调频容量受当前充放电功率限制 Constraints [Constraints, P_reg P_max - P_dis]; Constraints [Constraints, P_reg P_max - P_ch];SOC递推约束用for循环逐时刻添加P_ch和P_dis的系数要特别注意效率和容量单位的匹配我习惯把Δt也写进表达式方便以后改成15分钟或5分钟步长% SOC递推与上下界 Constraints [Constraints, SOC(1) SOC0 eta_ch*P_ch(1)*dt/E_total - P_dis(1)*dt/(eta_dis*E_total)]; for t 2:T Constraints [Constraints, SOC(t) SOC(t-1) eta_ch*P_ch(t)*dt/E_total - P_dis(t)*dt/(eta_dis*E_total)]; end Constraints [Constraints, SOC_min SOC SOC_max]; Constraints [Constraints, SOC(T) SOC_end];功率平衡约束把负荷、新能源出力、储能和电网购电统一在一个等式里% 功率平衡约束 Constraints [Constraints, Load P_w P_pv - P_cur P_dis - P_ch P_buy];这个等式里的Load、P_w、P_pv都是提前读入的列向量。之后定义目标函数并调用求解器% 目标函数 Objective K_maint*sum(P_ch P_dis)*dt ... sum(price_buy.*P_buy)*dt ... lambda_curt*sum(P_cur)*dt ... - sum(price_sell.*P_dis)*dt ... - price_cap*sum(P_reg)*dt ... - price_mile*K_use*sum(P_reg)*dt; % 求解 ops sdpsettings(solver,cplex,verbose,2,showprogress,1); optimize(Constraints, Objective, ops);把目标函数和求解调用分开写是为了调试时能快速注释掉某一项收益测试模型行为变化。我最初写这套代码时把所有收益计算挤在一个表达式里后来发现定位问题非常麻烦拆开之后每次只要注释一行就能知道某一项对结果的影响。3.3 不想装YALMIP怎么办用intlinprog手动线性化有些场景不允许装第三方建模工具箱或者导师要求直接用MATLAB内置求解器。这时候可以用intlinprog实现同样的模型。思路是把所有决策变量堆成一个长向量x再把所有约束写成矩阵不等式A·x≤b和线性等式Aeq·xbeq。变量排列顺序可以定为x [P_ch; P_dis; P_reg; u_ch; u_dis; SOC; P_cur; P_buy]长度是8T。充电上限约束P_ch ≤ u_ch·P_max是典型的混合整数线性约束写成矩阵形式时需要使用稀疏矩阵避免大维度下内存爆炸。SOC递推是线性等式直接填入Aeq。目标函数写成f·x的最小化形式f向量按变量排列顺序填入运行维护成本、购电电价、调频容量价格和里程收益等系数。这种写法的缺点是调试时很容易因为某个约束的系数符号错位导致infeasible而且报错信息不如YALMIP直观。我的建议是初学阶段先用YALMIP把模型逻辑验证清楚再考虑用intlinprog重写不要一上来就手推矩阵。YALMIP本质上是帮你做了线性化表达和矩阵组装理解它背后如何生成约束矩阵对后续处理更大规模问题很有帮助。4. 典型算例与结果分析4.1 算例参数设置为了验证模型的可用性我构造了一个小型微电网算例。微电网内配置20MW/40MWh储能风电额定功率30MW光伏额定功率40MW负荷峰值60MW。风电和光伏采用典型日内预测曲线风电夜间出力高、白天低光伏中午高、早晚低。分时电价设为低谷0.25元/kWh、平段0.55元/kWh、高峰0.95元/kWh售电价与购电价先取相同值便于对照。调频容量补偿取50元/MW·h里程收益折算系数K_use取0.1。这个规模下的优化问题是24个二进制变量加168个连续变量的MILPCPLEX求解时间通常在0.2秒到2秒之间。如果滚动时域扩展到96个时段求解时间会上升到10秒左右但仍处于可接受范围。我通常会在多组参数之间做敏感性分析比如把调频容量价格从0元依次增加到100元观察储能充放电行为和总收益的变化这一步对判断项目在不同市场环境下的盈利能力很有价值。4.2 储能充放电曲线和SOC轨迹怎么看画出结果后最值得看的是三条曲线充电功率曲线、放电功率曲线和调频容量曲线。典型结果中储能会在凌晨低负荷、电价低时充电白天光伏大发但负荷未达峰值时继续小功率充电傍晚负荷高峰时放电。调频容量曲线则表现为在储能处于中间电量时预留较多在满充或满放时段被压缩到很低的水平。这个现象完全符合物理直觉因为只有SOC距离上下界都有一定空间时储能才同时具备向上和向下的调节能力。SOC曲线建议单独画一幅重点观察是否频繁撞边界。如果SOC在几个时段内反复触及0.1和0.9说明电池被当作纯粹的套利工具使用频繁深度充放电会加速寿命衰减。实际工程中我会在目标函数中加入充放电切换惩罚项或者加入最小连续运行时间约束让电池避免在相邻时段来回切换状态。另外还要检查SOC(T)是否回到SOC_end。如果模型只写了SOC(T)≥SOC_end那么它有可能在最后一个时点故意放空电量换收益看曲线时这个细节容易忽略但直接关系到下一轮滚动优化的初始状态是否合理。4.3 联合优化与单独优化的收益对比同一个算例如果把P_reg全部设为0并移除调频收益项得到的就是纯调峰优化结果。把调峰计划固定后再把调频收益纳入目标函数得到的是简易的调频优化场景。对比多组结果的收益构成可以看到一个反直觉的现象联合优化下的峰谷套利收益会小幅下降因为一部分容量被调频占用但调频容量收益和里程收益加入之后总收益明显高于纯调峰。在我上面的参数下联合优化比纯调峰的总收益大约提高15%到20%具体比例取决于调频价格、SOC上下界限制和新能源消纳压力。这说明联合优化的真正价值不是让单一指标最大化而是让储能整体收益最大化。如果只盯着调峰套利收益看可能会误以为联合优化亏了这其实是目标函数设计层面的理解问题。做收益对比分析时一定要把收益拆分表列出来单独标注调峰套利收益、调频容量收益、调频里程收益和弃电惩罚费用这样评审项目或写论文时才讲得清楚也能直观展示联合优化的经济性改善在哪里。5. 实操经验从建模到出图的几个关键坑5.1 求解器与license的那些坑YALMIP本身不包含求解器它只是一个建模层。默认情况下optimize会调用MATLAB自带的linprog或intlinprog。如果想用CPLEX或Gurobi必须自己安装完整版并配置license。常见错误是安装了免费社区版CPLEX然后执行optimize时报“No suitable solver for MILP”或者“License not available”。遇到这种情况先在命令行执行yalmiptest确认CPLEX那一行显示OK再检查环境变量路径和license文件位置。Gurobi的配置路径略有不同安装后需要在MATLAB里执行gurobi_setup把Gurobi的MATLAB接口加入路径。我的自检顺序是三步YALMIP先跑通yalmiptestCPLEX或Gurobi能运行官方demo最后才在YALMIP里用sdpsettings指定solver。顺序反了很容易出现“明明装了却调用不了”的情况花很多时间在排查网络上根本不存在的兼容问题。另外sdpsettings里如果指定了错误求解器名称YALMIP并不会报错而是会静默回退到默认求解器得到的结果可能完全不对做敏感性分析时尤其要留意这一点。5.2 数值尺度、收敛性与求解速度的权衡求解失败最多的情况是数值尺度太差。如果把储能容量写成40000kWh、电价写成950元/MWh、时间步长写成3600秒同一个表达式里就会混进1e4量级和1e8量级的项MILP预求解器在做消元时会因为数值差别过大产生伪整数解结果就是看似收敛但充放电曲线完全不正常。我在项目中统一采用MW、MWh、元/MWh、小时作为基本单位所有时间序列读取后立刻换算一次不在模型内部做二次换算。目标函数里各项数量级尽量相近也很重要。如果弃风惩罚价格和调频容量价格差了两个数量级以上求解器会优先压低价格较低的项这不见得是物理问题而是数值问题。处理办法是对价格向量做归一化或缩小惩罚项系数差距。对更大规模的问题比如T96或T168二进制变量数量线性增长CPLEX依然能处理但滚动优化场景下每次求解都要求快就要考虑松弛策略。我常用两次求解的做法先用松弛连续模型算SOC参考轨迹然后用固定SOC序列的连续规划修正功率分配速度提升明显代价是最优性稍有损失。5.3 常见报错与排查速查表下面这个表格来自我自己调试这套模型时记录的真实问题按报错关键字排列适合直接拿来对照排查。报错或现象可能原因处理办法Infeasible problemSOC约束与功率平衡矛盾或充电放电互斥导致某时段无解检查SOC初值和末端值是否相容检查功率平衡等式是否漏了购电变量先把SOC上下界放宽到0和1再测试YALMIP提示No suitable solver没有安装MILP求解器或指定solver名称不对执行yalmiptest确认CPLEX或Gurobi许可证可用核对sdpsettings里的solver名称SOC曲线始终不变dt和单位不匹配SOC递推项数值太小被忽略检查E_total单位是否为MWh、Δt是否为h如果SOC变化量低于1e-6基本是单位错误目标函数出现NaN或Inf输入数据含空值或除零检查风电、光伏和负荷序列是否有NaN检查E_total不为0求解时间很长卡在根节点二进制变量太多或约束矩阵非稀疏在sdpsettings中打开详细日志观察瓶颈减少时间步长或把部分整数变量改成连续代理变量表格里的第一行也就是infeasible是我自己遇到最多的。一个非常实用的调试技巧是先把所有涉及调频的约束去掉只优化调峰确认模型可行然后逐步加上调频容量约束和收益项。每加一个约束就求解一次通常很快就能定位是哪条约束把可行域搞空的。我最早有一次调了一个下午都没找到问题就是因为把SOC末端约束写成了SOC(T)≥SOC0而SOC0设得偏高加上充电效率损耗到不了末端值。我个人做完这套模型之后最大的体会是储能调峰调频联合优化难点从来不在求解器调用或者公式记忆而在约束条件之间微妙的耦合关系。同一个P_reg既要小于无放电时的可用功率又要小于无充电时的可用功率这条约束写错结果图再好看也是错的。如果你准备拿这份代码做自己的项目建议先从T24、不含整数变量的松弛版本跑通再逐步加入电池状态变量、切换惩罚和里程收益。后续想扩展的方向也很多比如把调频需求换成真实的AGC指令序列、引入锂离子电池循环寿命衰减模型、或者把滚动时域和日前计划结合做双层优化都可以在现在这个骨架上直接加模块模型的扩展性比一次性算例代码重要得多。