1. 为什么做电热综合能源调度:从供热季的“东风”困局说起
这几年做综合能源系统优化的同行应该都深有体会——真正的痛点不在夏季,而在北方供热季。
风电在冬季夜间往往大发,尤其“三北”地区,风资源最好的时段恰好是热负荷需求最大的时段。但传统热电联产机组(CHP)按“以热定电”方式运行,为了保证供热,机组电出力被压得很低,留给风电的上网空间极为有限。结果就是后半夜风机被大面积限功率,弃风率一路飙升。某北方省份冬季弃风率历史上甚至超过20%,白白浪费的可再生能源相当惊人。
电热综合能源系统其实就是针对这个矛盾提出的解法:把电力系统和热力系统放在同一个优化框架里统筹调度,让热网侧的柔性资源——电锅炉、热泵、储热罐——参与风电消纳,而不是让CHP机组孤军奋战。我个人给刚接触这个方向的同学打个比方:热力系统本质上是一个“天然储能”,热水罐存几小时热量毫无压力,但电池存几小时电是要花大价钱的。你要是能把风电大发时段的多余电量转化成热量“存”起来,供热季消纳难题一大半就解决了。
我这次要分享的,就是这类系统里最基础也最核心的问题——日前经济调度模型——我用Matlab完整实现了一遍,还跑了算例验证。这篇文章不会只贴一堆公式然后拍拍屁股走人,我会把目标函数怎么搭、约束条件怎么拆、代码怎么组织、求解器怎么选、踩过哪些坑,一步步说清楚。适合正在做综合能源调度、微电网优化或者毕业论文做相关方向的同学,也适合想快速搭一套可复现MILP算例的工程师。
2. 模型设计的关键思路:为什么“日前”和“经济调度”要分开理解
2.1 “日前”这个时间尺度有什么用
调度问题按时间尺度分为日前、日内、实时三层,各管各的事。日前调度是在前一天(一般是上午或中午)对未来24小时做出决策,用的是预测数据(负荷预测、风电预测、光伏预测),时间分辨率通常取1小时。它的输出给出的是各机组每个时段的开停机计划、出力计划、储热罐充放热计划等。
有人可能会问:既然预测有误差,为什么还要做日前?直接实时调度不更准吗?
这里涉及电力系统运行的底层逻辑。火电机组、CHP机组不是按个按钮就能启动和带负荷的。机组的启停需要提前安排,爬坡能力也有限,而且热力系统的惯性比电力系统大得多——热网水温变化是个缓慢过程,坡道提前一天就得铺好。日前调度的价值就是把这些“需要提前决策”的事情定下来,给实时调度留出调节空间。系统里如果有人告诉你“不用做日前,实时全搞定”,那是没在真实系统干过活的表现。
2.2 经济调度模型到底在优化什么
经济调度,说白了就是在满足负荷需求和运行约束的前提下,让系统总运行成本最小。目标函数里的成本通常包含这几块:
- 火电/CHP机组的燃料成本(一般是出力的二次或线性函数)
- 机组启停成本(如果做机组组合)
- 弃风弃光惩罚成本
- 购电成本(如果系统与上级电网存在交互)
- 运维成本(部分研究会加,我这版先不含)
整个模型落在数学上是一个**混合整数线性规划(MILP)**问题。为什么是MILP?因为里面有0-1整数变量——机组启停、储热罐充放热状态,这些没法用连续变量表达;目标函数和约束如果做线性化处理,求解效率和全局收敛性都要好得多。Matlab里我用的求解路径是YALMIP建模,配Gurobi或CPLEX求解(如果装不上商业求解器,先用默认的linprog对付小规模算例也行,但大一点的问题会遇到性能瓶颈)。
2.3 CHP机组特性:模型的“硬骨头”
电热综合能源系统模型的难度核心在CHP机组的可行域建模。CHP机组的电出力和热出力不是独立的,供热越多、发电的调节范围就越窄。常见的热电联产可行域模型有两种,一种是用一组线性不等式描述电热出力耦合关系,工程上够用,是我这次采用的方式;另一种是更为精确的凸包模型,引入更多顶点做凸组合,精度更高,但建模复杂度明显上升。
注意:CHP可行域如果建模不对,结果会导向两种极端——要么电热耦合被弱化,消纳效果虚高;要么约束过紧,优化无解或成本被严重高估。这块是最容易出现“看起来对了、实际上错得离谱”的地方。
电锅炉和储热罐承担的是“解耦”角色。电锅炉把多余风电转成热直接进热网,储热罐则把热量暂时存起来,等到热负荷高峰再放出来。有了这两个资源,CHP机组就可以在风电大发时段降低供暖出力,让出电负荷空间给风电,等风电低谷期再“补热”或“放热”。整个系统灵活性的关键就在这个“时空平移”。
3. Matlab代码实现:从数据结构到求解器调用
3.1 代码整体框架:模块化才是正道
我看过很多刚入门的同学把代码写成一整坨——数据读了、约束写了、结果也画图了,全挤在一个脚本里。一旦换算例或者发现约束写错,改起来想死的心都有。
我的建议是拆分四个模块:数据输入模块、模型构建模块、求解执行模块、结果分析模块。四个模块通过Matlab的脚本或函数接口串联,主程序就是几行核心调用,每个模块自成一体。
%% 主程序入口 % 加载数据 [loadData, windData, heatData, genPara] = loadCaseData('case24h.mat'); % 构建优化模型 prob = buildEconomicDispatchModel(loadData, windData, heatData, genPara); % 求解 result = solveDispatchModel(prob); % 后处理与可视化 plotDispatchResult(result, loadData, windData);这套结构的最大好处是测试和修改成本极低。想换机组参数?改数据文件。想加储能约束?只动模型构建模块。想换求解器?只动求解模块。没有数据文件驱动,每跑一个新场景都要改代码的日子,我是受够了。
3.2 决策变量怎么定义:先把“维度”想清楚
建模最忌拿到问题就狂写变量。我习惯先把时间尺度、单元集合、变量维度列一张表,再动手写代码。
以我这次的系统为例,假设含一台CHP机组、一台纯凝火电机组、一台风电场、一台电锅炉、一个储热罐,调度周期取24小时,时间分辨率1小时。每个时段的决策变量包括:
| 变量 | 含义 | 维度 | 类型 |
|---|---|---|---|
| p_1(t) | CHP电出力 | 24×1 | 连续 |
| h_1(t) | CHP热出力 | 24×1 | 连续 |
| u_1(t) | CHP启停状态 | 24×1 | 0-1 |
| p_2(t) | 纯凝机组电出力 | 24×1 | 连续 |
| p_w(t) | 风电实际消纳功率 | 24×1 | 连续 |
| p_eb(t) | 电锅炉消耗电功率 | 24×1 | 连续 |
| h_ch(t) | 储热罐充热功率 | 24×1 | 连续(含正负) |
| s(t) | 储热罐储热量/荷电状态 | 24×1 | 连续 |
这些变量在YALMPI里用sdpvar和binvar定义,前者建连续变量,后者建二进制变量。定义完之后,目标函数和约束就是把这些变量组合起来。
3.3 目标函数和约束的代码化表达
目标函数用YALMPI写起来非常直白。我这里做了简化处理,假设燃料成本为二次函数并通过分段线性化转化为线性形式,风电弃风惩罚成本设为一个大数乘以弃风量:
% 目标函数:总成本最小 costGen = sum(genCostCoeff1 .* p_chp + genCostCoeff2 .* u_chp) + ... % CHP燃料成本 sum(genCostCoeff3 .* p_thermal + genCostCoeff4 .* u_thermal); % 火电成本 costWind = lambda_curtail * sum(windForecast - p_w); % 弃风惩罚 objective = costGen + costWind; constraints = constraints + [objective <= ...]; % 跑完一轮再取最小值 % 实际YALMPI写法:直接调用optimize diagnosis = optimize(constraints, objective, sdpsettings('solver','gurobi'));约束条件方面,核心有这几类:
电力平衡约束:每个时段的发电与负荷必须平衡,风电消纳量不能超过预测出力。
constraints = constraints + [p_chp + p_thermal + p_w == elecLoad + p_eb]; % 电平衡 constraints = constraints + [0 <= p_w <= windForecast]; % 风电消纳上限热力平衡约束:CHP热出力加电锅炉产热加储热罐放热,等于热负荷加储热罐充热。储热罐的充放热我用一个变量h_ch取正负来表示,正值充热、负值放热,这样不用额外引入两个0-1变量。
CHP可行域约束:这是模型里最考验功力的部分。我把电出力和热出力限制在一组线性不等式围成的区域内:
% CHP电热耦合可行域(简化版) constraints = constraints + [p_chp >= max(0, P_CHP_min - c_hl * h_chp)]; % 电出力下限随热出力上移 constraints = constraints + [p_chp <= P_CHP_max - c_hh * h_chp]; % 电出力上限随热出力下移 constraints = constraints + [0 <= h_chp <= H_CHP_max]; % 热出力范围这几条不等式看起来简单,背后是有物理意义的:热出力越高,CHP抽汽量越大,电出力的可调区间越窄。系数c_hl和c_hh需要从机组实际运行数据中拟合,别凭空拍脑袋。
储热罐约束:储热罐有容量上限和充放热功率上限,同时还要保证一个调度周期前后储热量大致守恒,否则模型会把储热罐当成“免费的无限能源”来滥用:
constraints = constraints + [0 <= s(t) <= S_max]; % 容量约束 constraints = constraints + [-H_ch_max <= h_ch(t) <= H_dis_max]; % 功率约束 constraints = constraints + [s(t+1) == s(t) + h_ch(t) * eta_ch - h_ch(t) / eta_dis]; % 动态约束(简化形式) constraints = constraints + [s(1) == s(25)]; % 周期守恒(末时段归初值)这里有个细节容易踩坑:充热和放热效率不同,负的h_ch在放热时实际释放到热网的热量要打一个折扣。我在初版代码里忽略了这一层,导致储热罐能凭空增加热量,结果消纳率数据异常好看——一看就是bug。遇到这种问题,先检查能量守恒,再检查有没有违反热力学第二定律。
3.4 求解器选择与参数配置
Matlab生态下做MILP建模,两条路最主流:YALMIP + Gurobi/CPLEX和Matlab Optimization Toolbox自带的intlinprog。我这次用的是YALMIP + Gurobi,原因是:
- YALMIP语法简洁,约束追加用加号拼接,代码可读性好,适合快速迭代
- Gurobi求解MILP的性能比intlinprog高一个量级,100个0-1变量以上差距非常明显
- Gurobi学术授权免费,学生申请不花一分钱
求解器参数也值得留意。调度模型求解慢,往往是MIP gap卡住不动。我习惯设置两个关键参数:mipgap设为0.01%(一般工程问题1%就够,但做结果分析建议收紧到0.1%),timelimit设30分钟,避免极端工况下跑一天。
提示:如果Gurobi的license激活出问题(macOS或者Linux下常见的是License manager error),先检查环境变量
GRB_LICENSE_FILE是否指向正确的lic文件,别一上来就重装软件。这个问题我在Windows和Ubuntu各遇过一次,十个有九个是环境变量路径问题。
3.5 数据处理的隐蔽坑位:量纲和基准值
说一个我初学时踩过的大跟头。数据里电负荷单位是MW,热负荷单位是GJ/h,CHP煤耗成本单位是元/MWh,结果目标函数里热出力直接乘了电成本系数——算出来的调度结果荒唐到热负荷几乎是零,电锅炉拼命发热。原因就是量纲不统一,热功率和电功率直接用同一个成本系数换算,没有做单位统一。
正确的做法是统一都用功率单位(MW),或者用基准值做标幺化。热负荷注意一下:热功率和热量是两个概念。1 MW的热功率持续一小时,对应热量是3.6 GJ,你如果从历史报表里拿到的热负荷单位是GJ/h,就要先除以3.6换算成MW的热功率再进模型。
另外,时间分辨率决定了离散方程的表达方式。1小时分辨率下,储热罐的h_ch(t)单位是MW,乘以1小时就是该时段充入的热量MWh(或GJ),所以在动态约束里h_ch(t)本身就代表了能量变化量,不需要再乘时间。如果你把分辨率改为15分钟,这里就要除以4,别忘了。
4. 算例设计与结果分析:消纳率提升了多少才算有效
4.1 基础算例:参数怎么设才合理
为了验证模型,我设计了一个24小时典型供热日算例。系统配置如下:一台CHP机组,额定电出力100 MW、热出力80 MW;一台纯凝火电,额定200 MW;风电场额定装机150 MW,取典型冬季夜间大风出力曲线;电锅炉容量30 MW;储热罐容量150 MWh,充放热功率上限30 MW。热负荷曲线按典型北方供热季日负荷取,白天稍低、夜间较高;电负荷取典型冬季日负荷曲线,峰值出现在晚间。
场景设计上,我做了三组对比:
- 场景A(对照):不含电锅炉与储热罐,CHP以热定电运行
- 场景B:仅含电锅炉参与消纳
- 场景C:电锅炉 + 储热罐协同参与
这样做对比不是为了凑论文篇幅,而是想看看不同灵活性资源的边际贡献到底有多大,分开看才能看清每种资源的“摊位费”值不值。
4.2 结果解读:不能只盯成本一个指标
优化结果跑出来,先看几个核心指标:总运行成本、弃风量、弃风率、CHP电出力区间、储热罐运行轨迹。
场景A(无灵活性资源)的弃风率大约在18%左右,集中在凌晨2点到6点。场景B加了电锅炉之后,弃风率降到8%左右,降幅明显。场景C储热罐加入后,弃风率进一步降到3%左右,而且弃风时段被推后到风资源最极端的少数时刻。
我画了张图,横轴是24小时,纵轴是功率,把风电预测曲线、实际消纳曲线、电锅炉耗电曲线叠在一起看。图中风电消纳曲线“贴着”预测曲线的时间段明显变长,说明模型确实在利用一切成本允许的手段把风电用起来。储热罐的荷电状态变化也很有意思:凌晨风电大发时罐体从30%冲到接近100%,上午热负荷上升后再逐步放电,到晚上又补一轮——完全符合物理直觉。
成本数据上,场景C比场景A的总运行成本低了约12%,其中燃料成本下降是主因,因为风电替代了煤电。但注意,电锅炉本身是耗电设备,它消耗的风电如果来自弃风时段,边际成本几乎为零,但如果调度时段不合适、消纳了本可消纳的风电,反而会抬高系统成本——这就考验优化模型的功力了,约束设得不全,模型会把所有风电硬塞给电锅炉,看着消纳率拉满,实际经济性却在恶化。
4.3 灵敏度分析:储能容量到底配多大
结果分析做完,我又做了个储能容量灵敏度测试:储热罐容量从50 MWh一直加到300 MWh,看弃风率和成本的变化曲线。结果是典型的边际递减规律——容量从50到150 MWh时,弃风率下降很快;从150到300 MWh时,降幅明显放缓。这说明在这个系统配置下,150 MWh左右的储热容量已经是“性价比拐点”,继续加罐子边际收益过低。
这类灵敏度分析在论文里很重要,它能告诉决策者“钱该花在哪”。实际工程项目里,储热罐容量选型不能只看弃风率,还要把设备投资成本加进来做综合评估——日前调度模型输出的是运行层的决策依据,投资层需要另行建模。
5. 踩坑记录与调试心得:写给正在调模型的你
5.1 求解无解(Infeasible)怎么排查
MILP模型报infeasible是最让人头秃的,因为你不知道是数据问题、约束矛盾还是建模错误。我的排查顺序是:
- 先把整数变量全部固定为1(或0),转成LP问题,看有没有可行解——如果LP都无解,说明连续约束之间存在矛盾
- 用“约束逐条注释法”排查——每次注释掉一组约束,重新求解,观察哪组约束被注释后模型恢复可行,矛盾基本就锁定了
- 重点怀疑储能约束——周期守恒约束(s(1) == s(25))和动态约束最容易互相打架,尤其是储热初始值设置不合理时
我遇到过最隐蔽的一次是CHP可行域约束和机组爬坡约束冲突:某个时段热出力调度值要求CHP电出力变化率超过了爬坡上限,模型直接infessible。排查后发现是热负荷预测数据里有一个时段跳变异常,修了数据源就解决了。所以也提醒大家:数据清洗和校验的优先级不亚于建模本身。
5.2 求解慢:为什么MIP GAP卡住不动
中小规模算例(24时段、几台机组)Gurobi几十秒内基本都能搞定。如果卡到几分钟以上,排查方向主要在:
- 0-1变量是不是意外膨胀了——比如储热罐充放热状态用了两组二进制变量,其实一个连续变量带正负范围就够了
- 是否存在大量对称性——机组参数完全一致时,解空间存在对称结构,求解器要探索大量等价分支,给同类型机组加微小的参数扰动可以有效打破对称性
- 时段时间长度——考虑24时段改成96时段(15分钟分辨率),求解规模是四次方级别的增长,不是线性增长
5.3 结果“太完美”也要警惕
有些同学的模型跑出来弃风率只有0.1%,成本低得不像话,发给导师还挺得意——往往说明约束少了。常见的情况是:
- 漏了储热罐充放热不能同时进行的约束(如果你用单一变量带正负号表示,这个物理限制天然满足;如果分两个变量建,就需要加互补约束)
- 电锅炉效率设成100%,现实中一般95%左右
- 电网交互没考虑传输容量限制,以为线路能无穷无尽地送电
- 火电机组最小开停机时间约束缺失,机组被模型频繁启停来投机取巧
总的来说,做优化调度,“结果合理”比“结果漂亮”重要一万倍。如果你画的储热罐轨迹不符合物理直觉,或者CHP机组的电出力曲线出现频繁跳变,别急着截图发朋友圈,大概率是模型哪里出了bug。
6. 模型扩展方向:往哪里延伸更有价值
这套模型我目前还停在日前单级优化层面,但实际项目中往往需要继续延伸。这里简单说几个值得做的扩展方向,也是目前学术界和工业界关注的热点:
多时间尺度协调:日前调度定长周期计划,日内要重新滚动优化修正预测误差。两阶段目前已经很主流,日内用MPC(模型预测控制)思路做滚动优化,对风电预测误差的应对能力明显更强。
考虑网络拓扑约束:我这里做的是单节点汇流母线的简化——全系统一个电平衡方程、一个热平衡方程。实际热网存在传输损耗、节点温度约束、管道延迟,电网也存在线路潮流约束。如果要研究网络阻塞对消纳的影响,就要引入直流潮流或者更精确的热网水力-热力模型,模型复杂度会直线上升。
引入需求侧响应:热负荷不是刚性的,可以通过分时热价或者室温设定值调节来平移热负荷。需求响应参与进来以后,优化变量从供给侧扩展到用户侧,目标函数也会加入用户舒适度约束,LSTM预测用户热负荷曲线加上调度模型,是这两年论文里的高频组合。
不确定性优化:风电出力和负荷预测都有误差,日前阶段可以用鲁棒优化或者随机规划来处理概率场景。这部分我后续会单独整理一篇文章,核心思想是让调度计划面对预测偏差时也有足够的“安全垫”。
如果你正在做相关课题,我个人建议是先把基础模型吃透,再按需做扩展——不要一上来就叠加各种高级特性,模型一旦跑不通,排查的复杂度会指数级上升。先在小算例上把基础模型和代码验证扎实了,再做加法不迟。