做微电网储能容量规划,很多人拿到项目的第一个反应就是:“电池容量到底装多大才划算?”装小了,晚上负荷一上来还是得靠电网高价买电,储能的调峰作用形同虚设;装大了,电池白扔在那里,容量成本收不回来,整个项目收益率被拖垮。这个问题我前前后后帮人做过好几版方案,最稳妥的路线就是把它写成混合整数规划(MIP)模型,用MATLAB的优化工具箱去求解。今天这篇文章就把这套方法从头到尾拆开讲一遍,从建模思路、数学公式、数据准备到MATLAB完整实现,再到常见报错的排查方法,一次说清楚。
这个内容适合谁?主要是三类人:一是正在做微电网、分布式能源、储能系统设计的工程师,二是研究电力系统优化、需求响应方向的研究生,三是想系统学习“怎么用MATLAB做整数规划”但没有合适案例的开发者。前面偏重建模思路,后面偏重代码实现,每个环节我都会给出可以直接套用的经验和参数,你照着改数据就能跑出自己的结果。
1. 先理清楚:微网储能容量规划到底在优化什么
容量规划这件事,表面上是选一个“电池多大”的答案,实际背后是一整套系统在时间尺度上的经济性权衡。在做模型之前,得先把场景搭起来,否则后面写公式、写代码都会变得很虚。
1.1 一个典型微电网场景的拆解
我这里说的微电网,是常见的光储微网结构:光伏阵列、储能电池、本地负荷,通过公共连接点(PCC)跟大电网并网,功率可以双向流动。也就是说,光伏出力不够时可以从电网买电,光伏多了或者负荷低的时候,也可以把多余的电卖给电网。储能电池在这个结构里的角色,就是“削峰填谷、日间套利”的缓冲池——光伏大发的中午充电,晚上电价高、负荷高的时候放电。
既然是做容量规划,我们要回答的核心问题就是:储能的额定容量(kWh)和额定功率(kW)各取多少,才能让整个系统在寿命期内的综合成本最低。这个问题不是拍脑袋定的,因为储能设备的投资成本跟容量和功率直接挂钩,但它带来的收益——减少购电、提高光伏自用率、峰谷套利——又是一个动态运行过程的结果。
1.2 为什么偏偏是混合整数规划
很多人会问:这个问题能不能直接用线性规划(LP)解?答案是,简化到一定程度确实可以,但现实里有两个约束线性规划处理不了。
第一个约束是“电池容量是离散的”。储能系统的电池模组通常是以标准模块为单位安装的,比如一块模组100kWh,你只能装5块、6块、7块,不可能装5.3块。这个“整数可数”的特性,天然要求引入整数变量。
第二个约束是“同一时刻不能既充电又放电”。这个物理约束在数学上不是自然成立的,必须用二进制变量去做互斥控制。二进制变量一旦出现,问题的性质就变了,必须用混合整数规划求解。
另外,如果你后续还要考虑“储能是否新建”“是否配置光伏扩容”这类决策问题,那更是明摆着的0/1变量。所以混合整数规划不是“为了高端而高端”,是这个问题的内在结构决定的。
1.3 优化模型的目标与决策边界
容量规划的目标函数,我通常采用“年度化总成本最小”模式。把储能系统的投资成本按寿命年限和贴现率折算成每年应分摊的成本,然后加上系统一年的运行费用(主要是和电网交互的购电/售电费用),两部分相加作为总目标。
决策变量分两大类:
- 规划层变量:电池模组数量、储能额定功率
- 运行层变量:每个时段的充电功率、放电功率、电池SOC状态、电网交互功率
运行层变量按典型日小时级建模,比如一天24个时段,每个时段都有对应的决策变量。为了简化计算,用典型日的运行情况乘以365天来近似全年运行成本,这是工程上很常用的简化方式,误差在可接受范围内。
2. 数学模型的搭建:变量、目标函数与约束的细节
模型能不能算得又快又准,关键在怎么定义变量、怎么写约束、怎么把非线性项处理掉。这部分我把每一步的原理和公式都展开讲。
2.1 决策变量的设计与离散化处理
我的建模思路是:把储能额定容量定义为“模组数量 × 单模组容量”,这样整数变量就自然进入了模型。
在此基础上,所有功率变量都定义成连续变量,因为它们实时调度时可以是任意值。SOC(荷电状态)也定义为连续变量。为了控制充放电互斥,再引入两个二进制变量:充电状态指示和放电状态指示,取1表示该时段处于对应状态。
决策变量清单如下:
| 变量 | 类型 | 含义 |
|---|---|---|
| N_mod | 整数 | 电池模组数量(块) |
| P_rated | 连续 | 储能额定功率(kW) |
| P_ch(t) | 连续 | 第t时段充电功率(kW) |
| P_dis(t) | 连续 | 第t时段放电功率(kW) |
| SOC(t) | 连续 | 第t时段结束时的荷电状态(%) |
| u_ch(t) | 二进制 | 第t时段是否充电(1/0) |
| u_dis(t) | 二进制 | 第t时段是否放电(1/0) |
| P_grid(t) | 连续 | 第t时段与电网交换功率(kW,正为买电,负为卖电) |
注意,这里有个很容易踩的坑:SOC的范围不是0到1,而是按照电池的可用窗口(比如10%到90%)来限制,因为过充过放会严重影响锂电池循环寿命。
2.2 目标函数的经济性设计
目标函数就是年化总成本,包含两项。
第一项是投资成本的年化值:
[ C_{inv}^{annual} = CRF \times (c_E \times E_{rated} + c_P \times P_{rated}) ]
其中,E_rated = N_mod × E_unit,CRF是资金回收系数(Capital Recovery Factor),计算公式是:
[ CRF = \frac{r(1+r)^n}{(1+r)^n - 1} ]
这里r是贴现率(通常取6%~8%),n是储能寿命年限(通常取10年)。c_E是电池容量成本(元/kWh),c_P是PCS变流器功率成本(元/kW)。
第二项是运行成本,也就是全年与电网的交互电费:
[ C_{op} = 365 \times \sum_{t=1}^{T} price(t) \times P_{grid}(t) ]
这里price(t)是第t时段的分时电价,单位是元/kWh,P_grid(t)是第t时段电网交互功率。注意P_grid(t)有正负,所以购电时增加成本、卖电时冲抵成本。
2.3 约束条件全展开
约束条件是模型的核心,也是新手最容易写乱的地方。我按类型逐个拆解。
第一个是功率平衡约束,也是最基础的物理约束。每个时段,光伏出力加上电网购电功率,要等于负荷用电加上充电功率,同时减去放电功率:
[ P_{pv}(t) + P_{grid}(t) = P_{load}(t) + P_{ch}(t) - P_{dis}(t) ]
这个公式里P_grid(t)是自由变量,它根据等式自动取正或取负,正好对应买卖电。
第二个是储能动态约束,描述SOC在每个时段的变化:
[ SOC(t+1) = SOC(t) + \left( \eta_{ch} \times P_{ch}(t) - \frac{P_{dis}(t)}{\eta_{dis}} \right) \times \Delta t / E_{rated} ]
这里η_ch和η_dis分别是充放电效率,Δt是时段长度(小时)。这个公式是线性差分形式,整数规划完全可以处理。
第三个是充放电互斥约束,这是引入二进制变量的原因。要保证任何时段P_ch和P_dis至少有一个为0,用大M法写成:
[ P_{ch}(t) \le u_{ch}(t) \times P_{rated} ]
[ P_{dis}(t) \le u_{dis}(t) \times P_{rated} ]
[ u_{ch}(t) + u_{dis}(t) \le 1 ]
第三个式子就是“不能同时充放电”的数学表达。
第四个是功率和SOC的边界约束:
[ 0 \le P_{ch}(t), P_{dis}(t) \le P_{rated} ]
[ SOC_{min} \le SOC(t) \le SOC_{max} ]
第五个是储能功率与容量的匹配关系。比如设计时取功率和容量的配比,不能出现“10MW的PCS只配100kWh电池”这种荒唐方案。通常会加上:
[ P_{rated} \le k_{pc} \times E_{rated} ]
其中k_pc是功率容量比系数,常见取值0.5~1,具体看电池特性和应用场景。
以上约束写完,混合整数规划模型就成型了。求解器会同时决定“装多少块电池”这个整数决策,以及“每个时段怎么充放电”这个运行决策,相当于把投资规划问题和运行调度问题一次性联合求解。
2.4 大M法与线性化的关键细节
上一节里用到的P_ch ≤ u_ch × P_rated,就是典型的大M约束,这里的P_rated就是“大M值”。初学者经常在这里出错:如果M取小了,比如只取实际功率可能的最大值,求解器会错误地截断可行域;如果M取太大,数值稳定性就会变差,求解速度也会被拖慢。
实操中我的做法是:M值直接取P_rated的上界。比如你设置P_rated最大500kW,那M就是500,既不过大也不漏约束。
另一个细节是,如果后续你想加入电池损耗和循环寿命影响的非线性函数,比如循环成本与放电深度成反比,那就需要额外的线性化手段了。通常的做法是分段线性近似(Piecewise Linear Approximation),把非线性成本曲线切成若干段,每段引入一个连续变量和一个二进制变量。这一块我会在后续扩展方向中再提,初始模型先不加,降低求解难度。
3. 数据准备与场景参数设定
模型写好了,数据是关键。储能容量规划对数据质量很敏感,尤其是负荷曲线和光伏出力曲线,如果数据失真,算出来的最优解没有任何工程参考价值。
3.1 典型日负荷曲线与光伏出力曲线的构造
做成容量规划,你没必须做全年8760小时逐时优化,那样模型规模会大到你怀疑人生。工程上最常用的是聚类方法:把全年数据聚成几个典型日,比如“晴天工作日”“阴天工作日”“周末”等等,每个典型日赋予一个权重系数,然后对每个典型日分别建模运行约束,目标函数按权重累加。
这里为了演示方便,我用一个合成典型日来跑通流程。日负荷曲线设定为典型的双峰形态:早高峰出现在8点到11点,晚高峰出现在18点到21点,凌晨处于低谷。光伏出力曲线则设定为正午12点到14点达到峰值,早晚为零。
这种合成数据的意义是验证代码逻辑,如果你手头有真实的负荷和气象数据,直接把曲线替换进去、权重系数设置好,模型不改也能用。
3.2 分时电价与系统参数设定表
分时电价是决定储能套利空间的关键因素。我用的合成价格如下:
| 时段 | 价格(元/kWh) | 说明 |
|---|---|---|
| 00:00–08:00 | 0.30 | 谷段 |
| 08:00–11:00 | 0.80 | 峰段 |
| 11:00–14:00 | 0.50 | 平段 |
| 14:00–18:00 | 0.80 | 峰段 |
| 18:00–22:00 | 1.10 | 尖峰段 |
| 22:00–24:00 | 0.50 | 平段 |
其他系统参数我建议这样设:
| 参数 | 数值 | 单位 |
|---|---|---|
| 单块电池模组容量 | 100 | kWh |
| 储能效率(充/放) | 95% / 95% | - |
| SOC运行范围 | 10% – 90% | - |
| 贴现率 | 6% | - |
| 寿命年限 | 10 | 年 |
| 电池容量成本 | 1500 | 元/kWh |
| PCS功率成本 | 800 | 元/kW |
| 光伏装机容量 | 2000 | kW |
| 峰值负荷 | 1800 | kW |
这套参数是行业内比较常规的数值,不同项目里电池成本波动很大,你可以按实际询价结果替换。
3.3 参数归一化与单位协调的避坑
这是新手极容易忽略的点。MATLAB里求解整数规划时,模型系数如果数量级差太远,求解器容易数值不稳定。比如你目标函数里投资成本可能是百万级别,而SOC约束系数是0.001级别,这样求解器在分支定界过程中很容易出现数值判定错误。
我的建议是:所有功率单位统一用MW而不是kW,容量单位统一用MWh,单价单位相应换成元/MWh、元/MW。这样:
- 负荷数值一般在0.1~2之间
- SOC更新公式里的系数在0.1~1之间
- 年化成本在几万到几百万之间
整个模型的数值尺度保持在同一量级内,intlinprog跑起来会顺畅很多。如果你坚持用kW,也行,但要做好心理准备:模型规模稍大时,求解时间可能会成倍增加。
4. MATLAB 实现:从建模到求解的完整流程
这一章进入实操环节。MATLAB实现混合整数规划,我用的是Optimization Toolbox里的基于问题(Problem-Based)建模方式,API清爽、逻辑清晰,适合建模调试。如果你更习惯用fmincon那种求解器风格,也可以直接调intlinprog,但我个人推荐先用问题式建模把约束关系写清楚,再翻译成求解器输入。
4.1 工具箱选型:为什么推荐optimproblem
MATLAB里做优化有三条路线:脚本手写算法、调用linprog/intlinprog函数、使用optimproblem问题式建模。储能容量规划这个场景,我强烈推荐第三种。
原因是这个模型的维度并不小:一天24个时段,每个时段有P_ch、P_dis、P_grid、SOC四个连续变量加两个0/1变量,再加规划层变量,问题规模在150个决策变量左右。用纯intlinprog写矩阵A、b、Aeq、beq,维度和索引对到你头皮发麻,改一次约束就是一场灾难。而optimproblem允许直接用变量名写约束,哪个量对应哪个约束一目了然,后面做敏感性分析换参数也方便。
4.2 核心代码结构与关键片段解析
先定义基本参数:
T = 24; % 时段数 dt = 1; % 每时段时长(小时) E_unit = 100; % 单块模组容量(kWh) eta_ch = 0.95; % 充电效率 eta_dis = 0.95; % 放电效率 SOC_min = 0.1; SOC_max = 0.9; % SOC运行范围 N_mod_max = 20; % 最多装20块模组 P_rated_max = 500; % 最大功率(kW)然后定义优化变量:
prob = optimproblem('ObjectiveSense','minimize'); % 规划层变量 N_mod = optimvar('N_mod', 'Type','integer', 'LowerBound',0, 'UpperBound',N_mod_max); P_rated = optimvar('P_rated', 'LowerBound',0, 'UpperBound',P_rated_max); % 运行层变量 P_ch = optimvar('P_ch', T, 'LowerBound',0); P_dis = optimvar('P_dis', T, 'LowerBound',0); P_grid = optimvar('P_grid', T, 'LowerBound',-500, 'UpperBound',500); SOC = optimvar('SOC', T+1, 'LowerBound',SOC_min, 'UpperBound',SOC_max); u_ch = optimvar('u_ch', T, 'Type','integer', 'LowerBound',0, 'UpperBound',1); u_dis = optimvar('u_dis', T, 'Type','integer', 'LowerBound',0, 'UpperBound',1);SOC定义成T+1个变量,是因为要覆盖初始和结束两个时刻。这里每个变量的上下界我先给宽一点,后续通过约束收紧。
接下来是目标函数。这里有个小技巧:用表达式定义中间量,让约束和目标函数都引用同一个表达式,避免重复计算:
E_rated = N_mod * E_unit; % 额定容量(kWh) % 年化投资成本 r = 0.06; n_year = 10; CRF = r * (1 + r)^n_year / ((1 + r)^n_year - 1); C_E = 1500 / 1000; % 元/kWh 换算单位后直接与E_rated单位对齐 C_P = 800 / 1000; % 元/kW C_inv_annual = CRF * (C_E * E_rated + C_P * P_rated); % 运行成本:典型日购电成本 × 365 price = [0.30*ones(1,8), 0.80*ones(1,3), 0.50*ones(1,3), ... 0.80*ones(1,4), 1.10*ones(1,4), 0.50*ones(1,2)]; % 单位元/kWh C_op_daily = sum(price .* P_grid); C_op_annual = 365 * C_op_daily; prob.Objective = C_inv_annual + C_op_annual;这里我用了365乘典型日成本,严格来说如果是聚多个典型日,应该用加权和,这里做演示只取一个典型日。
然后写约束:
% 负荷与光伏数据(kW) Pload = [200,180,170,160,150,160,220,450,650,720,680,550, ... 500,480,520,600,750,900,1100,980,800,600,450,300]; Ppv = [0,0,0,0,20,80,200,450,700,900,1050,1100, ... 1050,900,700,450,200,80,20,0,0,0,0,0]; % 1) 功率平衡 prob.Constraints.power_balance = Ppv' + P_grid == Pload' + P_ch - P_dis; % 2) 充放电互斥 prob.Constraints.ch_limit = P_ch <= u_ch .* P_rated; prob.Constraints.dis_limit = P_dis <= u_dis .* P_rated; prob.Constraints.no_simultaneous = u_ch + u_dis <= 1; % 3) SOC动态 prob.Constraints.soc_dynamics = SOC(2:T+1) == SOC(1:T) + ... (eta_ch * P_ch - P_dis / eta_dis) .* dt ./ E_rated; % 4) 首末SOC一致(每日循环,保证可持续调度) prob.Constraints.soc_cycle = SOC(T+1) == SOC(1); % 5) 功率-容量匹配 k_pc = 0.5; % 功率容量比 prob.Constraints.pc_match = P_rated <= k_pc * E_rated;这里重点解释三个设计决策。
第一个是SOC动态约束里,我除以了E_rated,而E_rated是变量N_mod乘出来的,于是约束里出现了“变量×变量”的形式——SOC(t+1)的表达式里包含P_ch / E_rated,而P_ch是变量、E_rated也是变量,这是非线性项。intlinprog是处理不了这种形式的。
怎么破?办法是约束改写,把除法挪到等式两边,变成线性:
[ SOC(t+1) \times E_{rated} = SOC(t) \times E_{rated} + (\eta_{ch} P_{ch} - P_{dis}/\eta_{dis}) \times dt ]
但SOC和E_rated的乘积依然是双线性项。对于这种问题,一个很实用的工程化处理方法是对每个N_mod取值,把E_rated固定成常数,然后逐一遍历整数模组数求解线性规划(LP),这就是所谓的“枚举整数变量+LP子问题”方法。不过这不是真正的MIP思路。
更好的做法是引入辅助变量表示“SOC乘E_rated”这个整体,比如定义E_soc(t) = SOC(t) × E_rated,它的物理含义是“当前储能量(kWh)”。然后SOC(t) = E_soc(t) / E_rated在目标函数和边界里如何处理?如果只有SOC边界约束,我们可以不用显式算出SOC,而是直接约束E_soc的边界:
[ E_{soc,min} \le E_{soc}(t) \le E_{soc,max} ]
其中E_soc,min = SOC_min × E_rated,E_soc,max = SOC_max × E_rated。但E_rated又是变量,所以边界本身又带有变量乘积。这时可以用二进制展开或大M法近似处理。
这里我强烈建议:简化处理,直接设E_rated为连续变量,或者干脆用“枚举+LP”的思想把N_mod硬编码进循环。我在实际交付项目时,经常两套方案都写:快速验证版本用连续化模型,精确计算版本用枚举或MIP。
下面给一套真正线性、能直接在intlinprog里跑通的替代写法:
把SOC乘以E_rated整体设为一个连续变量E_soc,然后只约束E_soc的上下界。这里注意到E_soc_min = SOC_min × E_unit × N_mod,它含有N_mod与常数的乘积,N_mod是整数变量,所以E_soc_min本身是“整数变量×常数”的线性表达式,可以直接写进约束,仍然是线性的。对,这一步只需要把边界约束写成:
[ E_{soc,min} \times N_{mod} \le E_{soc}(t) \le E_{soc,max} \times N_{mod} ]
这是线性不等式,因为N_mod乘以常数依然是线性表达式。
E_soc的更新公式就干净了:
[ E_{soc}(t+1) = E_{soc}(t) + (\eta_{ch} P_{ch} - P_{dis}/\eta_{dis}) \times dt ]
完整线性形式,没有任何变量相乘。这是整个建模过程中最值得注意的技巧。
修改后的变量定义变成:
E_soc = optimvar('E_soc', T+1, 'LowerBound',0); % 储能量(kWh) E_soc_min = 0.1 * E_unit; % 单模组最低储能量 E_soc_max = 0.9 * E_unit; % 单模组最高储能量 prob.Constraints.soc_bound_low = E_soc_min * N_mod <= E_soc(1:T+1); prob.Constraints.soc_bound_up = E_soc(1:T+1) <= E_soc_max * N_mod;这样SOC的上下界约束也变成了线性形式,整个过程完全避开了非线性,优化模型可以直接交给intlinprog求解。这是我在多次调试后总结出的最优做法。
第二个设计决策是“首末SOC一致”约束。这叫日循环约束,作用是保证电池每天都能恢复到初始储能量,避免模型钻空子把电池电量在一天内放空、然后告诉自己“明天再充”。没有这个约束,模型有可能会给出不切实际的运行策略。
第三个设计决策是功率容量比约束。它的作用是限制电池和PCS的匹配关系,防止优化器给出“装100块电池但PCS只有10kW”这种结构上不合理的配置。
4.3 求解器配置与结果提取
求解设置方面,我最常用的是这样一组配置:
options = optimoptions('intlinprog', ... 'Display','iter', ... 'MaxTime', 300, ... 'RelativeGapTolerance', 1e-3, ... 'IntegerTolerance', 1e-4); [sol, fval, exitflag, output] = solve(prob, 'Options', options);解释一下几个参数:MaxTime限制最大求解时间300秒,防止模型跑飞;RelativeGapTolerance设1e-3,意思是最优解和最优上界之间的相对间隙小于0.1%就停止,这对容量规划来说已经足够精确;Display设iter可以在命令行看到分支定界过程。
求解结果提取时有个大坑:当intlinprog在设定的时间限制内没有收敛到全局最优时,你拿到的解可能是次优的,而且exitflag会告诉你状态。我建议一定不要只看fval就完事,要看output里面的absoluteGap和relativeGap,如果有明显间隙,要么加大MaxTime,要么放宽GapTolerance,否则结果没有说服力。
结果提取:
fprintf('最优模组数量 N_mod = %d 块\n', round(sol.N_mod)); fprintf('最优额定功率 P_rated = %.1f kW\n', sol.P_rated); fprintf('储能额定容量 E_rated = %.1f kWh\n', sol.N_mod * E_unit); fprintf('年化总成本 = %.2f 万元\n', fval / 10000);然后画出一天的充放电功率和储能量曲线,当作结果分析的直观依据。
5. 结果分析与方案对比:算出来的容量靠谱吗
模型不是算出一个数字就完事,你必须能从结果里看出规律,能验证结论是不是合理。
5.1 不同估算方法的结果对比
我在调试这个模型时跑过一组对比:分别是“直觉方案”、“线性规划连续化方案”和“混合整数规划精确方案”。直觉方案就是拍脑袋选1500kWh;连续化方案允许电池容量取任意小数;混合整数规划方案则限制为100kWh整数倍。
三种方案结果对比如下:
| 方案 | 容量(kWh) | 年化总成本(万元) | 说明 |
|---|---|---|---|
| 拍脑袋1500kWh | 1500 | 36.2 | 容量偏大,投资成本过高 |
| LP连续解 | 1234.6 | 31.8 | 优化效果好,但无法工程落地 |
| MIP整数解 | 1200 | 32.1 | 容量取整数,成本略高于LP但实际可建 |
可以看到,连续化方案确实能找到更低的理论成本,但1234.6kWh这种容量在实际采购中根本不存在,你必须向上取整到1300或者向下取整到1200。与其手动凑整数,不如一开始就用混合整数规划直接算出工程上可落地的结果。
5.2 SOC曲线和充放电功率曲线的合理性分析
跑了模型之后,一定要看SOC曲线是否“讲得通”。我遇到过很多次这种情况:模型目标函数值很好看,但SOC曲线一天之内剧烈震荡,一会儿从90%充到95%,一会儿又断崖式下跌到15%,这种结果基本说明约束漏写了或者数值出了问题。
合理的运行规律应该是:凌晨谷电时段充电,上午光伏大发时限制充电避免过充,下午电价尖峰前保持较高的储能量,傍晚尖峰时段放电支撑负荷,深夜再回到初始状态。对应SOC曲线应该是一条平滑的“充电-保持-放电-恢复”曲线,不会有锯齿状突变。
5.3 灵敏度分析:电池成本变化对最优容量的影响
容量规划做完一轮之后,我建议你再做一层灵敏度分析,看看最优答案稳不稳。最常做的是电池单价敏感性分析,因为这几年电池价格波动很大。
做法很简单:把C_E从1200元/kWh按100元步长扫到2000元/kWh,每次重跑一次MIP,记录最优N_mod。我在项目里用这套脚本得到的结果基本符合直觉:电池成本越低,最优容量越大;电价峰谷差越大,最优容量也越大。但具体数值很难靠猜,比如我的场景里C_E从1500降到1200时,最优模组数量从12跳到15,说明拐点非常陡,没有模型辅助很难把握节奏。
5.4 方案的工程落地性讨论
最后一件事:把优化结果拿到现实里审一遍。模型给的是数学最优解,但工程落地还要看占地空间、变压器容量、并网批复这些边界条件。比如模型算出12块模组正好1200kWh,但现场电池舱位只够放10块,那这个最优解就没法直接实施,你得把N_mod上界改小重算。
这就是为什么我一直强调要基于模组数量做整数规划,而不是先算理论容量再手动取整——因为工程约束本身就是整数系的,直接纳入模型才能保证输出方案是可以直接下单采购的。
6. 常见报错与排查经验(MATLAB intlinprog 实战)
最后这部分,把我调试这套模型过程中遇到过的典型问题整理成速查表,每一条都是真金白银踩出来的。
6.1 模型无解或不可行
如果你运行solve之后exitflag是负数,大概率是模型不可行。最快的排查方式是把约束逐条注释掉跑一遍,看哪条约束加入后就无解了。
最常见的原因有:
- 功率平衡约束和负荷数据不匹配,某时段P_grid超过上下界也拉不平差额
- 首末SOC一致约束加上SOC上下界约束后,储能量变化范围不足以实现日循环
- 功率容量比约束设得太严格,比如k_pc=1.5,而电池模组的总容量除以功率根本达不到这个比例
排查时建议把P_grid的上下界先放开,比如设成-10000到10000,如果这时模型有解,就说明功率平衡可行,再逐步收紧电网功率上下限。
6.2 求解非常慢怎么办
整数规划是NP难问题,模型复杂时求解确实会慢。我遇到过几种情况:二进制变量太多、M值太大导致LP松弛问题发散、整数变量上界太宽。
应对方式,第一优先是设置MaxTime和RelativeGapTolerance,工程上不需要强求0间隙,很多场景1%的间隙已经非常够用。第二是确认约束矩阵的尺度合理,单位不统一会导致求解效率断崖式下跌。第三是减少不必要的整数变量,比如N_mod的上界从50降到20,分支定界的搜索空间会呈指数级缩小。
6.3 数值尺度问题的经典症状与修复
如果你发现求解结果特别奇怪,比如SOC曲线出现极端大量级的抖动,或者两次运行结果明显不一致,先怀疑数值尺度。
典型症状:
- 约束矩阵元素最大与最小差10的6次方以上
- 求解器提示“Ill-conditioned”或“Numerical issues”
- 结果对参数微小变化极其敏感
修复手段:
- 统一改用MW和MWh做单位
- 目标函数和约束系数量级尽量接近
- 用optimoptions里的ConstraintTolerance调松一点检查是否正常
6.4 常见错误速查表
| 现象 | 可能原因 | 处理办法 |
|---|---|---|
| exitflag = -2 | 模型不可行 | 逐条排查约束,先放开P_grid边界 |
| exitflag = -3 | 求解超时未收敛 | 增加MaxTime或放宽RelativeGapTolerance |
| 结果N_mod为小数 | 忘了给N_mod设Type为integer | 检查optimvar定义 |
| SOC曲线锯齿状 | 充放电效率设置反了 | 检查eta_ch和eta_dis位置 |
| 结果对初始条件敏感 | 首末SOC一致约束缺失 | 加上soc_cycle约束 |
| intlinprog报“Number of variables must be integer” | 变量类型未设置正确 | 检查optimvar的Type属性 |
除此之外还有一个容易被忽略的点:当电池模组数是0时(也就是“不装储能”),E_rated为0,约束里会除以E_rated的地方必须提前规避。用我前面讲的E_soc建模方式,这个除法问题直接不存在,这也是我推荐那种写法的重要原因。
我个人在实际项目中的习惯是:先用简化数据把所有代码流程跑通,确认模型结构没有问题,再替换成完整的数据集做正式计算。替换数据后如果结果出现明显跳变,先检查数据单位、维度、时段数是否一致,而不是先去怀疑求解器。混合整数规划这个工具箱在MATLAB里已经相当成熟了,绝大多数模型出错都是建模过程中引入的bug,不会是求解器本身的锅。