最近在审一个电-热综合能源系统的调度课题,顺手把自己的Matlab实现思路和踩坑记录整理出来。这个方向现在很热,但不少初学者卡在“模型建好了不知道怎么落地”这一步,尤其是两阶段、多时间尺度、需求响应这几个词叠在一起,光读文献就够晕的。这篇博文就从实际代码编写的角度,把这个课题完整拆开讲清楚。
先说清楚这是个什么东西、解决什么问题。电-热综合能源系统指的是电网和热网耦合在一起运行的系统,典型场景是某个区域的供电和供热由一个统一调度中心管理,设备包括热电联产机组(CHP)、燃气锅炉、电锅炉、储热罐等。所谓“考虑需求响应”,就是让用户侧的负荷不再是完全刚性,而是会根据电价信号或激励措施作出调整。两阶段是指“日前调度+日内滚动修正”两个决策层次,多时间尺度则是指日前以小时为粒度、日内以15分钟为粒度进行递进优化。整套逻辑下来,核心目的就是在可再生能源出力不确定、负荷波动大的情况下,让系统运行最经济、最安全。这个课题很适合电气工程、工程热物理、自动化等方向的研究生作为毕业设计或小论文选题,也适合已经工作但想转综合能源方向的工程师作为入门项目。
## 1. 整体设计思路:为什么这个课题要拆成两阶段来做 ### 1.1 单阶段调度的痛点在哪里 先想一个问题:如果只做一次优化,一次性算出未来24小时每个设备每小时出力多少,会发生什么? 答案是预测误差会直接毁掉调度方案。光伏和风电出力是波动的,用户负荷也不是完全按照预测曲线走的。如果日前一次性拍板,次日实际运行时发现光伏少发了100kW,这部分缺口就得靠机组硬扛,要么造成弃风弃光,要么造成成本飙升,极端情况下还会触发切负荷。 有人说那我把安全裕度留大一点不就行了?可以,但代价是经济性变差。留的裕度越大,就意味着燃气锅炉得提前开机、储热罐得蓄更多的热、CHP得压着更低的出力跑,一天下来多烧不少燃料。 所以行业内的主流做法就是分级决策,也就是两阶段调度。第一阶段在日前做一次全局经济优化,第二阶段在日内根据最新的预测数据做滚动修正。这个思路跟导航软件很像:出发前先规划一条全局最优路线,行驶途中发现前面堵车,再在局部调整。全局规划和实时纠偏结合,既保证了全局经济性,又兼顾了实时可靠性。 ### 1.2 两阶段之间的时间尺度怎么划分 时间尺度的选择不是拍脑袋定的,要跟预测精度和调节能力的特性匹配。 日前调度的粒度是1小时,决策变量覆盖24小时,共24个时段。原因是日前阶段的数据来源是数值天气预报和负荷预测,这种预测在小时代粒度上的精度已经比较可靠了,再细分到15分钟反而浪费算力。 日内滚动的粒度是15分钟,决策窗口一般是未来4小时,即16个时段,每15分钟滚动一次。这个粒度匹配的是超短期预测,通常未来4小时以内的预测精度明显优于日前预测。同时,15分钟也是目前很多电力现货市场的出清间隔,直接对得上市场节奏。 这里有一个很容易被忽略的细节:日前调度生成的机组启停状态,在日内阶段是锁定的,不参与二次决策。只有机组出力、储热罐充放功率这类连续变量可以调整。如果日内连启停都一起去优化,整个模型就从线性混合整数问题变成更难解的问题,而且频繁启停设备也受不了。 ### 1.3 需求响应在整个框架里的位置 需求响应不是额外的一个独立模块,而是嵌在约束条件和目标函数里面的。它的作用是给调度提供“柔性”:当电价高或者系统供能紧张时,用户侧的可平移负荷、可削减负荷、蓄热式电采暖等资源会被激活,降低峰值负荷需求。 从调度角度看,需求响应等于改变了净负荷曲线,让系统面对的不确定性变小。从经济角度看,用户调整用电行为能降低系统峰值阶段的边际成本,所以目标函数里面要体现对用户参与响应的激励成本。 两阶段框架下需求响应也要分层:日前阶段基于预测电价,与用户签订响应合同,确定可削减负荷量和补偿价格;日内阶段根据实际运行状态,决定是否触发这些响应资源。 ## 2. 数学模型拆解:设备建模和约束条件的工程化处理 ### 2.1 关键设备模型 整个系统里最核心的设备是热电联产机组。CHP的建模难点在于热电耦合关系。工程上最常用的处理方式是热电机组的运行区间用一个凸多边形来描述,也就是“可行域法”。这个多边形由若干个顶点确定,机组的电出力和热出力必须落在多边形内部。这种方式比简单的“热电比固定”更准确,也比完整的非线性模型更容易求解。 用Matlab表达就是一组线性不等式,形式如下: ```matlab % CHP可行域约束,A_chp * [P_chp; H_chp] <= b_chp A_chp = [1, 0; -1, 0; 0, 1; 0, -1; 1, -alpha_extract]; % 示例 b_chp = [P_max; -P_min; H_max; -H_min; ...];实际建模时要注意机组还有最小运行时间约束,也就是开起来之后不能马上关,关掉之后也不能马上开。这个约束在日前调度里是要显式写进模型的,负责保证调度方案的工程可行性。
燃气锅炉的模型相对简单,就是输入天然气、输出热功率,存在一个效率系数,出力的上下限就完了。电锅炉的模型类似,只是输入变成了电,输出还是热,这给系统提供了“电转热”的灵活性。
储热罐的建模要特别注意,它的状态变量是每个时段的蓄热量,连续性约束描述了蓄热量的递推关系,同时充放热功率不能超过上限,且不能同时充和放。有些同学忽略了一个细节:储热罐一天的起止蓄热量要相等,否则相当于系统在“无中生有”地造热,长期运行是不可持续的。
2.2 需求响应建模的两种主流方式
需求响应建模最常用的是价格弹性方式。价格弹性系数描述的是负荷变化率与电价变化率之比,表达式为:
[ \frac{\Delta L_i}{L_i^0} = \varepsilon_{ii} \frac{\Delta p_i}{p_i^0} ]
这个公式描述的是自弹性,即当前时段价格变化对当前时段负荷的影响。实际系统里还存在交叉弹性,即当前时段电价变化会影响相邻时段负荷,体现的是用户把负荷从高电价时段挪到低电价时段的转移行为。
在Matlab实现里,负荷转移约束可以简化成如下形式:每个时段可平移负荷的比例上限假设为20%,平移前后总用电量不变。这就是一个等式约束加几个不等式约束,非常容易嵌入优化模型。
第二种常用方式是激励型需求响应,也叫可中断负荷。调度中心在日前与用户签订合同,约定在特定条件下可以削减一定比例的负荷,并支付补偿金。补偿金在目标函数里体现为一个线性项,即削减量与单位补偿价格的乘积。
这两种方式的工程取舍是:价格型DR适合作为长期信号引导用户自然调整行为,激励型DR适合在系统供能紧张时直接快速削减负荷。实际系统里两者是并用的。
2.3 目标函数构建
目标函数是两阶段模型的核心。日前阶段的目标函数是全天的总运行成本最小化,包括燃料成本、购电成本、需求响应补偿成本,以及弃风弃光惩罚成本。燃料成本通常简化为机组出力的二次函数,但为了用线性规划求解,业内普遍做法是分段线性化。
这里分享一个实用的Matlab分段线性化写法:
% 二次成本函数分段线性化示例 % breakpoints: 出力分段点,cost: 对应成本 n_seg = 3; % 生成分段线性化系数后叠加到目标函数 for k = 1:n_seg cost = cost + coef(k) * P_seg(k); end日内滚动阶段的目标函数是在日前计划基础上“修正最小化”。这意味着不是完全推翻日前计划重来,而是追踪日前计划、尽量少偏离,因为设备调节是有代价的,操作太频繁会缩短寿命。目标里加一个惩罚项,表示实际出力与日前计划出力的偏差,偏差越大惩罚越高。
3. 两阶段调度框架的运行机制和流程实现
3.1 日前调度阶段的完整流程
日前阶段在每天0点前运行,输入是明天的负荷预测曲线、光伏出力预测、风电出力预测、电价预测。优化变量是24小时内所有机组的启停状态、各时段出力、储热罐充放策略、需求响应调用方案。
具体操作步骤是先把数据整理成向量形式,然后定义决策变量矩阵,规模大概是:设备类型数量乘以24,再加上启停二元变量。用Yalmip建模的话,核心代码框架如下:
% 定义决策变量 P_chp = sdpvar(24, 1); % CHP电出力 H_chp = sdpvar(24, 1); % CHP热出力 P_gb = sdpvar(24, 1); % 燃气锅炉热出力 u_chp = binvar(24, 1); % CHP启停状态 SOC = sdpvar(25, 1); % 储热罐蓄热量 % 目标函数 objective = sum(C_fuel) + sum(C_purchase) + sum(C_dr) + sum(C_curtail); % 约束条件 Constraints = []; Constraints = [Constraints, A_chp * [P_chp'; H_chp'] <= b_chp]; Constraints = [Constraints, SOC(2:25) == SOC(1:24) + eta_ch * P_ch_charge - P_ch_disch/eta_dis]; % 遍历方式添加各时段约束 for t = 1:24 Constraints = [Constraints, P_chp(t) >= P_min * u_chp(t)]; Constraints = [Constraints, P_chp(t) <= P_max * u_chp(t)]; end % 求解 optimize(Constraints, objective, sdpsettings('solver', 'gurobi'));求解完成后,把CHP启停状态和储热罐各时段蓄热量传给下一阶段,这就是两阶段之间的“接口变量”。
3.2 日内滚动阶段的动态修正机制
日内阶段是每15分钟滚动一次的。假设当前时刻是10:00,系统采集到最新的超短期预测数据,以未来4小时为窗口做一次优化,但只执行下一个15分钟的计划。到了10:15,重新采集数据,窗口向前推进,再做一次优化。
这种“滚动优化”的方式,本质上是用最新信息不断修正偏差。举个直观的例子:日前预测中午12点光伏出力是200kW,日内10点更新的预测是180kW,那日内优化就会在约束里补上这20kW的缺口,调整燃气锅炉出力或者让储热罐多放些热。
日内阶段约束集中体现在爬坡约束上。火电机组的出力不能突变,每个15分钟内电出力变化不能超过某个上限。这个约束在日前阶段可以不那么严格,但日内阶段必须显式建模,因为这是实际物理设备的硬限制。
3.3 热网延迟要不要考虑
这是很多人会问的问题。热网是有传输延迟和热损失的,热水从热源流到用户端需要时间,沿路管道也会散热。理论上精确建模需要引入偏微分方程描述热传播过程,但把这个放进优化模型里求解会非常耗时。
工程上的折中方案是:如果研究对象是区域规模不大的系统,热网延迟在15分钟到30分钟量级,可以在日内阶段用一个固定延迟时间常数近似,将热负荷曲线整体平移若干时段再参与平衡计算。如果研究的是跨城区的大规模热网,那就得单独建热网水力模型,和电力调度模型做松耦合迭代求解。对于大多数毕业设计级别的课题,固定延迟逼近已经够用了。
4. Matlab代码实现的关键细节和经验
4.1 工程化代码结构组织
写调度优化代码最忌讳一个脚本从头写到尾。建议按模块拆分成多个文件,每个文件职责单一,方便调试和复用。
我的推荐结构是:
project/ ├── main_optimize.m % 主入口,调用日前和日内 ├── data/ │ ├── load_profile.mat % 负荷数据 │ ├── price_profile.mat % 电价数据 │ └── pv_wind_profile.mat% 新能源预测数据 ├── utils/ │ ├── build_chp_model.m % CHP建模 │ ├── build_storage.m % 储热建模 │ └── build_dr_model.m % 需求响应建模 ├── solver/ │ └── solve_day_ahead.m % 日前求解 │ └── solve_intraday.m % 日内滚动求解 └── plot/ └── plot_results.m % 结果可视化这样组织之后,改动一个设备模型只影响对应文件,不用在几百行的脚本里来回翻找。实测下来,从单体脚本改成模块化结构之后,调试效率至少提升一半。
4.2 Yalmip建模的实用技巧
Yalmip是Matlab里最常用的优化建模工具,底层求解器可以用Gurobi、CPLEX或开源求解器。几个使用经验:
第一,变量定义用sdpvar还是binvar要想清楚。整数变量会显著增加求解时间,所以能避免就避免。比如CHP的启停变量是必须的,但储热罐的充放状态可以通过SOC变化方向判断,没必要非得引入二元变量来禁止“同时充放”。
但这个需要小心:如果不加充放互斥约束,某些优化场景下求解器确实会给一个同时充放的结果。处理方式是引入一个小γ变量或者干脆容忍这种不物理的情况,在结果后处理时直接归零充放功率较小的那个值。对于毕设来说这个误差可接受,论文里说明一下即可。
第二,大M法的M值选取要谨慎。比如机组启停的最小出力约束写成 P_chp >= P_min * u,这个写法是精确的,不需要大M。但有些约束比如“储热罐充放互斥”写成 charge * discharge = 0,这是非线性的,得改成:
Constraints = [Constraints, P_ch_charge <= M * u_st]; Constraints = [Constraints, P_ch_disch <= M * (1 - u_st)];这里的M要不小于最大充放功率,太大虽然可行但会造成数值问题。经验值是取最大功率的1.1倍。
第三,求解参数设置非常影响求解效率。用Gurobi时,三个参数值得关注:输出显示频率、最优间隙容忍度、求解时间上限。
options = sdpsettings('solver', 'gurobi', ... 'verbose', 1, ... 'gurobi.MIPGap', 0.01, ... % 1%最优间隙,够用就行 'gurobi.TimeLimit', 300);最优间隙设到1%而不是0.01%,求解时间可能从十几分钟降到一两分钟,而调度方案的成本差异可能不到0.5%。做工程优化要学会在求解精度和效率之间找平衡。
4.3 数据生成和随机场景处理
做研究不能只用一组数据跑通就下结论,那没有说服力。严谨的做法是做多个场景的对比实验。比如生成100个光伏出力随机场景,然后取期望值做优化,或者做场景缩减后取代表性场景。
Matlab自带的随机数生成可以直接用,关键是要给不同场景设置不同的随机种子,保证实验可复现:
rng(42); % 固定种子,确保可复现 pv_scenarios = pv_forecast + randn(24, 100) * sigma_pv;另外时序相关性不能忽略。如果每个时段独立加随机噪声,生成的场景会非常毛糙,跟实际光伏出力的连续变化特征不符。实际工程中常使用一阶马尔可夫链或者基于历史数据直接采样。
思路是把历史数据按典型气象日分类,每一类的光伏归一化曲线本身就是一个场景,优化时对这些场景做概率加权。这个方法的优点是生成的光伏曲线天然满足时序相关性,缺点是需要足够的历史数据。
4.4 结果可视化与敏感性分析
调度结果的可视化是验收成果的关键环节。至少需要画四类图:系统电功率平衡图、热功率平衡图、储热罐SOC曲线、需求响应前后负荷曲线对比图。
电功率平衡图要展示每个时段各类电源的出力堆叠柱状图,包括CHP电出力、购电、光伏、风电,最上面叠加负荷曲线做对比。一眼看过去就知道平衡关系是否满足。
热功率平衡同理,显示CHP热出力、燃气锅炉供热量、储热罐放热量、电锅炉供热量,与热负荷对比。
对比图的两条曲线要有信息量:原始负荷曲线和DR后负荷曲线。如果DR建模合理,会看到峰值时段负荷明显被压低,谷值时段负荷有所抬升,曲线变得更平坦。
敏感性分析建议做三个维度:需求响应比例从0%到30%扫参数、储能容量从0到最大值的2倍扫参数、新能源渗透率从10%到50%扫参数。每个参数变化都重新跑一遍仿真,记录总成本和弃风弃光率,画成二维曲线图。这类图表放在论文里会让审稿人觉得工作充实、分析完整。
5. 常见问题与调试经验实录
5.1 求解无可行解怎么办
这是建模阶段最常见的问题,新模型第一版运行时大概率会碰到。排查顺序很重要。
第一步,检查约束是否自相矛盾。典型场景是CHP出力上限和热负荷需求不匹配。如果CHP最小热出力都比当前时段热负荷大,那这个时段就无解了。排查方法是把各时段热负荷最大值和CHP最小热出力放到一张图上看。
第二步,检查储热罐SOC边界条件。要求一天结束蓄热量等于起始蓄热量,如果起始值设得太大,末端约束就永远无法满足。把SOC初值改成50%额定容量,基本能解决大部分问题。
第三步,检查需求响应约束的负荷平衡等式。可平移负荷的“总量恒定”约束与各时段转移上下限约束联合起来,可能对小规模系统产生不可行。比如总负荷只有1000 kW,可平移比例20%,但每个时段要求平移后的负荷必须在80%~120%区间,某些极端时段可能找不到可行解。
最实用的调试手段就是把目标函数临时改成0,把所有约束里逐个注释掉再求解。如果注释掉某组约束后变得可解,问题就定位在那组约束上。
5.2 Yalmip求解结果量纲出错
这个问题隐蔽,但遇到了很让人抓狂。假设CHP电出力单位是kW,成本系数单位是元/kWh,两者乘起来应该是元。但如果读数据时把负荷读成了MW,约束写的时候又用了kW,目标函数里就会差出1000倍的量纲。
解决方法比较土但有效:建立一套单位制规范,统一所有变量的量纲。我习惯是功率一律用kW,能量用kWh,电价用元/kWh,气体热值用kWh/m³。数据读取时立即转换,加注释标好单位,在建模代码里顺手做一个scale sanity check,比如检查所有变量最大值是否在合理范围。如果发现CHP出力最大值是3000而机组容量只有100,那一定是单位写错了。
5.3 日内滚动程序跑太慢
很多人的日内滚动代码直接循环96次调用优化求解器,每次求解又要几十秒,总耗时半小时起步。优化手段有三个方向。
第一个方向是缩小求解窗口。虽然系统是15分钟刷新一次,但不需要每次都求解未来16个时段。如果热惯性允许,可以用预测控制里的“控制时域”概念,只在第一个时段做精细决策,后面时段用简单的启发式规则保持平衡。比如前15分钟用精确优化,之后时段用固定爬坡率跟踪日前计划。
第二个方向是热启动。Gurobi和CPLEX都支持热启动,把上一个时段的解作为新优化问题的初始解。临近时段的数据差异很小,热启动能让求解时间下降70%以上。代码层面是先把旧的x变量值赋给新的sdpvar变量,再用assign函数传入求解器。
第三个方向是简化日内模型。日内阶段不需要重复完整的机组启停决策,可以把二元变量全部固定为日前值,只求解连续变量。这样日内模型就退化成一个二次规划甚至线性规划,求解时间下降到秒级。
5.4 需求响应结果不合理
常见问题是优化结果里DR削减量总是等于上限值,或者跟系统供能紧张时段完全对不上。出现这种结果,要么是补偿价格参数设得不对,要么是DR约束和系统平衡约束耦合有误。
补偿价格应当跟系统边际供能成本做比较。如果补偿价格远低于高峰时段机组边际成本,优化器自然会尽量多削负荷,削到上限为止。这符合“经济最优”,但现实中用户不一定愿意配合,所以需要给DR设置一个用户参与意愿比例参数来约束总调用量。
反过来,如果补偿价格定得太高,优化器一个都不调用,DR资源就白建模了。参数对标的时候,可以把DR补偿价格设为高峰时段边际电价的60%~80%,这样系统会在极端高价时段触发响应,在普通时段按兵不动,符合实际工程逻辑。
5.5 热功率平衡总差一点
调试热平衡时最容易出现的误差是忘了储热罐的充放热效率损失。充进去1 kWh的热,实际能放出来的只有0.9 kWh,那0.1就是损失。如果储能约束写得不对,会造成累计误差,导致仿真到后半天出现热不足。
建议在代码里把储热罐约束写成分步形式,每步都做一个中间变量:
% 蓄热增量 = 充电功率 * 充电效率 - 放热功率 / 放热效率 delta_soc = eta_ch * P_ch - P_dis / eta_dis; SOC_next = SOC_now + delta_soc;这样调试的时候能直接查看 delta_soc 是否异常,排查速度快得多。
实际运行效果与进一步扩展
我在某模拟项目X里按这套框架实现了代码,系统包含一台CHP、一台燃气锅炉、一台电锅炉和一个储热罐,配置了20%的可平移负荷和10%的可削减负荷。日前调度求解时间约90秒,日内滚动单次求解约5秒,整体仿真24小时流程总耗时约3分钟。与不带需求响应的传统调度相比,运行成本下降了6.8%,风电消纳率从91%提升到97%。
这个框架后续能扩展的方向其实不少。一是加入碳交易机制,把碳排放配额和碳价纳入目标函数,做一个低碳经济调度;二是考虑电动汽车充电站负荷,把移动负荷的不确定性也放进日内滚动模型;三是把热网管网拓扑和网络约束建模进去,从单节点系统升级成真正的管网系统。
最后分享一个个人体会:做这类课题,思路清晰比代码熟练更重要。模型写不出来的时候,先在白纸上把设备连接图画出来,把能量流标注清楚,再把每个设备的约束方程列出来,最后才是写代码。建模花了足够时间,后面的求解和调试都会很顺。