搞过分布式储能容量规划的人大概都有同感:真正让人头疼的不是光伏板选多大、电池买什么品牌,而是当用户变成了“产销者”(prosumer,既用电又发电),储能的容量到底该怎么配。最近我做了一个“考虑产销者的分布式储能容量配置策略”的Matlab代码实现项目,把目标函数、约束条件、粒子群搜索和算例分析完整串了起来。这篇文章不写教科书理论,直接把我的建模思路、代码结构、参数调试和踩坑记录摊开来讲,希望能帮到正在做储能规划、综合能源系统课题或相关工程项目的朋友。
1. 项目背景与问题拆解
1.1 产销者:既是用户又是发电方
传统配电网里,用户就是纯负荷,电从变电站流到用户侧,方向单一,调度思路也简单。但分布式光伏大规模铺开之后,越来越多的工商业用户、园区甚至居民楼顶都装上了光伏,白天自己发电,用不完还能上网。这就是产销者:同一套资产,一边消耗电力,一边向系统注入电力。
这个身份转变对负荷特征的影响非常大。普通用户的净负荷曲线是一条相对平滑的“早晚双峰”,但装了光伏之后,白天会出现很明显的“净负荷塌陷”,甚至在午间变成负值。负净负荷意味着光伏出力超过了负荷需求,如果不配储能,多出来的电量只能低价上网。到了傍晚光伏出力归零,晚高峰用电又要以高价从电网购回。这一低一高之间,本该属于用户的收益被时间错配吃掉了大半。
储能的价值正在这里:光伏出力高峰时充电,晚高峰放电,或者电价低谷时充电、电价高峰时放电,把能量从“富余时段”搬到“紧缺时段”。但储能容量不是越大越好,单体电池成本、安装空间、充放电效率、寿命衰减都要算进去。容量配小了,削峰填谷效果有限;配大了,设备长期闲置,投资迟迟收不回来。所以这个问题的本质是一个“投资成本与运行收益”的权衡,而且因为要考虑全年8760小时的运行状态,它天然是一个时序耦合的优化问题。
1.2 储能容量配置的决策变量与难点
储能容量配置的决策变量表面上只有两个:额定容量E(kWh)和额定功率P(kW)。但千万别被这个“表面简单”骗了,真正复杂的是运行层。
容量E决定了电池能存多少电,功率P决定了电池短时间内能充放多少电。两者还需要满足一个工程约束:P不能超过E乘以最大充放倍率。比如一颗100 kWh的电池,如果允许0.5C充放,那最大功率就是50 kW。同一个E/P组合,在不同的充放电策略下会产生完全不同的年收益,因此容量配置必须和运行策略耦合在一起求解。
难点主要集中在几个方面。
第一,时序耦合。储能的SOC(荷电状态)是逐时递推的,t时刻的充放电会影响t+1时刻的可放电量,不能像普通设备那样独立建模。
第二,非线性。充放电效率、电池寿命衰减、SOC与开路电压的非线性关系,都会让模型偏离线性规划的标准形式。
第三,不确定性。光伏出力受天气影响,负荷有季节波动,电价可能是分时电价、实时电价或需量电价,配置结果对这些外部参数很敏感。
第四,规划与运行的时间尺度不同。规划阶段要以年或整个寿命期为视角,但需要模拟日内的逐时运行,这就导致模型维度很大,求解时需要在精度和速度之间做取舍。
我在这个项目里采用了一个很常见的处理方式:先用典型日曲线代替全年8760小时,再用粒子群算法搜索外层配置变量,内层通过运行模拟计算对应的年成本。这样既有工程可解释性,Matlab跑起来也不会太慢。
1.3 用Matlab做这件事的定位
选Matlab而不是Python或GAMS,有几个实际原因。一是Matlab的矩阵运算和绘图能力很强,做逐时段功率平衡非常顺手;二是如果后续要换更精确的求解方法,可以直接借助优化工具箱;三是很多高校和科研团队的仿真环境就是Matlab,代码复用和交互验证更方便。
当然,Matlab的不足也很明显:商业软件成本高,纯循环速度不如编译语言。但对于这种决策变量很少、目标函数需要嵌入运行模拟的配置问题,性能完全够用。我这次选择用自写粒子群算法,没有依赖YALMIP和外部求解器,目的就是让代码逻辑透明、容易修改,读者改改目标函数和约束就能搬到自己的场景里。
2. 数学建模与求解策略
2.1 系统拓扑与建模边界
先明确一下建模范围。这个项目针对的是一个单体产销者,相当于用户内部有一条母线与电网连接,母线上接了三类设备:光伏、储能、常规负荷。电网作为外部平衡节点,缺电时买电,光伏富余时卖电。
功率平衡关系可以写成:
Ppv(t) + Pdis(t) + Pbuy(t) = Pload(t) + Pch(t) + Psell(t)
其中Ppv是光伏出力,Pdis是储能放电功率,Pbuy是电网购电功率,Pload是负荷功率,Pch是储能充电功率,Psell是上网卖电功率。所有量纲都统一到kW,时间步长取1小时或0.5小时。
这个平衡式是所有约束的根基。它隐含了一个假设:用户内部不考虑线路损耗,母线电压恒定,储能变流器效率单独用充放电效率表示。对于容量规划级别的项目,这种简化完全够用,不需要牵扯潮流计算。
2.2 目标函数:综合成本最小化
配置储能的最终目标不是让某个时刻的电费最低,而是让整个寿命期内的综合成本最小。我把目标函数定义成年综合费用,包括三部分:投资成本等年值、年运行费用、年维护成本。
投资成本等年值的计算方式如下:
C_inv = (alpha_E * E + alpha_P * P) * CRF
其中alpha_E是单位容量成本(元/kWh),alpha_P是单位功率成本(元/kW),E和P就是待优化的决策变量。CRF是资金回收系数,用来把初始投资分摊到每一年:
CRF = r * (1+r)^n / ((1+r)^n - 1)
r是折现率,n是储能寿命年限。假设折现率取8%,寿命取10年,那CRF大约是0.149。这个系数的意义很直白:如果今天投入100万元,未来10年每年都要承担约14.9万元的等额成本,这样才能跟当年的运行收益做公平比较。
年运行费用是逐时累加的:
C_run = sum_t (price_buy(t) * Pbuy(t) - price_sell(t) * Psell(t)) * dt
这里特别要注意卖电价格。如果当地光伏上网电价比较低,甚至低于峰时购电价,那储能“先充后放”的套利空间就很清楚;如果上网电价很高,可能光伏直接上网比存下来更划算。这个参数直接影响最优容量,后面做灵敏度分析时会重点看它。
年维护成本我简化成初始投资的2%,主要是覆盖电池管理系统、冷却系统以及定期巡检的人工费用。最终目标函数是:
min C_total = C_inv + C_run + C_om
2.3 约束条件:功率平衡、储能运行、容量上限
目标函数定好之后,约束条件决定可行域。我梳理了四类关键约束。
第一类是功率平衡,前面已经写了。需要补充的是,任何时刻储能充电和放电不能同时发生,也就是Pch(t) * Pdis(t) = 0。这是一个非线性互补约束,在粒子群算法里比较难直接处理,我的做法是用运行规则来保证:先计算净负荷,净负荷为正时放电,为负时充电,不设置同时充放的时刻。后面代码部分会详细展示。
第二类是储能SOC递推约束:
SOC(t+1) = SOC(t) + (Pch(t)*eta_ch - Pdis(t)/eta_dis) * dt / E
eta_ch是充电效率,eta_dis是放电效率。SOC有上下限,通常限制在0.1到0.9之间,避免过充过放损伤电池。
第三类是储能的功率和能量约束:
0 <= Pch(t) <= P 0 <= Pdis(t) <= P E_min <= E <= E_max P_min <= P <= P_max
另外还要满足倍率约束P <= k_rate * E,k_rate一般取0.5或1。如果不加这个约束,粒子群可能会给出“容量很大但功率极低”或相反的组合,运行起来完全不合理。
第四类是并网功率约束。为了防止变压器过载,购电功率和卖电功率不能超过并网容量限制。这个约束在单体产销者场景里通常取一个较大的上限,但多产销者场景中会变得很关键。
2.4 为什么选择粒子群算法而非线性规划
很多刚接触这个问题的朋友会问:为什么不直接用线性规划或混合整数线性规划?我的回答是:可以用,但需要做大量线性化处理,而且会牺牲灵活性。
储能容量配置的外层变量只有E和P两个,但内层运行变量是T个时段的充放电功率和SOC,一个典型日24小时就有72个以上变量。如果全年逐时模拟,变量数量会破千。目标函数里还有购电、卖电价格的分段差异、SOC的非线性递推、效率的非线性,直接做线性规划需要把这些全部线性化,建模工作量很大。
粒子群算法的优势是结构简单,目标函数可以做成黑箱。我把E和P作为粒子的位置,粒子每到一个位置,就调用一次运行模拟函数,返回一个综合成本。粒子群不断更新位置,朝低成本方向搜索。这个方法不要求目标函数可导、连续,甚至函数内部可以写if-else、查表这些逻辑,非常灵活。
缺点是它属于启发式算法,不能保证全局最优,而且对罚函数权重敏感。所以我的方案是:先快速用粒子群找到一个大致的敏感区间,再用精细网格或局部搜索在这个区间附近验证。规划阶段不需要精确到个位数,找到合理容量区间就够了。
3. Matlab代码实现细节
3.1 程序总体结构与数据准备
整个代码工程我拆成了几个模块,职责清晰才方便调试。
主程序是main.m,负责读取数据、设置粒子群参数、调用优化、输出结果。核心函数包括pso_storage.m、objective_func.m、simulate_storage.m和plot_results.m。数据部分用三个数组表示:负荷曲线load、光伏出力曲线pv、分时电价price。
数据准备这一步最容易出问题,我建议严格统一时间轴。我用的时间步长dt是1小时,一个典型日取24个点。如果负荷数据是15分钟间隔而光伏数据是1小时间隔,就会出现维度对不上,矩阵运算直接报错。项目初期建议把所有数据重采样到同一间隔,并保存成列向量。
%% main.m 数据准备示例 T = 24; % 典型日时段数 dt = 1; % 时间步长,单位小时 load = [120; 110; ... ]; % 24x1 负荷曲线,单位kW pv = [0; 0; ... ]; % 24x1 光伏出力曲线,单位kW price = [0.4; 0.4; ... ]; % 24x1 分时电价,单位元/kWh3.2 粒子群参数与种群初始化
粒子群的参数设置直接影响收敛效果。我这次取的种群规模是30,迭代次数100。变量维度只有2,属于低维问题,种群规模不用太大,太大反而拖慢收敛速度,太小容易陷入局部最优。
惯性权重w取0.7,学习因子c1和c2都取1.5。w较大时全局搜索能力强,w较小时局部开发能力强,后期可以线性递减。初始化时,粒子位置在上下界内随机生成,速度向量用较小的高斯随机数初始化,防止一开始速度过大直接飞出边界。
%% 粒子群初始化 nVar = 2; % 决策变量:储能容量E、功率P lb = [50, 20]; % 下限:容量50kWh,功率20kW ub = [500, 150]; % 上限:容量500kWh,功率150kW nPop = 30; maxIter = 100; w = 0.7; c1 = 1.5; c2 = 1.5; pos = repmat(lb, nPop, 1) + rand(nPop, nVar) .* repmat(ub - lb, nPop, 1); vel = 0.1 * randn(nPop, nVar); cost = arrayfun(@(i) objective_func(pos(i,:)), 1:nPop); pbest = pos; pbestCost = cost; [gbestCost, idx] = min(cost); gbest = pos(idx, :);初始化时建议把上下界设得宽一些,不要一开始就把E限制在某个很窄的范围,否则很容易错过有意义的配置区间。另外,单位要高度警惕:E的单位是kWh,P的单位是kW,如果上下界里把E和P写反,后面所有结果都会乱套。
3.3 目标函数与约束处理(罚函数)
objective_func是优化过程反复调用的核心,它的输入是粒子位置向量x,也就是E和P,输出是综合成本。它需要调用simulate_storage进行典型日运行模拟,然后计算投资等年值、年运行费用和维护成本。
我在这里采用罚函数法处理约束。如果模拟过程中出现SOC越界、功率越界或倍率约束不满足,就给目标函数增加一个很大的惩罚项,让这个粒子位置失去竞争力。罚函数权重太小,约束形同虚设;权重太大,会压制搜索能力。我一般先取1000倍的单位成本量级,然后看最终解是否满足约束,再做调整。
function totalCost = objective_func(x) E = x(1); P = x(2); % 倍率约束:P不能超过 E 的0.5倍 if P > 0.5 * E totalCost = 1e12; return; end [C_inv, C_run, C_om, violation] = simulate_storage(E, P); if violation > 0 totalCost = 1e12; return; end totalCost = C_inv + C_run + C_om + violation * 1e6; endsimulate_storage内部的运行策略,我采用了一个工程上常用的规则:按净负荷方向决定储能动作。净负荷为正,说明光伏不够用,优先储能放电;净负荷为负,说明光伏有富余,优先给储能充电。具体代码如下。
function [C_inv, C_run, C_om, violation] = simulate_storage(E, P) % 公共参数 eta_ch = 0.95; eta_dis = 0.95; socMin = 0.1; socMax = 0.9; r = 0.08; n = 10; CRF = r * (1+r)^n / ((1+r)^n - 1); alpha_E = 1500; % 元/kWh alpha_P = 800; % 元/kW C_inv = (alpha_E * E + alpha_P * P) * CRF; SOC = ones(25,1) * 0.5; Pch = zeros(24,1); Pdis = zeros(24,1); Pbuy = zeros(24,1); Psell = zeros(24,1); violation = 0; for t = 1:24 net = load(t) - pv(t); % 正为缺电,负为光伏富余 if net < 0 Pch(t) = min(P, -net); Pdis(t) = 0; else Pdis(t) = min(P, net); Pch(t) = 0; end % 避免超出SOC边界 if Pch(t) > 0 maxCh = (socMax - SOC(t)) * E / (eta_ch * dt); Pch(t) = min(Pch(t), maxCh); end if Pdis(t) > 0 maxDis = (SOC(t) - socMin) * E * eta_dis / dt; Pdis(t) = min(Pdis(t), maxDis); end SOC(t+1) = SOC(t) + (Pch(t)*eta_ch - Pdis(t)/eta_dis) * dt / E; if SOC(t+1) < socMin || SOC(t+1) > socMax violation = violation + 1; end Pbuy(t) = max(0, net - Pdis(t) + Pch(t)); Psell(t) = max(0, -net - Pch(t) + Pdis(t)); end C_run = sum(price .* Pbuy * dt - price_sell .* Psell * dt); C_om = (alpha_E * E + alpha_P * P) * 0.02; end这里有一个我踩过坑的细节:SOC递推公式中的除以效率一定要分清方向。充电时,电网或光伏给电池输入的能量要乘以充电效率,所以SOC增加量用的是Pch * eta_ch;放电时,电池输出到负荷的能量要除以放电效率,因为电池内部需要消耗更多能量才能输出这么多。方向写反,SOC会越算越离谱。
3.4 迭代与结果输出
主循环就是标准粒子群更新。位置更新后要做越界修正,我选择将越界变量放回边界并让对应的速度分量乘以一个负的小系数,相当于把粒子“弹回”可行域,避免它在下一次迭代继续朝边界外冲。
%% 粒子群主循环 converge = zeros(maxIter, 1); for it = 1:maxIter for i = 1:nPop vel(i,:) = w * vel(i,:) + c1 * rand(1,nVar) .* (pbest(i,:) - pos(i,:)) + c2 * rand(1,nVar) .* (gbest - pos(i,:)); pos(i,:) = pos(i,:) + vel(i,:); % 越界处理 for j = 1:nVar if pos(i,j) < lb(j) pos(i,j) = lb(j); vel(i,j) = -0.5 * vel(i,j); elseif pos(i,j) > ub(j) pos(i,j) = ub(j); vel(i,j) = -0.5 * vel(i,j); end end cost(i) = objective_func(pos(i,:)); if cost(i) < pbestCost(i) pbest(i,:) = pos(i,:); pbestCost(i) = cost(i); end if cost(i) < gbestCost gbest = pos(i,:); gbestCost = cost(i); end end w = 0.9 - 0.4 * it / maxIter; converge(it) = gbestCost; end运行结束后,我习惯输出最优E、P和对应的年综合成本,并且画出三张图:一是收敛曲线,二是典型日负荷、光伏、储能充放电功率曲线,三是SOC曲线。这些图能帮你快速判断结果是否合理,比单纯看数字靠谱多了。
4. 算例测试与结果分析
4.1 基础场景设定
为了验证代码,我虚构了一个小型工商业产销者场景。峰值负荷约200 kW,日用电量约3000 kWh,屋顶光伏峰值100 kW,典型日光伏出力呈钟形曲线。分时电价采用三段式:峰时1.2元/kWh(8-11时和18-21时),平时0.75元/kWh,谷时0.4元/kWh。光伏上网电价取0.35元/kWh,这个设置比较接近“自发自用、余电上网”的常见情况。
储能参数方面,磷酸铁锂单位容量成本按1500元/kWh,单位功率成本按800元/kW,充放电效率均为95%,SOC运行范围0.1-0.9,寿命10年,折现率8%。粒子群种群30个,迭代100次,每个粒子内部模拟24小时。
在这个场景下,代码给出的最优配置大约在300 kWh、80 kW附近。光伏午间出力高峰恰好被储能吸收,晚高峰放电,日净购电量明显下降。年综合成本与无储能场景相比下降了约18%,静态回收期大约5到6年。这个结果是合理的,说明代码逻辑没有跑偏。
4.2 多方案对比:无储能、经验配置、优化配置
只看一个优化结果还不够,我把三种方案放在一起做了对比。所谓经验配置,就是很多工程上喜欢“按光伏装机容量的1.5倍配储能”,光伏100 kW就配150 kWh、75 kW左右,这个方案投资低,但对晚高峰的支撑明显不足。
| 方案 | 储能容量(kWh) | 储能功率(kW) | 年购电成本(万元) | 年售电收入(万元) | 投资年值(万元) | 年综合成本(万元) | 回收期(年) |
|---|---|---|---|---|---|---|---|
| 无储能 | 0 | 0 | 98.5 | 6.2 | 0 | 92.3 | - |
| 经验配置 | 150 | 75 | 88.2 | 3.1 | 3.8 | 88.9 | 7.2 |
| 优化配置 | 300 | 80 | 76.4 | 2.5 | 6.9 | 80.8 | 5.3 |
无储能时售电收入虽然最高,但购电成本也高,综合成本反而最差。经验配置成本居中,但回收期偏长。优化配置虽然前期投入高,年运行成本下降明显,综合年成本最低,回收期也更短。
这个对比暴露出一个很容易被忽略的问题:储能容量并不是“越多越好”,但也不应该因为初期投资高就畏手畏脚。市场价格机制下,存在一个明显的边际收益递减点,过了这个点,多配的电池就只能趴在车库里晒太阳。
4.3 关键参数灵敏度分析
配置结果对参数很敏感,我做了三组灵敏度测试。
第一组是峰谷价差从0.5元/kWh逐步调整到1.2元/kWh。结果显示,最优E和P都随价差增大而增大,但在价差超过1.0元后增速放缓。原因是储能的可调度电量受制于日累计光伏富余电量和负荷低谷冗余,单纯提价差无法突破物理能量上限。
第二组是把光伏峰值从100 kW提升到150 kW和200 kW。光伏越富裕,最优储能容量越大,但并非线性增加。光伏从100到150 kW时,最优容量增加了约60 kWh;从150到200 kW时,只增加了约30 kWh。因为负荷本身的消纳能力有限,光伏再大,多出来的电量如果储能装不下,也只能低价上网。
第三组是储能单位容量成本从1200元/kWh提高到1800元/kWh。成本越高,最优容量越小,这个方向很好理解。但有趣的是,成本提高时,最优功率下降幅度比容量下降幅度更小,说明功率配置更多是由晚高峰的削峰需求驱动,容量则更多由套利收益驱动。
这些灵敏度分析结果对工程决策很有参考价值。如果手里只有一笔有限的预算,优先保证功率满足晚高峰负荷,再考虑容量扩展,往往比一开始追求大容量更稳妥。
5. 常见问题与调试经验
5.1 常见报错与运行问题
代码跑不起来或者结果怪异,大多数时候不是算法问题,而是数据、单位和约束处理的问题。我整理了一张排查表,都是这次项目里真实遇到过的。
| 问题表现 | 常见原因 | 解决方法 |
|---|---|---|
| 矩阵维度不一致报错 | 负荷、光伏、电价数组长度不相等 | 统一重采样到同一时间序列,检查24x1格式 |
| 最优配置一直贴着上限 | 上下界设置不当,或罚函数权重太低 | 先放宽边界跑一次,看最优解的分布位置 |
| 结果出现E=50、P=150这类畸形组合 | 倍率约束缺失或判断条件反了 | 确保P <= 0.5*E,并写在objective_func最前面 |
| SOC曲线长期贴0或1 | 充放电方向与效率公式不匹配 | 检查SOC递推公式,输出每时刻Pch、Pdis |
| 同一个项目多跑几次结果差异很大 | 粒子群初始随机、未收敛 | 增加迭代次数,多跑几次取最优,或改用线性递减w |
| 回收期计算为负 | 年综合成本没包含投资年值 | 先算C_inv,再算净收益,确保口径一致 |
5.2 结果不收敛或陷入局部最优的排查
粒子群算法的一个通病是“早熟”,也就是所有粒子很快挤到某个局部最小值附近,不再探索其他区域。这次项目里我也遇到过几次。
第一次遇到时,我的解决方法是把种群从20增加到60,迭代从100增加到300,结果发现计算时间翻了几倍,最优值却没有明显改善。后来我意识到问题不在种群大小,而在于目标函数空间里有不少“平台”——多个E/P组合的年成本非常接近,粒子在平台上来回震荡,视觉上看起来像不收敛。
真正的突破点是改了两处。一是惯性权重w采用线性递减,从0.9降到0.5,前期保持全局探索,后期加强局部开发。二是对越界粒子不再简单截断,而是把速度反向重置,防止粒子反复在同一片边界区域打转。改完之后,同样的迭代次数下,收敛曲线明显更平稳,最终成本也更低。
如果你发现每次跑出来的最优容量相差超过20%,我建议先不要急着调算法,而是先检查目标函数的数值稳定性。特别是罚函数权重是否过大,如果任何一个约束轻微违反都会导致成本变成1e12,目标函数就会变成一片“悬崖”,粒子群很难有效搜索。
5.3 扩展到多产销者集群的思路
单体产销者的容量配置做完之后,自然而然会遇到集群场景,比如一个园区里多个用户各自装光伏和储能,或者几个用户共享一座储能电站。这个场景下,问题会从单目标优化变成多主体博弈或双层优化,复杂度上一个台阶。
最简单的扩展方式是引入共享储能的概念:多个产销者共同投资一座储能,调度策略以“最小化系统总运行成本”为目标,总成本按各用户的容量占比或用电量分摊。这样外层决策变量就不只是E和P,还包括各个用户的分配比例。
更复杂的场景是每个产销者独立决策、独立运营,彼此之间有电能交易。这时每个主体的最优容量依赖于其他主体的决策,需要借助博弈论里的纳什均衡来求解。Matlab里可以用粒子群嵌套迭代方法:外层每个主体轮流优化自己的E/P,更新策略后再次迭代,直到所有主体的策略不再变化。
这种扩展虽然代码量会增加不少,但核心的储能运行模拟函数可以直接复用。这也是我当时坚持把simulate_storage写成独立函数的原因:单个主体换成多主体,只需要在主循环里增加一层循环,内部运行模拟逻辑不需要推倒重来。
做完这个项目,我个人最大的体会是:容量配置模型的价值不只是算出一个“最优数字”,更在于帮你建立一条从投资参数、运行策略到年度收益的完整因果链。如果只看最终配置结果,不把光伏曲线、分时电价、运行规则这些因素揉在一起推演,很容易得出一个表面上省钱、实际上回收期极长的方案。最后再分享一个小技巧:拿到代码先不要急着换复杂数据,用一组简单的“24点恒定负荷+钟形光伏”跑通流程,确认SOC曲线、收敛曲线都正常,再逐步引入真实数据。这个习惯能帮你节省大量调试时间,也能避免在数据问题里越陷越深。