1. 内容整体设计与思路拆解
1.1 核心需求解析:共享储能为什么值得研究
这几年做园区综合能源优化的项目,绕不开一个痛点:储能投资成本高,单一用户自建储能很难回本。一台100kW/200kWh的磷酸铁锂储能系统,设备采购加安装,动辄几十万起步,折算到全生命周期,度电成本普遍在0.6元/kWh以上。对园区里那些峰谷价差不到0.7元的省份,自建储能算下来基本是赔本买卖。
共享储能就是把这个问题换了个解法。它把储能的所有权和使用权分开,由第三方的储能运营商投资建设,园区内多个用户按需租赁容量。用户不用承担高昂的设备投资,只用付容量租赁费加充放电服务费,单价虽然比自建一度电成本略高,但胜在初始投资几乎为零,而且可以根据自身用能特征灵活增减租赁容量。从园区运营方角度,还能通过聚合多类负荷的调节能力,统一参与电网需求响应,获取额外收益。这个“共享+需求响应+多类型负荷协同”的组合,就是标题里最值得拆解的核心逻辑。
我做这个项目时,把整个问题定成了三层:第一层是园区内部多类负荷的用电特性建模,第二层是共享储能的充放电运行策略,第三层是需求响应机制下用户用电行为的优化调整。这三层合在一起,最终落到一个目标上——园区综合运行成本最小。用Matlab求解这个优化问题,得到的是一份可复现的储能配置参考和一套运行调度方案。
1.2 多类型负荷分类:先搞清楚园区里到底有哪些“可调节”资源
园区负荷不是铁板一块。办公楼里的空调、照明、电梯,工厂车间的生产线设备,冷库的制冷机组,还有电动车充电桩,这些负荷的响应特性和可控程度完全不同。建模的第一步就是把它们分类,分类方式直接决定后续需求响应策略的复杂度。
我把园区负荷分成三类:
- 刚性负荷:比如实验室仪器、医院手术室用电、安防系统,这类负荷在任何情况下都不能中断、不能平移、不能削减,必须全额满足,不参与任何形式的调节。
- 可转移负荷:这类负荷的特点是,用电总量在一个调度周期内是固定的,但用电时间可以平移。典型代表是工厂的清洗工序、污水处理站的间歇泵组、充电桩的充电计划。洗衣机和洗碗机就是家庭场景里的可转移负荷,工作时段可以从峰时挪到谷时,但总耗电量不变。
- 可削减负荷:这类负荷可以短暂降低用电功率,但总量会减少。典型代表是空调、照明、电梯,夏天高峰期把空调温度设定从24度调高到26度,照明亮度降低20%,电梯限用一半,这些就是削减。削减会带来用户舒适度损失,所以模型里要配一个补偿成本。
实际建模时,可转移负荷用“时间窗+总能量不变”来约束,可削减负荷用“削减比例上限+单位削减成本”来约束。这个分类逻辑在Matlab代码里很好实现,但不同负荷的参数差异很大,后面会在参数设置部分细说。
1.3 需求响应机制设计:价格型与激励型怎么选
需求响应分两大类,价格型需求响应和激励型需求响应。
价格型DR是靠电价信号引导用户自发调整用电行为,比如峰谷电价、实时电价、尖峰电价。用户看到峰时电价高,主动把可转移负荷挪到谷时,把可削减负荷降下来。这种方式的实施成本低,不需要额外的通信和控制系统,只要电价传导机制够灵敏,用户的响应意愿够强。我在模型里基于分时电价来做负荷侧的价格弹性系数建模,电力系统经典的“价格弹性矩阵”方法,能用不多参数描述负荷随电价变化的程度。
激励型DR是用户提前和电网或负荷聚合商签协议,在电网需要时(比如迎峰度夏的尖峰时段)按指令削减用电,完成后获得补偿。这种方式响应速度快、可控性强,但需要一套完善的计量、确认和结算机制。园区场景我更推荐价格型为主、激励型为辅的混合机制,价格型管日常的负荷调整,激励型管尖峰时刻的保底响应。模型里分两块:一块是分时电价下的负荷转移,一块是激励型DR的削减补偿。
2. 数学模型构建与参数设计
2.1 目标函数:先算清成本组成再写代码
经济运行研究,落到数学上就是一个优化问题,目标函数选“系统总运行成本最小”。“运行成本”这四个字背后,完整拆开至少包含六项:
第一项是向电网购电的电费,第二项是余电上网的收益(很多园区屋顶装了光伏,夜间低谷时也可能储能放电上网),第三项是支付给共享储能运营商的服务费,第四项是参与需求响应获得的补贴收益,第五项是可转移负荷调整产生的调度成本(比如调整生产计划导致的效率损失),第六项是可削减负荷削减后的舒适度补偿成本。目标函数写成数学形式大概是:
min C = sum(P_buy(t)*price_buy(t)*dt) - sum(P_sell(t)*price_sell(t)*dt) + sum(P_ess_c(t)*price_ess_c*dt) + sum(P_ess_d(t)*price_ess_d*dt) + sum(P_DR_curtail(t)*C_curtail*dt) + C_shift - sum(P_DR_incentive(t)*R_incentive*dt)写代码时我习惯把目标函数按成本项拆开写注释,每行对应一项成本,方便后续调试。有一点容易被忽略:储能充放电服务费要双向收,充电时按充电电量收服务费,放电时按放电电量再收一次,因为运营商承担了电池循环寿命损耗。这个在代码里如果漏了放电侧的服务费,算出来的储能利用率会虚高,和真实工程情况不符。
2.2 约束条件:一个都不能少的平衡关系
目标函数定完,难题全在约束里。我梳理了一下,整个模型至少需要五组约束,缺一组解法就跑飛:
功率平衡约束。这个最简单也最基础,任意时刻,园区总用电负荷加储能充电功率,等于光伏输出加储能放电功率加购电功率加余电上网功率。这个约束保证了系统能量的物理守恒,是优化模型的“宪法”。
共享储能运行约束。包括储能荷电状态(SOC)的递推方程,SOC上下限约束(一般取10%到90%,留一点余量防止过充过放),充放电功率上下限约束,以及最重要的充放电互斥约束——同一时刻储能不能既充电又放电,物理上分量也不允许同时流动。这个互斥约束在Matlab里用Big-M法处理,引入0-1变量,放电状态为1时充电功率强制为0,反过来也一样。Big-M的取值有讲究,取恰好大于功率上限的值即可,取太大求解器数值稳定性会变差。
可转移负荷约束。这类负荷有两重约束:总用电能量在一个调度周期内不改变;允许转移的时间窗口有限制,不能把白班的负荷挪到凌晨三点。用数学语言表达,就是累加转移后各时段用电量等于原始总用电量,同时每个时段的实际用电量不超过该设备的最大功率上限。
可削减负荷约束。每个时段的削减量不超过该时段基线负荷的一定比例,且最优建议设一个全天累计削减上限,防止某些算法为了省钱把某个时段削得太狠。削减比例上限设30%比较常见,太高了会严重影响用户生产生活体验。
需求响应参与约束。如果园区以聚合商身份参与电网的DR项目,还要满足最小响应容量、最长连续响应时间、单次响应持续时间等合约条款。这些约束有时候才是真正卡住方案的硬约束。
2.3 典型场景与参数设计
参数怎么设,直接决定结果靠不靠谱。我用的测试场景是一个中等规模的多功能园区,包含办公楼、轻工业厂房、数据中心和充电桩四个业态,配了分布式光伏和一套共享储能系统。关键参数列一张表:
| 参数名称 | 数值 | 说明 |
|---|---|---|
| 调度周期 | 24h | 单位时段15min,共96个点 |
| 分时电价峰值 | 1.15元/kWh | 峰段10:00-12:00, 18:00-20:00 |
| 分时电价谷值 | 0.32元/kWh | 谷段00:00-06:00 |
| 分时电价平值 | 0.68元/kWh | 其余时段 |
| 光伏装机容量 | 4MW | 典型日曲线按实际光照数据标幺化 |
| 共享储能容量 | 2MW/8MWh | 租赁方式,容量按需租用 |
| 储能充电效率 | 0.95 | 含PCS效率 |
| 储能放电效率 | 0.95 | 含PCS效率 |
| SOC范围 | [0.1, 0.9] | 保护电池寿命 |
| 可转移负荷比例 | 20% | 总负荷中的可平移部分 |
| 可削减负荷比例上限 | 15% | 峰时最大削减比例 |
| 削减补偿单价 | 0.8元/kWh | 高于峰时电价才有响应动力 |
这套参数模拟的是华东某地区真实的分时电价和光照特性。做完一组实验后我建议至少做三组对比:有共享储能无需求响应、有需求响应无共享储能、两者全有。三者对比才能看出两个措施分别贡献了多少,协同作用到底有多大。
3. Matlab实操:从建模到求解的全流程
3.1 工具选型:Yalmip+Cplex的组合为什么最稳
Matlab里求解优化问题的方案有好几种。最原始的fmincon适合非线性问题,但我们的模型是混合整数线性规划(MILP),因为那组充放电互斥约束的0-1变量把问题“整数化”了,用fmincon不仅慢,而且很容易陷入局部最优解。时代变了,做MILP还是用求解器最靠谱。
我的推荐组合是Matlab + Yalmip + Cplex(或Gurobi)。Yalmip是一个建模工具箱,它最大的价值是把你从求解器的语法泥潭里解放出来,用人类能理解的符号写约束,底层自动转换成求解器需要的矩阵形式。Cplex是IBM的商业求解器,对MILP问题的求解速度非常快,学术使用有免费授权。
选Cplex而不是Gurobi的原因纯粹是个人习惯,很多人问我Cplex和Gurobi哪个更好用。坦白说Gurobi这几年在某些问题类型上速度略快,但Cplex的老牌稳定性在MILP上的表现依然能打。如果你用的是较新版本的Matlab,Yalmip也能一键切换到Gurobi,代码几乎不用改动。
安装Yalmip很简单,去官网下载压缩包,解压后把文件夹加到Matlab路径即可。Cplex需要装对应版本的IBM ILOG Cplex Optimization Studio,装好后在Matlab里运行cplex.setup()完成接口配置。配好后在Matlab命令行运行yalmiptest,看到一连串的“Successfully solved”就说明环境没问题了。
3.2 代码框架:变量定义、约束构建、求解器的衔接
下面这段代码是我“模板化”以后的核心框架,每一步都有注释,照着这个骨架套数据就能跑通:
%% 含共享储能的园区多类型负荷需求响应模型 % 清除工作区 clear; clc; close all; % 时间参数定义 T = 96; dt = 0.25; % 96个时段,每时段15分钟 %% 基础数据读入 % 分时电价 price_buy = [repmat(0.32, 1, 24), repmat(0.68, 1, 16), repmat(1.15, 1, 8), ... repmat(0.68, 1, 16), repmat(1.15, 1, 8), repmat(0.68, 1, 24)]; price_buy = price_buy(1:T)'; price_sell = price_buy * 0.85; % 上网电价取购电电价的85% % 光伏出力标幺值曲线(典型日光照数据) pv_pu = [zeros(1,24), 0.1, 0.25, 0.45, 0.68, 0.85, 0.95, 1.0, 0.92, ... 0.78, 0.62, 0.42, 0.2, 0.05, zeros(1, 55)]; % 长度需与T对应 pv_pu = pv_pu(1:T)'; P_pv = 4000 * pv_pu; % 光伏功率(kW) % 负荷基线(四类负荷聚合后的总负荷曲线) load_base = [3500, 3400, 3300, 3200, ...]; % 实际用几天真实数据平均得到 % 可转移负荷占20%,可削减负荷占15% load_shift_base = 0.2 * load_base; load_curtail_base = 0.15 * load_base; load_fixed = load_base - load_shift_base - load_curtail_base; %% 定义决策变量 % 设置Yalmip求解器 ops = sdpsettings('solver', 'cplex', 'verbose', 2); % 连续变量 P_buy = sdpvar(1, T); % 购电功率 P_sell = sdpvar(1, T); % 售电功率 P_ess_c = sdpvar(1, T); % 储能充电功率 P_ess_d = sdpvar(1, T); % 储能放电功率 SOC = sdpvar(1, T+1); % 储能SOC,多一个初始点 P_shift = sdpvar(1, T); % 可转移负荷实际用电功率 P_curtail = sdpvar(1, T); % 可削减负荷实际用电功率 % 整数变量(充放电互斥变量) u_charge = binvar(1, T); % 充电状态,1为充电 u_discharge = binvar(1, T); % 放电状态,1为放电 %% 构建约束条件 constraints = []; % 约束1:储能SOC递推 P_ess_c_max = 2000; % 最大充电功率kW P_ess_d_max = 2000; % 最大放电功率kW E_ess_cap = 8000; % 储能总容量kWh eta_c = 0.95; % 充电效率 eta_d = 0.95; % 放电效率 SOC_min = 0.1; SOC_max = 0.9; SOC(1) = 0.2; % 初始SOC for t = 1:T constraints = [constraints, ... SOC(t+1) == SOC(t) + (P_ess_c(t)*eta_c - P_ess_d(t)/eta_d)*dt / E_ess_cap]; constraints = [constraints, SOC_min <= SOC(t+1) <= SOC_max]; % 充放电功率与互斥条件(Big-M法) constraints = [constraints, 0 <= P_ess_c(t) <= u_charge(t) * P_ess_c_max]; constraints = [constraints, 0 <= P_ess_d(t) <= u_discharge(t) * P_ess_d_max]; constraints = [constraints, u_charge(t) + u_discharge(t) <= 1]; % 购售电互斥 constraints = [constraints, 0 <= P_buy(t) <= (1-u_discharge(t)) * 10000]; constraints = [constraints, 0 <= P_sell(t) <= u_discharge(t) * 10000]; end % 约束2:功率平衡 for t = 1:T constraints = [constraints, ... load_fixed(t) + P_shift(t) + P_curtail(t) + P_ess_c(t) ... == P_pv(t) + P_buy(t) + P_ess_d(t) + P_sell(t)]; end % 约束3:可转移负荷约束(总能量不变,功率有上下限) for t = 1:T constraints = [constraints, 0 <= P_shift(t) <= 1.2 * load_shift_base(t)]; end constraints = [constraints, sum(P_shift)*dt == sum(load_shift_base)*dt]; % 约束4:可削减负荷约束 for t = 1:T constraints = [constraints, ... 0 <= P_curtail(t) <= load_curtail_base(t)]; end %% 目标函数 P_DR_incentive = (load_curtail_base - P_curtail); % 削减量 C_curtail_cost = 0.8; % 单位削减补偿元/kWh C_ess_charge = 0.05; % 充电服务费元/kWh C_ess_discharge = 0.08;% 放电服务费元/kWh objective = sum(P_buy .* price_buy * dt) ... - sum(P_sell .* price_sell * dt) ... + sum(P_ess_c * C_ess_charge * dt) ... + sum(P_ess_d * C_ess_discharge * dt) ... + sum((load_curtail_base - P_curtail) * C_curtail_cost * dt); %% 求解 optimize(constraints, objective, ops);这段代码如果顺利跑通,一般几秒到几十秒就能出结果,具体时间取决于整数变量的数量。96个时段、两组0-1变量,总共192个整数变量,对Cplex来说是“开胃菜”级别,通常10秒内收敛。
3.3 结果输出与关键指标分析
拿到求解结果之后,千万别只盯着一个总成本数就完事,一定要把关键变量的曲线画出来,逐条分析四条关键曲线的形态是否正确:
第一,储能SOC曲线必须呈现“谷充峰放”的形态。要是你看到SOC在峰时反而上升,肯定是约束或者电价数据出了问题。SOC曲线的整体走势和分时电价的对应关系是检验模型正确性的第一道直观指标。
第二,可转移负荷的功率曲线。正常情况下它应该整体从高电价时段向低电价时段平移。判断标准是看峰时段的P_shift是否被压低了,谷时段是否抬高了,但要检查总能量守恒约束有没有被违背。
第三,可削减负荷的削减量分布。削减量应该集中在电价最高的时段,且不超过设置的15%上限。
第四,购电功率曲线。优化后购电功率应该避开峰时段的高电价,出现明显的“削峰填谷”效果。如果购电功率曲线依然在峰值时段大幅攀升,要么是储能容量不够,要么是负荷不可调动。
我习惯最后生成两张对比图:一张是所有负荷叠加的优化前后总用电曲线对比,另一张是成本构成堆叠柱状图(购电成本、储能服务费、DR补偿成本三大块)。这两张图放论文里或者项目汇报材料里,都是最直观的成果展示。
4. 常见问题与排查技巧实录
4.1 求解器报“Infeasible”怎么排查
模型无解是我做这个项目时遇到最多的问题,十个初跑者有八个会遇到。多数情况不是数学公式写错了,而是约束条件设置过紧,留给优化算法的可行空间被压缩成了空集。
第一查功率平衡约束。先看等式左右的单位是不是一致,kW和kWh千万别混了。很多人漏乘了dt,导致功率和能量对不上,模型直接无解。这是最常见的低级错误,但排查起来最花时间。
第二查储能约束。初始SOC设太低,而前几个时段处于谷价,储能既想充电又受到充电功率限制,就有可能导致前面时段出现约束冲突。最简单的办法是把初始SOC适当调高到0.3左右,或者调整SOC上下限范围,给算法多一点空间。
第三查可转移负荷的总量约束。总量守恒虽然简单,但两边的时间粒度必须一致性。sum(P_shift)求的是总功率(kW),乘以dt才是总电量(kWh),而load_shift_base这一侧如果已经是电量,两边就对不上了。这种单位不一致引起的无解,属于每个做优化的人都要踩一遍的坑。
第四,检查Big-M的取值。M设置得太大虽然不影响可行性,但会影响数值稳定性,可能导致求解器误判约束条件,报出虚假的无解结果。M取功率上限的1.02倍到1.1倍就够了,不需要取1e6这种夸张值。
4.2 求解速度慢怎么办
模型规模一大,96个时段、多个储能系统、多个负荷聚合商同时优化,整数变量能到上千个,Cplex求解速度从秒级变成分钟级甚至小时级。遇到这种场景,我常用的解法有三个,从简单到复杂排列。
最优先的解法是减少时间分辨率。96个时段如果你精度要求不高,可以改成48个时段甚至24个时段,求解速度能提升一个量级。代价是结果精度略降,但对方案层面的可行性分析完全够用。
其次是固定一部分整数变量。在实际工程场景里,储能的充放电状态是受电价结构主导的,可以先跑一次松弛解,把那些松弛解中充放电状态已经很明确的整数变量固定住,只留状态模糊的时段做整数优化,速度能快很多。这种启发式方法虽然理论上不能保证全局最优,但工程上够用。
最后是给求解器设置合理的MIP gap容差。Cplex默认追求最优解,但实际项目里最优解和次优解的成本差异可能不到1%,没必要花几十分钟去抠那百分之零点几。用sdpsettings('cplex.mip.tolerances.mipgap', 0.01)把gap放宽到1%,求解时间往往能缩短到原来的十分之一。
4.3 储能无收益、负荷不响应的深层原因分析
群里经常看到有人问:“为什么我的模型结果里储能几乎不工作?”或者是“需求响应根本没起作用,负荷曲线和基线几乎完全重合”。
储能不工作,先看峰谷价差。价差太小的时候,充电的低谷电费加双向服务费加损耗,可能比直接高峰购电还贵,储能自然没有运行空间。共享储能本身也是有运行成本的,如果C_ess_charge和C_ess_discharge设置过高,尤其充电服务费和放电服务费都收的时候,储能的套利空间会被严重压缩。
负荷不响应,看需求响应补偿单价和电价的相对关系。如果单位削减补偿单价明显低于峰时电价,优化算法会倾向于从电网买电而不是削减负荷,可削减约束虽然允许削减但目标函数会全面拒绝削减。原则上补偿单价要高于峰时电价减去购电成本的差值,用户才有参与DR的动力。这个差值建模的时候可能没想清楚,结果算出来就是不响应。
还有一个很多人忽略的点:可转移负荷占比。如果load_shift_base设得太小,比如只有总负荷的5%,那就算可转移比例拉到100%,整体负荷曲线也看不出明显变化。实际园区数据里可转移负荷的真实占比需要调研或从历史负荷数据里估算,不能拍脑袋。
4.4 参数敏感性分析的三个重要观察
做了参数敏感性分析后,我梳理出三条规律,写论文或者做方案时可以直接引用:
第一条,共享储能容量配置存在“饱和效应”。储能容量从0增到8MWh时,总成本下降非常明显;但从8MWh增到12MWh时,成本的边际下降显著缩小,甚至可能因为服务费增加而总成本反弹。这个拐点就是最优配置容量点,对投资方决策是核心参考。
第二条,需求响应补偿单价不宜过高,也不宜过低,存在一个“最优激励区间”。补偿单价太低,用户不响应;单价过高,虽然负荷转移和削减量增加了,但系统支付给用户的补偿成本也水涨船高,总成本反而上升。我测算的这个场景里最优补偿单价约为1.2倍峰时电价,具体值依场景差异而变。
第三条,共享储能和需求响应有显著的协同效应。单独加需求响应,总成本降幅大约8%到10%,单独加共享储能,降幅大约12%到15%,两个措施同时上,总成本降幅不是22%到25%的简单叠加,而是在30%以上的超线性叠加。原因在于:需求响应转移了负荷,让储能有机会在更低的谷时充电、在更高的峰时放电,两边在一起把峰谷价差这个“利润空间”用得更加极致。
5. 最后再分享两句实在话
做完这个项目,我的切身体会是:模型和代码只是门槛,真正决定成果质量的,是参数的取舍和对结果的解读。调参过程中我在分时电价曲线、补偿单价、储能SOC范围之间反复调整了几十次,每调一个参数都要重新分析一次结果,这个过程很磨人,但也是理解储能经济性的最好方式。
如果你打算在这个方向继续深入,我建议下一步可以加一个时间维度上的不确定性分析,比如光伏出力和负荷预测误差,用场景法或者鲁棒优化的方法做,更贴近实际运行环境。也可以在共享储能侧加一个运营商的利润模型,做成主从博弈结构,更能体现园区内“共享”两个字的商业本质。这些扩展方向的研究实验我都在逐步验证中,等有了完整结果再来分享。