这标题一眼看去很容易让人觉得“又是套模板的综合能源代码”,但真正动手写过的朋友都清楚:光热电站、ORC循环、9节点电网、气网热网耦合,每一个单独拎出来都不算新鲜,难的是把它们塞进同一个Matlab优化框架里还能稳定求解。前一阵我就在做这样一套仿真框架——光热电站提供中低温热能,ORC把热能转成电,并网接入改进后的9节点电网,同时考虑热网和气网的运行约束,做典型日的多能流仿真与优化调度。今天就把这套代码的建模思路、求解陷阱和调试经验完整拆一遍,给正在做综合能源系统优化、或准备要入坑的研究生和工程师一个可以直接参考的路线。
1. 先说清楚:这套代码到底解决什么问题
1.1 大标题里的每个词都不是摆设
“Matlab综合能源系统优化代码:光热电站与ORC建模求解及9节点电网等多网仿真分析”这句话信息密度很高,拆开看分别对应了四层任务:
- 光热电站建模:要描述太阳辐照怎么变成集热场的热功率,储热罐怎么按时序充放,最后在夜间还能继续放热驱动发电;
- ORC建模求解:有机朗肯循环把光热过来的中低温热能转成电,得考虑效率、启停、部分负荷等实际运行特性;
- 9节点电网:电不能凭空消纳,要接进一个典型电网结构,用潮流约束校验并网后的功率分布和线路阻塞;
- 多网仿真分析:电网不是孤立的,燃气轮机连着气网,电锅炉连着热网,储热和ORC把热网与电网串在一起,仿真必须同时处理这三张网络。
所以这套代码本质上是做日内多能流优化调度:给定典型日的太阳能资源、负荷曲线、电价气价,求各机组、储热、ORC和各种耦合设备的最优出力。它的输出不只是“系统能不能运行”,而是“在满足所有平衡和网络约束的前提下,哪个时段该烧气、哪个时段该放热、哪个时段该从电网购电”。
我见过不少类似的代码,最要命的问题就是“模型写得很漂亮但跑不出结果”,或者“跑出结果但里面的SOC曲线和工程经验完全对不上”。这通常不是算法的问题,而是建模细节和单位处理出了问题。下面我从模型层开始,按实际开发顺序逐个讲。
1.2 我为什么把代码拆成数据、模型、优化、绘图四层
一开始我做原型的时候也是把所有东西写在一个巨大的.m脚本里,改一个负荷曲线要找半天,后来实在受不了就重构了。这套框架的分层思路其实很简单:
| 模块目录 | 职责 | 对应解决的问题 |
|---|---|---|
| data/ | 存放所有输入:DNI曲线、负荷曲线、电价气价、网络参数 | 换场景只改数据,不碰模型 |
| model/ | 光热场、储热罐、ORC、燃气轮机、电锅炉、气网的约束生成函数 | 每个设备一个函数,输出约束表达式 |
| optimize/ | 目标函数、变量汇总、求解器调用 | 把model层生成的约束全部装箱求解 |
| result/ | 绘图、表格化输出、报表计算 | 结果可视化,快速发现问题 |
这种分层对调试特别有帮助。模型不可行的时候,我可以单独把某个设备的约束拿出来看是否矛盾;结果曲线异常的时候,也可以逐模块核对中间变量,而不是对着上千行代码发呆。
2. 光热电站建模:关键是“可时移”的热能
2.1 从DNI到集热场出口热功率
光热电站的物理链条是:太阳直射辐照(DNI)→ 聚光镜场 → 吸热器/集热管 → 导热油或熔盐升温 → 储热罐或直接进动力岛。
在优化模型里,我通常把集热场出口热功率写成:
Q_sf = k_sf × DNI × S_area其中S_area是镜场集热面积,k_sf是一个综合系数,包含了光学效率、镜面清洁度、余弦损失、遮挡损失和管道热损。工程判断上,k_sf取0.6到0.75比较常见。比如一个5万平方米的槽式光热场,DNI在800 W/m²时,理论热功率是:
Q_sf = 0.7 × 800 × 50000 = 28,000,000 W = 28 MW这里有个必须强调的坑:DNI单位是W/m²,计算时要和面积的m²对消,但最终要转成MW,数量级差10⁶倍。我见过不止一次因为单位没抓牢,把集热场功率算大了几百倍,优化结果自然是荒谬的。
更精细的建模会考虑入射角余弦因子随时间变化,上午下午低、正午高。但如果是做24小时1小时间隔的经济调度,把k_sf当作常数一般是够用的,没必要把光热模型变成光学仿真软件。要研究的东西是多能流优化,不是聚光器详细设计。
2.2 储热罐的时序约束怎么写
光热电站最值钱的就是储热罐,它让“热”可以挪到晚上再用。储热的SOC递推公式我习惯写成:
SOC(t+1) = SOC(t) + η_ch × Q_ch(t) - Q_dis(t)/η_dis - Q_dump(t)这里所有热功率单位统一为MW,时间步长Δt=1h,所以SOC的变化单位正好是MWh,罐容量C_tes也取MWh。方程里每一项都有明确物理意义:
- Q_ch:从集热场进罐的充热功率;
- Q_dis:从罐出来送去ORC或热负荷的放热功率;
- Q_dump:弃热功率,这个变量必须有,否则在某个太阳辐照强、负荷低的时刻,集热产热没地方去,模型直接不可行。
我调试过程中遇到过无数次模型无解,最后发现都是因为没加弃热/弃光变量。物理世界可以有散热、可以主动弃光,但数学模型里如果不给多余能量一个去处,那就是死路一条。
另外还要考虑罐不能同时充放热。一条约束就能表达:
Q_ch(t) + Q_dis(t) ≤ Q_tes_max前提是Q_ch和Q_dis都非负,且充放热功率上限相同。这个约束实际上让同一时刻要么充、要么放、要么都小于上限,避免了“一边充一边放”这种无意义但模型可能会钻的漏洞。如果想更严格,可以用两个二进制变量,但实际算例里多花的时间和收益不成正比,我一般用上面这种宽松约束。
还有一个容易忽略的点是SOC的周期闭合约束,也就是当天结束罐内剩余热量要回到初始水平。这是为了让优化结果可以滚动复现,避免把第一天的储热能量“白嫖”掉。常见写法是SOC(T) == SOC(0),如果你想留一点灵活性,也可以大于某个比例。
2.3 光热跟光伏的本质区别:可调度性
为什么不直接用光伏而要大费周章建光热模型?因为在优化调度里,光伏出力是“看天吃饭”的刚性出力,最多只能弃光,而光热电站通过储热罐改变了时间维度。
举个例子:光伏电站晚上基本不能出力,但光热电站在晚上完全可以靠白天储的热继续输出电力。它本质上成了一个“带能量缓冲的可调度可再生能源电源”。在电网调度模型里,这就意味着:
- 光热并网功率上限可以是恒定值(取决于ORC容量或汽轮机容量);
- 它是可调的:白天可以少发电、多储热,留给晚高峰;
- 爬坡率约束相对燃气轮机更宽松,不会出现分钟级骤变;
- 出力下限由储热状态和最小运行功率共同决定。
这套代码里,我把光热-储热-ORC看成一个“虚拟发电单元”,对外只暴露并网电功率P_ctsp。它的内部约束是储热SOC、集热场产热、ORC吸热三者之间的热平衡,电网侧只需要看到P_ctsp的上下限和爬坡约束。这样既没有牺牲光热内部的时序特性,又不会让电网潮流模型变得一团乱。
3. ORC建模求解:把低温热能变成能进优化模型的形式
3.1 为什么综合能源系统里要加ORC
光热电站动力岛不一定非得用蒸汽朗肯循环。槽式光热的导热油出口温度通常在290到390℃,熔盐塔式的温度高一些,但如果系统中还有工业余热、地热、或者燃气轮机的缸套水余热,这些中低温热源用蒸汽循环就不太划算。这时候ORC就是更合适的选择——有机工质沸点低,能在低温区间获得比较高的热效率,设备结构紧凑,启动速度快,非常适合作为综合能源系统中的灵活发电单元。
工程上经常被人问“ORC效率怎么这么低,才百分之十几?”——这是热源品位决定的。卡诺循环效率的通俗理解就是:温差越大,效率越高;ORC的蒸发温度就一两百度,冷凝温度也就二三十度,热效率落在8%到15%是非常正常的。优化时不纠结效率高低,关键是把它能不能在正确的时间出正确的力。
3.2 热力学模型如何简化并嵌入优化
最严格的ORC模型要逐部件做热力学计算:蒸发器、膨胀机、冷凝器、工质泵,每个部件都要查工质物性表,算焓、熵、流量。这种细节模型适合做设计校核,但不适合直接塞进以“天”为周期的MILP优化里——非线性项太多,求解器直接罢工。
在优化框架里,我采用“效率-热量-电功率”的黑箱简化:
P_orc = η_cycle × Q_orc_in其中Q_orc_in是ORC从储热罐或热网取到的热功率,η_cycle是综合热效率,通常不是一个常数。简单做法是把η_cycle设为一个固定值,比如0.12,对很多调度场景足够用;更精细一点,可以把部分负荷率PLR定义为当前出力与额定出力之比,用分段线性函数表达效率对PLR的修正:
PLR(t) = P_orc(t) / P_rated η_cycle(t) = η_base + k1 × PLR(t) + k2 × PLR(t)²二次项在MILP里不能直接用,那就做分段线性近似:比如把PLR从0.2到1.0分成4到5段,每段用一个线性表达式,通过二进制变量选择所在段。这种方法在工程上很成熟,收敛也稳定。
ORC模型里还必须有冷源约束。热力学第二定律决定了输入的热量不可能全部变成电,剩下的大概85%到90%都要排到冷源里去。如果周围没有冷却塔、冷却水或者充足的环境散热带,ORC出力就得打折。所以我会加一个约束:机组运行需要冷源条件,比如室外温度高于某阈值时最大出力受限。在小算例里也可以暂不考虑,但做工程预研时这项不能省。
3.3 优化框架里的ORC启停与耦合约束
ORC作为发电设备,在优化模型里需要被“框”住,否则会出现奇怪的运行方式。典型的约束组是:
- 额定热输入上限和最小热输入限制;
- 电功率与热输入比例关系;
- 启停逻辑:如果机组不运行,电功率和热输入都必须是0;
- 爬坡约束:短时间内不能跳变。
在Yalmip里写起来很直接,我习惯的写法是:
T = 24; P_orc = sdpvar(1, T); % ORC输出电功率,MW Q_orc = sdpvar(1, T); % ORC输入热功率,MW z_orc = binvar(1, T); % 启停状态,1运行 0停机 P_rated = 10; % 额定电功率,MW Q_rated = P_rated / eta; % 对应额定热输入,MW P_min = 0.2 * P_rated; % 最小技术出力 Constraints = [Constraints, P_orc == eta * Q_orc]; Constraints = [Constraints, 0 <= Q_orc <= Q_rated]; Constraints = [Constraints, z_orc * P_min <= P_orc <= z_orc * P_rated]; Constraints = [Constraints, z_orc * Q_min <= Q_orc <= z_orc * Q_rated];这里最关键的是把P_orc和Q_orc同时乘以z_orc。只给P_orc加启停约束、忘记给Q_orc加,就会出现“机组停机但热输入不为0”的怪象——能量不守恒,结果必然错得离谱。
另一个实际中很常见的坑是:ORC最小运行功率如果定太高(比如0.5P_rated),优化会频繁启停机组来应对早晚负荷波动。解决办法是加一个最小连续运行时间约束,或者干脆把最小运行功率调低一点。光热-ORC系统的启停成本相对燃气轮机低一些,但也不能随意启停,我一般会在目标函数里放一个轻量级的启停惩罚项。
4. 9节点电网与多网耦合仿真:不是简单拼三个网络
4.1 9节点电网怎么搭起来
标题里专门提到9节点电网,这是电力系统里非常经典的算例结构。我在这套代码里以经典3机9节点系统为蓝本做了扩展:保留原有的9个节点和三条负荷支路,但把其中一台原同步机替换为光热-ORC虚拟电源,另一台机组替换为燃气轮机,剩下一个节点当作与外部电网的购电联络点。
之所以这么改,是为了让算例既有代表性又不失真:经典9节点系统本身有一套成熟的线路参数,网络损耗、线路潮流、节点电压这些指标都有标准答案可以对照校核;而多能系统仿真绕不开电网约束,用太简单的单母线模型没法回答“线路会不会过载、潮流是否合理”这类工程问题。
在优化模型内部,我采用的是直流潮流。它的本质是忽略无功和电压幅值,只保留节点有功平衡与线路有功传输约束,写成矩阵形式就是:
P_inj = B × θP_inj是各节点净注入有功,B是节点导纳矩阵的虚部(B矩阵),θ是电压相角。直流潮流是线性的,可以直接塞进MILP,这是综合能源系统优化里最常见的做法。
但直流潮流毕竟是近似,它不考虑无功平衡和电压越限。所以我在仿真流程里做了两步:先用直流潮流做优化调度,再把优化结果交给交流潮流校核,看电压和线路潮流是否还在合理范围。这一步非常实用——很多时候调度方案在直流潮流下完全可行,交流校核却发现某个节点电压偏低,那就需要回到优化模型里加电压约束或者增加无功补偿设备。
4.2 电网、热网、气网的耦合变量到底在哪
很多初学者把“多网耦合”理解成把三个网络的模型写到同一个脚本里,只要变量在,就算耦合了。这是不完整的。真正的耦合在于能源站和转换设备——它们同时是多个网络的节点,把一种能源转换成另一种能源。
我把这套系统里的耦合关系整理成一张能流表:
| 设备 | 输入侧网络 | 输出侧网络 | 建模要点 |
|---|---|---|---|
| 燃气轮机 | 气网 | 电网、热网 | 耗气量 = 电功率/发电效率,余热回收 |
| 光热-ORC | 热网、太阳能 | 电网 | 集热产热入储热,ORC放热转电 |
| 电锅炉/热泵 | 电网 | 热网 | 电转热,热泵要乘COP |
| 储热罐 | 热网 | 热网 | 只做时序搬移,不跨网 |
| 外部购电 | 电网 | 电网 | 联络线功率约束 |
写约束的时候,三个网络的平衡方程要在各自的“网络层”写,设备层只负责连接输入和输出。比如燃气轮机的模型里会有:
Gas_consumption(t) = P_gt(t) / η_gt Heat_output(t) = P_gt(t) × α_gt然后把Gas_consumption汇总到气网节点平衡里,把Heat_output汇总到热网节点平衡里,把P_gt汇总到电网节点平衡里。这样写的好处是,每一个网络的平衡约束都是干净的,设备只是换能的映射,调试时可以直接看某个网络缺多少能量。
气网建模这里我多说一句。如果只是做一天的经济调度,气网不必上完整的瞬态管道模型,可以把气源、储气和主要气负荷做成一个节点气压约束:每条母线上的供气量在上下限内,节点气平衡即可。如果非要考虑管道流量,就会遇到Weymouth方程这个非线性项,优化模型会从MILP变成MINLP,求解难度上一个台阶。我在代码里默认用节点气平衡加气压限值,同时在备注里留了非线性扩展接口。
4.3 典型日仿真怎么组织,结果看什么
多网仿真分析最终要落到典型日的运行曲线上。我的做法是:
- 时间尺度取24小时,步长1小时,这个尺度下直流潮流和储热时序都足够精确;
- 输入数据包括DNI曲线(晴空日、多云日各一)、电负荷、热负荷、气负荷曲线,以及分时电价和气价;
- 优化输出包括光热-ORC出力、储热SOC、燃气轮机出力、电锅炉/热泵功率、购电功率、各网络节点流量/潮流。
结果分析我重点看三个指标:
- 弃光弃热率:集热场产热里有多少被抛弃。如果远超0,说明储热容量或ORC容量配置不合理。
- 日运行成本:购电成本、购气成本、设备运维成本的总和。这个值可以对比“去掉ORC”或“去掉储热”的对照组,直接量化某个设备的价值。
- 多能互补程度:观察燃气轮机和ORC在不同时段的出力关系。理想情况下,白天太阳能充足时ORC多出力、燃气轮机少烧气,晚峰时段燃气轮机顶上。
我做完一次仿真,第一件事永远是先看SOC曲线。如果SOC在夜间断崖式下跌而在白天突然满罐,说明储热罐被优化算法强行当成了“大号电池”,调度策略可能偏离工程实际。这时候就要检查是不是SOC末端约束、放热效率惩罚或者罐容量边界设置出了问题。
5. 优化求解与Matlab实现:能稳定跑出结果才有价值
5.1 目标函数与约束怎么取舍
这套代码的目标函数我选的是系统日运行成本最小化,包含三块:外部购电成本、气网购气成本、所有设备的运行维护成本。表达式可以写成:
min Σ [ c_elec(t) × P_buy(t) × Δt + c_gas(t) × F_gas(t) × Δt + Σ_i c_om_i × P_i(t) × Δt ]如果想研究低碳运行,可以在目标函数里加上碳排放成本,也就是给每个设备的耗气量乘一个碳因子再乘碳价。我建议第一次做的时候先只跑纯经济目标,把系统调通了再叠加碳目标,否则模型不可行时你分不清是约束写错还是目标函数方向不对。
约束的优先级我的处理习惯是:
- 必须性约束:供需平衡、设备上下限、启停逻辑、储热SOC边界。这些一项都不能少。
- 网络性约束:直流潮流、线路容量、节点气压上下限。这些决定结果是否工程可接受。
- 增强性约束:爬坡限制、最小连续运行时间、旋转备用。这些能提升结果质量,但要增加变量和计算时间。
我见过不少论文里约束写得又多又杂,实际上很多约束之间是冗余甚至冲突的。一个务实的处理是把约束分模块生成,先跑一遍,如果求解器报告不可行,就逐模块释放非必须约束,看是哪个模块引起的冲突。这条排错流程能帮你节约大量时间。
5.2 Yalmip建模里的几个实战习惯
这套代码的优化层用Matlab + Yalmip搭建。Yalmip最大的好处是可以把模型写成符号表达式,不用手工转换大规模矩阵。我总结了几个非常实用的习惯:
第一,所有变量维度先定好。比如24小时模型里,凡是时变变量都用1×T的sdpvar,凡是0/1状态用binvar。这样可以避免维度不匹配的报错。
第二,约束先收集到cell里再拼接。不能在循环里反复用Constraints = [Constraints, ...],一万行循环会慢得人崩溃。正确做法是先Constraints = {},循环里Constraints{end+1} = ...,循环结束后一次性拼起来。
第三,单位统一到MW/MWh和元/MWh。千万别电功率用MW、热功率用kW、气流量用m³,最后目标函数里价格单位又是元/度,计算完的结果你会怀疑人生。我统一规定:有功功率、热功率都是MW,气功率折算成同等热值的MW(基于天然气低位热值),时间步长是h,价格单位全部换算成元/MWh。
第四,对求解器友好。大系统用默认的intlinprog可能很慢,我一般先用宽松求解设置跑通,再逐步提高求解精度。如果模型规模大,优化时间从几秒到几分钟都是正常的,不必非要追求极致速度。
下面是一段核心调度模型的主干伪代码,展示怎么把上述思路落到Yalmip:
%% 变量定义 P_gt = sdpvar(1, T); P_orc = sdpvar(1, T); P_buy = sdpvar(1, T); Q_ch = sdpvar(1, T); Q_dis = sdpvar(1, T); Q_dump = sdpvar(1, T); SOC = sdpvar(1, T+1); z_gt = binvar(1, T); z_orc = binvar(1, T); %% 网络约束、设备约束省略... %% 储热约束 Constraints{end+1} = SOC(2:T+1) == SOC(1:T) + eta_ch*Q_ch - Q_dis/eta_dis - Q_dump; Constraints{end+1} = SOC >= 0; Constraints{end+1} = SOC <= C_tes; Constraints{end+1} = SOC(1) == SOC_init; Constraints{end+1} = SOC(T+1) == SOC_init; Constraints{end+1} = Q_ch + Q_dis <= Q_tes_max; %% 目标函数 Objective = sum(c_elec.*P_buy + c_gas.*Gas_gt + c_om_gt.*P_gt + c_om_orc.*P_orc); %% 求解 ops = sdpsettings('solver','intlinprog','verbose',1); sol = optimize([Constraints{:}], Objective, ops); P_gt_opt = value(P_gt);5.3 常见报错与排查对照表
我在调试这套框架时踩过的坑,整理成一张对照表,按出现频率排序:
| 现象 | 根本原因 | 解决方法 |
|---|---|---|
| 求解器直接报Infeasible | 热功率不平衡,多余热量没排除路径 | 加Q_dump弃热变量,检查储热SOC初末值是否过严 |
| SOC出现负值或溢出 | SOC边界忘了写,或时间步长与功率单位不匹配 | 补SOC≥0和SOC≤C_tes约束,检查Δt单位 |
| ORC停机时P为0但Q不为0 | 启停变量只绑定了P,没绑定Q | 对Q_orc也乘z_orc |
| 潮流约束导致无解 | 直流潮流矩阵奇异,或平衡节点功率超限 | 检查B矩阵是否正定,扩大购电联络线功率上限 |
| 结果里燃气轮机在夜间疯狂启停 | 最小技术出力设置太高,或缺少启停惩罚 | 降低最小出力,目标函数加启停惩罚项 |
| 数量级差得离谱 | 电功率用MW,热功率用kW混用 | 全局统一单位,用基准值归算 |
| 求解时间过长 | 二进制变量过多,或加了太多0/1耦合约束 | 固定部分机组组合,减少不必要的二进制变量 |
排查Infeasible是我最想强调的场景。模型报不可行时,不要急着删约束,先检查是否有属于每类网络的多余能量都有出路:电网多余能量可以外送,热网多余热量可以弃热,气网多余气压可以放散。综合能源系统优化的“不可行”,八成是某个网络少了一个“泄放出口”。
5.4 代码目录结构与常见扩展方向
最后说一下这套代码的目录组织。如果你要自己复用,建议按这个骨架来:
case9/ ├── data/ │ ├── load_curve.m % 电、热、气负荷曲线 │ ├── dni_curve.m % 太阳直射辐照曲线 │ └── network_9bus.m % 9节点电网拓扑与线路参数 ├── model/ │ ├── csp_model.m % 光热集热场与储热罐约束 │ ├── orc_model.m % ORC热转电约束 │ ├── gas_model.m % 气源与气网节点约束 │ └── coupling_model.m % 燃气轮机、电锅炉等耦合设备 ├── optimize/ │ ├── build_model.m % 组装变量、约束、目标函数 │ └── solve_case.m % 调用求解器并输出结果 └── result/ └── plot_results.m % 绘图与指标统计往这个框架里加新设备,比如地源热泵、储能电池、氢能系统,本质上都是重复同一个动作:定义输入输出变量、写物理模型约束、d加到对应网络的平衡约束里、把运维成本加进目标函数。模型层不入侵到网络层,网络层也不改动设备约束,这是这套代码能持续演化的核心原因。
扩展方向上,我看到比较多人做的是碳交易机制下的多目标调度、考虑新能源出力不确定性的鲁棒优化、以及把时间尺度从1小时细化到15分钟后重新求解。后者的计算量会爆炸,但如果你把变量规模和求解器参数调好,还是能跑下来的。另一个实用方向是给典型日加上“极端场景”——连续阴天导致DNI几乎为零,此时储热什么时候放完、燃气轮机能不能顶上,这种压力测试往往比晴天场景更能暴露模型问题。
最后说一个我自己的心得,也算是一个小经验:综合能源系统优化的代码,最怕的不是模型复杂,而是你对自己写下的每一个约束没有一张能流守恒表。每次模型不可行,我都先把所有设备的输入输出能量列表格,按节点逐个时刻加起来,缺的那一块就是问题所在。光热电站、ORC、9节点电网,拆开都不难,难的是它们凑在一起时你还记得每个变量的单位和物理意义。把这套东西理顺了,你就已经超越了大半打着“综合能源仿真”旗号却只会在单一网络里打转的代码。