尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Matlab实现独立储能电能量与调频市场联合出清仿真

Matlab实现独立储能电能量与调频市场联合出清仿真 独立储能在电能量和调频两个市场里怎么同时出清我这次用Matlab把整套协调机制跑通了正好把思路和实现细节整理出来。一直有朋友问独立储能在电力市场里到底应该怎么建模、怎么参与出清。电能量市场和调频辅助服务市场是两种完全不同的交易品种储能机组同时参与这两个市场时容量怎么分配、SOC怎么约束、机会成本怎么算每一步都有讲究。我这次仿真的是一个典型的协调出清机制把现货电能量市场与调频辅助服务市场放在同一个优化框架里联立求解而不是传统意义上先出清能量市场、再单独出清调频市场。整个程序用Matlab编写调用线性规划求解器跑出来的结果能清楚看到储能机组在各时段的充放电计划、调频容量预留、SOC轨迹以及两个市场的出清价格。这套代码的适用场景很明确做电力市场方向研究的学生、从事现货市场系统建设的工程师、以及关注储能商业模式的投资分析人员都能拿它做机制验证或算例分析。我今天把整体设计思路、数学建模过程、代码实现细节和调试中踩过的坑一次性讲清楚争取让拿到代码的人不只是会运行还能根据自己需要改机制、换参数、扩展功能。1. 项目场景与机制设计思路1.1 独立储能参与市场的现状与困境先说清楚独立储能在当前电力市场里到底面临什么问题。储能和传统火电最大的不同在于能量双性它既能充电也能放电并且存进去的能量有时间属性这一时刻充了多少电下一时刻才能放出来。很多人在仿真里把储能当成一个可正可负的出力机组这是不对的至少在市场出清层面忽略SOC时序耦合会造成非常严重的容量误配。独立储能要同时参与现货电能量市场和调频市场核心矛盾在于调频服务要求储能提前预留一部分容量这部分容量不能再参与电能量的充放电。如果两个市场分别独立出清储能会倾向于把全部容量投到收益更高的市场里导致另一个市场出清结果失真。举个极端例子某个时段电能量价格高储能如果把全部容量用于放电调频市场就没有储能资源可用反过来如果调频里程价格高储能把所有容量都上报为调频备用电能量市场就失去了这个低价电源。两个市场的出清价格都会因此被扭曲。解决这个问题的标准做法是联合出清也有人叫协调出清本质上是把两个市场统一放到一个优化问题里让储能容量在两个市场间的分配由一个全局目标函数决定而不是人为先定好。这一思路在国内现货市场试点中已经有实际落地最早是在调频市场引入储能之后调度机构发现独立储能申报电能量曲线和调频容量之间经常冲突才逐步把两个品种放在一起考虑。我这个仿真代码模拟的正是这种联合出清机制。1.2 协调机制的核心逻辑分时耦合与容量竞争联合出清机制最核心的逻辑是两条分时耦合和容量竞争。分时耦合很容易理解储能的SOC是跨时段递推的。t时段的充放电决策会影响t1、t2甚至更晚时段的可用能量。出清模型必须建立完整的SOC转移约束通常写成SOC(t1) SOC(t) η_c · P_c(t)·Δt - P_d(t)·Δt / η_d其中η_c和η_d是充放电效率P_c和P_d是充放电功率。这个方程看起来简单但在市场出清模型中它带来的是跨时段耦合约束无论用什么样的求解器这都会增加问题复杂度。容量竞争指的是在一个时段内储能的总可用功率要在电能量出力和调频备用之间分配。如果储能的最大充放电功率是P_max那么|P_energy| P_reserve ≤ P_max这里的P_reserve就是储能承诺参与调频而预留的功率容量。注意在联合出清里P_energy和P_reserve都是决策变量它们之间的最优分配由两个市场的边际收益共同决定。储能参与电能量市场的收益来自电能量价格差低充高放参与调频市场的收益来自调频容量补偿和里程补偿。如果调频市场价格高模型就会自动把更多的容量分给调频如果电能量市场峰谷价差大模型就会倾向于多充电放电赚钱。我在建模时把这两个逻辑都做成了线性约束这样整个问题就保持为线性规划。后续想扩展成混合整数规划也很方便只需要给充放电状态加二进制变量。1.3 为什么选择Matlab实现联合出清模型关于实现工具我做过对比。Python的Pyomo和GAMS都适合做这类问题GAMS建模能力强但license费用高学生和中小团队很难承担。Pyomo开源免费但要拼装solver环境对电力系统背景的工程师来说YalmipMatlab反而是最顺手的一套组合。Matlab做电力市场出清仿真有一个显著优势Yalmip工具箱把建模语言封装得非常好它支持直接用符号表达式写约束条件底层求解器可以随时切换比如从默认的linprog切换到Gurobi或者Cplex。这意味着你在建模阶段不需要考虑求解器语法只需要关心数学模型本身写出来的代码和论文里的数学公式几乎是一一对应的排查问题非常方便。另外Matlab的绘图能力在处理出清结果可视化时很占优势。市场出清代码跑完之后通常要输出机组出力曲线、储能SOC曲线、节点电价或统一出清价曲线、调频容量分配饼图等这些图表用Matlab自带的plot、stairs、area、bar函数几行代码就能完成效果也够专业。对于需要频繁调参数跑仿真的场景Matlab的脚本化运行方式也比GUI操作更高效。这套代码的调试环境是Matlab R2022a及以上版本需要安装Yalmip工具箱求解器可以选择Gurobi 9.5或Cplex 12.10如果没有商业求解器直接用Matlab自带的linprog也能跑通小规模算例只是求解速度会慢一些。我后面会详细说明各个求解器的配置差异。2. 市场出清模型的构建与求解2.1 目标函数两个市场价格信号如何统一联合出清的目标函数设计是整个模型的核心。传统分步出清的顺序是先出清电能量市场得到机组组合和出力计划再在已知电能量出力的基础上出清调频市场。但这种方式对于储能这种具备能量时间转移属性的资源非常不友好因为在电能量市场出清时根本没有考虑调频预留容量储能可能被安排满发到调频市场里就完全丧失了响应能力。联合出清代价是目标函数里同时包含两个市场的报价成本。电能量市场的成本项是所有出售电能量的机组包括储能放电的报价乘以出力再加上购买电能量储能充电的报价乘以充电功率调频市场的成本项是调频容量价格乘以预留容量加上调频里程价格乘以预期里程。具体到我的仿真里目标函数写成min Σ_t [ Σ_g C_g(t)·P_g(t) C_buy(t)·P_ch(t) - C_sell(t)·P_dis(t) Σ_f R_f(t)·Q_f(t) M_f(t)·Mile_f(t) ]其中C_g是常规机组报价P_g是出力C_buy是储能充电报价P_ch是充电功率C_sell是储能放电报价P_dis是放电功率R_f是调频容量价格Q_f是预留容量M_f是调频里程价格Mile_f是预期调频里程。目标函数前两项是电能量市场总购电成本第三项是储能放电视为机组出力的收益抵扣在最小化成本框架里收益以负成本形式出现最后两项是调频备用采购成本。通过统一最小化模型自动在储能放电收益和调频机会成本之间做权衡。这个建模方式在数学上等价于最大化社会福利只要需求侧弹性忽略不计或者按固定负荷处理。2.2 储能约束集充放电逻辑与SOC递推储能约束是协调出清模型里最需要小心处理的部分。常见的问题是把充放电功率直接写成-1到1倍数的一个连续变量但这样会在某些时段同时出现充电和放电的非零值物理上不可能经济上也可能占便宜。为了避免这个情况我采用了两种方案。第一种是哐分变量方案也是我代码里默认的方式充电功率P_ch和放电功率P_dis分别建模两者都有非负下界和功率上界0 ≤ P_ch(t) ≤ u_ch(t)·P_max 0 ≤ P_dis(t) ≤ u_dis(t)·P_max u_ch(t) u_dis(t) ≤ 1这里u_ch和u_dis是0/1变量。使用混合整数线性规划MILP可以严格防止同时充放电。不过MILP求解在小规模算例上没问题扩展到大系统时计算负担会显著增加。第二种方案是去掉0/1变量直接加一条互补约束P_ch(t)·P_dis(t) 0。但这个约束是非线性的会让问题变成非凸优化求解不稳定。实际工程中更常用的做法是直接允许微小偏差或者利用市场规则防止同时充放电比如储能充电按负荷处理、放电按机组处理在费用结算时设置不合理套利惩罚。我的代码里默认采用MILP方案因为它的物理意义清晰、求解结果可解释性强。对于线性规划偏好的用户代码里也留了开关可以切换成线性规划模型忽略同时充放电约束速度快很多但个别极端价格场景下会出现充电放电同时为正的结果。SOC递推约束前面已经写过还要注意加上SOC的上下限约束SOC_min ≤ SOC(t) ≤ SOC_max同时为了体现储能寿命管理我设置了SOC_min不低于0.1SOC_max不高于0.9避免了深度充放电对电池寿命的影响。调频服务对储能SOC的影响通过调频备用约束体现预留调频容量意味着充电上限和放电上限都要相应收缩。2.3 调频市场约束与里程电价联动关系调频辅助服务市场的建模远比电能量市场复杂因为调频服务有两个维度的回报容量价格和里程价格。容量价格是为机组预留可用响应能力付费相当于占位费里程价格是为机组实际完成的调节里程付费相当于工作量费。在我的模型里储能参与调频时预留了功率容量Q_f这部分容量需要同时约束充电方向和放电方向的可调节裕度。具体约束是Q_f(t) ≤ min( P_max - P_dis(t) P_ch(t), P_max - P_ch(t) P_dis(t) )这个约束的含义是在给定当前充放电功率的情况下储能向上调节的容量减少充电或增加放电和向下调节的容量增加充电或减少放电都必须至少覆盖承诺的Q_f(t)。很多初学者会漏掉这个约束结果模型给出的调频容量远超储能实际可调能力出清结果在物理上不可行。里程和容量的关系在实际市场中不是线性的。调频里程通常等于调频容量乘以一个与系统频率偏差概率分布有关的系数λ在我的模型里简化处理为Mile_f(t) λ · Q_f(t)λ一般取1.5到3之间的常数代表单位容量在单位时间内可能产生的调节里程期望值。不同系统的λ差异很大风电光伏占比高的系统频率扰动大λ取偏高值火电为主的系统λ相对较低。在代码里我提供了λ的参数配置用户可以在config文件中直接修改。另一个容易被忽视的约束是调频容量需求约束系统每个时段对调频资源的总需求是固定的Σ_f Q_f(t) Q_storage(t) D_reg(t)D_reg是系统调频容量总需求一般按预测负荷的一定比例设定华北地区现货规则里调频容量需求通常是最大负荷的1.5%到3%。这个约束用的是等号因为调频市场出清必须保证系统的安全备用需求完全满足如果资源不足价格会飙升到报价上限。3. Matlab代码实现与算例验证3.1 代码架构与运行流程整体代码结构我按照仿真项目的惯例分成五个模块main脚本、参数配置、模型构建、求解与结果导出、画图。这样设计的好处是模块之间解耦改参数不用动模型改模型不用动参数减轻调试负担。要在Matlab里跑通这套程序步骤如下安装Yalmip工具箱把yalmip文件夹添加到Matlab路径readme文件里有配置方法如果已经有Gurobi或Cplex按对应接口文件配置好求解器路径。打开config_params.m设置机组数、负荷曲线、储能参数、调频参数、报价参数等。直接运行main.m脚本会自动加载配置、调用fmincon或optimize构建模型并求解、输出结果变量。运行plot_results.m查看电能量出清价格、储能充放电计划、SOC轨迹、调频容量分配图。我建议第一次跑的时候先用自带的小算例规模是10台机组、24个时段、1个储能电站求解时间大概在几秒钟到十几秒之间取决于所选的求解器。这个规模方便对照检查结果是否合理。3.2 关键代码段解析储能出力与调频预留下面贴一段核心代码。这是储能参与联合出清的约束构建代码能直接看出充放电分离变量和调频预留约束是怎么处理的。% 储能充放电功率变量 P_ch sdpvar(1, T, full); % 充电功率 P_dis sdpvar(1, T, full); % 放电功率 u_ch binvar(1, T); % 充电状态0/1 u_dis binvar(1, T); % 放电状态0/1 Q_f sdpvar(1, T, full); % 调频预留容量 SOC sdpvar(1, T1, full); % 荷电状态包含初始时刻 Constraints [Constraints, P_ch 0, P_ch P_max_test * u_ch]; Constraints [Constraints, P_dis 0, P_dis P_max_test * u_dis]; Constraints [Constraints, u_ch u_dis 1]; % SOC递推关系充电效率与放电效率分开处理 Constraints [Constraints, SOC(1) SOC_init]; % SOC 直接采用线性近似令k1为充放电状态下的综合效率系数 for t 1:T Constraints [Constraints, SOC(t1) SOC(t) ... eta_ch * P_ch(t) * delta_t / Cap ... - P_dis(t) * delta_t / (eta_dis * Cap)]; end % SOC上下限约束 Constraints [Constraints, SOC_min SOC(1:T) SOC_max]; % 调频容量与充放电功率的耦合约束 Constraints [Constraints, Q_f 0, Q_f P_max_test]; for t 1:T Constraints [Constraints, Q_f(t) P_max_test - P_dis(t) P_ch(t)]; Constraints [Constraints, Q_f(t) P_max_test P_dis(t) - P_ch(t)]; end注意第二个Q_f约束写成Q_f(t) P_max P_dis(t) - P_ch(t)的含义这里体现的是当储能正在放电时它向下调节停止放电、转为充电的能力受到P_max P_dis(t) - P_ch(t)的限制。两个不等式合在一起保证了预留容量不会超过正反两个方向的可调节裕度。调频里程成本的计算放在目标函数部分写法是% 调频里程成本项 Mile_cost sum( lambda * Q_f .* mile_price );如果调频市场价格是按容量出清价格和里程出清价格分开结算的也可以用两组价格分别乘以容量和里程。我的代码里统一为容量价格加里程价格的方式。3.3 参考算例参数设置与结果解读我设了一个参考算例参数如下时间分辨率1小时共24个时段常规机组2台火电机组容量分别为300MW和500MW报价分别是280元/MWh和350元/MWh独立储能装机100MW/200MWh初始SOC为0.5SOC上下限0.1/0.9充放电效率0.95最大充放功率100MW调频需求设定为系统预测负荷的2%在每个时段固定调频容量报价上限80元/MW里程报价上限15元/MW负荷曲线采用典型的双峰曲线早高峰在9点到11点晚高峰在18点到21点。电价由模型内生决定电能量出清价格按边际机组报价形成。跑完求解器之后可以看到几个关键现象第一个现象是在负荷低谷时段储能基本处于充电状态SOC不断爬升同时预留少量调频容量在负荷高峰时段储能转为放电状态SOC下降调频预留容量会适当减少。第二个现象是调频容量预留的最优值并不总是上限。在电能量价格较低而调频价格相对较高的时段模型会预留较多容量参与调频反之在电能量价格高涨的时段储能把大部分容量用于放电赚取峰谷价差调频预留就压缩到满足系统需求的最小值。这说明联合出清机制确实能捕捉到两个市场之间的机会成本替代。第三个现象是最终出清结果里没有出现同时充放电的情况。通过状态变量的约束模型在正常报价区间下都能找到互斥解这验证了MILP建模的正确性。参数对结果的影响也很明显。如果把储能初始SOC调高到0.8在早高峰时段它可以先放电获利如果调低到0.2那么早高峰时段它就需要先充电蓄能放电时段整体后移。这说明初始SOC状态在市场出清中扮演着重要角色实际运行时需要对储能的实际SOC状态做准确上报。4. 常见问题与调试经验速查4.1 求解器选择和Yalmip版本兼容性问题这套代码使用Yalmip作为建模语言对求解器的依赖主要在MILP求解能力上。如果你用Gurobi或Cplex基本上不需要额外设置Yalmip会自动识别并调用。但如果你只有Matlab自带的linprog那混合整数变量u_ch和u_dis将无法被处理程序会报错提示是integer variables not supported by selected solver。遇到这种情况最简单的解决方案是把问题退化为纯线性规划。具体操作是注释掉u_ch和u_dis的binvar定义直接让P_ch和P_dis在0到P_max之间连续变化并通过调整目标函数中充电和放电的报价来尽量避免同时充放电。但这只是权宜之计如果要严格物理建模还是建议装一个Gurobi或Cplex。Gurobi对学术用户免费申请licenseCplex也有社区版我用下来Gurobi的求解速度在中小规模问题上比Cplex快一些。Yalmip版本方面我遇到过几个旧版本不支持某些约束写法的坑。比如早期版本对sdpvar(1, T, full)的三参数写法支持不完整容易报错。建议直接从GitHub下载最新版Yalmip或者用MATLAB的File Exchange镜像安装时确保Yalmip路径在Matlab搜索路径的最前面避免和旧版本冲突。4.2 模型不可行的排查方法跑市场出清模型最常遇到的问题就是求解器返回Infeasible problem。这个问题几乎每个做电力市场仿真的学生都会遇到。排查的思路一般是逐个约束检查但在Yalmip里有个更高效的排查方法。Yalmip提供了check函数求解之后可以逐个检查约束的残差diagnostics optimize(Constraints, Objective, options); checks check(Constraints); [trouble, idx] max(checks);如果约束不满足check返回的数值会是负数或者很小的正数对应的索引可以帮助你快速定位是哪一组约束出了问题。在我自己的调试经验里调频容量约束是最容易导致不可行的元凶。原因是Q_f(t) ≤ P_max - P_dis(t) P_ch(t)这类约束在P_dis和P_ch取值较大时可能把Q_f压到负值下界以下。比如某个时段P_dis 90MW、P_ch 0那么P_max - P_dis P_ch 10MW如果此时系统调频需求要求Q_f至少达到20MW就只能用增加充电功率P_ch来扩大调节裕度但充电功率增加又受到SOC上限限制多重约束叠加最终可能无解。典型的解决方法是在目标函数里加一个松弛变量例如设置调频容量不足是可以接受的但会产生惩罚成本Q_slack sdpvar(1, T, full); Constraints [Constraints, Q_f Q_slack D_reg]; Objective Objective 100000 * sum(Q_slack);这个技巧在实际工程里非常常用它能让你在调试初期就先看到模型在其他约束下的行为等确认其它模块无误后再把松弛变量去掉恢复严格出清。4.3 出清价格计算和储能结算逻辑的易错点联合出清的价格计算是很多代码实现时容易出错的地方。调频辅助服务市场出清价格通常不直接是优化对偶变量尤其当有容量价格上限和里程价格上限约束时价格形成机制更复杂。我的代码里电能量出清价格通过获取平衡约束的对偶乘子得到。在Yalmip中可以通过dual函数获取某个约束的对偶值。例如功率平衡约束是Balance_con [Balance_con, sum(P_g(:,t)) P_dis(t) - P_ch(t) D_load(t)];则λ_load(t) dual(Balance_con(t))即为该时段的电能量出清价格。注意如果使用linprog对偶信息也能获取但需要把问题转为标准形式比较麻烦。储能参与两个市场的结算收益是另一个容易搞混的点。储能在电能量市场的收益是放电收入减充电支出在调频市场的收益是调频容量收入加里程收入。但要注意使用同一组价格时不能把机会成本重复计算。仿真结果里我习惯把储能收益拆成三列电能量套利收益、调频容量收益、调频里程收益这样便于分析它在每个市场里的经济性贡献。4.4 调频里程系数λ的标定经验最后提一下调频里程系数λ的取值问题。很多文献里对λ的处理是直接取固定值2但这个做法在特定系统里会产生明显偏差。λ的实际含义是单位调频容量期望完成的调节里程它和系统的频率质量指标强相关。如果系统ACE较小、CPS指标高λ通常偏低如果新能源出力的随机波动大λ会显著上升。在做仿真时建议先用历史运行数据估算λ采集一段时间内的AGC指令和机组实际响应里程数据统计总里程与总调频容量的比值。我在模型的config文件里默认了λ 2同时提供了灵敏度分析模式。用户可以把λ从1.5到3.0按步长0.25逐步测试观察储能收益的变化规律。实测结果表明λ每增加0.5储能在调频市场的收益大约能增加20%到30%这会反过来影响电能量市场和调频市场之间的容量分配因此λ标定对整个出清结果影响很大不能随意填写。5. 从算例到扩展机制还可以怎么改代码跑通只是第一步后续的机制扩展空间非常大。我在这套框架上做过几个方向的延伸效果都不错。第一个方向是加入储能的退化成本模型。电池每完成一次充放电循环和每次调频调节都会消耗循环寿命。在目标函数里加入状态相关的退化成本后储能的最优出力会明显变得保守充放电深度变小、调频出力更平滑算出来的设备寿命也更接近实际情况。退化成本系数可以通过储能厂商的循环寿命曲线拟合得到。第二个方向是多储能协同。当系统里有多个独立储能电站时它们之间存在调频容量和电能量的相互竞争这实际上是一个多参与者博弈问题。在出清框架里把多储能各自的SOC和容量约束并列加入模型可以观察市场出清结果对不同储能初始SOC、功率容量参数的敏感性这对接入电网的储能规划研究有参考价值。第三个方向是加入网络约束。现货电能量市场通常要满足输电线路的潮流约束我的代码简化处理为单节点模型如果要做区域市场或节点电价分析需要在模型中加入直流潮流方程在Yalmip里这并不难用转移分布因子矩阵或者导纳矩阵即可。我认为这套代码最大的价值是让电能量调频协调出清这个抽象概念变成了一个可以反复实验、改参数、换场景的仿真平台。拿到代码后先跑通自带算例再逐步修改约束和目标函数就能对市场机制有非常直观的理解。如果你在跑代码过程中遇到不可行、价格异常、SOC溢出之类的问题很大概率就在我上面讲过的几个坑里按速查表排查一轮基本都能解决。我自己把这套模型从单时段静态优化改到多时段动态出清从单节点改到IEEE 14节点系统需要额外补潮流数据前前后后折腾了两周踩过的坑包括MILP变量初始化问题、极端报价导致的数值病态、还有求解器超时设置这些经验都写进代码注释了。建议你在使用的时候先从benchmark案例开始逐项打开功能开关每一步都确认结果合理再进入下一步别一次性把所有复杂约束都加上否则出问题根本定位不到源头。
返回列表