1. 项目到底在解决什么问题
做储能优化配置这个方向,绕不开一个最核心也最头疼的问题:系统里的“不确定性”到底怎么算。这几年我做过不少微网和配电网侧的储能规划项目,一个很深的感触是:很多人最初把储能配置当成一道给定负荷曲线下的容量计算题,但真正落地去跑数据时才发现,风光出力是波动的,负荷预测是有误差的,连机组可调节能力都会随工况变化。如果这些问题不处理,“最优配置”到了实际运行中往往不是偏保守浪费,就是偏激进失负荷。“考虑灵活性供需不确定性的储能优化配置”这个题目,本质就是把储能选址定容和运行策略放在一个充满不确定性的真实环境里重新回答,而Matlab代码实现则是把这个回答变成可计算、可复现、可验证的工程工具。
我写这篇内容,想和你聊的是:这类问题从建模思路到代码落地,完整是怎么走通的。适合正在做微电网规划、储能经济性评估、或者刚接触Yalmip和优化建模的工程师、研究生参考。内容不绕弯子,先把概念理清,再给模型,再给代码框架和调试经验,尽量让你在自己电脑上也能跑通一条主线。
1.1 “灵活性供需”到底是什么
储能配置不是孤立的“买多大电池”问题,它服务的对象是系统的灵活性平衡。所谓灵活性,简单理解就是系统在短时间内通过调节手段去应对净负荷变化的能力。净负荷通常是“负荷减去新能源出力”,比如白天光伏大发时净负荷骤降,傍晚光伏退出后净负荷陡升,这两个时刻就是灵活性最紧张的时刻。
供需两侧的不确定性会直接影响这个平衡。需求侧,负荷预测本身有误差,工商业用电还有突发冲击;供给侧,风光的随机波动更是主力。于是,对灵活性的“需求”不是一条确定曲线,而是一条带置信区间的区间;对灵活性的“供给”同样不是固定值,受机组检修、爬坡速率、储能荷电状态等因素影响。这个双重不确定性叠加后,配置储能就是在为“最坏但可能发生”的那些场景留出调节裕度。
1.2 为什么不能按确定性方法拍板容量
如果只取一组预测曲线做确定性优化,得到的结果往往是单一场景下的最优解。这个最优解有两个典型问题:一是没有预留足够的向上调节能力,当实际净负荷高于预测时,储能放电不够、机组爬坡跟不上,就会切负荷;二是没有区分“大概率不出问题的冗余”和“小概率但代价巨大的风险”,容易把储能配得过大,造成投资浪费。
拿一个很常见的例子来说,某园区配电网的晚高峰净负荷实测值比预测值高了8%,但因为储能配置只按预测最大值去校核,放电功率不够,结果出现了短时失负荷。事后复盘发现,如果当初把净负荷预测误差按90%置信区间纳入配置约束,储能放电功率再增加500kW就能完全覆盖风险。这就是“考虑不确定性”和“不考虑不确定性”的直观差别。
1.3 这套模型能解决什么、影响范围有多大
这套优化配置方法能输出三个东西:储能额定功率、储能额定容量、以及典型运行日下的充放电策略。它适合的场景包括独立微网、园区级配电网、新能源渗透率较高的馈线系统,也可以扩展到多能互补系统的储能模块规划。
从影响范围看,合理的配置方案最直接的效果是降低年综合成本,同时显著压降失负荷风险和弃电风险。对可研阶段,它能把“储能装多大”从拍脑袋变成有依据;对运行规划阶段,它能把储能策略和机组调度、新能源消纳统一到一个优化框架里。要知道,储能投资在微网项目中的占比往往很高,一个偏保守的容量选择可能多出几百万元成本,而一套考虑不确定性的模型能把这个冗余压到合理水平。
2. 建模思路:怎么把不确定性装进优化模型
这一章的思路选择直接决定代码写出来的复杂度。我在不同项目里试过三种主流办法,这里说说各自的适用边界,以及为什么最终选择“场景法+机会约束”的组合。
2.1 三种不确定性处理思路的对比
处理不确定性,常见的无非是鲁棒优化、随机规划、机会约束规划。
鲁棒优化是让方案在整个不确定集合内都可行,好处是绝对安全,缺点是往往过于保守,尤其是盒式不确定集取最大值时,储能配置结果可能比确定性结果高出三四成,项目经济性很难接受。随机规划(也叫两阶段随机优化)通过离散场景逼近真实分布,结果比较温和,但需要合理的场景生成和缩减,否则计算量会爆炸。机会约束规划则允许在少数低概率场景下牺牲约束,只要求约束满足度达到一个置信水平,工程上更像“够用就好”。
我把三者列成一个简单对照表,方便你按项目特点选:
| 方法 | 保守度 | 计算复杂度 | 适用场景 |
|---|---|---|---|
| 鲁棒优化 | 高 | 低到中 | 对安全极度敏感、例如重要用户供电 |
| 随机规划 | 中 | 中到高 | 场景丰富、概率分布较明确的规划问题 |
| 机会约束规划 | 中低 | 中 | 允许小概率风险、追求经济性最优 |
我在实际项目里常用的是“场景法描述不确定性,机会约束控制风险水平”。原因是风电、光伏、负荷的预测误差分布资料相对充分,可以用历史数据生成场景,而且机会约束的置信水平可以通过一个参数直观调节,对业主也比较好解释。
2.2 不确定场景怎么生成和缩减
场景生成的思路是:假设知道负荷和新能源预测误差的概率分布,然后用采样方法构造出若干条可能的净负荷曲线。我常用的方式是拉丁超立方采样,相比纯蒙特卡洛,它能让采样点更均匀地覆盖分布区间。
具体步骤我一般分四步走:
- 对负荷、风电、光伏的预测值分别加上误差项,误差项服从正态分布或t分布,均值取预测值,标准差按历史预测误差统计;
- 用拉丁超立方采样生成500个初始场景;
- 使用Cholesky分解或简单排序法引入变量间的相关性,避免风电和光伏场景完全独立失真;
- 用K-means聚类把500个场景缩减到30到50个代表性场景,每个场景附带概率。
缩减这一步很关键。我最早用500个场景直接塞进模型,求解时间从几分钟飙升到几十分钟,而且很多场景相似度极高,对结果几乎没有贡献。聚成40个场景后,误差不到0.5%,求解时间降到五六分钟以内。如果你的问题规模更大,“先采样,再缩减,后建模”这个顺序一定不要省。
2.3 为什么用Matlab+Yalmip来做
这个问题的建模涉及大量约束和连续、整数变量混合,直接用Matlab自带优化工具箱写矩阵很痛苦,改一个参数要四处调整矩阵维度。Yalmip的好处是它把优化建模变成“声明变量、写约束、求解、取结果”四步,语义清晰,改模型快,调试也直观。
更重要的是,Yalmip有成熟的求解器接口,底层可以调用商业求解器或者开源求解器,MILP模型可以直接硬解。我踩过很多次手工编线性化逻辑的坑,最后发现用Yalmip声明二进制变量和约束,可读性和可维护性都高出一大截。如果你的建模基础一般,Yalmip几乎是Matlab平台上最低门槛的选择。
3. 储能配置与运行联合优化模型
模型是整套代码的核心,写清楚目标函数和约束条件,后面代码只是把这些数学关系翻译给求解器听。这一章我把模型的每一块拆开讲,并给出可直接落地的数学表达。
3.1 目标函数怎么定才不跑偏
储能配置的经济性目标,不能只看储能本身投资,还要把失负荷惩罚和弃电惩罚放进去,否则优化结果会倾向“不装储能最省投资”,这显然不符合工程意图。我的目标函数采用等年值成本形式:
[ \min \quad C_{\text{inv}} + C_{\text{om}} + C_{\text{cut}} + C_{\text{loss}} ]
其中:
[ C_{\text{inv}} = R_{\text{crf}} \cdot (c_p \cdot P_{\text{ess}} + c_e \cdot E_{\text{ess}}) ]
[ R_{\text{crf}} = \frac{r(1+r)^Y}{(1+r)^Y - 1} ]
(c_p)是单位功率成本,单位万元/MW;(c_e)是单位容量成本,单位万元/MWh;(r)是折现率,(Y)是储能寿命年数。(C_{\text{om}})是运行维护成本,按充放电电量乘一个单位成本和单位步长累加。(C_{\text{cut}})是弃风弃光惩罚,(C_{\text{loss}})是失负荷惩罚,惩罚系数一般设成高于正常购电成本或过剩发电收益,这样才能保证优化器优先通过储能调节来避免风险。
我调试这类模型有个心得:惩罚系数不要拍脑袋设成特别大的数,比如10的6次方这种,容易造成目标函数数值范围差异过大,影响求解器数值稳定性。一般取正常电价或度电成本的5到10倍就够了,既能保证优先级正确,又不至于引起病态矩阵。
3.2 储能运行约束与线性化处理
储能自身的运行约束是模型里最容易写错的部分,主要是SOC递推、功率限值和充放电互斥。
SOC递推关系为:
[ SOC(t+1) = SOC(t) + \eta_{\text{ch}} \cdot P_{\text{ch}}(t) \cdot \Delta t - \frac{P_{\text{dis}}(t)}{\eta_{\text{dis}}} \cdot \Delta t ]
其中(\eta_{\text{ch}})和(\eta_{\text{dis}})分别是充电和放电效率,(\Delta t)是调度步长。充放电功率还要满足:
[ 0 \le P_{\text{ch}}(t) \le P_{\text{ess}}, \quad 0 \le P_{\text{dis}}(t) \le P_{\text{ess}} ]
以及SOC上下限约束:(SOC_{\min} \le SOC(t) \le SOC_{\max})。
需要注意,同一时刻储能不能既充电又放电,这个逻辑不能靠物理参数直接表达,必须引入两个二进制变量(u_{\text{ch}}(t))和(u_{\text{dis}}(t)),并加约束:
[ P_{\text{ch}}(t) \le M \cdot u_{\text{ch}}(t) ]
[ P_{\text{dis}}(t) \le M \cdot u_{\text{dis}}(t) ]
[ u_{\text{ch}}(t) + u_{\text{dis}}(t) \le 1 ]
这里的(M)是足够大的常数,让二进制变量为1时功率约束自然放开,为0时强制功率为0。这种大M法在MILP里很常用,但(M)不能取得太大,我见过取1e6的项目,求解器内部数值检查直接报警,很多可行方案被误判为不可行。实际取法一般是“最大可能功率数值的10倍左右”,比如候选额定功率最大是5MW,取M=50就足够。
3.3 灵活性供需平衡的机会约束
这一块是模型和普通确定性配置最大的区别。定义第(t)个时段的上调灵活性需求为净负荷的预测波动上限,下调灵活性需求为净负荷预测波动下限。对应的灵活性供给包括常规机组剩余爬坡能力、储能放电功率、可中断负荷等。
写成机会约束的形式就是:
[ Pr\left{ \sum_{i \in G} F_{up,i}(t) + F_{\text{ess},up}(t) \ge D_{up}(t) \right} \ge \beta ]
(\beta)是置信水平,比如0.9或0.95。这个约束的含义是,在所有考虑的随机场景下,系统在至少(\beta)比例的概率场景中能够提供足够的向上灵活性。
直接求解概率约束很困难,但有了场景集,可以把它转化为混合整数线性约束。思路是:对每个场景(s)引入一个0-1变量(z_s),表示该场景下灵活性不足,如果灵活性不足则(z_s=1),那么求和概率约束为:
[ \sum_{s=1}^{N_s} \pi_s \cdot z_s \le 1 - \beta ]
再通过大M法把“灵活性不足”的逻辑写进约束:
[ \sum_{i \in G} F_{up,i}(t,s) + F_{\text{ess},up}(t,s) + M \cdot z_s \ge D_{up}(t,s) ]
当(z_s=0)时,该场景必须满足灵活性供给不小于需求;当(z_s=1)时,该场景可以被舍弃,但所有被舍弃场景的总概率不超过(1-\beta)。
这样处理完,整个模型就是一个标准的MILP,可以交给求解器直接求解。这也是我把这个模型用在实际项目里最推荐的写法,既保留了不确定性的表达,也保证了可计算性。
3.4 模型的可求解性分析
把目标函数、储能约束、功率平衡约束、机会约束合在一起后,我大概估算过变量规模:假设调度时段(T=24),场景数(N_s=40),候选储能功率有10个档位,候选储能容量有10个档位,那么连续变量大约有(24\times40\times2)个充放电功率、加SOC变量,二进制变量有充电状态、放电状态、场景标记等,整体规模在几千到上万级。对于MILP来说,这个规模用商业求解器是可以接受的。
所以建模阶段最重要的原则就是:尽可能把非线性问题线性化。比如用分段线性化处理储能充放电效率曲线、用大M法处理逻辑约束、用场景概率汇总处理机会约束。这是我在这个项目里最核心的经验之一:模型能不能在五分钟内求解,和你的线性化程度几乎成正比。
4. Matlab代码实现与关键模块拆解
说完了模型,这一章直接进入代码层面。我会把项目的主流程、关键代码片段、参数标定方式都贴出来,并解释每一段的意图和容易踩的坑。
4.1 工程目录与整体流程
我习惯把代码分成四个模块,目录结构大致如下:
storage_sizing_project/ ├── data/ │ ├── load_forecast.mat │ ├── wind_forecast.mat │ ├── pv_forecast.mat └── src/ │ ├── scenario_generation.m │ ├── scenario_reduction.m │ ├── build_model.m │ ├── solve_model.m │ └── plot_results.m └── main.mmain.m是整个项目的入口,按顺序完成五件事:读取基础数据、生成场景、缩减场景、构建并求解MILP模型、输出结果曲线。这种模块化写法最大的好处是:改场景生成方式时不用动优化模型代码,改目标函数时不用重新生成场景,各模块能独立测试。
4.2 核心建模代码逐段拆解
这里给出build_model.m中的核心代码,我尽量保持可读性,实际项目可以在这个框架上扩展。
%% 决策变量定义 P_ess = sdpvar(1, 1); % 储能额定功率(MW) E_ess = sdpvar(1, 1); % 储能额定容量(MWh) P_ch = sdpvar(T, Ns); % 各场景各时段充电功率 P_dis = sdpvar(T, Ns); % 各场景各时段放电功率 SOC = sdpvar(T+1, Ns); % 各场景各时段的荷电状态 u_ch = binvar(T, Ns); % 充电状态标志 u_dis = binvar(T, Ns); % 放电状态标志 z_flex = binvar(Ns, 1); % 灵活性不足标志变量定义是整个模型的骨架。注意SOC我定义成(T+1)×Ns,第一行是初始SOC,后面每行对应一个时段结束值,这样写递推关系比较顺手。
接下来是约束部分:
Constraints = []; %% 储能运行约束 for s = 1:Ns Constraints = [Constraints, SOC(1, s) == 0.5 * E_ess]; for t = 1:T Constraints = [Constraints, ... SOC(t+1, s) == SOC(t, s) + eta_ch * P_ch(t, s) * dt ... - (P_dis(t, s) / eta_dis) * dt]; Constraints = [Constraints, 0 <= P_ch(t, s) <= P_ess]; Constraints = [Constraints, 0 <= P_dis(t, s) <= P_ess]; Constraints = [Constraints, P_ch(t, s) <= M_big * u_ch(t, s)]; Constraints = [Constraints, P_dis(t, s) <= M_big * u_dis(t, s)]; Constraints = [Constraints, u_ch(t, s) + u_dis(t, s) <= 1]; end end这段代码里,我最想强调的还是M_big的取值。在数值调试阶段,我建议先用变量候选上限的10倍做初始值,然后观察求解结果里是否存在“充电和放电同时被允许但不同时发生”的现象。如果出现数值异常,就适当调整M_big,通常是往小了调。
目标函数这部分按等年值成本写:
Objective = annual_factor * (c_p * P_ess * 1000 + c_e * E_ess * 1000) ... + sum(sum(om_cost * (P_ch + P_dis))) * dt ... + curt_penalty * sum(sum(P_curt)) ... + loss_penalty * sum(sum(P_loss));这里我把功率单位统一成千瓦计算,因为成本和惩罚单位大多按元/kWh或元/kW给定。如果你在代码里混用了MW和kW,成本数字会整整差三个数量级,结果完全没法看。建议在数据准备阶段就把所有单位统一,并在文件头部注释里写清楚。
4.3 关键参数如何标定
参数标定是我的老本行,这里整理出一套比较实用的初始值,你可以根据具体项目调整:
| 参数 | 含义 | 典型取值 |
|---|---|---|
| c_p | 储能单位功率成本 | 800元/kW |
| c_e | 储能单位容量成本 | 1200元/kWh |
| Y | 储能寿命 | 10年 |
| r | 折现率 | 0.06 |
| eta_ch | 充电效率 | 0.95 |
| eta_dis | 放电效率 | 0.92 |
| SOC_min | 荷电状态下限 | 0.1 |
| SOC_max | 荷电状态上限 | 0.9 |
| beta | 机会约束置信水平 | 0.9 |
其中P_ess和E_ess如果作为连续变量直接求解,得到的值可能带有小数,工程上不好安装。实际我一般把候选功率离散成[1,2,3,4,5]MW五档,容量按倍率离散成[2,4,6,8]MWh四档,然后引入两个二进制选择变量,形成混合整数规划。这样结果更贴近产品选型,求解速度也不会慢太多。
4.4 求解与结果输出
求解部分用Yalmip的optimize函数即可:
ops = sdpsettings('verbose', 2, 'solver', ''); diagnosis = optimize(Constraints, Objective, ops); if diagnosis.problem == 0 disp('求解成功'); else disp(['求解失败,错误码:', num2str(diagnosis.problem)]); endsolver参数留空,让Yalmip自动选择当前机器上配置好的可用求解器,省心。结果提取时,直接取value(P_ess)和value(E_ess),再重放一遍运行曲线,用stairs或plot把SOC和充放电功率画出来。我每次都会把SOC曲线作为第一张图检查,因为SOC超界或突变往往意味着约束写错了,一眼就能看出来。
5. 算例结果与效果验证
模型和代码都齐了,这一章用一个典型算例展示结果长什么样。数据来自我做过的一个园区微网项目,此处保留关键特征,细节做了脱敏。
5.1 算例设置
系统取某园区微网模型,峰值负荷8MW,风电装机2MW,光伏装机3MW。调度周期24小时,步长1小时。初始场景500个,缩减后保留40个。负荷预测误差标准差取预测值的5%,风电和光伏预测误差标准差分别取10%和8%,并假设它们与负荷相互独立。
先做一个确定性对照,即把所有预测误差设为零。得到的最优储能配置为2MW/4MWh,年度总成本约312万元。这个结果看着经济性很好,但我对这个园区实际历史数据做蒙特卡洛后验时发现,失负荷期望达到每年8小时,对应惩罚成本接近60万元,也就是说确定性最优解在真实环境下根本不够用。
5.2 不确定性置信水平对配置结果的影响
在相同的预测误差分布下,把机会约束置信水平从0.9调整到0.99,储能配置结果变化很明显:
| 置信水平 | 储能功率(MW) | 储能容量(MWh) | 年总成本(万元) | 失负荷期望(h/a) |
|---|---|---|---|---|
| 确定性 | 2 | 4 | 312 | 8.0 |
| 0.85 | 2.5 | 5 | 358 | 3.2 |
| 0.90 | 3 | 6 | 386 | 1.5 |
| 0.95 | 4 | 8 | 445 | 0.6 |
| 0.99 | 5 | 10 | 508 | 0.1 |
这组结果很有意思。置信水平从0.9提到0.99,储能容量几乎翻倍,年总成本增加超过120万元,但失负荷期望只从1.5小时压到0.1小时。很多项目业主看到这个曲线后,开始反思“零风险”到底值不值。实际工程中,我一般建议结合业主的风险偏好选0.90到0.95,这个区间是经济性和可靠性的平衡区。
5.3 机会约束满足度的蒙特卡洛后验验证
模型算完之后,我并不直接采信置信水平参数,而是做一次独立的后验验证。方法很简单:重新生成500个新的随机场景,然后把储能配置和运行策略代入这套场景,逐个检查灵活性供需约束是否满足,统计实际满足度。
在我做的这个算例中,设定的置信水平是0.90,后验统计实际满足度约0.925,略高于设定值。这说明机会约束的转化是有效的,没有出现保守度大幅偏离的情况。如果出现后验满足度显著低于设定值,通常原因是场景缩减后没有保留住尾部分布,我需要回头检查缩减前后的场景分布图,看看最极端的尾部场景是否被聚类算法削掉了。
这个后验验证步骤,是我的项目比普通教学代码多出来的一个环节。论文里可能不写,但工程报告中非常重要:它能证明你的模型不只是“解出来了”,而且解在真实分布意义下确实成立。
6. 实战中的常见问题与调试心得
最后这一章,我把自己在这个项目上踩过的几个比较典型的坑列出来。这些坑在课本和手册里很少提到,但实际运行代码时几乎每个都会碰上,我这里直接给出排查思路。
6.1 模型不可行:先怀疑“硬约束”过紧
模型返回不可行时,不要急着调求解器参数,先用排除法定位是哪一组约束导致不可行。我惯用的方法是在optimize之前先把目标函数临时删掉,只求可行解,如果仍然不可行,就说明约束之间有冲突。
最常见的冲突来源有三个:一是SOC末时段约束和初始SOC一起定死了,导致储能没有足够循环空间;二是灵活性供需约束里的置信水平设成1,相当于所有极端场景都必须满足;三是充放电互斥的大M约束和功率上限冲突。逐个注释掉可疑约束再跑一遍,就能快速缩小范围。
6.2 大M参数引起的数值病态
大M取太大,求解器会把原本可行的解判为不可行,或者求解时间暴增;取太小,又会让二进制变量失去约束力。我的经验是,大M值不要全模型统一用一个非常大的数,而是按所在约束的物理量纲分别设置。比如充放电功率约束中的M取候选最大功率的10倍,场景中断约束中的M取该场景最大灵活性需求的10倍。这样数值尺度接近,求解器数值稳定性会好很多。
6.3 场景数量和求解时间的取舍
我把场景数从40提升到80,结果几乎不变,但求解时间多一倍;从40降到20,结果偏差开始明显。所以40是个比较稳的默认值。如果你的时间步长变动到15分钟,T变成96,建议先按40场景跑通再考虑增加场景,不然调试阶段的重启成本太高。
另外,调试时可以先只跑3到5个场景,确认代码逻辑没有明显问题,再放开到完整场景集。不要一上来就跑全量,否则一次调试半小时起步,效率太低。
6.4 数据预处理和结果检查的实用建议
我会在数据进入模型前,先画一张“负荷、风光出力、净负荷”的堆叠图。看净负荷曲线形态有两个作用:一是判断哪些时段灵活性最紧张,心里有数;二是提前发现异常数据,比如某天负荷突变20%,可能是数据采集错误而不是真实波动。如果异常数据进了优化模型,求解器会把储能容量配得莫名其妙大,而且很难通过模型本身的约束检查出来。
结果检查也是一样,储能SOC曲线必须连续且保持在边界内,充放电功率不能出现同时为正的时段,目标函数各成本项占比要合理。这些检查其实都很基础,但确实帮我拦下了好几次模型写错却没发现的情况。
从我个人的实际经验来讲,这个项目的工程量不在于模型有多复杂,而在于“把不确定性想清楚,把约束写对,把参数调稳”。只要把这三件事做到位,Matlab代码本身其实是这整个流程里最容易的部分。希望这篇内容能把你在储能配置项目上少走几步弯路。