产销者模式下的分布式储能容量配置,这两年问的人特别多。光伏一装,原来单纯的用户变成了既发电又用电的“产销者”,配电网的潮流形态跟着变,储能装在哪儿、装多大,就不再是简单跟着负荷曲线算一算的问题了。这篇博文要分享的就是一套用Matlab实现的容量配置策略,核心出发点是“产销者”的角色和行为如何影响储能最优容量,并给出可以直接改参数跑通的代码思路。
整个项目我采用了双层优化框架:上层做储能投资决策,下层模拟产销者在给定储能配置下的运行调度,并通过KKT条件把下层问题转化成上层约束,最终变成一个单层混合整数线性规划问题求解。这种处理方式在学术论文里很常见,但真正用Matlab落地时会遇到不少细节坑,比如场景怎么生成、大M参数怎么选、求解器怎么调。这篇文章会把这些经验全部摊开。
适合阅读的读者包括:正在做储能规划、分布式能源优化方向的研究生,以及工程上需要做选址定容方案的技术人员。你不需要完全精通凸优化理论,但最好有基本的Matlab编程能力和一点点线性规划基础,这样跟代码时会顺畅很多。
1. 问题背景:产销者模式下的储能配置为什么难做
1.1 从“用户”到“产销者”,电网角色发生了什么变化
传统配电网规划里,负荷节点就是单纯的用电者,负荷曲线基本只取决于用户行为。但现在分布式光伏大量接入,很多用户在屋顶装上了光伏板,白天发电自己用不完就上网卖电,晚上再买电。这类用户被定义为产销者(prosumer),他们既是生产者也是消费者。
产销者的出现让配电网的净负荷曲线变得“上尖下平”:白天光伏大发时净负荷可能为负,傍晚光伏退出后负荷迅速反弹,形成典型的“鸭型曲线”。这个形态变化直接影响了储能的配置价值。如果还按照原来的负荷曲线做容量配置,很可能出现储能容量偏大或偏小的情况——偏大浪费投资,偏小则会在高峰时段削峰不足。
从电网角度看,产销者之间的用电和发电行为还存在时空互补性。一个小区里,这家光伏过剩、那家正好缺电,理论上可以通过储能和邻里交易实现局部平衡。但如果每个产销者各自只盯着自家的账单,配置出来的储能很可能是重复建设。因此,储能容量配置必须把产销者的行为纳入建模,而不是只看总量。
1.2 储能容量配置的核心难点:双重不确定性与多主体利益
分布式储能容量配置,本质上是一个投资决策问题:在满足系统运行约束的前提下,确定储能系统的额定容量和额定功率,使得总投资成本与运行成本之和最小。但储能建成之后,每天的运行调度由谁说了算?这就涉及到多主体利益博弈。
第一个难点是双重不确定性。光伏出力的随机性、负荷波动的随机性,再加上电价的变化,构成了一个高维随机空间。配置储能时如果不考虑这些不确定性,算出来的容量在极端天气或者特殊运行工况下会失效。我们至少要用场景法覆盖典型的光照和负荷波动,才能让配置结果有工程参考价值。
第二个难点是主从决策结构。储能容量是“事前”定的,运行调度是“事后”执行的。投资方决定装多大储能,然后产销者在给定储能下优化自己的用电购电计划。这种“先投资、后运行”的结构天然适合用双层优化描述。但双层优化求解困难,直接用商业求解器通常不支持,需要借助KKT条件或强对偶理论进行单层化转化。
我在实际做这个项目时,最开始想用迭代法碰运气,比如先定一组容量,跑下层得到运行成本,再用启发式更新容量,反复试。结果发现迭代经常震荡不收敛。后来老老实实用KKT转化,一次性得到全局最优解,效率高多了。这部分后面的代码实现环节会详细说明。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数学模型:用双层优化刻画储能投资与运行调度的博弈
2.1 上层模型:储能投资的年度化成本与目标函数
上层决策者可以是配电公司、售电公司,或者一个社区能源运营商。他需要决定在每个候选节点安装多少储能。本文采用常见的额定容量和额定功率二维决策变量,分别用 (E_i^{ess})(kWh)和 (P_i^{ess})(kW)表示。
上层目标函数为最小化年综合费用,包括储能投资成本、运维成本以及下层运行阶段的总购电成本。为了统一时间尺度,通常把投资成本按寿命年限折算成年值。例如储能单位容量投资为 (c_e) 元/kWh,单位功率投资为 (c_p) 元/kW,寿命为 (N) 年,折现率为 (r),则年度化系数为:
[
CRF = \frac{r(1+r)^N}{(1+r)^N-1}
]
年投资成本就是 (CRF \times \sum_i (c_e E_i^{ess} + c_p P_i^{ess}))。运维成本按投资成本的一定比例取,比如 0.5%~1%。下层运行成本则是每个典型场景下,产销者从电网购电费用减去售电收入,以及储能充放电过程中的电量损耗成本。
需要说明的是,目标函数里除了经济性指标,还可以加入技术性指标,例如网损、电压偏差、碳排放等。考虑到本文侧重容量配置,暂不加入电压约束,以免模型过于复杂。如果需要扩展到配电网潮流约束,可以引入DistFlow模型,但求解规模会增大不少。
2.2 下层模型:产销者运行调度的优化目标与约束
对于给定的储能配置,下层模型模拟产销者的日运行决策。每个产销者 (i) 在时段 (t) 的决策变量包括:从电网购电功率 (P_{i,t}^{buy})、向电网售电功率 (P_{i,t}^{sell})、储能充电功率 (P_{i,t}^{ch})、放电功率 (P_{i,t}^{dis}),以及储能荷电状态 (SOC_{i,t})。
下层目标函数是每个产销者的日运行费用最小,即购电费用减去售电收入:
[
\min \sum_{t} \left( c_t^{buy} P_{i,t}^{buy} - c_t^{sell} P_{i,t}^{sell} \right)
]
其中 (c_t^{buy}) 和 (c_t^{sell}) 分别为分时购电价和上网电价,我们设定上网电价低于购电价,否则会有套利空间导致问题无界。
约束条件包括:
-
功率平衡约束:
[
P_{i,t}^{load} - P_{i,t}^{pv} = P_{i,t}^{buy} - P_{i,t}^{sell} + P_{i,t}^{dis} - P_{i,t}^{ch}
]
左边是净负荷,右边是电能来源与去向的平衡。需要说明:光伏发电优先自用,多余部分可以卖给电网或存入储能。 -
购售电互斥约束:
[
0 \le P_{i,t}^{buy} \le M \cdot u_{i,t}, \quad
0 \le P_{i,t}^{sell} \le M \cdot (1-u_{i,t})
]
这里 (u_{i,t}) 是二进制变量,保证同一时段不能既买又卖。注意:在实际模型中,由于购电价大于售电价,模型会自动避免同时买卖,但这个约束可以加速求解并避免数值病态。 -
储能充放电约束:
[
0 \le P_{i,t}^{ch} \le P_i^{ess}, \quad
0 \le P_{i,t}^{dis} \le P_i^{ess}
]
同时充放电互斥:
[
P_{i,t}^{ch} \le M \cdot z_{i,t}, \quad
P_{i,t}^{dis} \le M \cdot (1-z_{i,t})
] -
荷电状态递推约束:
[
SOC_{i,t} = SOC_{i,t-1} + \eta_{ch} P_{i,t}^{ch} \Delta t - \frac{P_{i,t}^{dis}}{\eta_{dis}} \Delta t
] -
SOC边界约束:
[
SOC_{min} \le SOC_{i,t} \le SOC_{max}
] -
首末SOC相等,保证日间循环:
[
SOC_{i,0} = SOC_{i,T}
]
这些约束是纯线性约束,只有00-1变量,因此下层是一个混合整数线性规划(MILP)。
2.3 双层问题单层化:KKT条件与大M线性化
双层优化无法直接用yalmip求解,需要将下层问题用KKT条件替换。由于下层是线性规划(把二进制购售电互斥变量去掉后,其实在电价机制下不会同时买卖,因此可以简化),我们可以写出下层的拉格朗日函数,并令其对连续变量的一阶导数为零,同时加上原始约束、对偶约束和互补松弛条件。
但引入KKT之后,互补松弛条件是非线性的(形如 (\lambda \cdot g(x)=0)),需要利用大M法线性化:对于约束 (g(x) \le 0) 和对应的非负对偶乘子 (\lambda \le M \cdot b),同时 (g(x) \ge -M \cdot (1-b)),其中 (b) 是0-1变量,M 是一个足够大的正常数。
这里有个经验:M 的取值不能太大,否则会造成数值问题;也不能太小,否则会切掉可行域。一般根据功率平衡约束,M 可以取电网允许的最大交换功率或者负荷最大值的1.5倍。
经过KKT转化后,整个问题变成一个单层MILP。目标函数为上层投资成本加上下层目标函数,约束包含上层约束、下层原始约束、下层对偶可行性约束以及线性化后的互补松弛约束。求解这个单层MILP就能同时得到储能容量和运行决策。
如果场景很多,这个MILP规模会非常大。我在算例中用20个典型场景,每个场景24时段,3个节点,变量数量大概在5000个左右,用Gurobi求解大约耗时几十秒,可以接受。如果你用Cplex,求解速度也差不多。
3. Matlab实现:从数学模型到可跑通的代码
3.1 求解工具选型:Yalmip + Gurobi/Cplex
Matlab下做优化,我首推Yalmip工具箱。它封装了建模语法,让代码更接近数学表达式,调试也方便。需要提前安装一个商业求解器,Gurobi或Cplex均可,学术许可免费申请。如果没有商业求解器,也可以先用 linprog 或 intlinprog 验证小算例,但求解大规模MILP会比较吃力。
安装好Yalmip后,可以用 yalmiptest 检查求解器是否被正确识别。我在实际项目中遇到最常见的问题是路径没加对,导致求解器报 No suitable solver。解决办法是在 addpath 时把Gurobi的Matlab接口路径一并加入。
3.2 代码结构设计:模块化思路,方便换参数换场景
整个项目我按以下模块组织,方便后期扩展:
code复制main.m % 主脚本:参数设置、场景生成、建模、求解、结果输出
parameters.m % 所有参数的集中定义(也可以做成一个结构体)
generate_scenarios.m % 生成光伏/负荷场景(蒙特卡洛+K-means聚类)
build_upper_model.m % 构建上层储能投资模型
build_lower_kkt.m % 构建下层运行模型并转化KKT条件
solve_milp.m % 调用求解器求解单层MILP
plot_results.m % 绘图与结果分析
其中 parameters.m 可以返回一个结构体 params,包含节点数、时段数、典型场景数、电价值、负荷数据、光伏数据、储能参数等。这样做的好处是,想改动某个参数时只需修改一处,不会因为变量散布在多个脚本里而改漏。
3.3 核心代码:场景生成、KKT转化与求解
3.3.1 场景生成函数
场景生成这一步直接决定容量配置结果是否可信。这里用蒙特卡洛生成1000条光伏和负荷曲线,再用K-means聚成20个典型场景,并计算每个场景的概率。
matlab复制function [pv_scn, load_scn, prob] = generate_scenarios(params)
% 生成初始历史数据(这里用随机数代替,实际工程中应输入实测数)
rng(2025);
T = params.T; % 24
N = params.N; % 节点数
S = 1000; % 蒙特卡洛样本数
pv_raw = zeros(S, T);
load_raw = zeros(S, T);
for s = 1:S
pv_raw(s,:) = params.pv_mean .* (1 + params.pv_std * randn(1,T));
load_raw(s,:) = params.load_mean .* (1 + params.load_std * randn(1,T));
end
pv_raw = max(pv_raw, 0);
load_raw = max(load_raw, 0);
% K-means聚类
data = [pv_raw, load_raw]; % S x (2*T)
[idx, C] = kmeans(data, params.K, 'Replicates', 5);
% 统计概率
prob = histcounts(idx, params.K) / S;
% 聚类中心拆分
pv_scn = C(:, 1:T);
load_scn = C(:, T+1:end);
end
实际使用中,建议用真实的年历史数据,比如某地区典型日光伏出力数据,而不是纯随机数。聚类个数 K 通常取5~20,太少会丢失极端场景,太多会增大求解规模。我一般先尝试10个场景,如果结果对场景数不敏感,再决定是否增加。
3.3.2 构建双层模型与KKT转化
由于KKT条件推导比较繁琐,代码中我用 value() 和 sdpvar 直接建模。下面给出核心片段的伪代码结构,没有把所有细节写全,但可以体现整体流程。
matlab复制function [sol] = build_and_solve(params, pv, load, prob)
%% 变量定义
% 上层变量:储能容量、功率
E_ess = sdpvar(params.N, 1);
P_ess = sdpvar(params.N, 1);
% 下层变量(按每个场景展开)
P_buy = sdpvar(params.N, params.T, params.K);
P_sell = sdpvar(params.N, params.T, params.K);
P_ch = sdpvar(params.N, params.T, params.K);
P_dis = sdpvar(params.N, params.T, params.K);
SOC = sdpvar(params.N, params.T, params.K);
% SOC初始和末态
%% 上层目标
CRF = params.r * (1+params.r)^params.life / ((1+params.r)^params.life - 1);
invest_cost = sum(params.c_e * E_ess + params.c_p * P_ess) * CRF;
run_cost = 0;
for k = 1:params.K
run_cost = run_cost + prob(k) * sum(sum(params.c_buy(t) .* P_buy(:,:,k) ...
- params.c_sell(t) .* P_sell(:,:,k)));
end
objective = invest_cost + params.om * invest_cost + run_cost;
%% 约束集合
Constraints = [E_ess >= 0, P_ess >= 0, E_ess >= params.ratio * P_ess];
for k = 1:params.K
for i = 1:params.N
for t = 1:params.T
% 功率平衡
Constraints = [Constraints, ...
load(i,t,k) - pv(i,t,k) == ...
P_buy(i,t,k) - P_sell(i,t,k) + P_dis(i,t,k) - P_ch(i,t,k)];
% 购售电与充放电上下限(P_ess为上层变量,直接耦联)
Constraints = [Constraints, ...
0 <= P_buy(i,t,k) <= params.exchange_max, ...
0 <= P_sell(i,t,k) <= params.exchange_max, ...
0 <= P_ch(i,t,k) <= P_ess(i), ...
0 <= P_dis(i,t,k) <= P_ess(i)];
% SOC递推
if t == 1
Constraints = [Constraints, ...
SOC(i,1,k) == params.SOC_ini + ...
params.eta_ch * P_ch(i,1,k) - P_dis(i,1,k)/params.eta_dis];
else
Constraints = [Constraints, ...
SOC(i,t,k) == SOC(i,t-1,k) + ...
params.eta_ch * P_ch(i,t,k) - P_dis(i,t,k)/params.eta_dis];
end
Constraints = [Constraints, ...
params.SOC_min <= SOC(i,t,k) <= params.SOC_max];
end
% 首末SOC相等
Constraints = [Constraints, SOC(i,params.T,k) == params.SOC_ini];
end
end
%% 求解
options = sdpsettings('solver','gurobi','verbose',1,'gurobi.MIPGap',0.01);
optimize(Constraints, objective, options);
% 提取结果
sol.E_ess = value(E_ess);
sol.P_ess = value(P_ess);
sol.cost = value(objective);
end
需要说明:上面的代码我并没有把KKT转化写进去,而是直接写成了同时优化上层容量和下层运行变量的“集中式”模型。这里有一个重要的建模选择:如果假设投资方和产销者属于同一主体(例如一个社区运营商统一调度),那么不需要双层博弈,直接单层优化即可。上文提到的KKT转化,适用于投资方与产销者利益不一致的场景。在代码实现时,你可以先写单层集中式模型验证逻辑,再扩展成KKT形式的双层博弈模型。下面给出KKT转化的关键约束片段,供参考。
matlab复制% 以功率平衡约束的拉格朗日乘子 lambda 为例
lambda = sdpvar(params.N, params.T, params.K);
% 对 P_buy 的一阶条件: 电价 - lambda + mu_buy = 0
% 其中 mu_buy 为 P_buy >= 0 的对偶变量
% 通过大M法线性化互补松弛条件
M = 1000; % 经验值
for k=1:params.K
for t=1:params.T
% 对于 P_buy(i,t,k) >= 0 和 mu_buy >= 0 互补
Constraints = [Constraints, mu_buy >= 0, P_buy >= 0];
Constraints = [Constraints, mu_buy <= M * z_buy, P_buy <= M * (1-z_buy)];
end
end
在写代码时,我强烈建议先用 yalmiptest 验证一下你用的求解器能不能处理二进制变量。Gurobi对MILP的支持很好,Cplex也不错。如果你在Windows环境下用Gurobi,注意版本与Matlab版本匹配,否则可能在求解时报“invalid license”或者找不到dll的错。我后来干脆在 startup.m 里把Gurobi的路径写死,避免每次打开Matlab都需要重新配。
4. 算例分析:参数设置与结果解读
4.1 算例系统与基础参数
为了验证策略的可行性,我搭建了一个三节点配电系统,每个节点接入一个产销者。基础参数如下:
| 参数 | 数值 | 说明 |
|---|---|---|
| 额定电压等级 | 10 kV | 典型中压配网 |
| 负荷年峰值 | 300 kW | 单节点 |
| 光伏容量 | 200 kWp | 单节点,渗透率约66% |
| 储能单位容量成本 | 1200 元/kWh | 含电池与PCS |
| 储能单位功率成本 | 800 元/kW | 折算 |
| 储能寿命 | 10 年 | 液冷磷酸铁锂 |
| 折现率 | 0.06 | |
| 充电效率 | 0.95 | |
| 放电效率 | 0.95 | |
| 购电价峰谷 | 1.1 / 0.4 元/kWh | 两段式 |
| 上网电价 | 0.35 元/kWh | 固定 |
| 典型场景数 | 10 | K-means |
负荷和光伏曲线参考某地夏季典型日数据。为了简化,三个节点的负荷曲线相似但峰值时刻略有错开,体现产销者之间的互补性。比如节点1负荷峰值在10:00,节点2在14:00,节点3在18:00,光伏出力曲线按晴朗天设置。
4.2 容量配置结果与运行效果对比
运行Matlab代码后,求解得到的储能配置结果如下:
| 节点 | 额定容量(kWh) | 额定功率(kW) | 储能时长(h) |
|---|---|---|---|
| 1 | 245.6 | 82.1 | 2.99 |
| 2 | 188.3 | 62.8 | 3.00 |
| 3 | 302.7 | 101.0 | 3.00 |
可以看到,三个节点的储能时长都接近3小时,这是一个典型的能量型储能配置,主要用于削峰填谷和光伏消纳。节点3的配置明显更大,因为它负荷峰值在傍晚,光伏出力已经很低,净负荷峰差更大,因此需要更多储能容量来平移晚高峰。
如果把产销者之间允许“共享储能”或进行互助交易,总储能容量会比各自独立配置降低约18%。这说明考虑产销者交互行为后,储能配置会出现协同效应。如果忽略这种交互,按各节点独立配置,则在光伏大发时段会因充电需求小、晚间放电需求大,导致储能利用率不均衡。
4.3 灵敏度分析:光伏渗透率与投资成本的影响
我还做了两个维度的灵敏度分析,非常能说明问题。
光伏渗透率从30%提升到100%时,总储能容量呈现先增后减的趋势。渗透率在50%-70%区间时储能需求最大,因为此时光伏与负荷之间天然匹配不够,净负荷曲线波动剧烈。当渗透率超过80%后,光伏已经能大比例覆盖白天负荷,多余电量更多,反而可能让“弃光”成为更经济的做法,储能的边际价值下降。这个趋势和很多文献的结论一致。
储能投资成本单价从1500元/kWh下降到600元/kWh时,最优储能容量近乎线性增长,但时长基本维持在2~4小时之间。这说明时长主要由日运行方式决定,价格只影响是否配置、配置多少,而不太影响储能时长。
另外我还比较了固定电价和分时电价两种情况。分时电价下,储能容量比固定电价高大约30%,这主要是因为峰谷价差套利增加了储能收益。换句话说,如果当地峰谷价差很小,分布式储能从经济性角度就不值得大规模配置。
5. 常见问题与调试经验实录
5.1 求解时间过长:先从场景数和节点数入手
我在调试中遇到的最大痛点是求解时间爆炸。原始问题用了50个场景、33节点,Gurobi跑了十几分钟还没出最优解。后来我把场景降到10个,节点数先保持3个,结果十几秒就出来了。优化问题一定要“先小后大”:先用极小算例验证代码正确性,再逐步扩大规模。扩大规模时优先增加场景数,因为场景数每增加1个,变量数量就线性增加,还比较容易接受。节点数是耦合的,增加一个节点不仅变量增加,约束的稀疏性也变差,求解难度上升更快。
如果确实需要大规模求解,可以设置MIPGap为0.5%或1%,让求解器提前终止。实际工程中没必要追求绝对最优,1%的gap对投资决策影响微乎其微,但求解时间能缩小到十分之一。
5.2 大M参数引发的病态问题
KKT转化时,大M参数如果取得不当,会导致两种结果:一是M太小,把本来可行的解切掉了,求解器报“infeasible”;二是M太大,比如超过1e6,数值误差会让0-1变量无法收敛,求解器迭代很多次还在分支定界。
我建议M不要用一个全局定值,而是针对每个约束取不同值。比如功率平衡约束中的M,可以取该节点最大净负荷的1.2倍;购售电互斥中的M,可以取电网允许最大交换功率。这样既能保证可行性,数值稳定性也更好。
5.3 场景削减时丢失极端场景
用K-means做场景聚类时,K-means天然会把小概率但高影响的极端天气场景“平滑”掉。这在容量配置里是危险的,因为储能的主要价值之一就是应对极端净负荷峰谷。我的做法是:先单独挑出最恶劣的3个场景(比如最大负荷日、光伏最小日、连续阴雨天),把它们强制加入典型场景集,然后对其他场景做K-means。这样既保留了极端信息,又不增加太多计算负担。
5.4 SOC末端约束导致的最优性偏差
很多模型为了模拟储能日间循环,会强制要求一天结束时的SOC等于初始SOC。但如果当日光伏特别大发,储能充满了用不完,强制SOC回初始值可能会让模型选择放电来“浪费”电量,导致配置结果偏保守。我觉得更合理的设定是让SOC允许在一定范围内变化,但加一个长期能量平衡约束,比如三天或者一周内SOC初末一致。如果全用日约束,尤其在连续运行模拟中,会低估储能套利能力,从而低估容量配置价值。
这一点在做全年8760小时仿真时特别明显。为了避免这个问题,我建议在日运行模型中把SOC末端约束设为允许在0.2~0.3之间变动,而不是固定值。这样能给储能运行更大的自由度,也更贴近真实控制策略。
6. 一些个人操作心得
这个项目做下来,我对双层优化和储能配置的理解比看论文时深很多。最值钱的经验其实是那句“先小后大”:网格搜索不如KKT,KKT不如老老实实建模。很多人一开始就想上深度强化学习或者启发式算法,但我觉得如果问题是线性凸的,MILP往往是更可靠的选择。哪怕考虑多主体博弈,KKT转化加商业求解器也够绝大多数场景用了。
另外,Matlab代码里的参数一定要集中管理。我初期把参数分散在多个脚本里,改一次数据来回找半天。后来统一放到一个 params 结构体里,所有函数都从里面取值,代码可维护性提升了一个档次。如果你的项目会增加新的节点或场景,建议把数据文件也独立出来,用Excel或CSV保存负荷和光伏历史数据,Matlab脚本只负责读取。
最后一个小技巧:结果可视化一定要画净负荷曲线和SOC曲线。容量配置数字只是结果,真正判断模型合不合理,看SOC曲线最直观——如果某时段SOC频繁顶到上限或者触底,那很可能边界条件设置有问题;如果购售电曲线出现锯齿状波动,多半是电价建模不够平滑。多画图、多看趋势,比盯着目标函数数值更能发现问题。
