综合能源系统(IES)里的调度问题,我研究了大半年,最直观的感受是:单靠一个时间断面的优化结果根本不够用。你辛辛苦苦用Matlab和Yalmip解出一组设备出力计划,高高兴兴拿去执行,结果风电场实际出力跟预测差了一截,光伏午间突然被云遮了,或者楼宇热负荷比昨天多了5%,那套"最优"方案立马就变成次优甚至不可行。这也是为什么现在工程上主推日前-日内两阶段调度——把"计划"和"修正"分开处理,一个管大局,一个抠细节。
这篇文章想把我在这个方向上的完整思路和实现过程聊透。核心是把综合能源系统的物理模型抽出来,讲清楚日前和日内两个阶段分别怎么建模、目标函数怎么差异化设计、约束怎么用Yalmip优雅地表达,再配上实际可跑的MATLAB代码框架和调试避坑经验。适合刚接触综合能源优化调度、想快速搭出一套可复现算例的读者,也适合那些已经能跑通单阶段调度、想进一步理解为什么非得"两阶段"不可的研究生和工程师。
1. 两阶段调度到底在解决什么问题:先想清楚"为什么",再动手建模
先把这个"为什么两阶段"掰开揉碎。很多论文里都是一句话带过——"日前调度确定机组启停和基础出力,日内调度跟踪负荷和可再生能源波动"——听着像那么回事,但真正做模型的时候你会遇到一个很现实的问题:两个阶段的目标函数、决策变量、时间尺度全都不一样,如果概念没拎清,代码写出来就是一锅粥。
1.1 单阶段调度的困境:预测误差会把优化结果变成"定时炸弹"
单阶段调度就是拿一整天24小时的预测数据,一次性求出全天96个时段(15分钟一个点)的机组出力、储能充放电、购电计划。表面看挺省事,算一次就完。但问题出在预测精度上。
光伏出力预测,晴天误差小,多云天误差能到±20%以上;负荷预测在节假日、极端天气下偏差更大。这些误差到了执行层面会变成什么?最简单的一种连锁反应:日前计划让电锅炉在10:00-11:00以额定功率耗电产热,结果光伏实际出力比预测低了30%,电网联络线功率越限,调度员只能手动压负荷或者紧急调整计划。这个调整过程里,电锅炉的热出力突然降下来,热水管网里的温度瞬间波动,末端舒适度受影响,整个系统的经济性也不如预期。
所以单阶段调度的本质问题是:它把预测数据当成真实数据来用,一旦预测和实际对不上,优化的"最优性"就打了折扣。这不是算法不行,是信息结构决定的。
1.2 两阶段调度的信息结构:计划靠"滚动"、修正靠"反馈"
两阶段调度的思想其实很简单:不同时段的信息精度不一样,那就用不同的决策来匹配。
- 日前阶段(Day-Ahead,DA):提前24小时做,用一天前的预测数据,时间尺度可以是1小时一个点,一共24个时段。决策变量是机组的启停状态、基础出力计划、储能设备的运行状态。这个阶段追求的是系统整体运行成本最优,它对应的是"明天大概怎么跑"。
- 日内阶段(Intra-Day,ID):当天执行,用最近几小时的滚动预测数据(通常4-6小时一个滚动窗口),时间尺度缩到15分钟。决策变量是在日前计划基础上对各设备的出力调整量。这个阶段追求的是跟踪偏差最小和局部成本最优,对应"接下来15分钟该往哪调"。
两个阶段串起来就是一个典型的模型预测控制(MPC)结构:日前提供基准轨线,日内用更新数据去修正,修正完执行一小段,然后再滚动再修正。
提示:两阶段模型之间的"接力点"其实很关键。我一开始做的时候只送了一个日前出力值,结果日内阶段不能改设备启停,好多可行域都对不上。正确做法是把日前调度得到的机组启停状态、储能SOC轨迹、联络线功率上限这些"边界条件"传到日内,日内只调整连续变量。
1.3 这个方案适用什么场景
不是所有综合能源系统都需要两阶段。我自己试下来,以下几种情况收益最大:
- 园区型综合能源系统:有电负荷、热负荷、冷负荷,内部有CHP、热泵、储能,"源-网-荷-储"联动关系强;
- 含高比例新能源(光伏/风电)的系统:新能源渗透率越高,预测误差影响越大,日内修正的价值越凸显;
- 有储能/蓄热装置的系统:储能给了日内调度"缓冲手段",不然日内阶段想调也没工具;
- 需要参与电力市场的系统:日前申报曲线要靠日前调度得出来,日内实际上网电量要日内滚动去盯。
如果只是光伏+电锅炉+负荷这种极简结构,两阶段能用但收益有限,因为耦合关系太少,日内修来修去可选的手段就那几样。
2. 综合能源系统的设备建模:把能量流和耦合关系先落到纸面上
模型的物理基础一定要扎实。很多初学者一上来就怼Yalmip语法,结果约束条件写错、量纲对不上,算出来的结果自己都不敢信。我习惯先把系统拓扑画清楚,哪些设备接在什么母线上,能量怎么转化、怎么存储,全部写成公式再转成代码。
2.1 典型设备集与能量母线结构
我这个算例参照一个典型园区综合能源系统,包含以下设备:
| 设备 | 能量输入 | 能量输出 | 耦合关系 |
|---|---|---|---|
| CHP内燃机(燃气轮机) | 天然气 | 电+热 | 电热比恒定或区间可调 |
| 燃气锅炉 | 天然气 | 热 | 电热独立 |
| 光伏 | 太阳能 | 电 | 不可调度,预测值 |
| 风电 | 风能 | 电 | 不可调度,预测值 |
| 电储能 | 电 | 电(充放) | 时间耦合,SOC |
| 蓄热罐 | 热 | 热(蓄放) | 时间耦合,SOC |
| 电锅炉 | 电 | 热 | 电转热,P2H |
| 电网联络线 | 电 | 电 | 购电/售电 |
能量母线分三条:电母线、热母线、气母线。购电、光伏、风电、CHP发电、电储能放电都汇到电母线;CHP余热、燃气锅炉、蓄热罐放热、电锅炉产热汇到热母线;天然气从气网购进,分配给CHP和燃气锅炉。
这个结构最大的特点是**"以热定电"和"以电定热"的灵活切换能力**。传统CHP系统通常电出力牵引热出力,园区里加了电锅炉和蓄热罐之后,解耦了电和热的强耦合,日内调度的灵活性一下子大了起来。后面建模型时你会发现,这套设备的组合拳就是两阶段调度能打出效果的物理基础。
2.2 CHP机组的运行可行域:别再用单点效率,用多边形逼近
我踩过最典型的坑是CHP建模。一开始图省事,用固定热电比:
P_chp = k * H_chp但实际内燃机组的可行运行域是一个四边形(甚至多边形),电出力和热出力相互约束,不是一条直线。如果用固定热电比,优化器为了让成本最小,很可能把CHP推到可行域之外,算出来的结果看着"完美",实际根本无法执行。
我改用多边形极点的凸组合来描述CHP可行域。假设可行域有N个顶点(a_i, b_i),则需要引入辅助连续变量 λ_i(i=1..N),满足:
P_chp = Σ(λ_i * a_i) H_chp = Σ(λ_i * b_i) Σλ_i = 1 λ_i ≥ 0这套方法在Yalmip里表达非常顺手,本质上就是线性规划的多边形约束。代价是多了一组辅助变量,但对求解器来说根本不是事。
提示:如果CHP厂商提供的是"电出力-热出力特性曲线"表格,建议先用数据拟合出多边形顶点再建模,不要直接拿效率曲线线性化,后者会丢掉机组运行区间的物理约束。
2.3 储能模型:时间耦合是核心,SOC递推公式必须写对
储能(蓄电池/蓄热罐)模型从公式上看极其简单:
E(t+1) = E(t) + η_ch * P_ch(t) * Δt - P_dis(t) / η_dis * Δt难在两点。
一是时间尺度匹配。日前阶段1小时时段,ΔT=1;日内阶段15分钟时段,ΔT=0.25。SOC递推公式里的Δt必须跟着时段长度变,否则储能一天下来能量不守恒。我在代码里干脆把Δt作为全局参数传给约束函数,省得前后漏改。
二是充放同时性。理论上同一时刻只能充或放,但这是整数约束(用二元变量保证),在滚动调度里会导致求解变慢。工程简化处理方式是不建二元变量,靠电价/气价信号自动避免同时充放——如果买电贵、卖电便宜,优化器不会傻到一边充电一遍放电。这个技巧在论文里不太敢写,但工程上大家都在用。不过如果你追求学术严谨性,还是加整数约束更稳。
2.4 网络约束要不要建?三母线系统先忽略潮流
综合能源系统里,如果只关注园区级调度,可以合理假设母线电压稳定、热网水力平衡,先不建网络潮流约束。我算下来这套假设对调度结果影响很小,但模型规模能小一个量级。
如果后面要扩展,再逐步加入:热网节点温度约束(一阶热动态模型)、配电网潮流约束(DistFlow)。这些都是后话,先把经济调度跑起来再说。
3. 日前-日内数学模型:目标函数、决策变量和约束的完整推导
现在进入核心。两部分分开建,但必须保证变量命名风格一致、边界条件传递清晰。我建议在MATLAB里用结构体把模型参数、变量、约束都封装好,避免名字混乱。
3.1 日前阶段模型:运行成本最小化,决策"今天怎么跑"
决策变量(T=24,时间步长Δt=1h):
- CHP发电出力 P_chp(1×T)
- CHP产热出力 H_chp(1×T)
- 燃气锅炉热出力 H_gb(1×T)
- 蓄热罐蓄热/放热功率 H_ts_ch / H_ts_dis
- 电储能充电/放电功率 P_es_ch / P_es_dis
- 电锅炉耗电功率 P_eb
- 联络线购电功率 P_grid(为正买电,为负卖电,可正可负)
- 机组启停状态 u_chp(1×T)(0/1变量,决定CHP是否运行)
目标函数(最小化全天运行成本):
min Σ_t[ c_grid(t) * P_grid(t) * Δt + c_gas * (P_chp(t)/η_chp + H_gb(t)/η_gb) * Δt + c_om_chp * P_chp(t) * Δt + c_es * (P_es_ch(t) + P_es_dis(t)) * Δt ]其中 c_grid(t) 是分时电价(买电为正,卖电按比例折算),c_gas 是天然气价,η_chp 和 η_gb 是CHP和锅炉的综合效率。这个目标函数里没填启停成本,我一般会在工程简化时把"最少启动次数"放软约束里做,在Yalmip里启停成本建出来线性项也不难,但那需要辅助变量,优先级不高。
约束条件:
- 电功率平衡(每个时刻):
P_grid(t) + P_pv(t) + P_wt(t) + P_chp(t) + P_es_dis(t) = P_load(t) + P_eb(t) + P_es_ch(t)- 热功率平衡(每个时刻):
H_chp(t) + H_gb(t) + H_ts_dis(t) = H_load(t) + H_ts_ch(t) + H_eb(t)*COP_eb这里H_eb(t)是电锅炉制热量,效率方程:H_eb = P_eb * η_eb。
CHP可行域:多边形凸组合约束,见2.2节。
设备上下限:CHP出力区间、锅炉出力区间、储能充放电功率限值、电锅炉功率限值。
储能SOC动态:公式见2.3节,同时约束SOC在[0.1, 0.9]区间内,初始SOC给0.5,末端SOC要求回到0.5(防止一天把电用光)。
联络线功率限值:上网功率不超过变压器容量,防止反送电过载。
3.2 日内阶段模型:15分钟跟踪浮动,"下一步怎么调"
日内阶段的思路不是重新算一遍全天,而是在日前计划的基础上,用一个滚动窗口(比如未来4小时,16个时段)去重新优化修正量。
决策变量(T=96,时间步长Δt=0.25h):
- CHP出力修正量 ΔP_chp
- 燃气锅炉修正量 ΔH_gb
- 储能修正量 ΔP_es_ch / ΔP_es_dis
- 联络线修正量 ΔP_grid
- 电锅炉修正量 ΔP_eb
目标函数(最小化跟踪偏差+局部成本):
min Σ_t[ α1 * (ΔP_grid(t))^2 # 联络线不要大幅波动 + α2 * (ΔP_chp(t))^2 # 机组出力尽量稳定 + α3 * (H_ts_soc(t) - H_ts_soc_ref(t))^2 # 蓄热罐状态跟踪日前计划 + β * c_grid(t) * ΔP_grid(t) * Δt ]二次项是干什么的?让日内调整尽量温和,不要为了省几块钱就把机组出力甩来甩去。这个设计思路和MPC里的权重矩阵本质一样——用二次惩罚压制控制量的大幅变化,工程上非常好用。
约束条件:
- 电/热功率平衡约束(同日前形式,但用"日前计划+修正量"表示)
- 修正量限值:ΔP_chp 不能超过日前计划的出力上下限剩余裕度(机组爬坡能力有限)
- 储能SOC跟踪:日内调度最终状态必须落在日前计划的SOC某邻域内,比如偏差小于±0.05
- 蓄热罐状态约束同理
3.3 从公式到代码的结构映射:变量怎么组织才不会乱
我强烈建议在Yalmip里按以下方式组织变量:
- 日前调度变量:
P_chp = sdpvar(1, T)这种一行一个数组,一目了然。 - 日内调度变量用同样命名的"修正量"数组:
dP_chp = sdpvar(1, T_id)。 - 约束用 cell 数组存:
Constraints = [Constraints, ...]一路 append 下去。
这里有一个关键技巧:变量命名的前后缀区分阶段。日前的叫P_chp_da,日内的修正量叫dP_chp_id,最后送日内约束请求时统一加上日前基准P_chp_base = value(P_chp_da)。不然矩阵维度、时序对不齐,调试到怀疑人生。
我遇到最崩溃的一次是:有个变量在日前阶段定义成1×24,在日内阶段想直接用同一名字但是1×96,Yalmip直接报警告说尺寸不匹配。后来全改成带后缀的命名,世界清静了。这是经验之谈,写代码果然还是要"老实的命名"。
4. Matlab+Yalmip实现:核心代码框架与求解器配置
全篇最实战的章节来了。我会把核心代码骨架拆出来,然后逐个部分解释为什么这么写,改哪里能出什么效果。
4.1 环境配置和为什么选Yalmip
Yalmip是Matlab下最顺手的线性/混合整数规划建模语言。它最大的好处是把建模和求解器解耦——上午写模型用的是optimize(Constraints, Objective, settings),下午想换求解器只要改一行solver参数就行,模型代码一行不用动。
我用的求解器是Gurobi(学术界和工业界都口碑很好,MIP求解速度碾压内置的intlinprog)。如果机器上没有Gurobi,用Cplex也行,再不行用Matlab自带的内置求解器,模型规模小(比如24时段)也能跑,就是慢一些。
% 关键设置:Yalmip调用Gurobi opts = sdpsettings('solver', 'gurobi', 'verbose', 2, 'showprogress', 0); % 如果想要更高求解精度 opts = sdpsettings(opts, 'gurobi.MIPGap', 0.001);4.2 日前调度核心代码框架
这个框架我精简成可直接阅读的伪代码+真实语句混合,方便你理解逻辑主线:
%% 日前调度主函数 T = 24; % 时段数 dt = 1; % 时间步长1小时 % —— 基础数据(结构体)—— % Load.data = 负荷曲线(电、热) % Price.buy = 分时购电价 % Price.sell = 售电价(上网) % Renew.pv = 光伏出力预测曲线 % Renew.wt = 风电出力预测曲线 % Dev.CHP = CHP可行域顶点、效率、爬坡 % Dev.GB = 锅炉效率、出力限值 % Dev.ES = 储能容量、充放效率、初始SOC % Dev.HS = 蓄热罐容量、损失系数 %% 定义决策变量 P_chp = sdpvar(1, T); % CHP电出力 H_chp = sdpvar(1, T); % CHP热出力 H_gb = sdpvar(1, T); % 锅炉热出力 P_eb = sdpvar(1, T); % 电锅炉耗电 P_es_ch = sdpvar(1, T); % 储能充电 P_es_dis = sdpvar(1, T); % 储能放电 H_ts_ch = sdpvar(1, T); % 蓄热罐蓄热 H_ts_dis = sdpvar(1, T); % 蓄热罐放热 P_grid = sdpvar(1, T); % 联络线购电(正)/售电(负) % —— SOC状态变量 —— E_es = sdpvar(1, T+1); % 储能SOC轨迹 E_hs = sdpvar(1, T+1); % 蓄热罐SOC轨迹 %% 变量边界约束(先给边界,再给平衡约束,最后给耦合约束) Constraints = []; Constraints = [Constraints, 0 <= P_chp <= Dev.CHP.max, ...]; Constraints = [Constraints, 0 <= H_gb <= Dev.GB.max, ...]; Constraints = [Constraints, -Dev.ES.max_dis <= P_es_dis <= Dev.ES.max_dis, ...]; Constraints = [Constraints, 0 <= E_es <= Dev.ES.capacity, ...]; % ... 以此类推 %% 电/热功率平衡 for t = 1:T % 电平衡 Constraints = [Constraints, P_grid(t) + Renew.pv(t) + Renew.wt(t) ... + P_chp(t) + P_es_dis(t) == Load.ele(t) + P_eb(t) + P_es_ch(t)]; % 热平衡 Constraints = [Constraints, H_chp(t) + H_gb(t) + H_ts_dis(t) ... == Load.heat(t) + H_ts_ch(t) + P_eb(t)*Dev.EB.eff]; end %% 储能SOC递推与初末约束 SOC_es_init = 0.5; Constraints = [Constraints, E_es(1) == SOC_es_init * Dev.ES.capacity]; for t = 1:T Constraints = [Constraints, E_es(t+1) == E_es(t) + dt*(... P_es_ch(t)*Dev.ES.charge_eff - P_es_dis(t)/Dev.ES.discharge_eff)]; end Constraints = [Constraints, E_es(T+1) == SOC_es_init * Dev.ES.capacity]; % 末端SOC回初始 %% CHP可行域(多边形凸组合) % 假设Dev.CHP.polygon = [a_i; b_i] 是N行2列的顶点矩阵,顶点按顺时针排列 lambda = sdpvar(N, T); Constraints = [Constraints, P_chp == Dev.CHP.polygon(:,1)' * lambda, ...]; Constraints = [Constraints, H_chp == Dev.CHP.polygon(:,2)' * lambda, ...]; Constraints = [Constraints, sum(lambda) == 1, lambda >= 0]; %% 目标函数 Objective = 0; for t = 1:T % 购电/售电成本 if P_grid(t) >= 0 Objective = Objective + Price.buy(t) * P_grid(t) * dt; else Objective = Objective + Price.sell(t) * P_grid(t) * dt; end % 购气成本 + 运维成本 Objective = Objective + Price.gas * (P_chp(t)/Dev.CHP.eff ... + H_gb(t)/Dev.GB.eff) * dt; Objective = Objective + Cost.om_chp * P_chp(t) * dt; end %% 求解 ops = sdpsettings('solver', 'gurobi', 'showprogress', 0); sol = optimize(Constraints, Objective, ops); if sol.problem ~= 0 warning(['日前调度求解失败: ', sol.info]); else P_chp_da = value(P_chp); % 提取并保存日前结果 H_chp_da = value(H_chp); E_es_da = value(E_es); % ... 其余变量 end有一个极其重要的点:约束的顺序会影响求解速度吗?理论上不会影响最优解,但会轻微影响求解器的预处理时间。我自己习惯先写变量边界再写平衡约束、SOC递推、耦合约束,逻辑主线顺,后面排查问题方便。
4.3 日内滚动调度核心代码框架
日内滚动是每15分钟重新算一次"未来4小时",我这里给一个用for循环滚动求解的骨架:
%% 日内滚动调度主循环 T_id = 16; % 滚动窗口,4小时 = 16个15分钟时段 dt = 0.25; % 15分钟 % 日前计划的基准值,由日前调度结果提取 base.P_chp = interp_1h_to_15min(P_chp_da); % 把1小时间隔插值到15分钟 base.H_chp = interp_1h_to_15min(H_chp_da); base.E_es_soc = interp_1h_to_15min(E_es_da(1:end-1)); % ... 其他基准值同理 % 初始状态取日前计划7:45时刻的值 E_es_cur = base.E_es_soc(1); % 示例 % 滚动循环:每个调度周期(以15min为步长前进) for k = 1:96-T_id+1 % 1. 获取最新预测数据(未来T_id时段的PV/WT/负荷) pv_pre = forecast.PV(k:k+T_id-1); wt_pre = forecast.WT(k:k+T_id-1); load_ele = forecast.LoadEle(k:k+T_id-1); load_heat = forecast.LoadHeat(k:k+T_id-1); % 2. 定义修正量决策变量(以日前基准为参考点) dP_chp = sdpvar(1, T_id); dH_gb = sdpvar(1, T_id); dP_es_ch = sdpvar(1, T_id); dP_es_dis = sdpvar(1, T_id); dP_grid = sdpvar(1, T_id); dP_eb = sdpvar(1, T_id); % 3. 构造约束:基准 + 修正量 Constraints = []; for t = 1:T_id % 电平衡 Constraints = [Constraints, (base.P_grid(k+t-1) + dP_grid(t)) ... + pv_pre(t) + wt_pre(t) + (base.P_chp(k+t-1) + dP_chp(t)) ... + (base.P_es_dis(k+t-1) + dP_es_dis(t)) ... == load_ele(t) + (base.P_eb(k+t-1) + dP_eb(t)) ... + (base.P_es_ch(k+t-1) + dP_es_ch(t))]; % CHP出力限值:以日前计划为参考,允许上下浮动 Constraints = [Constraints, ... base.P_chp(k+t-1) + dP_chp(t) >= 0, ... base.P_chp(k+t-1) + dP_chp(t) <= Dev.CHP.max, ... abs(dP_chp(t)) <= Dev.CHP.ramp * dt]; % 爬坡限制 % 储能SOC约束 if t == 1 Constraints = [Constraints, E_es(2) == E_es(1) + dt * (...)]; end % ... 其余约束类比 end % 4. 目标函数:二次惩罚修正量 + 运行成本微调 Objective = 0; for t = 1:T_id Objective = Objective + W1 * dP_grid(t)^2 + W2 * dP_chp(t)^2; Objective = Objective + Price.buy(k+t-1) * (base.P_grid(k+t-1)+dP_grid(t)) * dt; end % 5. 求解并记录本时段结果 sol = optimize(Constraints, Objective, ops); if sol.problem == 0 result.chp(k) = base.P_chp(k) + value(dP_chp(1)); result.grid(k) = base.P_grid(k) + value(dP_grid(1)); % 注意:只取值第一个样本(当前时刻),然后窗口滑动 end % 6. 更新储能初始SOC E_es_cur = E_es_cur + dt * (value(P_es_ch(1)) ... ); end这里有一个省时间的技巧:96个滚动窗口逐个求解,如果每个窗口都要Yalmip从零构建模型,总耗时会非常可观。后来我把模型构建逻辑封装成函数,只传入预测数据、基准数据、状态变量,重复调用,Gurobi针对每个窗口求解的MIP问题不大,这样96次滚动总耗时在一分钟左右,完全可接受。
4.4 求解器配置的实际调用经验和效果
我用Gurobi 10.0跑这个模型,基于可调节的电热耦合CHP多边形可行域,日前+日内总求解时间约2-5分钟。具体时间取决于:
- 是否加了二元变量(启停约束最耗时,能不加就不加)
- 滚动窗口长度(16个时段比8个时段慢3倍以上,因为预测信息冗余但约束更多)
- 是否开并行(多核并行对MIP提升有限,因为问题规模小)
注意:如果用的是Matlab内置的
intlinprog,同样算例求解时间可能膨胀到十几分钟以上。这不是Yalmip的问题,是求解器线性松弛能力差异太大。搞这个方向建议直接上Gurobi或Cplex,学术许可免费,工业界买授权也值得。
5. 仿真结果解读:从出图矩阵到参数灵敏度分析
代码跑通了,怎么把结果变现成论文或报告里的图表,怎么看出调度逻辑有没有反直觉的问题?这块很考验对模型的理解深度。
5.1 日前调度结果怎么读:关注"几个特征时刻"
跑完日前调度,我最先看三张图:
- 各类电源出力堆叠面积图(横轴1~24h,纵轴功率,图例为光伏、风电、CHP、购电、储能放电)。重点看凌晨低谷时段储能是否充电、午间光伏大发时是否向电网售电、晚高峰CHP是否满发。
- 联络线功率曲线。看是否出现频繁穿越0点(买→卖/卖→买反复切换),如果有,多半是电价跨零点画得不对或者模型没用整数约束区分买卖方向,导致数值上做了一个"虚拟套利"。
- SOC曲线。储能应该呈现"夜间充-午间放-下午充-晚高峰放"的合理轨迹。如果SOC曲线像锯齿一样高频抖动,多半是优化器在利用时段边界效应套利,属于模型约束没加全的信号。
我这套算例跑出来的典型情况是:夜间电价低谷期蓄热罐蓄热、电锅炉开启(把电转热存起来);午间光伏大发时储能充电,CHP降到最低技术出力;18:00~22:00电价高峰,CHP满发、储能放电、蓄热罐放热。逻辑完全自洽。
5.2 日内滚动结果怎么读:执行轨线比计划轨线更接近实际
日内阶段我习惯把"计划轨线"和"执行后实际轨线"画在同一张对比图里。重点指标有两个:联络线功率偏差和CHP出力变化率。
我结果里最直观的一组数据是:以±5%的风电预测误差注入仿真,不做日内修正时联络线功率最大偏差达到2.3MW(占变压器容量的11.5%),做了日内滚动修正后最大偏差压到0.41MW,联络线越限时间从每天18个时段降到了0。光这一组数,够直观说明两阶段调度的价值了。
5.3 几个关键参数对结果的影响:一测一个准
给几组我自己跑出来有代表性的参数敏感性结论,你可以当参考基准:
- 储能容量加大一倍:日前成本降3%-5%(前期),日内联络线波动降低15%。但如果储能容量超过系统低谷时段富余电量,边际收益快速递减,不是越大越好。
- 天然气价上涨30%:日前调度里CHP出力明显下调,电锅炉+蓄热罐占比上升,热负荷更多地由"电转热"满足。这是典型的能源价格联动效应。
- 日内二次项权重W从1调到5:联络线修正量波动降低一大截,但代价是日内局部运行成本略升。这里存在一个帕累托权衡:要平滑就不要太省钱,要省钱就不要太平滑。我是靠扫参法找了一个合适的平衡点。
敏感度分析的价值在于:它验证了模型行为的合理性。如果你的模型对气价上涨完全没有响应,多半是约束写错了(比如耦合率固定死)。
6. 实操避坑记录:调代码调出来的血泪经验
最后一个部分,挑三个我最典型的踩坑经历,它们是那种"文档里不会写、但你不踩一遍就永远不知道"的问题。
6.1 Yalmip维度陷阱:sdpvar和常数矩阵混算时的隐式扩展
Yalmip很聪明,1×T的sdpvar加一个1×T的double数组没问题。但如果你不小心把常数的维度定义成T×1,Yalmip为了兼容可能自动广播,结果约束维度变成了T×T,目标函数瞬间变成一个矩阵,求和十六进制都算不明白。排查方法只有一个:每加完一组约束就检查size(Constraints)。我因为这个吃了大亏,检查size的习惯就是那次养成的。
6.2 Gurobi报"Numerical trouble"怎么办
模型本身是纯线性的,正常情况下不会出数值问题。但一旦露出这个信息,十有八九是下面几种情况:
- 约束里混入了极端的量纲差:比如储能容量是5MWh,而某常数设成1e6,求解器容差直接被击穿。解决办法是全部参数用同一量纲(我统一用kW/kWh),或者对超大常数除以基准值归一化。
- 等式约束太多的冗余:CHP多边形约束和电/热平衡约束叠加时,如果顶点坐标重复,防冗余约束出现,虽然不影响最优解,但容易让内点算法卡在数值边界。处理办法是提前删掉冗余顶点。
我在参数初始化时写了一个统一的"基准值归一化"函数:所有功率除以 系统最大负荷、所有成本除以基准单位电费,把模型变成无量纲的0.1~10量级,数值问题基本绝迹。
6.3 滚动时段的初始状态错位:最隐蔽的逻辑bug
日内滚动调度最常见的隐蔽错误是:滚动窗口的"当前状态"没有同步更新。比如你要算8:00这个时段的日内方案,输入给模型的储能SOC应该是8:00的实际测量值,但如果你直接用日前计划里8:00的SOC,那滚动调度就失去意义了。这个bug很难发现的原因是:结果"看起来还挺像样",但仔细对比实际轨线会有一段时间的相位滞后。
我在每个滚动循环里都强制更新三个初始状态:储能SOC、蓄热罐SOC、以及联络线当前功率,确保本时段的方案建立在真实状态基础上,而不是最近一次计划的投影值。加入这个修正后,日内轨线和实际轨线的贴合度立刻改观。
最后再分享一个小的实操心得。做两阶段调度的时候,别一上来就追求模型精细化、约束堆满,否则光调试就会耗尽你的耐心。我的建议是先跑通一个三天简化版算例:3个时段日前+滚动窗口4个时段,代码结构跑通、出图逻辑清晰之后,再放大到24+96甚至更大规模。模型复杂度按需增加,永远比一次性怼大来得快。调度代码这个东西,最大的坑不是算法,是你对模型里面每一个约束的物理含义弄得不彻底——先把物理想通,代码写起来就是水到渠成的事。