MATLAB共享储能日前经济调度代码详解:分时电价下的优化策略
先说说这个项目是干嘛的。最近几年分时电价机制在各省铺开很多工业园区白天峰段电价高、夜间谷段电价低配上储能“夜间充电、白天放电”差价就是实打实的利润。但真要工业用户自己掏钱建一套储能电站初始投资动辄几百万加上运维、消防、电池寿命管理很多中小制造企业根本吃不消。共享储能电站的思路就是第三方建站、用户按需租用容量把一次性重资产变成了日常运营开支储能利用率提上来用电成本降下去。这篇博文把我写的一套MATLAB日前优化经济调度代码完整拆解了一遍在给定工业负荷曲线、光伏出力和分时电价的前提下提前一天算出未来24小时储能什么时候充、什么时候放、从电网买多少电、自己的光伏怎么分配最终让一天的综合用电成本最低。适合正在做储能调度、综合能源系统优化方向的研究生也适合想用MATLAB把调度算法落地验证、跑出真实调度曲线的工程师。1. 这个课题到底在解决什么问题共享储能与日前调度的来龙去脉1.1 工业用户为什么需要“租”储能要想理解这套代码的价值先得搞清楚一个现实问题工业用户自己建储能账到底怎么算。一套1MW/2MWh的磷酸铁锂电池储能系统设备加施工加并网改造总成本通常在150万到250万之间这还不算每年的电池衰减更换和运维人力。工厂的现金流动性本来就紧让老板一次性掏两百万出来就为了在峰谷电价上赚差价决策周期会非常长。共享储能把这件事变成了“按需购买服务”。用户不需要拥有电池只需要向储能电站运营商租用一定容量或者按实际充电量支付服务费。运营商集中建设大型储能电站通过服务多个工业用户提升电池利用率从而把单位成本摊薄。对工业用户来说初始投资归零风险从“资产风险”变成“成本风险”决策门槛一下就低了。这也是共享储能在山东、浙江、广东等地快速落地的核心原因。但这套模式对调度的要求比自建储能更高。你自己的储能充放电策略只有你一个用户关心共享储能电站同时服务多个用户容量分配、充放电时段、电网交互功率都必须提前规划清楚。日前优化调度就是在这个背景下成为刚需的头一天把第二天的充放电计划排好用户据此安排生产储能电站据此安排容量电网侧也据此做电力平衡。三方都需要这份“计划表”。1.2 日前优化调度到底“优化”了什么先说“日前”两个字。电力市场和大工业用户的用电计划通常按天滚动申报一般是每天上午申报次日的96点或24点用电曲线电网侧根据申报做发电调度。所以储能调度也得在这个时间尺度上做决策提前一天给出逐时的充放电功率、购电功率和光伏上网功率这就是“日前调度”。再说“优化”。调度涉及多个可调变量储能充多少、放多少、什么时候充、什么时候放、从电网买多少电。不同的充放电策略对应的电费账单完全不同所以需要一个数学优化模型来回答“怎么调最省钱”。这个模型的核心是目标函数和约束条件目标函数量化“一天总成本”约束条件刻画“物理上能不能做到”。把这两部分写清楚剩下的就是让求解器算。这个优化不是简单的“谷充峰放”拍脑袋规则因为实际约束非常多。储能容量有限功率有限电池不能过充过放变压器最大需量有限制光伏出力又是个变量。手排规则往往在边界情况下顾此失彼而优化模型能自动在所有可行策略里找出成本最低的那条。2. 优化模型的数学骨架目标函数与约束条件的逐条拆解2.1 目标函数一天的电费账单怎么写经济调度的目标很直接让用户一天的总成本最小。总成本由三部分构成。第一部分是向电网购电的成本逐时购电功率乘以逐时电价再累加。第二部分是光伏余电上网的收益如果政策允许余电卖给电网就在目标函数里做减法。第三部分是共享储能的使用费用如果是容量租赁模式就是固定日租费如果按电量计费就按充电量收服务费。用数学式子表达就是min Σ_{t1}^{24} ( P_buy(t) × price_buy(t) - P_sell(t) × price_sell(t) ) × Δt C_lease Σ_{t1}^{24} c_ch × P_ch(t) × Δt其中 t 是小时索引Δt取1小时P_buy是购电功率price_buy是分时电价P_sell是上网功率price_sell是上网电价C_lease是共享储能日租赁费c_ch是充电服务费单价P_ch是充电功率。这里有一个容易忽略的点目标函数里要不要加充放电的损耗成本我的建议是不加。电池损耗本质上是一个寿命折损成本除非你能拿到非常精确的循环寿命与放电深度关系曲线否则加进去反而会让模型失真。更常见的做法是在储能参数里直接限制日充放电次数或者给出一个寿命约束这个可以在后面扩展初版模型不需要。2.2 约束条件能跑通模型的几道锁目标函数只是回答了“怎么算账”约束条件才决定了这个账能不能实现。我在这套代码里用了六类约束。第一类是功率平衡约束。每一时刻负荷功率加上充电功率、上网功率必须等于光伏出力加购电功率加放电功率即P_load(t) P_ch(t) P_sell(t) P_pv(t) P_buy(t) P_dis(t)这条约束是模型的地基。只要功率平衡不满足其他所有结果都不可信。写代码时尤其注意单位负荷如果给的是千瓦光伏和储能也必须是千瓦混用兆瓦会导致结果毫无意义。第二类是储能SOC动态方程。SOC是电池荷电状态表示剩余电量占额定容量的比例它的递推关系是SOC(t1) SOC(t) ( η_ch × P_ch(t) - P_dis(t) / η_dis ) × Δt / E_rated其中η_ch是充电效率η_dis是放电效率E_rated是储能额定容量。这条式子刻画了电池“充进去的电不能全部放出来”的物理事实损耗在充放电环节被吃掉。第三类是SOC上下限约束。锂电池一般不允许放到0也不建议充满到100%工程上通常限制在0.1到0.9之间延长寿命。第四类是充放电功率限值充电功率和放电功率分别不能超过额定功率。第五类是变压器容量约束用户从电网购电的功率不能超过变压器申报容量否则会触发需量电费或安全风险。第六类是SOC循环约束也是很多人第一次写会漏掉的一天的起始SOC要等于结束SOC。因为日前调度是滚动重复的如果今天把电放空了明天早上就没有余量继续调度所以模型中必须强制SOC(1) SOC(25)保证调度的可持续性。2.3 要不要用二进制变量充放电互斥的两种写法优化模型有个经典的细节问题能不能同时充电和放电物理上当然不行同一个电池不可能一边充一边放。但在数学模型里如果不加限制求解器可能会给出同时充放的结果尤其是在某些电价场景下模型会用“充电损耗放电损耗”来制造虚假的能量流。处理方式有两种。第一种是引入两个二进制变量u_ch和u_dis分别表示充电状态和放电状态再加约束u_ch u_dis ≤ 1同时让P_ch ≤ P_ch_max × u_chP_dis ≤ P_dis_max × u_dis。这样模型就严格禁止同时充放但代价是模型从线性规划变成混合整数线性规划求解时间会变长。第二种方式是直接不加互斥约束靠目标函数里的经济逻辑“自然避免”。因为在正常情况下同时充放电意味着白白损失效率只会让成本更高最优解自动不会选它。但这条逻辑依赖电价、损耗参数的设置极端边界情况下可能失效。我个人的建议是初版模型先不加二进制变量用纯线性规划快速验证数据和处理逻辑等整个框架跑通了再加上互斥约束做成MILP对比一下结果差异。很多论文里为了严谨直接上MILP实际上对单用户单储能的小规模问题用intlinprog或Gurobi求解也就几秒的事并没有太大性能压力。3. MATLAB代码实现从原始数据到调度指令的完整链路3.1 代码整体结构与数据准备这套代码的整体结构分四块数据准备、模型构建、求解、结果输出。数据准备是最枯燥但最容易出错的部分我把负荷曲线、光伏出力和分时电价都放在脚本开头方便替换成实际项目数据。这里给出一个基础数据段的骨架读者可以根据自己的项目替换%% 基础数据 % 时间参数 T 24; % 调度时段数小时 dt 1; % 时间步长小时 % 分时电价元/kWh顺序为0-23时 price_buy repmat(0.68, 1, 24); price_buy(1:6) 0.31; % 谷段 0-5时 price_buy(8:11) 1.12; % 峰段 7-10时 price_buy(18:21) 1.12; % 峰段 17-20时 % 负荷曲线kW可替换为真实负荷数据 P_load [350, 320, 300, 280, 270, 310, 520, 780, 950, 1020, ... 1050, 980, 920, 900, 860, 890, 930, 960, 880, 820, ... 740, 650, 540, 420]; % 光伏出力曲线kW P_pv [0, 0, 0, 0, 0, 10, 80, 180, 300, 380, 420, 440, ... 410, 360, 280, 180, 90, 20, 0, 0, 0, 0, 0, 0]; % 储能参数 E_rated 1000; % 额定容量kWh P_ch_max 500; % 最大充电功率kW P_dis_max 500; % 最大放电功率kW eta_ch 0.95; % 充电效率 eta_dis 0.95; % 放电效率 SOC_min 0.1; % 最小荷电状态 SOC_max 0.9; % 最大荷电状态 SOC_init 0.2; % 初始荷电状态 % 变压器容量kW P_trafo_max 1200; % 共享储能租赁参数 C_lease 400; % 日租赁费元/天 c_ch 0.05; % 充电服务费元/kWh这段数据设定有几个细节可以多说一句。分时电价我用repmat先铺了一个平段底价0.68元再按实际峰谷时段覆盖这样写比逐个元素赋值清晰得多。负荷曲线和光伏曲线都是24维行向量索引从1到24对应0点到23点这个对应关系非常容易搞混建议在数据前面加注释把时刻标注清楚。需要提醒的是实际项目中的负荷和光伏数据往往是从SCADA系统或者气象预测接口拿到的单位可能不是千瓦而是兆瓦时间分辨率可能是15分钟而不是1小时。拿到数据后第一步就是统一单位、统一时间分辨率否则后续所有结果都是错的。15分钟分辨率的数据可以先聚合成小时均值再输入模型。3.2 构建决策变量、约束条件与求解数据准备好之后进入模型构建阶段。我用YALMIP工具箱描述优化问题这样可以在后端自由切换CPLEX、Gurobi、intlinprog等求解器不用因为换求解器重写模型。决策变量的定义方式如下%% 决策变量 P_buy sdpvar(1, 24, full); % 购电功率kW P_sell sdpvar(1, 24, full); % 光伏上网功率kW P_ch sdpvar(1, 24, full); % 充电功率kW P_dis sdpvar(1, 24, full); % 放电功率kW SOC sdpvar(1, 25, full); % 荷电状态0-24时刻共25个点注意SOC定义成了25维而不是24维。因为SOC的递推关系是从SOC(t)推到SOC(t1)需要多一个端点来承载第24小时之后的状态。这样SOC(1)对应0时的初值SOC(25)对应24时的终值循环约束就是SOC(1)等于SOC(25)。很多初学者把SOC定义成24维导致循环约束写不进去最后算出来的调度策略无法在第二天继续使用。约束条件的代码实现如下%% 约束条件 Constraints []; % 1. 功率平衡约束 for t 1:24 Constraints [Constraints, ... P_load(t) P_ch(t) P_sell(t) ... P_pv(t) P_buy(t) P_dis(t)]; end % 2. SOC动态递推约束 for t 1:24 Constraints [Constraints, ... SOC(t1) SOC(t) ... (eta_ch * P_ch(t) - P_dis(t) / eta_dis) * dt / E_rated]; end % 3. SOC上下限约束 for t 1:25 Constraints [Constraints, ... SOC_min SOC(t) SOC_max]; end % 4. 充放电功率限值约束 Constraints [Constraints, ... 0 P_ch P_ch_max, ... 0 P_dis P_dis_max]; % 5. 购电功率与变压器容量约束 Constraints [Constraints, ... 0 P_buy P_trafo_max, ... 0 P_sell P_pv(t)]; % 6. 循环约束始末SOC相等 Constraints [Constraints, ... SOC(1) SOC_init, ... SOC(25) SOC_init]; %% 目标函数 objective sum(P_buy .* price_buy * dt) ... - sum(P_sell .* 0.35 * dt) ... C_lease ... c_ch * sum(P_ch * dt); %% 求解 ops sdpsettings(solver, gurobi, verbose, 1); sol optimize(Constraints, objective, ops); %% 结果提取 if sol.problem 0 P_buy_opt value(P_buy); P_sell_opt value(P_sell); P_ch_opt value(P_ch); P_dis_opt value(P_dis); SOC_opt value(SOC); disp(优化求解成功); else disp([求解失败: , sol.info]); end这里有一个很容易犯的错误就是P_sell的上限约束写成0 P_sell P_pv但它不能直接引用整个向量。我在代码里用P_pv(t)但这样写其实有一个隐患t在约束循环结束后保留的是最后一个时刻的值如果整个P_sell向量共用同一个上限会出现约束不准确的问题。更严谨的写法是在循环内逐条加约束for t 1:24 Constraints [Constraints, 0 P_sell(t) P_pv(t)]; end这是一个实际调试中容易被忽略的细节。YALMIP支持向量化约束但如果你混用了循环和向量索引变量的最终值会悄悄污染你的约束排查起来特别费劲。我建议所有涉及逐时变化的约束都放进循环里写宁可代码长一点也不要用这种“看上去没问题”的写法。3.3 结果可视化与调度指令导出优化跑完不能只看一个目标函数值还要把调度曲线画出来直观判断储能行为是否符合预期。我习惯用subplot画三张图第一张是功率平衡图把负荷、光伏、购电、放电画在一起第二张是储能功率与SOC曲线第三张是电价曲线用来对照储能充放时段和电价峰谷是否对应。%% 结果可视化 t_hours 0:23; figure(Position, [100, 100, 900, 700]); % 功率平衡图 subplot(3,1,1); plot(t_hours, P_load, k-o, LineWidth, 1.2); hold on; plot(t_hours, P_pv, g-s, LineWidth, 1.2); plot(t_hours, P_buy_opt, b-d, LineWidth, 1.2); plot(t_hours, P_dis_opt - P_ch_opt, r-^, LineWidth, 1.2); grid on; xlabel(时刻 (h)); ylabel(功率 (kW)); legend(负荷, 光伏, 购电, 储能净放电, Location, best); title(日前调度功率平衡曲线); % SOC曲线 subplot(3,1,2); stairs(0:24, SOC_opt, m-, LineWidth, 1.5); ylim([0 1]); grid on; xlabel(时刻 (h)); ylabel(SOC); title(储能荷电状态变化曲线); % 电价曲线 subplot(3,1,3); stairs(t_hours, price_buy, c-, LineWidth, 1.5); xlabel(时刻 (h)); ylabel(电价 (元/kWh)); title(分时电价曲线); grid on;这三张图放一起基本能一眼判断出模型行为是否合理。正常情况下SOC曲线应该是谷段上升、峰段下降充电功率集中在夜间低价时段放电功率集中在白天高价时段购电功率的峰值明显低于原始负荷的峰值。如果发现SOC曲线在白天谷段也上升或者储能充放电时段和电价峰谷完全对不上那就得回头检查电价向量和时刻索引是否对齐了。调度指令导出也很重要因为优化结果最终是要给现场执行的。我一般会把结果保存成一个CSV文件包含时刻、购电功率、充电功率、放电功率、SOC、光伏上网功率这些字段方便后续生成调度单或者接入能量管理平台%% 导出调度指令 result_table table((0:23), round(P_buy_opt, 2), ... round(P_ch_opt, 2), round(P_dis_opt, 2), ... round(SOC_opt(1:24), 4), round(P_sell_opt, 2), ... VariableNames, {时刻, 购电功率kW, 充电功率kW, ... 放电功率kW, SOC, 上网功率kW}); writetable(result_table, day_ahead_schedule.csv);4. 算例分析一个典型工业用户的日前调度结果4.1 基础数据设定为了让大家更直观地理解调度效果我用一个虚拟的典型工业用户来跑算例。这个用户是一个中型机械加工厂日间生产为主负荷高峰出现在上午9点到下午4点左右夜间有少量的保温负荷和办公照明负荷。同时厂房屋顶装了一套400kW的光伏系统。我给这个场景设定的关键参数如下表所示参数数值备注储能额定容量1000 kWh共享储能租赁容量储能额定功率500 kW充放电同功率充放电效率95%磷酸铁锂电池典型值SOC范围0.1 ~ 0.9保护电池寿命初始SOC0.20时状态变压器容量1200 kW购电上限谷段电价0.31 元/kWh0-5时峰段电价1.12 元/kWh8-11时、17-20时平段电价0.68 元/kWh其余时段共享储能日租赁费400 元/天固定容量费充电服务费0.05 元/kWh按充电量计这个场景设计包含了共享储能模式最典型的几个要素分时电价、光伏波动、变压器限值、租赁费用。模型需要在这些条件的共同约束下找出最优调度。4.2 调度曲线解读储能如何实现“低充高放”跑完优化模型我看到的调度行为是这样的凌晨0点到5点的谷段电价只有0.31元储能以接近额定功率的500kW开始充电4个小时左右把SOC从0.2一路充到0.9左右。这里注意充电不是一直充到满因为最后要留一点余量给循环约束和上午的生产计划。到了上午8点到11点的峰段电价升到1.12元储能开始以500kW功率放电正好覆盖了工厂上午的生产高峰负荷。光伏出力在这个时段也逐渐上升到400kW左右加上储能放电购电功率明显比原始负荷曲线低了一大截。下午17点到20点第二个峰段储能把剩余的电量放完SOC回落到初始值附近。整个过程里储能的净放电功率曲线和电价曲线形成漂亮的镜像关系电价高的地方放电电价低的地方充电。这个行为就是典型的分时套利。光伏出力则主要用于就地消纳余电上网的量非常少因为上网电价只有0.35元低于平段购电电价0.68元把光伏自己用掉比卖掉更划算。这也是优化模型自动算出来的结果而不是人为规则指定的。4.3 经济性对比共享储能到底省了多少钱算例最核心的产出是经济性对比。我把有无共享储能两种场景分别跑了一遍结果汇总如下成本项无共享储能有共享储能日购电成本元93208610光伏上网收益元0-40储能租赁费元0400充电服务费元0110日总成本元93209080日节省元-240这个案例里的日节省大约是240元年化下来约8.7万元。对于只租了1000kWh容量的中型用户来说这个收益水平符合工程实际。如果峰谷价差更大、或者储能容量翻倍节省额会明显上升。有一个细节值得展开无共享储能场景下光伏发电在午间负荷低谷时段会有一小部分余电但我这个算例设置的上网电价为0.35元略低于平段电价所以模型选择全部就地消纳或者少量上网收益很小。如果用户所在省份的上网电价补贴更高或者负荷曲线完全不同光伏上网量就会明显增加目标函数里那一项的作用也会变大。关于经济性解释我的个人体会是共享储能对于工业用户的价值不只是直接电费节省还包括了减少变压器需量电费、提升光伏消纳率、规避自建储能的安全运维风险。单看峰谷套利数字可能没那么性感但综合账算下来往往能支撑项目落地。5. 常见问题与排查技巧实录5.1 YALMIP与求解器配置的坑这套代码依赖YALMIP工具箱但YALMIP本身不包含求解器它只是一个建模层。很多读者第一次跑会碰到“No suitable solver”或者“No solver available”的报错就是因为电脑里只装了YALMIP没有装任何底层求解器。解决办法是先确认你有哪些求解器。在MATLAB命令行运行yalmiptest它会列出YALMIP识别到的所有求解器。如果列表里没有CPLEX、Gurobi或者intlinprog之外的东西就说明求解器没配好。MATLAB自带的intlinprog可以直接解决这种中小规模线性规划问题不需要额外安装但需要在sdpsettings里显式指定ops sdpsettings(solver, intlinprog, verbose, 1);如果用的是CPLEX或Gurobi这类商业求解器安装后还要确认MATLAB的搜索路径里包含了求解器接口文件。很多情况下明明装好了求解器但YALMIP就是识别不到十有八九是路径问题把求解器的MATLAB接口目录用addpath加进去或者用求解器自带的setup脚本配置一遍就好。5.2 模型报无解时从哪几个方向排查“Problem infeasible”是调度优化里最让人头疼的报错因为YALMIP只告诉你有解不告诉你哪里冲突。我踩过几次坑之后总结了一套排查顺序。先检查SOC循环约束。SOC(1)0.2同时SOC(25)0.2这个约束本身没问题但如果你把SOC上下限设置得太紧比如SOC_max0.9而初始SOC是0.5、额定容量只有500kWh、最大充电功率200kW那么一个谷段8小时最多充进去1600kWh但容量只能到450kWh这其实没问题。真正容易出问题的是你同时要求终值SOC等于0.2但最后一个峰段18点到20点只有3小时放电功率上限又太小导致放不完这就可能无解。然后检查功率平衡约束。如果负荷峰值大于变压器容量加储能最大放电功率之和任何时刻都无法同时满足负荷需求和购电上限模型必然无解。这个约束在数学上无解在物理上就是变压器容量不够需要扩容或者增加储能容量。再检查数据里是否有NaN或者Inf。从Excel读数据经常带入空值YALMIP遇到NaN不会报语法错误但求解器会直接判定无解。我的习惯是在读入数据之后加一行断言assert(all(isfinite(P_load)), 负荷数据包含NaN或Inf);5.3 SOC初值对结果的影响SOC初值的设置有讲究。如果设成0.5模型第一个谷段就不用充电或者少充电因为电池本来就有电。但如果设成0.9接近上限那么日间峰段放电会更充裕同时夜间谷段几乎没有充电空间。实际工程项目中SOC初值应当来自储能系统的实际状态而不是随便拍一个数。如果是离线仿真那么初值设成和终值相等是最稳妥的这保证了调度方案的长期可复用性。如果是接入实时系统做滚动优化那么初值应该读取当前实测SOC同时把终值约束改成“不强制等于初值但限制在一定范围内”给当天调度留一点灵活性。5.4 单位与时间索引的小细节我见过太多结果差1000倍的情况最后发现是kW和MW混用了。比如负荷数据从SCADA系统导出时单位是MW电价单位是元/kWh储能容量单位是kWh三组数据没统一就直接进模型算出来的功率平衡和成本全部失真。时间索引的错位更隐蔽。电价序列的第1个元素到底对应0点还是1点负荷是0-23时还是1-24时如果两个序列错位了一个小时储能就会出现“在电价谷段不充电、在峰段充电”的诡异行为而且不容易被察觉。我的习惯是在数据向量前面写注释标明时刻并在绘图时用横坐标0:23和1:24两种方式相互校验一旦发现峰谷错位立刻就能看出来。5.5 结果不合理时的自查清单最后给一张速查表遇到结果不符合预期时按照这个顺序逐条排查现象可能原因检查方法SOC曲线不变化储能功率约束过紧或电价序列错误看储能是否在任意时刻都到了功率上限充放电时段与峰谷不符电价向量对应时刻错位打印电价序列与时段标签对比购电功率超过变压器上限变压器约束漏写检查Constraints中是否有P_trafo_max项优化结果成本反而升高上网电价设置高于购电电价检查price_sell与price_buy的大小关系求解时间过长启用了二进制变量先撤掉互斥约束跑LP看结果差异功率平衡验证不通过结果提取后使用了错误的索引用value函数重新验算每个时段的功率和6. 这套代码还能往哪些方向扩展写完基础的日前调度之后你会发现这套框架的价值在于它是一个底座很多实际问题都可以在它上面做二次开发。最直接的扩展是加入需量电费。目前的目标函数只算了电度电费但大工业用户还有变压器容量费或最大需量电费这个费用跟一个月内出现的最大购电功率有关。如果把月度最大需量纳入目标函数模型就会在峰段主动控制购电功率峰值这比单纯峰谷套利更贴近实际账单。第二个扩展方向是考虑多用户共享容量。一套储能服务多个工业用户时功率平衡约束会变成多用户联合约束各用户的负荷共享同一个储能SOC但各自的购电成本和租赁费用分摊方式需要设计。你可以把单用户模型封装成一个函数再在上层用迭代算法分配容量就变成一个小型共享储能调度系统。第三个方向是应对光伏预测误差。日前调度用到的光伏曲线是预测值实际出力可能偏差很大。更稳健的做法是改成两阶段鲁棒优化或者模型预测控制用滚动时域把预测误差逐时修正。这个扩展对编程能力要求高一些但基础设施还是这套YALMIP框架只是把单次优化改成循环调用。我个人在实际操作中的体会是代码跑通只是第一步真正花时间的是数据清洗和结果校核。一个调度模型能不能被现场采纳往往取决于你画的功率平衡图能不能让生产主管一眼看懂取决于你导出的CSV能不能直接接入能量管理平台。建议在模型之外多花一点时间在结果可视化和数据交互上这套调度方案的工程价值会大很多。