ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

微电网经济调度入门:蓄电池与购售电的线性规划建模

微电网经济调度入门:蓄电池与购售电的线性规划建模 1. 这个程序解决的是什么问题从一道典型调度题说起先别急着看代码我们先把这道题的物理世界搞清楚。我见过太多新手拿到这类题目就扑向MATLAB或Python结果模型建了一半连我到底在优化什么都没想明白。这个标题里的几个关键词其实构成了一套完整的微电网——或者说园区级综合能源系统——日前经济调度问题。我拆开来讲蓄电池系统里的储能环节作用是搬运能量。它可以在电价低的时候充电电价高的时候放电赚取峰谷价差也可以在光伏或风电大发、电网消纳不了的时候充电避免弃风弃光。市场购电售电约束系统和大电网之间存在功率交换可以从电网买电购电也可以向电网卖电售电。这不是无限制的通常有最大购电功率、最大售电功率的限制甚至可能规定不能同时购电和售电。功率平衡约束这是整个模型的骨架。任意时刻系统内所有电源出力、储能充放电、负荷消耗、与电网交换的功率必须满足发用电平衡。用大白话说电不能凭空产生也不能凭空消失。目标函数为总费用最低也就是让系统在满足所有约束的前提下运行一天通常是24小时步长1小时的总成本最小。这类模型的本质是一个**线性规划Linear Programming, LP问题或者如果加入蓄电池的充放电效率、SOC递推方程就是一个混合整数线性规划MILP**问题——具体取决于蓄电池模型怎么建。新手阶段我强烈建议先做LP版本把逻辑跑通再往MILP扩展。那这个程序到底能干什么用最典型的应用是微电网经济调度、园区能源管理系统EMS的日前调度模块、电力市场环境下的用户侧储能优化。无论你是做毕设、做课程设计还是刚进公司做能源规划这套建模思路都是最基础、最核心的骨架。可以说你把这个模型吃透后面无论加光伏、加风电、加柴油机、加需求响应都只是在骨架上添砖加瓦。提示本文针对新手重点讲清楚为什么要这样建模而不是堆一堆花哨的算法。粒子群PSO、遗传算法GA这类智能算法在高维非线性问题上确实有优势但在这种纯线性模型上用它们属于用大炮打蚊子不仅慢而且结果不稳定。先用求解器把LP模型跑通比用什么算法重要一百倍。2. 模型构建的四个核心模块蓄电池、购售电、功率平衡、目标函数2.1 目标函数总费用最低的钱从哪省出来我们要优化的总费用一般由这几项组成向电网购电的费用sum(购电功率 × 分时电价)这是最主要的成本项。向电网售电的收入sum(售电功率 × 上网电价)注意这是收入在目标函数里是负项也就是减去它。蓄电池的折旧成本或运行维护成本很多新手会漏掉这一项。如果你完全不考虑蓄电池的损耗那么模型会疯狂地让电池每天满充满放只为了赚几毛钱的峰谷价差——这在实际中显然不合理。所以通常加一个很小的单位充放电成本系数比如每千瓦时0.02元让模型自己权衡。所以目标函数写成min C Σ_t [ λ_buy(t) × P_buy(t) - λ_sell(t) × P_sell(t) c_bat × (P_ch(t) P_dis(t)) ]其中t是时间索引1到24λ_buy是分时购电价λ_sell是上网电价P_buy和P_sell分别是购电和售电功率P_ch和P_dis是蓄电池的充电和放电功率c_bat是单位充放电成本。2.2 功率平衡约束一切约束的核心这个约束的物理含义是在任意时刻系统的发电能力必须等于用电需求。P_buy(t) P_dis(t) P_load(t) P_ch(t) P_sell(t)注意这里的正负号约定。我习惯把等式左边看成供给右边看成需求供给从电网买的电P_buy蓄电池放的电P_dis需求负荷P_load蓄电池充电P_ch充电也是一种用电卖给电网的电P_sell如果你的系统里有光伏或风电只需要在供给侧加上P_pv(t)和P_wt(t)即可而且它们通常是已知的预测出力曲线不是优化变量。2.3 蓄电池建模看似简单细节不少蓄电池的建模是整道题最容易出错的地方。我总结出四个必须写的约束第一充放电功率上限。0 ≤ P_ch(t) ≤ P_ch_max × u_ch(t) 0 ≤ P_dis(t) ≤ P_dis_max × u_dis(t)其中u_ch和u_dis是0-1变量表示充电和放电状态。如果不加这两个状态变量模型会出现一个荒谬的结果同一时刻既充电又放电凭空创造能量把目标函数做没。虽然线性规划中因为目标函数是费用最小一般不会出现同充同放但加上状态变量更加严谨也让模型可以扩展到更复杂的场景比如蓄电池不能频繁切换状态。第二同一时刻只能充或只能放。u_ch(t) u_dis(t) ≤ 1第三SOC荷电状态递推方程。SOC(t1) SOC(t) η_ch × P_ch(t) - P_dis(t) / η_dis其中η_ch和η_dis分别是充电和放电效率通常在0.9到0.95之间。这个递推关系是时序强耦合约束也是整个模型难的地方——它把24个小时的决定串成一串牵一发而动全身。第四SOC上下限和始末状态。SOC_min ≤ SOC(t) ≤ SOC_max SOC(1) SOC_init SOC(25) SOC_end最后一条很重要。很多程序跑出来一天结束蓄电池SOC是0第二天从头再算这种结果在实际中没法用。通常我们让一个调度周期结束时的SOC等于初始SOC这样才能循环滚动调度。2.4 购电售电约束和电网的边界条件这部分相对简单但边界条件必须写清楚0 ≤ P_buy(t) ≤ P_buy_max 0 ≤ P_sell(t) ≤ P_sell_max有些模型还会加一条同一时刻不能既购电又售电逻辑上这合理——你不可能左手从电网买电、右手把电卖回给电网白白缴纳过网费。加上这个约束需要引入另一个0-1变量把模型升级为MILPP_buy(t) ≤ P_buy_max × u_buy(t) P_sell(t) ≤ P_sell_max × u_sell(t) u_buy(t) u_sell(t) ≤ 1经验之谈如果是刚开始学建议先不加购售互斥约束。因为电价结构合理时购电价总是高于上网电价模型天然不会同时购售电加了互斥变量只是多了几个0-1变量对结果影响不大但求解时间会增加。先跑通再升级这是学习这类问题的最佳路径。3. 求解器的选型与代码实现MATLAB还是Python3.1 MATLAB YALMIP 求解器如果你用的是MATLAB我推荐YALMIP工具箱配合Cplex或Gurobi。YALMIP的建模语言非常接近数学表达式对新手极其友好。核心代码如下%% 参数定义 T 24; % 调度周期 P_load [60 55 50 45 45 50 55 65 75 80 85 90 95 100 105 100 95 90 85 80 75 70 65 60]; % 负荷曲线 lambda_buy [0.5 0.5 0.5 0.4 0.4 0.5 0.6 0.8 0.9 1.0 1.1 1.2 1.2 1.1 1.0 0.9 0.8 0.7 0.6 0.5 0.5 0.4 0.4 0.5]; % 分时购电价 lambda_sell lambda_buy * 0.8; % 上网电价按购电价的80% %% 变量定义 P_buy sdpvar(1, T); % 购电功率 P_sell sdpvar(1, T); % 售电功率 P_ch sdpvar(1, T); % 充电功率 P_dis sdpvar(1, T); % 放电功率 SOC sdpvar(1, T1); % 荷电状态多一个时刻方便递推 %% 蓄电池参数 P_ch_max 30; P_dis_max 30; SOC_min 0.2; SOC_max 0.9; SOC_init 0.5; SOC_end 0.5; eta_ch 0.95; eta_dis 0.95; c_bat 0.02; % 单位充放电成本 %% 约束集合 Constraints []; % 功率平衡约束 for t 1:T Constraints [Constraints, P_buy(t) P_dis(t) P_load(t) P_ch(t) P_sell(t)]; end % 蓄电池SOC递推方程 for t 1:T Constraints [Constraints, SOC(t1) SOC(t) eta_ch * P_ch(t) - P_dis(t) / eta_dis]; end % 蓄电池SOC上下限 for t 1:T1 Constraints [Constraints, SOC_min SOC(t) SOC_max]; end Constraints [Constraints, SOC(1) SOC_init, SOC(T1) SOC_end]; % 充放电功率上下限 for t 1:T Constraints [Constraints, 0 P_ch(t) P_ch_max]; Constraints [Constraints, 0 P_dis(t) P_dis_max]; end % 购电售电功率上下限 for t 1:T Constraints [Constraints, 0 P_buy(t) 100]; Constraints [Constraints, 0 P_sell(t) 50]; end %% 目标函数 Objective sum(lambda_buy .* P_buy - lambda_sell .* P_sell c_bat * (P_ch P_dis)); %% 求解 ops sdpsettings(solver, gurobi, verbose, 1); optimize(Constraints, Objective, ops); %% 结果可视化 figure; subplot(3,1,1); bar([value(P_buy); value(P_sell)], stacked); legend(购电, 售电); title(与电网交互功率); subplot(3,1,2); bar([value(P_ch); value(P_dis)], stacked); legend(充电, 放电); title(蓄电池充放电功率); subplot(3,1,3); plot(0:24, value(SOC), o-); xlabel(时间/h); ylabel(SOC); title(蓄电池荷电状态变化);这段代码是完整的LP模型没有购售互斥变量我故意没有加购售互斥你可以先跑通看结果然后自己试着加上0-1变量看看有什么变化。跑通之后你会发现几个有意思的现象蓄电池总是在电价高峰时段放电、低谷时段充电这就是峰谷套利的直观表现。在电价最高的一两个时段购电功率可能降到很低因为放电满足了大部分负荷。SOC曲线是一条平滑的锯齿波起始和终止都在0.5。3.2 Python Pyomo / PuLP如果你习惯Python我推荐Pyomo搭配Gurobi或CBC求解器。Pyomo的语法比YALMIP略啰嗦但胜在免费、开源生态好而且和机器学习工作流的对接更顺畅。import numpy as np import pyomo.environ as pyo from pyomo.opt import SolverFactory # 基础数据 T 24 P_load [60, 55, 50, 45, 45, 50, 55, 65, 75, 80, 85, 90, 95, 100, 105, 100, 95, 90, 85, 80, 75, 70, 65, 60] lambda_buy [0.5, 0.5, 0.5, 0.4, 0.4, 0.5, 0.6, 0.8, 0.9, 1.0, 1.1, 1.2, 1.2, 1.1, 1.0, 0.9, 0.8, 0.7, 0.6, 0.5, 0.5, 0.4, 0.4, 0.5] lambda_sell [x * 0.8 for x in lambda_buy] model pyo.ConcreteModel() model.T pyo.RangeSet(1, T) model.TS pyo.RangeSet(1, T1) # 变量 model.P_buy pyo.Var(model.T, withinpyo.NonNegativeReals) model.P_sell pyo.Var(model.T, withinpyo.NonNegativeReals) model.P_ch pyo.Var(model.T, withinpyo.NonNegativeReals) model.P_dis pyo.Var(model.T, withinpyo.NonNegativeReals) model.SOC pyo.Var(model.TS, bounds(0.2, 0.9)) # 参数 model.P_load pyo.Param(model.T, initializedict(enumerate(P_load, 1))) model.lambda_buy pyo.Param(model.T, initializedict(enumerate(lambda_buy, 1))) model.lambda_sell pyo.Param(model.T, initializedict(enumerate(lambda_sell, 1))) P_ch_max 30 P_dis_max 30 eta_ch 0.95 eta_dis 0.95 c_bat 0.02 # 约束功率平衡 def power_balance_rule(m, t): return m.P_buy[t] m.P_dis[t] m.P_load[t] m.P_ch[t] m.P_sell[t] model.power_balance pyo.Constraint(model.T, rulepower_balance_rule) # 约束SOC递推 def soc_update_rule(m, t): return m.SOC[t1] m.SOC[t] eta_ch * m.P_ch[t] - m.P_dis[t] / eta_dis model.soc_update pyo.Constraint(model.T, rulesoc_update_rule) # 约束始末SOC model.soc_init pyo.Constraint(exprmodel.SOC[1] 0.5) model.soc_end pyo.Constraint(exprmodel.SOC[T1] 0.5) # 约束充放电和购售电上下限 def ch_lim_rule(m, t): return m.P_ch[t] P_ch_max model.ch_lim pyo.Constraint(model.T, rulech_lim_rule) def dis_lim_rule(m, t): return m.P_dis[t] P_dis_max model.dis_lim pyo.Constraint(model.T, ruledis_lim_rule) def buy_lim_rule(m, t): return m.P_buy[t] 100 model.buy_lim pyo.Constraint(model.T, rulebuy_lim_rule) def sell_lim_rule(m, t): return m.P_sell[t] 50 model.sell_lim pyo.Constraint(model.T, rulesell_lim_rule) # 目标函数 def objective_rule(m): return sum(m.lambda_buy[t] * m.P_buy[t] - m.lambda_sell[t] * m.P_sell[t] c_bat * (m.P_ch[t] m.P_dis[t]) for t in model.T) model.objective pyo.Objective(ruleobjective_rule, sensepyo.minimize) # 求解 solver SolverFactory(gurobi) result solver.solve(model, teeTrue) # 输出 for t in model.T: print(ft{t}: buy{model.P_buy[t].value:.2f}, sell{model.P_sell[t].value:.2f}, fch{model.P_ch[t].value:.2f}, dis{model.P_dis[t].value:.2f}, SOC{model.SOC[t].value:.3f})3.3 求解器选型建议新手不要在这个环节纠结我强烈建议新手直接装Gurobi学术版免费安装简单求解速度快得惊人——这种24时段的小模型它能在0.1秒内解完。如果装不上GurobiCBC作为开源求解器也完全够用就是输出信息没那么详尽。注意国内部分高校用户访问Gurobi官网可能需要一点网络技巧如果你所在的环境下载不便可以考虑用scipy.optimize.linprog先做最简单版本的LP但那个只能处理纯LP处理不了MILP。新手还是优先把求解器装好一劳永逸。4. 建模中容易出错的六个坑逐个避雷讲完代码我觉得有必要把新手最容易踩的坑单独拎出来说。这些问题我帮人调试时见过无数次。4.1 功率平衡约束的正负号搞反这是最常见的错误。记住一句话等式左边是流入系统的能量右边是流出系统的能量。蓄电池放电是流入供给充电是流出消耗别搞混。我建议你写约束之前先在纸上画一个简单的能量流图标清楚每个变量的方向再写公式。4.2 SOC递推方程里效率的位置放错充电效率应该乘在充电功率上放电效率应该除在放电功率上。也就是SOC(t1) SOC(t) η_ch × P_ch(t) - P_dis(t) / η_dis这个公式的含义是充进去1度电实际只能存进0.95度因为有损耗要放出1度电实际需要消耗1/0.95 ≈ 1.05度的存储能量。如果你把效率位置放反会出现电池越用越多的笑话——当年我调试时看到SOC曲线一路向上就是从这找出来的问题。4.3 SOC变量维度少了一个注意我的代码里SOC的维度是1到T1也就是25个点不是24个。因为SOC(1)是初始状态SOC(t1)才是第t个时段结束后的状态。搞成24个点递推方程最后一步会越界报错都不好找。4.4 目标函数里漏掉蓄电池成本如果你不加c_bat这一项模型对蓄电池的使用是免费的它会让电池在每一个峰谷周期都满充满放。虽然从数学上看没错但从工程上看不合理。加上一个极小的成本项整个决策会变得理性得多。4.5 直接复制代码不检查数据和变量规模很多新手拿到一段能跑的代码直接把自己的负荷数据替换进去结果发现结果异常。原因往往不是代码问题而是数据本身的问题——比如负荷数据包含了尖峰毛刺导致购电功率顶到上限。建议先画出负荷曲线、电价曲线的图确认数据和实际场景匹配了再去跑优化这样出了问题也好定位。4.6 忽略购售互斥但又在结果里发现同购同售前面提过在购电价始终高于上网电价的前提下模型不会同购同售但我见过有人把售电价设置得比购电价还高或者为了简化随便填数据导致模型套利——低价买电、高价卖电利润无限购电功率顶到上限售电功率也顶到上限。这种结果一出来就知道是数据错了。经验之谈任何优化模型跑出反直觉的结果先怀疑数据再怀疑模型最后才怀疑求解器。这个排查顺序能帮你节省大量时间。5. 结果分析怎么看懂求解器给出的答案跑通模型只是第一步能看懂结果、能从结果里发现模型的合理性才算真正入门。我建议至少画下面几张图第一张堆叠条形图展示系统各时刻的功率构成。横轴是24小时纵轴是功率用堆叠柱状图分别显示购电、放电、充电、售电、负荷。这样可以直观看到每个时刻能量从哪来、到哪去。第二张SOC曲线。这是一条锯齿状曲线低谷时段充电SOC上升高峰时段放电SOC下降起始和终止都在0.5。如果SOC曲线出现突变、跳变或者超出了上下限那一定是约束写错了。第三张电价与购电量的对比图。把分时电价和购电量画在同一张图上你会清楚地看到购电量在电价低谷时段高、高峰时段低。这就是经济调度的直观体现。以我上面给出的负荷和电价数据为例典型结果应该是这样的凌晨1点到5点电价0.4到0.5元蓄电池充电购电功率除满足负荷外还有富余。上午9点到12点电价0.9到1.2元蓄电池放电购电功率明显降低。下午14点到18点电价0.8到1.0元蓄电池继续放电承担部分负荷。晚上19点到22点电价0.5到0.6元蓄电池开始回充恢复SOC到0.5。如果你跑出来的结果完全符合这个规律说明模型建对了。如果发现某个时段的行为不理智比如电价最高时还在充电那就要回头检查约束和逻辑了。6. 从入门到进阶下一步可以在这个模型上做什么如果你已经能独立跑通这个模型并且能看懂结果那恭喜你你已经掌握了经济调度问题的核心骨架。接下来有三条进阶路线按推荐程度排序路线一加入新能源出力。在功率平衡方程左侧加上光伏出力P_pv(t)和风电出力P_wt(t)设定为已知的预测曲线。你会发现蓄电池的角色从纯峰谷套利变成了平抑新能源波动。当光伏大发时充电避免向电网反送功率受限当光伏不足时放电补充负荷缺口。这一步改动不大但能让你理解储能和新能源的配合逻辑。路线二加入可平移负荷或需求响应。把部分负荷变成可调节变量比如从高峰时段平移到低谷时段。模型需要增加平移量守恒约束——被平移出去的负荷必须在其他时段被补回来。这一步会让模型更有趣也更贴近实际电力市场的需求响应场景。路线三加入网络拓扑约束多节点潮流。目前是单节点模型也就是一个能量池没有线路容量限制。如果扩展到多节点需要引入直流潮流方程或交流潮流方程复杂度陡增。这一步适合研究生阶段或者实际工程项目需求。我个人最推荐路线一。因为新能源接入是当前行业的绝对主流你把这个模型吃透加上光伏和风电已经可以应对许多实际项目的前期方案评估了。7. 写在最后的实操心得最后分享几点我个人在调试和教学过程中积累的心得希望对新手有帮助。第一建模之前先画能量流图。哪怕只是用笔在纸上画个方框标清楚哪些箭头进、哪些箭头出也比直接写数学公式靠谱得多。我见过太多人公式写得漂亮结果一检查发现物理方向都是反的。第二小规模测试永远比直接跑完整模型高效。别一上来就是24小时、100个变量。先设定10个时段跑通确认SOC递推没问题、目标函数没报错再扩展到24小时或168小时。这个习惯能让你从被报错折磨的状态中解脱出来。第三求解器报infeasible不可行时先查约束之间是否互相矛盾。常见原因是某个时刻负荷太大而购电上限和电池放电上限加起来都不够满足——这时候应该调整的是购电上限而不是去调其他参数。YALMIP可以用check(Constraints)查看哪些约束不满足Python侧可以用model.pprint()逐一检查。第四关于我为什么不推荐新手一上来就用强化学习或者启发式算法。原因很简单这类模型本质上是凸优化问题有高效且精确的求解器。先用数学优化的方法理解问题结构等把约束和变量关系吃透了再去尝试那些更高级的算法不迟。本末倒置的结果往往是算法调了好几天不如一个LP跑得快、来得稳。这个模型虽然小但它五脏俱全——目标函数、等式约束、不等式约束、时序耦合一个不少。把它的每一个细节都琢磨透你以后遇到任何调度优化问题都会觉得似曾相识。
返回列表