做综合能源系统优化这几年,我几乎每个项目都绕不开“计及源荷不确定性的综合能源生产单元运行调度与容量配置优化”这个命题。它表面上是一个学术味很浓的题目,骨子里却是一个很接地气的工程问题:手里有一堆设备(风电、光伏、热电联产机组、燃气锅炉、储能),每天怎么安排出力最省钱;长期来看,设备买多大容量的最划算。这两个问题一旦叠加“源荷不确定性”,难度直接上一个台阶——光伏明天发多少电、小区明天用多少热,你是提前不知道的。这篇文我会基于Matlab代码实现,把建模思路、双层求解套路、场景生成与缩减、以及我实际踩过的坑完整捋一遍,适合正在做微电网/综合能源方向毕业设计、横向课题,或者想复现类似论文代码的同学参考。
1. 项目本质拆解:先说清楚我们在优化什么
1.1 一个容易被忽略的前提:源和荷都是随机变量
很多刚接触这个方向的同学,一上来就急着写代码,结果做着做着发现不对劲——因为常规的优化调度,目标函数和约束条件里所有系数都是确定的数,光伏出力给一个24点曲线,负荷给一个24点曲线,丢进求解器,输出一个最优调度方案,完事。可是实际运行中,光伏出力预测值明天可能偏差30%,负荷预测也不可能完全准。所以“计及源荷不确定性”不是锦上添花,而是这类系统能不能在实际中落地的关键。
源荷不确定性具体指什么?源头侧,主要是可再生能源出力的随机性和间歇性——光照强度受云层遮挡影响,风速本身就是一个随机过程;负荷侧,电负荷和热负荷受用户行为、气温、节假日等因素影响,同样有随机波动。在综合能源生产单元里,电负荷和热负荷还往往是耦合的,热电联产机组(CHP)发电的同时产生热量,热电机组的电出力一旦波动,热出力也跟着波动,这又加剧了整个系统的协调难度。
1.2 运行调度和容量配置,为什么要放在一个框架里?
这两个问题本来是不同时间尺度的事。运行调度是小时级甚至分钟级的决策——今天每个时段,CHP发多少电、燃气锅炉出多少热、储能充放多少功率,属于短期优化;容量配置是年化尺度的决策——光伏装500kW还是800kW,储能配2MWh还是4MWh,属于长期投资规划。
但你不能分开做。道理很简单:一套设备组合,如果按某一年的典型场景调度,运行成本很低,那只能说明这套组合在“运气好”的情况下表现不错;换了极端天气和高峰负荷,可能要么切负荷,要么成本暴涨。反过来,容量配置的目标函数里,年化投资成本要和全生命周期的运行成本打成一个总账,而运行成本本身就是下层运行调度问题的优化结果。两个问题天然嵌套,这就是所谓的“双层优化”——上层决定容量,下层在给定容量下做最经济的运行决策,下层的最优值回传给上层作为评价依据。
我在实际项目中经常用“买车”来类比:容量配置是你要买多大排量的车,运行调度是每天怎么踩油门。排量买小了,平时省油钱,但满载爬坡时拉不动,或者频繁高转速运转,油耗反而高;排量买大了,动力是足了,但日常通勤的油耗和维护成本又涨了。最优的排量,必须结合你真实的用车工况(也就是源荷曲线)才能定。这就是为什么容量配置必然要把运行调度嵌套进去。
2. 数学建模:把物理系统翻译成优化问题
2.1 设备建模与能量枢纽框架
综合能源生产单元,学术上常叫Energy Hub(能量枢纽)。核心思想是:多种能源输入(天然气、光伏、风电、电网购电),经过内部的转换、存储设备,输出多种能源服务(电、热、冷)。建模时,我习惯先把每个设备看成一个“转换节点”:
CHP机组:输入天然气,输出电功率和热功率。建模时有两种常见方式。一种是把电出力和热出力设为独立决策变量,用可行域约束描述二者的耦合关系(背压式机组是固定的热电比,抽凝式机组是一个多边形可行域);另一种是直接用热电比系数关联,简单但精度差。我的建议是,除非论文要求简化,否则用可行域约束更贴近实际,代价只是多几个线性不等式。
燃气锅炉:天然气转热,效率近似常数,约束就是出力上下限和爬坡速率。
电锅炉:电转热,纯线性效率,是消纳弃电和提供热灵活性的关键设备。
电储能:用SOC(荷电状态)递推方程描述,充放电效率分开写,注意不能同时充放电——这个约束很多人一开始会漏掉,或者用一个大M法加二进制变量来强制互斥。
光伏和风电:在运行调度层是“负的负荷”,出力曲线作为参数输入;在容量配置层才是决策变量(装机容量),乘以归一化出力曲线得到实际出力。
能量平衡约束是模型的骨架。每个时段,电功率平衡要满足:光伏出力 + 风电出力 + CHP电出力 + 电网购电 + 储能放电 = 电负荷 + 电锅炉耗电 + 储能充电。热功率平衡类似:CHP热出力 + 燃气锅炉热出力 + 电锅炉热出力 = 热负荷 + 储热罐充热(如果有)。
这里特别容易踩坑的是设备之间的“能质不匹配”。比如,电锅炉用高品质的电能烧低品质的热水,能量转换效率虽然接近100%,但从火用(Exergy)角度是非常浪费的;CHP的品位匹配就合理很多。不过在传统的经济调度模型里,我们只看能量数量守恒和成本,所以这类问题不会显式出现,但你在结果里如果发现电锅炉大量启停而CHP出力很低,就要警惕是不是参数设置导致了不合理的运行策略。
2.2 不确定性建模:场景法、鲁棒优化与分布鲁棒怎么选
处理源荷不确定性,主流有三条路线,决策之前你要看得足够清楚。
**场景法(随机规划)**的思路是:用若干条典型场景曲线代表未来的不确定性。具体做法是,先画出一堆可能的源荷曲线(通常用蒙特卡洛抽样或拉丁超立方抽样从预测值和误差分布里生成),然后聚类缩减到十几个典型场景,让这些场景带概率地进入优化模型,目标函数变成“各场景下运行成本的期望值最小”。这种方法的优点是对不确定性“一视同仁”,得到的是一个在平均意义上最优的方案;缺点是,极端场景可能被聚类缩掉了,同时多场景会让模型变量数和约束数成倍膨胀。
鲁棒优化的思路恰好相反:不追求平均最优,而是保证在预设的不确定性区间内的最恶劣场景下,方案依然可行且成本可控。它把不确定性描述为盒式集合——光伏出力落在预测值上下浮动某个百分比之内,负荷也类似。你求解的是“在最坏情况下最小化成本”的“最保守”方案。大家常听到的“鲁棒预算”参数,就是用来调节保守程度的:预算为0时退化为确定性模型,预算取最大时就是全区间逐时段的最坏情况。几乎所有的实际项目,我都蹚过两种方法的浑水,说实话,鲁棒优化在工程上的可解释性更强,因为决策者想知道的是“最差能差到哪里”,而不是“平均能省多少钱”。
分布鲁棒优化是折中路线,这几年论文里很火:假设不确定量的真实分布在某一 Wasserstein 距离球内波动,优化最坏情况下的期望成本。它兼顾了随机规划的概率信息和鲁棒优化的保守性,但求解难度明显更高,代码实现复杂度也不是一个量级,一般不做横向课题需求建议直接放弃,除非你的方向就是研究这个。
我给个选型建议,特别接地气的那种:
| 方法 | 适合的场景 | 难点 | 我的使用频率 |
|---|---|---|---|
| 场景法 | 数据充足、需要典型工况分析 | 场景生成与缩减、计算量大 | 高 |
| 鲁棒优化 | 需要承诺最坏情况下的可靠性 | 区间设置、对偶变换 | 中高 |
| 分布鲁棒 | 学术研究,追求理论深度 | 求解复杂、代码难度大 | 低 |
在Matlab里,场景法的代码体系最成熟,容易和Yalmip+Cplex衔接,论文复现也方便;鲁棒优化则要把内层“最坏情况”的对偶问题推导清楚,转成单层再求解。下面的代码实现部分,我会以场景法为主线展开,实际项目的完整代码基本也是这样写的。
2.3 下层运行调度模型:给定容量的最经济运行决策
运行调度模型是整个双层优化的内层,给定各设备装机容量后,我要决定24小时(或更长)内每小时各类设备的出力。建模时我一般用混合整数线性规划(MILP),因为只要涉及设备启停,就要引入0-1变量。
目标函数是典型的多目标加权:
[ \min \sum_{t=1}^{T} \left( C_{grid,t} P_{grid,t} + C_{gas} F_{chp,t} + C_{gas} F_{boiler,t} + C_{om} P_{devices,t} + C_{curtail} P_{curtail,t} \right) ]
其中,第一项是电网购电费用,第二三项是CHP和燃气锅炉消耗天然气的燃料费用,第四项是设备运维费用,第五项是弃风弃光惩罚项。
约束条件必须覆盖齐这些:
- 能量平衡约束——电、热(、冷)逐时平衡。
- 设备出力上下限——CHP、锅炉、电锅炉、储能充放功率均在[0,容量]范围内。
- 爬坡约束——相邻时段出力变化不超过限值。这个约束常常被人忘记,但实际机组都有爬坡率限制,如果不约束,调度结果会出现相邻时段出力跳变,工程上无法执行。
- 储能约束——SOC递推式、SOC上下限约束、充放互斥约束、调度周期起止SOC相等(也可以按需设定,比如早晚各设置一个SOC目标)。
- 与电网交互约束——购电功率上限,以及是否允许向电网售电,这决定了要不要额外引入负功率变量。
所有非线性约束需要线性化。这里我踩过最深的一个坑是:把储能充放电功率和SOC的关系写成 SOC(t+1)=SOC(t)+Pch(t)*ηch-Pdis(t)/ηdis 时,如果充放电效率都用常数,那没问题,但一旦有人写成SOC(t+1)=SOC(t)+Pch(t)*ηch-Pdis(t),就会导致能量不守恒的系统误差。此外,储能充放互斥如果不用二进制变量,而只是通过目标函数成本差异间接约束,很可能求解器给出同时充放电的“循环套利”假象,白白消耗能量而没有实际价值。
2.4 上层容量配置模型:投资决策与全生命周期成本
上层的目标函数是年度总成本最小,包括年化投资成本和年度运行成本:
[ \min \sum_{i} \left( C_{inv,i} \cdot E_{i}^{cap} \cdot CRF \right) + C_{oper}^{*} ]
其中 (E_i^{cap}) 是设备i的配置容量,(C_{inv,i}) 是单位容量投资成本,(CRF) 是资金回收系数,把一次性投资折算成等年值,公式是 (CRF = r(1+r)^n / ((1+r)^n - 1)),(r)是贴现率,(n)是设备寿命。(C_{oper}^*) 是下层运行调度优化得到的最优运行成本(注意,在不确定性场景下,应该用所有场景的期望运行成本)。
上层约束比较简单:各设备容量在给定的候选范围内,总投资不超过预算上限。因为设备容量通常是连续变量,下层运行调度又嵌套在目标函数里,这就构成了一个典型的含下层优化的非线性优化问题(即使下层是线性的,整个问题也是不可微的,甚至非凸的)。
说到双层求解,新手最容易犯的错误是:以为可以直接在Yalmip里把上下层一起丢给求解器。实际上,除非你手动把下层问题的KKT条件推导出来,作为约束加入上层问题(这就是单层化方法),否则求解器根本不知道下层问题内嵌了一个优化过程。我在项目里常用的解法和它们的适用场景,后面第3部分详细说。
3. Matlab代码实现:从零跑通双层优化
3.1 开发环境与工具链选型
在我所有做过的综合能源优化项目中,Matlab + Yalmip + Cplex(或Gurobi)这套组合是最稳定的。许多横向课题学生问能不能用Matlab自带的linprog?如果只是纯线性规划且规模小,当然可以。但涉及MILP、以及后续可能要验证大场景连算的时候,商用求解器的速度差距非常明显。我推荐环境配置如下:
- Matlab R2020b及以上版本,推荐R2022b或R2023b,稳定性比较好。
- Yalmip工具箱,用于建模。它是免费的,MathWorks官网或GitHub都能下,需要添加到Matlab路径。Yalmip的价值在于把优化问题从“矩阵系数手工拼写”中解放出来,你可以用接近数学语言的方式直接写目标函数和约束。早期我试过直接调Cplex的API写系数矩阵,一个24时段的调度模型要维护几十个矩阵的索引,一旦设备多几个,改一个约束就要改半天,容易出错。Yalmip帮我彻底解决了这个问题。
- Cplex或Gurobi求解器,需要至少装一个。安装时注意版本匹配——Cplex 12.10可以支持到R2020b,更高版本建议去对应官网看兼容性列表。Gurobi则要注意许可证问题,学校一般有学术license,企业项目就需要商业license。
代码的目录结构我一般这样组织:
project_root/ ├── main_capacity_optimization.m % 主程序,双层迭代入口 ├── data_definition.m % 所有系统参数和源荷数据 ├── scenario_generation.m % 场景生成与缩减 ├── operation_scheduling.m % 下层运行调度(Yalmip建模) ├── capacity_upper_level.m % 上层容量搜索 ├── kkt_single_level.m % (可选)KKT单层化 └── plot_results.m % 结果可视化3.2 场景生成与缩减:用代码模拟不确定性
场景生成这一步,我强烈建议不要一上来就蒙特卡洛抽几千个场景丢进模型,求解时间你肯定扛不住。实操中更聪明的做法是:先用预测值和误差分布生成大量候选场景,然后用聚类算法缩减成10~20个典型场景。
先看场景生成的核心代码。假设光伏出力的预测曲线是24维向量,误差服从均值为0、标准差为预测值20%的正态分布,可以用Matlab这样生成N个候选场景:
% 生成600个光伏出力的随机场景,预测值为pv_forecast(1x24) N_scen = 600; pv_forecast = pv_norm * pv_capacity; % 归一化出力 * 装机容量 pv_scenarios = zeros(N_scen, 24); for i = 1:N_scen % 误差项逐时段独立,也可以加时序相关性 error_term = randn(1, 24) .* (0.2 * pv_forecast); pv_sim = pv_forecast + error_term; pv_sim = max(pv_sim, 0); % 出力不可能为负 pv_sim = min(pv_sim, pv_capacity);% 出力不可能超过装机容量 pv_scenarios(i, :) = pv_sim; end如果你想考虑时序相关性(比如连续阴天导致光伏连续多时段偏低),可以用AR(1)模型或者多元正态分布引入相关性,这会显著提升场景质量。但提醒一下,单时段独立抽样生成的场景,聚类后的典型场景往往会“过于平滑”,因为每个时段的极端值都被平均掉了,对容量配置这种需要考虑长期极端场景的问题来说,这可能导致结果偏乐观。这里我建议用拉丁超立方采样替代简单的randn,保证样本在概率空间上更均匀覆盖:
% 拉丁超立方采样生成光伏场景 n_sim = 600; X = lhsdesign(n_sim, 24); % 生成[0,1]均匀样本 error_norm = norminv(X, 0, 1); % 转换到标准正态分布 error_term = error_norm .* (0.2 * pv_forecast);% 缩放误差 pv_scenarios = max(min(pv_forecast + error_term, pv_capacity), 0);场景缩减用K-means聚类即可。注意聚类结束后,每个类里的场景数占总数比例,就是该典型场景的概率。聚类时建议用动态时间弯曲距离(DTW)替代欧氏距离,因为欧氏距离会把“时间对齐的相同形态、但整体平移”的两个场景误判为距离很大,而DTW能更合理地度量时序曲线的形状相似度。Matlab内置的kmeans默认用欧氏距离,如果要用DTW需要自己写距离矩阵,数据规模不大时完全可行。不过大多数论文和工程项目用欧氏距离就够用,不要过度设计。
3.3 下层运行调度的Yalmip实现
下层运行调度是双层问题的核心子程序,我用Yalmip+一个MILP求解器来解。下面贴一段精简但能直接运行的调度模型核心代码:
function [oper_cost, dispatch_result] = operation_scheduling(capacity, scenario_data) % capacity: 结构体,包含各设备容量 % scenario_data: 结构体,某场景的源荷曲线和参数 T = 24; % 决策变量 P_grid = sdpvar(1, T); % 电网购电功率 P_chp = sdpvar(1, T); % CHP电出力 H_chp = sdpvar(1, T); % CHP热出力 H_boiler = sdpvar(1, T); % 燃气锅炉热出力 P_eb = sdpvar(1, T); % 电锅炉耗电功率 P_sto_ch = sdpvar(1, T); % 储能充电功率 P_sto_dis = sdpvar(1, T); % 储能放电功率 SOC = sdpvar(1, T+1); % 储能荷电状态 u_sto = binvar(1, T); % 储能充放互斥标志位 u_chp = binvar(1, T); % CHP启停标志位 % 约束条件集合 Constraints = []; % 电功率平衡 Constraints = [Constraints, ... P_grid + P_chp + P_sto_dis + scenario_data.pv(:,1)' == ... scenario_data.load_e(:,1)' + P_eb + P_sto_ch]; % 热功率平衡 Constraints = [Constraints, ... H_chp + H_boiler + P_eb * eta_eb == scenario_data.load_h(:,1)']; % CHP可行域约束(简化为固定热电比的线性约束) Constraints = [Constraints, ... 0 <= P_chp <= capacity.chp, ... 0 <= H_chp <= capacity.chp * alpha_chp_max, ... H_chp >= alpha_chp_min * P_chp, ... H_chp <= alpha_chp_max * P_chp, ... P_chp <= M * u_chp, ... % 大M法处理启停 P_chp >= 1e-3 * u_chp]; % 锅炉与电锅炉 Constraints = [Constraints, ... 0 <= H_boiler <= capacity.boiler, ... 0 <= P_eb <= capacity.eb]; % 储能约束 Constraints = [Constraints, ... SOC(1) == capacity.sto * soc_init, ... SOC(T+1) >= SOC(1), ... % 周期始末SOC一致(或设目标区间) SOC(2:T+1) == SOC(1:T) + P_sto_ch * eta_ch - P_sto_dis / eta_dis, ... 0 <= SOC <= capacity.sto, ... 0 <= P_sto_ch <= capacity.sto_p * u_sto, ... 0 <= P_sto_dis <= capacity.sto_p * (1 - u_sto)]; % 目标函数:运行成本最小 Objective = sum(price_grid .* P_grid) + ... sum(price_gas * (P_chp / eta_chp + H_boiler / eta_boiler)) + ... sum(p_om * (P_chp + H_boiler + P_eb + P_sto_ch + P_sto_dis)); % 求解 options = sdpsettings('solver', 'cplex', 'verbose', 0, 'showprogress', 0); sol = optimize(Constraints, Objective, options); if sol.problem ~= 0 error('运行调度求解失败,请检查约束和参数'); end oper_cost = value(Objective); % 存储结果供上层调用 dispatch_result.P_chp = value(P_chp); dispatch_result.H_chp = value(H_chp); dispatch_result.P_grid = value(P_grid); end这段代码有几点要特别提醒:
第一,储能约束里的SOC递推式用了SOC(2:T+1) == SOC(1:T) + ...这种向量化写法,非常高效,但前提是矩阵维度要对齐,多一个时段(T+1)容易漏。我见过不少新手在写T和T+1时索引错位,结果SOC逐年漂移,模型看着能用,跑出来全是数值误差。
第二,大M法里M的值要足够大,但又不能过大到引起数值困难。我常用的经验是:取该设备最大出力上限的10~20倍即可。例如容量是5MW,M取50~100就够,完全没必要取1e6。太大的M会导致求解器的数值稳定性问题,尤其在可靠性要求高的场合,这个问题非常致命。
第三,sdpsettings里的verbose设成0,只会在求解失败时看到报错。实际项目调试阶段建议设成1,可以看到求解器的迭代信息和求解时间,方便定位瓶颈。
3.4 上层容量配置求解:三种主流策略
上层是容量自由度下的优化,不是标准的直接可求解线性问题,我实际用下来有三条路线可选。
路线一:枚举法。如果候选容量是离散的(比如光伏容量从300kW、400kW、500kW三档里选,储能容量从1MWh、2MWh、4MWh里选),全组合枚举可能是最简单的。每给定一组容量,调用下层调度3次(10个场景就是10次),将期望运行成本加上年化投资成本,取最小值对应的组合。这种方法胜在逻辑清晰、无收敛问题,但设备数量多了之后组合会爆炸。适用于设备数不超过4~5个的场景,基本上就是我做过的大多数微电网/能源站案例。
路线二:遗传算法/粒子群等启发式算法。外层用ga或particleswarm搜索容量解,内层针对每个个体调用下层调度。Matlab的Global Optimization Toolbox直接支持,不需要自己造轮子。但注意,启发式算法每评估一个种群个体就要调多次下层求解,假设种群20个个体、迭代30代、每代对10个场景调度,总耗时大约6000次下层求解,MILP一次0.1秒的话,跑一个算例就要10分钟,调参过程极其酸爽。所以我个人看优化次数多的时候,倾向于先用枚举法排除明显不合理的候选区间,再用启发式做精细搜索。
路线三:KKT单层化。这是理论上最优的方法——把下层调度问题的KKT条件推导出来,作为上层问题的约束,将整个双层问题一次性转为单层MILP(互补松弛条件用大M法线性化)。在Matlab里可以用Yalmip建模,然后整体丢给Cplex求解,解是全局最优的。但推导过程比较复杂,下层是MILP时KKT条件并不完备(含整数变量,强对偶未必成立,需要做凸松弛等处理),所以我建议仅在下层是纯线性规划(不含0-1变量)、且你对对偶理论掌握较强的场景下,才走这条路。多数实际项目,路线一加路线二已经能覆盖。
3.5 双层迭代主程序:代码骨架
主程序按场景法+枚举/遗传混合的思路来写,逻辑其实很清爽:
%% 1. 定义系统参数(设备候选容量、源荷预测、成本系数) data_definition; %% 2. 生成并缩减不确定性场景 scenarios = scenario_generation(pv_forecast, load_forecast, n_target_scenes); %% 3. 枚举候选容量组合(或调用ga搜索) capacity_candidates = generate_candidates(); % 生成设备容量候选表 best_cost = inf; for i = 1:size(capacity_candidates, 1) capacity_i = capacity_candidates(i, :); % 4. 对每个场景调用下层运行调度,求期望运行成本 oper_cost_sum = 0; for s = 1:length(scenarios) [cost_s] = operation_scheduling(capacity_i, scenarios(s)); oper_cost_sum = oper_cost_sum + scenarios(s).prob * cost_s; end % 5. 计算年化总成本 annual_inv_cost = sum(inv_cost_per_unit .* capacity_i) * CRF; total_cost = annual_inv_cost + oper_cost_sum * annual_hours; % 6. 更新最优方案 if total_cost < best_cost best_cost = total_cost; best_capacity = capacity_i; end end %% 7. 输出并绘制结果 plot_results(best_capacity, best_cost);其中annual_hours是把日运行成本折算到全年用的等效小时数(通常取365天,如果只算典型日则乘365;如果考虑四季典型日,则按权重加权)。
这套主程序骨架很通用,你换设备类型、换场景数据都不需要大改,只要把operation_scheduling内部的设备模型替换掉即可。
4. 算例设计与结果解读:用数据验证模型价值
4.1 典型算例参数设置
拿我做过的一个园区级综合能源系统算例来演示。系统包含:光伏(候选容量0~800kW)、CHP(候选容量0~600kW)、燃气锅炉(候选容量0~1000kW)、电锅炉(候选容量0~300kW)、电储能(候选容量0~200kWh/100kW)。基础数据大致如下:
| 参数 | 数值 | 说明 |
|---|---|---|
| 光伏单位投资成本 | 3500元/kW | 含安装 |
| CHP单位投资成本 | 5000元/kW | 含辅机 |
| 燃气锅炉单位投资成本 | 800元/kW | |
| 电储能单位投资成本 | 1800元/kWh | |
| 天然气价格 | 3.2元/m³ | 按热值折算约0.35元/kWh |
| 电网购电价格 | 峰0.83/谷0.38元/kWh | 分时电价 |
| 贴现率 | 8% | |
| 设备寿命 | 20年 |
光伏和负荷曲线用的是夏天典型日的归一化数据,光伏预测峰值出现在13:00,电负荷有两个峰(上午和傍晚),热负荷夜间高、白天低——这非常符合办公园区的用能特征。
不确定性设定:光伏出力预测误差取±20%标准差,电负荷预测误差取±10%,热负荷预测误差取±15%。初始生成600个场景后,用K-means缩减到10个典型场景,聚类后的场景概率由各类样本比例确定。我特别检查过聚类后的场景质量——缩减后的场景集合应当能在统计意义上保留原始场景集的均值、标准差和极端值范围,如果你的聚类结果把极端场景都稀释掉了,建议减少聚类中心数量或改用DTW距离重新聚类。
4.2 确定性调度 vs 不确定性调度的结果对比
不加不确定性时,模型直接用预测曲线作为唯一场景,求出来的最优容量组合是光伏600kW、CHP300kW、燃气锅炉500kW、电锅炉100kW、储能100kWh,年总成本约186万元。这个方案是不是就是最优?未必。因为源荷不确定性让模型“盲目乐观”了。
加了不确定性场景后,最优容量组合变为:光伏500kW、CHP400kW、燃气锅炉500kW、电锅炉150kW、储能150kWh。总成本约204万元。对比一下能看到:
- 光伏容量下降了。因为光伏出力不确定且夜间为零,过度投资光伏会在很多场景里造成弃光,边际收益下降;而CHP是可控机组,在承担峰荷和保障供热上更可靠,所以容量反而上调。
- 储能容量提高了。储能的作用从“套利”更多转向“应对不确定性”——当光伏突然少发或者负荷突增时,储能提供快速响应,避免高价购电和切负荷。
- 总成本上升了约10%。这就是“不确定性的代价”——你必须多花钱买更鲁棒的方案,换来的是系统在各种实际场景下的可靠性和成本可控性。如果你的研究只停留在确定性阶段,你会严重低估维持可靠供应的真实成本。
这些结论的可视化也很直观。我用柱状图画出10个场景下各设备平均出力分布,你会看到确定性方案的CHP出力集中在固定几个时段,而场景化方案的CHP出力带明显变宽,说明面对不同场景机组需要更灵活的调节能力。储能SOC曲线在这种方案下不再是经典的“谷充峰放”漂亮曲线,而是带着更多的“随机波动”修正动作——这恰恰是它在发挥不确定性的缓冲作用。
4.3 容量配置的敏感性分析
这个部分我强烈建议在做课题时加进去,因为审稿人和企业专家都爱看。核心问题是:场景数量、不确定性区间大小、电价水平、气价水平,这些参数扰动对最优容量和总成本的影响。
以不确定性区间为例:我把光伏误差从±10%逐步调到±40%,得到一组结果:
| 光伏误差水平 | 最优光伏容量(kW) | 最优储能容量(kWh) | 年总成本(万元) |
|---|---|---|---|
| ±10% | 650 | 120 | 193 |
| ±20% | 500 | 150 | 204 |
| ±30% | 400 | 200 | 218 |
| ±40% | 300 | 220 | 236 |
趋势非常清晰:不确定性越大,系统越倾向于用可控机组(CHP、储能)替代随机电源(光伏)。总成本随不确定性增大几乎线性上升。这给决策者的启示是:与其盲目增加投资去对抗不确定性,不如先想办法提高预测精度——把预测误差从40%降到20%,相当于省下的年成本是实实在在的。
再比如场景数量敏感性:我用5、10、20、30个场景分别计算,发现10个场景和20个场景的最优容量组合已经一致,总成本差异小于1%,但求解时间从2分钟涨到6分钟多。这说明10个场景对这种规模的系统已经足够;场景数继续增加,计算代价远大于精度收益。这个结论对不同项目有差异,但“先做场景数敏感性实验再定最终场景数”这个习惯,我强烈建议保持。
5. 常见问题与排查技巧实录
这部分是我最想写的,因为理论和代码框架大家都差不多,真正的差距往往在踩坑和填坑上。
5.1 求解器安装与版本匹配
Yalmip本身是个建模层,真正求解靠外部求解器。Cplex和Gurobi的安装是我被问过最多的问题。常见报错是:
YALMIP uses CPLEX 12.x but a newer version is installed——Yalmip缓存问题,用yalmiptest重新检测,或者清理clear yalmip再试。License Error -8——网络许可证或hostid不匹配,属于MathWorks许可服务的常见故障,一般重新激活或检查环境变量即可,这类问题多和本机系统配置绑在一起,解决办法网上很多,注意选与本机版本对应的教程。
如果Cplex实在装不上,建议直接换Gurobi,它的学术许可申请流程更顺畅,而且和Yalmip的集成几乎无缝。实在没有商用求解器可用,可以先用glpk顶着(Yalmip自带支持),小规模算例能跑通,但别指望它能求解大场景MILP,性能差距非常明显。
提示:Yalmip在求解完成后,一定要检查
sol.problem是否为0。我见过很多同学代码能跑、有输出,但实际上sol.problem返回了4(数值问题)或1(不可行),这意味着结果根本不可用,只是求解器给出了一个“看起来”正常的值而已。
5.2 双层优化不收敛或结果不合理
如果你发现容量配置结果出现“波动”或者“震荡”——同样一组候选容量,多跑几次得到的结果不一样,先想清楚是哪个层次出了问题。
第一,启发式算法的随机性会导致每次结果略有差异。解决办法有两个:设置随机种子(rng(42))保证实验可复现;增加种群代数和大小,减少陷入局部最优的概率。
第二,下层MILP在某些极端参数组合下不可行。容量配置搜索过程中,外层可能试探出一组“过小”的容量——比如储能容量只有10kWh,但负荷峰值很高,导致任意时段都无法满足平衡约束。这就需要在operation_scheduling里对不可行情况作特殊处理:设定橘色惩罚值(infeasibility cost),而不是让程序报错中断。调用失败时,给这组容量返回一个很大的成本,让优化算法知难而退。
if sol.problem ~= 0 oper_cost = 1e10; % 惩罚值远大于任何可行方案的成本 return; end这个处理非常实用,否则你跑一个遗传算法,可能迭代到第10代就因为一次不可行方案而整个程序崩溃,前功尽弃。
第三,储能SOC初值终值设置导致不可行。特别在你设定了SOC(T+1) == SOC(1)但初值设得很高、储能容量又小的情况下,很可能找不到可行解。解决办法是把周期始末SOC约束改成软约束,或者在约束里加入允许终值在一定范围内(比如SOC(1)-0.1 <= SOC(T+1) <= SOC(1)+0.1),这样既避免了不可行,又基本保持了调度的周期性。
5.3 场景数过多导致计算时间爆炸
场景法最现实的问题就是计算时间。一个10场景的算例,每场景下层MILP求解约0.8秒,外层枚举200组容量组合,总耗时约1600秒,接近半小时——这还是小规模算例。换到遗传算法动辄上千次下层求解,时间根本不可接受。
我的经验做法有两条:
一是减少场景数。先用20个场景跑通流程,再逐步减到10个、5个,对比最优容量和总成本变化。我多年课题经验,5~10个典型场景对大部分容量配置问题已经足够稳定,继续增加场景数主要增加的是“心理安慰”,对结果影响很小。
二是合并同类场景。把负荷和光伏曲线形态相近的场景、或者对运行调度结果影响极小的低概率场景直接剔除,专注保留高概率和极端场景。另一种技巧是分层调度:先把热负荷按典型日聚类成几类,每一类内部再细分电负荷场景,这能让场景数量从“乘法增长”降为“加法增长”,我在做季节性调度时就经常这么干。
5.4 单位与量纲:最不起眼但最致命
真的不是危言耸听。我见过三个项目的失败都栽在单位上:kW和MW混用、kWh和MWh混用、价格每度和每千瓦时混用。
在Matlab里,我一般统一采用:功率单位kW,能量单位kWh,价格单位元/kWh,热值单位kWh(把天然气按热值折算成kWh,而不是用m³)。初始化时写清楚全局变量单位,代码注释里标出来。然后做一次量纲校验:随便设一组容量和出力,手算几个时段的能量平衡,确认对得上再往下跑。如果模型很大,可以用一个简单的方法——把设备效率全设为1、价格全设为1,看看求出来的调度策略是否符合直觉(比如光伏和储能优先使用)。这一关过了,再改回真实参数。
5.5 结果可视化的选图技巧
做这个方向,图是论文和报告的门面,也是你判断结果合理性最重要的依据。我的固定搭配是:
- 各设备24小时出力堆叠图(电功率平衡直观展示)——用
bar或area堆叠。 - 热功率平衡图(同左,用于供热部分)。
- 储能SOC曲线图(验证调度周期性和充放深度的合理性)。
- 场景聚类前后对比图——原始场景半透明灰色细线,聚类中心用彩色粗线,一张图就能直观说明你场景缩减的可靠性。
- 容量配置结果的成本构成饼图(投资成本/运行成本/燃料成本/购电成本占比)。
画图时注意坐标系标签完整、单位明确、图例清晰。很多工程师读者不关心公式推导,但他们能一眼从图里看出问题——比如CHP在夜间满发而热负荷却很低,那就说明模型可能允许了浪费或约束条件漏了。
6. 一点经验总结:这条路还能往哪走
做了好几个类似的综合能源优化项目后,我个人最大的体会是:计及源荷不确定性的容量配置模型,难点从来不是数学公式,而是“怎么把不确定性刻画得既准确又不过度复杂”。场景法胜在直观,鲁棒法胜在稳健,分布鲁棒法胜在理论优雅,但真正决定项目价值的是你选的数据、参数和场景是否贴近实际。我在实际项目里最常做的,反而不是堆高级算法,而是反复跑敏感性分析、把场景数和误差参数的影响彻底摸透——这比任何花哨的求解技巧都能打动决策者。
最后再分享一个小技巧:如果这套代码将来还要做扩展,建议从第一天就把设备模型写成独立的函数接口,输入输出都是结构体。后面你要加入氢储能、碳捕集或者需求响应时,不需要重写主程序,只需要新增一个设备模型文件并接上能量平衡方程式——这个习惯让我在后续多个项目中省掉了至少一半的重构时间。
这个方向后续可以扩展的点还很多,比如多园区之间共享储能、考虑碳交易价格对容量配置的影响、把调度从日前扩展到实时滚动等。每一个点开下去都足够再写一篇新的实战文章,但先把单体的源荷不确定性下的调度与容量配置跑通,才是所有扩展的基础。希望这篇整理能让你少走一些我走过的弯路。