这个题目拿到手我第一反应是又是一个把多种设备捆进同一个调度模型的老朋友。光热电站、有机朗肯循环ORC、电转气P2G单独拎出来每个都有各自的物理坑放在同一个综合能源优化模型里时单位、时间尺度、启停逻辑、能量流方向但凡有一个地方没对齐算出来的结果就基本不能看了。我最近完整跑过一套这套系统的Matlab优化调度实现趁热乎写一篇实操复盘。这篇文章适合正在做综合能源调度、光热储能建模、余热发电或者P2G制氢方向仿真的朋友尤其适合那种“已经有了静态容量配置方案但还差一个日前调度模型把多能流跑顺”的场景。我会从建模思路、数学表达、Matlab代码落地、仿真结果读法和避坑经验四个层面讲尽量把那些论文里不会写、但做代码时经常卡壳的地方都点出来。1. 为什么要把光热、ORC和P2G放到同一个调度模型里1.1 三个设备在系统里分别承担什么角色先统一一个基本判断光热电站不是光伏。光伏的出力完全取决于光照强度而光热电站因为有熔盐储热罐可以把中午集热场产出的热量存下来留到晚上或者用电高峰再放热发电。用调度的话来说光热是具备“能量时移能力”的可调电源这是它区别于常规新能源最核心的地方。ORC在这里承担的角色是“低品位热源回收”。它不像光热电站里的蒸汽朗肯循环那样追求高参数、高效率它采用的是有机工质可以在80到200摄氏度左右的中低温热源条件下做功发电。这个温度区间蒸汽轮机基本玩不转但正好是光热储热释放过程中梯级利用的下游、P2G电解槽和甲烷化反应产生的废热区间。P2G则是连接“电-氢-气”三层能量网络的转换枢纽。当系统里风电、光伏出力过大或者电价处于低谷时P2G可以把富余电能转化为氢气氢气既能直接面向氢负荷供应也能进一步通过甲烷化反应合成天然气进入燃气网络。P2G的最大价值不是单纯消耗多余电能而是让电力系统与其他能源网络产生耦合把时间尺度不同的用能需求串在一起。把这三个设备放在同一个优化调度模型里本质原因只有一个它们之间存在热能的梯级利用关系。光热场产出高温热量先走蒸汽循环发电中低温热量供给ORCP2G运行过程中产生的废热也不该直接排掉而是尽可能送入ORC继续发电。这个“能级匹配、温度对口”的链条如果拆成三个独立模型分别优化就完全失效了。1.2 三者联合调度带来的实际收益联合建模后最直接的变化是系统灵活性来源变多了。传统电储能只能解决电力时移问题而光热储热解决的是热力时移问题储氢罐解决的是氢能时移问题。三者共享同一个调度时间轴优化器可以选择在电价低谷时多用P2G制氢而不是买电储能在中午光热集热富余时选择储热而不是弃热在晚间用储热启动ORC参与峰时供电。我举个典型场景中午光伏大发、风电出力也不错电网电价低常规思路是弃掉一部分新能源。但有了P2G后多余电可以变成氢再通过甲烷化生成天然气供给气负荷这个过程散发的热量又被ORC回收变成电力或热力。这几个环节的能量流是相互咬合的只有放进同一组变量、同一个目标函数里做联合优化调度方案才是全局最优的否则就只是各扫门前雪。所以这篇文章不会把三个设备分开讲三套模型而是按照“一个系统、一张能流网络、一组决策变量”的方式来组织。核心思路是先物理建模再数学化再Matlab代码实现最后用结果验证模型行为是否符合物理直觉。2. 优化建模从设备物理约束到数学表达2.1 光热电站的建模集热场、储热罐、发电岛光热电站的建模行业内已经有一套比较成熟的简化做法。先把光热电站拆成三块聚光集热场把太阳辐射转化为导热工质的热量输出热功率。熔盐储热系统实现热量的充放和存储类似于电池储能的“热版”。发电岛把高温热量通过蒸汽朗肯循环变成电当然也可以拆出一部分热量给ORC做梯级利用。集热场输出的热功率可以写成Q_sf(t) eta_field * DNI(t) * A_field其中eta_field是综合考虑镜场效率、光学损失、吸热器效率后的总转换效率一般在0.4到0.5之间。这个公式很简单但真正建模时要注意DNI(t)的数据粒度如果原始数据是分钟级、而调度模型是小时级需要先做能量守恒的聚合而不是简单取平均。储热罐的动态方程是E_tes(t1) E_tes(t) dt * (eta_ch * Q_tes_ch(t) - Q_tes_dis(t))其中Q_tes_ch是充热功率Q_tes_dis是放热功率eta_ch是充热换热效率。实际运行中还要考虑储热罐自然散热损失通常会给一个每小时百分比的损失系数。这里有一个容易忽略的细节充热和放热不能同时进行或者至少不能同时以额定功率进行。在代码里可以用二进制变量加Big-M约束来强制互斥后期求解压力大时也可以放松为一个软约束通过增加惩罚项来避免同时充放。发电岛的出力模型为P_csp(t) eta_csp_pb * Q_csp_power(t)Q_csp_power是进入蒸汽循环的热功率eta_csp_pb是发电岛热电转换效率。注意蒸汽循环本身有最小技术出力约束和爬坡约束尤其是启动阶段不能像储能电池那样自由从0跳到满发。这些约束在调度模型里必须显式表达否则优化器会给出一个物理上不可能执行的调度方案。2.2 ORC的建模效率不是常数热源温度才是关键ORC的建模看起来简单输入废热流量乘以一个转换效率就是输出电功率。但如果把效率取成恒定值结果会骗人。有机朗肯循环的效率与热源温度、冷却温度、工质种类、蒸发压力和冷凝压力都有关系。以常见工质R245fa为例热源温度60摄氏度时效率可能只有7%到8%热源温度150摄氏度时能达到14%左右。在我的模型里采用了一种折中方案把ORC输入的热源按温度区间分成几个档每档给一个效率值。比如热源温度低于100摄氏度效率取0.08热源温度在100到150摄氏度之间效率取0.10热源温度超过150摄氏度效率取0.13。这样在调度模型里效率是变化的但变化关系是分段常数不会引入非线性。用代码实现时根据热源来源判断温度区间即可。这样做精度肯定不如详细的换热模型但在日前调度尺度上已经足够而且求解稳定。ORC的另一个关键约束是热源输入不能突变。热源系统有热惯性ORC蒸发器也有最小热源流量要求所以Q_orc_in不仅要满足上下限还要满足爬坡约束。这是很多初学建模的人容易漏掉的一点。调度模型里爬坡约束通常只给电功率但ORC的爬坡本质上来自热源侧所以要把爬坡约束加到热输入变量上而不是发电功率上。P2G产生的废热温度通常在80到120摄氏度之间光热储热释放过程中也有一部分热量温度不够高、不足以进入蒸汽循环这两个热源合并到一起给ORC用才是符合工程实际的做法。模型里我会引入一个“热源汇”的概念所有低品位热量汇总到一根热母线上再分配给热负荷、ORC和弃热。2.3 P2G的建模电解、储氢、甲烷化三段式P2G链路按工艺拆成三段电解水制氢、储氢调峰、甲烷化合成天然气。调度模型里这三段可以分别建模也可以合并成一个模块。我推荐分开因为中间储能环节的容量、充放效率会直接影响调度结果。电解槽的模型很直接H2_rate(t) eta_H2 * P_p2g(t)其中P_p2g是电解槽消耗的电功率eta_H2是电解槽效率。这里的效率需要明确定义边界有的文献按氢气低热值算有的按高热值算边界是否包含整流器损耗、水泵损耗、辅助设备电耗差异非常大。同一台电解槽效率定义不同优化结果可能差出十几个百分点。我建议统一采用“电解槽总耗电功率到产出氢气的等值热功率”作为效率定义并写清楚边界包含整流器和水泵等辅助损耗。电解槽不能无限低负荷运行一般有最小负载率约束比如额定功率的20%。此外启停会损耗电极寿命所以每天启停次数要受限。这个约束在日用调度模型里很实用v_start(t) u_p2g(t) - u_p2g(t-1); sum(v_start) N_max_start;u_p2g是二进制启停变量v_start是启动动作变量。通过这个约束优化器不会让电解槽目标函数看起来更省钱就频繁启停。储氢环节的模型和蓄电池储能非常像H_soc(t1) H_soc(t) dt * (eta_h2_ch * H_ch(t) - H_dis(t))氢负荷优先由储氢罐放氢满足不足部分才会实时制氢补上。不过模型里要区分两个出口一部分氢气直接作为氢负荷对外供应另一部分氢气进入甲烷化反应器。甲烷化反应会消耗氢气和二氧化碳产生天然气同时释放大量反应热。从调度角度看甲烷化输出天然气的速率与消耗氢气速率之间的关系近似是线性比例再乘一个甲烷化效率即可。我建模时也把它写成线性关系G_meth(t) eta_meth * H2_meth(t)甲烷化反应释放的热量不是废品它会进入ORC的热源侧。也就是说P2GORC是该系统的第二组耦合点。2.4 系统级约束能量平衡把三个设备焊在一起所有设备建模完成后剩下的关键工作是写系统级能量平衡约束。一个综合能源系统的调度模型本质上就是一组能量守恒方程加一组设备运行边界。电平衡条件P_buy(t) P_wind_use(t) P_pv_use(t) P_csp(t) P_orc(t) P_load(t) P_p2g(t) P_sell(t)热平衡条件在处理时会稍微复杂一点因为要建立热母线方程Q_sf(t) Q_tes_dis(t) Q_h2_waste(t) Q_tes_ch(t) Q_csp_power(t) Q_orc_in(t) Q_h_load(t) Q_shed_h(t)这里面Q_shed_h是弃热量。如果集热场产热太多储热罐满了、热负荷也满足不了、ORC也达到上限多出来的热量只能通过调整集热场聚光比或放散处理。气平衡和氢平衡分别写G_buy(t) G_meth(t) G_load(t); H2_rate(t) H_dis(t) H_load(t) H2_meth(t) H_ch(t);目标函数则把购电费用、购气费用、设备运维成本、启动成本、弃电弃热惩罚都放进去例如min sum_t ( c_buy*P_buy(t) - c_sell*P_sell(t) c_gas*G_buy(t) c_om_csp*P_csp(t) c_om_orc*P_orc(t) c_om_p2g*P_p2g(t) c_start*p2g_start(t) M_curtail*(P_curtail_w(t) P_curtail_pv(t)) )到这里模型已经从物理问题变成了数学表达式决策变量是每个小时各设备的功率、启停状态、储能量约束条件是设备边界和能量平衡目标函数是系统运行总成本最小化。3. Matlab代码实现从公式到可运行模型3.1 参数准备与数据预处理代码层面第一步是统一单位。我不止一次看到有人电功率用MW、热功率用kW、电价用元/kWh、天然气价格用元/立方米算出来的目标函数值数量级乱七八糟。我的习惯是全部采用“功率用MW、能量用MWh、电价用元/MWh、气价用元/MWh等值热值”的统一体系。场景数据包括24小时的电负荷、热负荷、氢负荷、气负荷曲线风电、光伏可用出力曲线分时购电价格DNI数据用于计算光热集热场产热。以下代码是一个参数初始化的示例T 24; dt 1; % 时间步长单位小时 % 用一天的风电光伏和负荷曲线 P_load [ ... 电负荷数据 ... ]; P_h_load [ ... 热负荷数据 ... ]; H_load [ ... 氢负荷数据 ... ]; G_load [ ... 气负荷数据 ... ]; P_wind_avail [ ... 风电可用出力 ... ]; P_pv_avail [ ... 光伏可用出力 ... ]; DNI [ ... 法向直接辐射 ... ]; A_field 12000; % 集热场面积平方米 eta_field 0.45; Q_sf eta_field * DNI * A_field / 1e6; % 转换成MW热功率 c_buy [ ... 分时购电价 ... ]; c_sell max(c_buy) * 0.8; % 售电价通常低于购电价 c_gas 220; % 单位元/MWh要注意Q_sf的计算结果如果出现负值说明夜间DNI为零直接置零即可。DNI数据单位一般是W/m²乘以面积后是瓦除以1e6才换算成MW。这个小细节常被忽略。3.2 决策变量的Yalmip定义我在Matlab里习惯用Yalmip工具箱写优化模型因为它语法直接写约束和配方的效率比纯矩阵方式高很多。连续功率变量用sdpvar启停状态用binvar储罐的SOC也是连续变量。% 购售电/各类发电功率 P_buy sdpvar(1, T); P_sell sdpvar(1, T); P_csp sdpvar(1, T); P_orc sdpvar(1, T); P_p2g sdpvar(1, T); P_wind_use sdpvar(1, T); P_pv_use sdpvar(1, T); P_wind_curt sdpvar(1, T); P_pv_curt sdpvar(1, T); % 热量侧变量 Q_tes_ch sdpvar(1, T); Q_tes_dis sdpvar(1, T); Q_csp_power sdpvar(1, T); Q_orc_in sdpvar(1, T); Q_h_load_supply sdpvar(1, T); Q_shed sdpvar(1, T); % 储热罐/储氢罐SOC E_tes sdpvar(1, T1); H_soc sdpvar(1, T1); % P2G链路变量 H2_rate sdpvar(1, T); H2_meth sdpvar(1, T); H2_direct sdpvar(1, T); H2_ch sdpvar(1, T); H2_dis sdpvar(1, T); G_meth sdpvar(1, T); % 二进制变量 u_p2g binvar(1, T); % 电解槽运行状态 u_orc binvar(1, T); % ORC运行状态 v_p2g_start binvar(1, T); % 电解槽启动信号变量名要有可读性尤其是后面排查约束问题时命名清晰的模型能省大量时间。E_tes定义成1,T1是为了方便写储能量的递推关系T1是调度期末的状态量。3.3 目标函数与约束的代码表达目标函数在Yalmip里几乎可以直接照抄数学表达式% 目标函数系统总运行成本最小弃电惩罚系数取一个较大值 M_cur_big 5000; Objective sum(c_buy .* P_buy) - sum(c_sell .* P_sell) ... sum(c_gas .* G_meth) ... % 购气量计入 sum(12 * P_csp) sum(8 * P_orc) sum(15 * P_p2g) ... sum(20 * v_p2g_start) ... M_cur_big * (sum(P_wind_curt) sum(P_pv_curt) sum(Q_shed));约束条件按模块来写。先写电平衡和风电光伏消纳约束Constraints []; % 风电/光伏可用出力约束 Constraints [Constraints, P_wind_use P_wind_curt P_wind_avail]; Constraints [Constraints, P_pv_use P_pv_curt P_pv_avail]; % 电功率平衡 Constraints [Constraints, P_buy P_wind_use P_pv_use P_csp P_orc ... P_load P_p2g P_sell];热母线平衡和储热罐动态% 热平衡集热场产热 储热放热 P2G废热 储热充热 蒸汽循环 ORC 热负荷 弃热 Q_p2g_waste 0.15 * P_p2g; % P2G产生可回收废热系数可根据工艺调整 Constraints [Constraints, Q_sf Q_tes_dis Q_p2g_waste ... Q_tes_ch Q_csp_power Q_orc_in Q_h_load_supply Q_shed]; % 光热发电 Constraints [Constraints, P_csp 0.40 * Q_csp_power]; Constraints [Constraints, Q_csp_power 80]; % 储热罐动态与容量 Constraints [Constraints, E_tes(2:T1) E_tes(1:T) dt * (0.95 * Q_tes_ch - Q_tes_dis)]; Constraints [Constraints, E_tes 450]; Constraints [Constraints, E_tes 50]; Constraints [Constraints, Q_tes_ch 60]; Constraints [Constraints, Q_tes_dis 60];ORC和P2G的约束% ORC热输入转电功率效率取分段常数近似 eta_orc 0.12; Constraints [Constraints, P_orc eta_orc * Q_orc_in]; Constraints [Constraints, Q_orc_in 5 * u_orc]; Constraints [Constraints, Q_orc_in 40 * u_orc]; % P2G电解制氢 eta_p2g_h2 0.60; Constraints [Constraints, H2_rate eta_p2g_h2 * P_p2g]; Constraints [Constraints, P_p2g 5 * u_p2g]; Constraints [Constraints, P_p2g 25 * u_p2g]; % 氢平衡 Constraints [Constraints, H2_rate H2_dis H_load H2_meth H2_ch]; % 储氢罐动态 Constraints [Constraints, H_soc(2:T1) H_soc(1:T) dt * (H2_ch - H2_dis)]; Constraints [Constraints, H_soc 80]; Constraints [Constraints, H2_meth H2_rate H2_dis]; % 甲烷化消耗氢不能超过可用氢 % 甲烷化产气 Constraints [Constraints, G_meth 0.9 * H2_meth]; Constraints [Constraints, G_meth 15]; % 气平衡 Constraints [Constraints, G_meth G_load]; % 为了简化假设甲烷化产出优先满足气负荷不足部分购气 Constraints [Constraints, G_buy_dummy 0];这个气平衡写法并不完整实际代码里我会引入购气变量。为了让读者能直接理解气平衡约束应该写成G_buy sdpvar(1,T); constraints [constraints, G_buy G_meth G_load];不过上面示例里如果不需要购气也行关键是G_meth和G_load的关系要写对。P2G启停约束% 启动信号 Constraints [Constraints, v_p2g_start u_p2g - [u_p2g(1), u_p2g(1:T-1)]]; % 注意t1时刻的前一状态要单独处理这里简化为凌晨0点前为停机状态 % 每天启动次数限制 Constraints [Constraints, sum(v_p2g_start) 3];把所有这些约束堆进Constraints变量再调用求解器ops sdpsettings(verbose, 1); optimize(Constraints, Objective, ops);求解完成后取出变量并用value()转换为数值结果P_csp_val value(P_csp); P_orc_val value(P_orc); P_p2g_val value(P_p2g); Q_tes_soc value(E_tes(2:end)) 50; % 加上储热罐初始死容量有一点要提醒Yalmip里sdpvar变量在没有给初值、也没有雅可比矩阵参与的情况下连续模型的初值求解器自己会处理。但如果模型里二进制变量很多求解器默认的启动点可能不理想建议设置求解器的启动参数或多跑几个初值。4. 仿真案例解读看调度结果到底合理不合理4.1 典型日调度结果分析我跑通模型后的第一件事并不是直接看成本而是先把每个设备的功率曲线画出来按照能量平衡关系检查一遍。这里给出一个简化场景的规划结果示例设备参数和负荷曲线都是我随意设定的但行为逻辑具有典型性。时刻P_wind_use(MW)P_csp(MW)P_orc(MW)P_p2g(MW)P_buy(MW)储热SOC(MWh)储氢SOC(MWh)131.501.212.08.312035232.001.415.05.610849328.001.520.02.19568430.02.01.818.008895526.012.02.010.0098105615.028.02.508.0160105从结果可以看到几个符合物理直觉的现象凌晨风电出力高、电价低电解槽开得比较多储氢罐SOC持续上升ORC也从P2G废热中回收少量电力。到了早晨电负荷上涨后P2G逐渐关停光热储热开始放热CSP发电顶上负荷。白天光伏出力高的时候储热罐充电SOC上升为夜间高峰备热。这个结果验证了模型的行为是否正确P2G有没有在低电价时启动、光热储热有没有发挥时移作用、ORC有没有跟着热源变化而调节出力。如果凌晨电价很低但P2G却没启动要么是启停限制约束设得太紧要么是目标函数里启动成本权重太大。4.2 边界参数敏感性储热时长的影响建模完成后可以做的最有价值的分析是敏感性分析。比如改变熔盐储热罐的容量观察系统弃电率、购电成本的变化。以我的测试配置为例光热电站设计储热时长从4小时增加到8小时弃电率大约从9%下降到5%购电成本也有明显下降。但储热罐容量继续增加到10小时以上时边际收益迅速递减因为夜晚可消纳的新能源功率终归有限而且储热罐自身有散热损失储的时间越长热损失越大。这类分析的结论对方案设计有直接参考意义比单纯给一个最优调度值有用得多。如果你的导师或合作方问“光热到底配多少小时储热合适”拿出这条敏感性曲线就是最好的回答。4.3 设备利用率和经济性指标除了总成本和弃电率我还会统计三个利用率指标ORC年等效利用小时数用来判断ORC是否过度配置P2G电解槽的平均负载率太低说明制氢设备利用率不足CSP发电岛的启动次数频繁启停不仅伤设备也说明储热策略需要调整。这些指标在Matlab里通过统计value(P_orc)大于零的小时数就能得到不需要额外再跑模型。把指标和成本放在一起看才能区分“模型能跑”和“模型能指导决策”。5. 常见问题与排查技巧实录5.1 单位不统一导致目标函数出现无效解这是我见过最多的问题。比如电价单位是元/kWh但气价单位是元/立方米P2G制出的氢气又以kg计混在一个目标函数里求和优化器根本不理解这个目标的物理含义。我自己的做法是建立一个“单位基准表”所有能量、价格、损耗系数全部折算到MWh基准再写进代码。尤其是气体类建议都用等值热功率表示而不是质量流量或体积流量。5.2 储热罐SOC容易溢出或出现负值初始SOC给得不合适或者放电效率计算公式反了都会导致SOC在求解结果中出现越界。排查时不要只看最终SOC曲线要把E_tes的递推约束逐项代入检查。另外储热罐本身有最低容量限制。很多人只写上限不写下限结果优化器会把SOC压到接近0这在物理上通常意味着熔盐泵抽不出足够的热量。我在代码里会把下限设成容量的10%作为死容量。5.3 P2G不启动结果看起来像退化成普通光热模型如果P2G在任何时刻都不启动先看电价和启动成本是否合理。电解槽运行一小时消耗20MWh电电价如果是0.1元/kWh运行成本就是2000元产出的氢等值热值假设10MWh氢并没有价格收入目标函数里只算成本那么P2G当然不会启用。因此需要在目标函数中给氢负荷和气负荷的需求满足设置经济价值或者把它放进平衡约束里用购气成本体现替代价值否则优化器只会把P2G当作一个纯耗电设备永远不启动。另一个原因可能是P2G最小技术出力下限设得太高。电解槽最小负载率20%如果额定功率20MW最小出力就是4MW而凌晨风电富余出力可能只有3.5MW差一点点就不满足可行性。这种情况下需要调整P2G额定容量或者允许从电网买一些低谷电来给P2G补功率。5.4 ORC效率取定值导致出力曲线过于平滑ORC效率取常数会让ORC出力与热输入严格成正比看起来曲线光滑但这掩盖了热源温度变化的问题。更合理的做法是把ORC输入热源按温度等级分堆或者给ORC效率做一个温度修正系数。代码实现时可以用向量化的eta_orc_t替换单个常数不需要引入非线性约束。5.5 储热充放互斥约束写得太硬求解变慢用两个二进制变量分别表示充热状态和放热状态会让模型增加很多Binary变量。如果只是日调度T24小时大概增加48个0-1变量求解速度还能接受。如果再加入电解槽启停、ORC启停二进制变量数量快速膨胀。我的经验是先求解一个没有互斥约束的松弛版本看看松弛结果里是否大量出现“同时充放”的不合理工况如果很少出现就不用加互斥约束而是通过设置充热和放热成本来让优化器自觉规避。当二进制变量数量超过100个时优先考虑减少整数变量而不是急着换求解器。5.6 结果看起来“太完美”也要警惕如果某一天光热、ORC、P2G全部满负荷运行购电为零、弃风为零不用高兴太早。先把储热罐SOC和储氢罐SOC的初始值和终值打印出来。如果SOC从初值0.5拉到0.95意味着系统在“白嫖”储能容量。这是调度模型中常见的花招优化器通过透支储能的可用容量来降低运行成本。解决方法是在目标函数里给储能SOC轨迹一个终端恢复约束比如要求SOC_T不低于SOC_0。这样调度结果才是可持续的。最后再补充一个实操建议我每次搭建这类综合能源模型都会先跑一个“去掉所有二进制变量”的连续版本也就是把启停约束全部暂时注释掉让各设备可以连续调节。这个版本求解非常快用来检查能量平衡、单位换算、SOC递推公式是否正确是最高效的方式。连续版本跑通了再把启停变量逐个加回来每加一组就校核一次求解时间。这比一次性写完所有约束再疯狂debug要省太多时间。如果当前模型里二进制变量太多导致求解卡住我用的另一个技巧是把时间分辨率先拉长到2小时或者4小时先把逻辑跑顺再逐步加密时间步长直到恢复到1小时分辨率。这套方法几乎适用于所有综合能源优化调度模型的Matlab开发。