
做综合能源系统调度的朋友应该都有同感模型建起来不难难的是让优化结果在真实运行里站得住脚。我最早做的版本就是一个简单的24小时日前调度风电、负荷预测都给的是“完美曲线”算出来的成本漂亮得很但一拿到实际场景里就露馅——风电一波动、负荷一偏移原本的“最优计划”立刻变成高成本运行方案。后来我把思路改成两阶段也就是标题里说的“日前日内滚动优化调度”再叠加分时电价和需求响应效果才真正稳定下来。这篇就把整个建模思路、两层调度框架的分工、以及Matlab代码落地的关键细节一次讲透适合正在做综合能源系统优化方向毕业设计、科研复现或者园区级调度仿真的人参考。1. 为什么“一本调度模型跑到底”不现实1.1 单阶段调度的三个软肋单阶段日前调度的逻辑很直观预测未来24小时的风电出力和负荷曲线把设备出力、储能充放电、购电计划一次性全算出来然后按这个计划执行一整天。问题在于这个思路隐含了一个前提——预测是准的而现实中这个前提基本不成立。风电和光伏的预测误差在小规模园区场景里尤其明显。一个装机几百千瓦的风电场短期预测误差达到20%到30%很正常。负荷侧也没有好到哪里去工作日和节假日的用电模式完全不同即便同是工作日上午和下午的波动也很难精确预判。于是问题就来了日前计划定了燃气轮机满发、储能中午放电结果实际光伏突然大发储能放出来的电没人用只能低价反送电网反过来如果预测的负荷曲线偏高实际却偏低机组又得多出力白白烧气。第二个软肋是机组启停决策被“锁死”。很多园区级综合能源系统的燃气轮机一旦启动最少运行时间可能是两三个小时甚至更长日前计划的启停安排如果和实际偏差太大日内想改也改不动。这也是为什么单纯加大日前模型的预测精度并不能彻底解决问题。第三个软肋是经济性目标在实时层面“失真”。日前优化用的电价是分时电价没错但实际运行中如果出现短时功率缺额临时从电网买高价电的成本往往远超日前成本项里的估算。换句话说日前模型优化的是一条“平均意义上的最优路径”而实际运行需要的是一条“能应对波动的可行路径”。1.2 分时电价和需求响应不是“锦上添花”而是必须考虑分时电价把一天切成峰、平、谷几个时段本质上是给调度问题增加了一个时间维度上的价格信号。如果模型里不考虑这个信号所有时段都按同一个电价买东西储能就没有套利动机蓄热罐也不会知道该在哪个时段蓄热。这样的模型算出来结果虽然可行但经济性会差一大截。需求响应则让“负荷”这个概念从刚性变成了柔性。用户的用电行为会随着电价变化谷时段多用电峰时段少用电或者把洗衣、充电这些可转移负荷挪到低价时段。这种柔性如果调度模型不捕捉就会有两层损失一是放弃了削峰填谷带来的电费节省二是可能在峰时段被迫高价购电来满足刚性负荷。我见过不少初做这个方向的人把需求响应理解成“把负荷曲线改小一点”就完事这是不够的。需求响应的核心是“用户用电行为对价格信号的定量响应”需要建模成负荷转移量、转移约束和补偿成本而不是简单改一个负荷系数。后面第2.4节我会专门讲这个怎么落成约束。1.3 两阶段框架计划与修正的分工两阶段调度框架的设计思路说白了就是“宏观定盘、微观纠偏”。日前阶段Day-Ahead在当天之前运行时间分辨率一般是1小时优化未来24小时的机组启停、储能充放电、外购电计划、可转移负荷安排。这一阶段的优势是可以从全局视角做经济最优缺点是它依赖的预测精度有限。日内阶段Intraday在当天运行时间分辨率可以做到15分钟每隔15分钟重新优化一次未来4小时窗口内的各设备出力用滚动优化Receding Horizon的方式不断修正日前计划让实际运行跟随最新预测走。这套框架本质上就是模型预测控制MPC“预测-优化-执行-再预测”的循环。和开车导航一样出发前规划好路线是日前路上遇到堵车实时换路是日内。两阶段的结果是日前层保证系统运行在一个全局较优的区间里日内层保证系统在碰到扰动时依然平稳、经济地运行。2. 先把综合能源系统的“身子骨”建模建对2.1 系统拓扑我用的这套算例配置综合能源系统的拓扑方案很多取决于研究场景。我用的是一套比较典型的园区级配置既包含电-热耦合又不会复杂到让模型无法调试风机和光伏可再生能源优先消纳燃气轮机CHP热电联产供电供热燃气锅炉补热电锅炉把富余电能转成热能本质是电热转换的灵活性资源蓄电池电储能蓄热罐热储能外部电网连接点可购电也可反送受限外部气网连接点购气这套拓扑覆盖了“源-网-荷-储”各个环节又保留了电转热这种跨能源品种的耦合关系做日前日内两阶段优化非常合适。设备参数我调试时常取下面这组数量级不同项目可自行调整设备参数示意取值燃气轮机CHP额定电功率500 kW燃气轮机CHP热电比1.2燃气锅炉额定热功率800 kW电锅炉额定电功率300 kW电锅炉电热转换效率0.95蓄电池容量600 kWh蓄电池最大充/放电功率150 kW蓄热罐容量800 kWh光伏峰值功率400 kW分时电价峰/平/谷1.0 / 0.6 / 0.3 元/kWh天然气价格我按2.5元/m3处理天然气热值按约10 kWh/m3折算。这个参数体系下一台500 kW的CHP满发时输入燃气功率约1250 kW折合小时耗气量125 m3左右成本约312元/小时。2.2 关键设备模型CHP、储能、蓄热和可再生能源设备建模是整个优化问题的基础。模型精度不用太高但要抓住每个设备最影响调度决策的特征。**CHP热电联产**是系统里最重要的耦合设备。我用的简化模型是“定热电比效率系数”给定电出力P_chp(t)和热出力H_chp(t)满足H_chp(t) r_chp * P_chp(t)其中r_chp是热电比取1.2。燃气消耗量则由电效率和热效率共同决定G_chp(t) P_chp(t) / (eta_chp_e * LHV)这里eta_chp_e是发电效率可用0.4。这种近似模型在日前日内调度层面已经足够如果是做详细热力分析再用机组可行域精细化建模。蓄电池的模型核心是SOC荷电状态递推方程SOC(t1) SOC(t) (eta_c * P_es_c(t) - P_es_d(t) / eta_d) * dt / Cap其中eta_c、eta_d是充放电效率我在算例里都取0.95Cap是容量。充放电功率各有上下限SOC要限制在[0.1, 0.9]区间同时还要防止“同时充放电”这种违反物理常识的决策。蓄热罐和蓄电池的建模逻辑几乎一样只是能量载体换成了热能递推方程是Tank(t1) Tank(t) (eta_hc * H_tank_c(t) - H_tank_d(t) / eta_hd) * dt蓄热罐的效率我取0.9容量上下限和SOC一样处理。它的价值在于配合电锅炉当风电或者光伏大发而电负荷不足时用电锅炉把多余电能转成热存进罐里等到热负荷高峰或者电价峰时段再放出来。可再生能源的建模最简单一般当作负的负荷或者出力上限。日前阶段用24小时的短期预测曲线日内阶段用未来4小时超短期预测曲线。如果要做鲁棒优化可以把预测区间上下界作为不确定性集合这里是两阶段确定性优化直接取期望预测即可。2.3 功率平衡与网络交互约束能量平衡约束是模型“立身之本”分电和热两条独立的平衡等式。电力平衡约束P_grid(t) P_wt(t) P_pv(t) P_chp(t) P_es_d(t) P_load(t) P_es_c(t) P_eb(t)热力平衡约束H_chp(t) H_gb(t) H_tank_d(t) H_load(t) H_tank_c(t)其中P_grid(t)是从电网购电功率P_eb(t)是电锅炉的用电功率H_gb(t)是燃气锅炉热出力。与电网交互约束通常还包含两个细节第一是购电功率有上限配电容量限制了你能从电网拿多少电第二是反送电处理很多场景不允许反送或者反送功率有严格上限我在算例里设置反送功率上限为0也就是只允许购电。这里要注意如果允许反送电价和购电电价不一样目标函数里要分别设变量不能只用一个有正负的P_grid直接乘电价。燃气消耗则汇总CHP和燃气锅炉两部分购气成本按总耗气量计算。气网侧的容量约束相对宽松一般只设一个总购气上限。2.4 需求响应怎么转成可计算的约束需求响应的建模有两类常见做法实践里建议结合使用。第一类是价格弹性系数法它用来估计“用户看到分时电价后负荷会怎么变”。基本公式是负荷变化率等于自弹性系数乘以电价变化率同时还要叠加交叉弹性其他时段电价变化对本时段负荷的影响。这类方法的麻烦在于电价和负荷同时是决策变量或变量函数时会引入非线性项所以我在实际项目中通常把它放在“负荷预测预处理”环节先用已知的分时电价和弹性系数把原始基础负荷修正成“考虑价格响应后的负荷需求曲线”再作为已知参数输入优化模型。第二类是可转移负荷约束它把需求响应直接做进优化模型中。设L_shift(t)为t时段从原时段转移到其它时段的负荷量L_in(t)为其它时段转移到t时段的负荷量那么整个调度周期内需要满足转移量守恒sum(L_shift) sum(L_in)同时每个时段的转移量有上下限比如峰时段最多转移20%的负荷谷时段最多只能接受一定量的转入。实际用电负荷就变成原始负荷减去转出、加上转入这个值直接进电力平衡约束。为了保证转移的经济性合理目标函数里要加需求响应补偿成本。补偿单价我按0.1元/kWh设置这个量级和工业用户可中断负荷的常见补偿水平接近。有了补偿成本模型就会自动权衡“转移负荷带来的购电成本节省”和“需要付出的补偿成本”只有峰谷电价差足够大时才会选择大额转移——这正是需求响应该有的行为。3. 日前层定全天的盘子给机组一个稳定的启动计划3.1 日前目标函数成本项的拆分逻辑日前层的目标函数是系统日运行综合成本最小我拆成四个部分购电成本各时段购电功率乘以分时电价这是最直观的一块。分时电价的价格信号就是通过这一项影响储能的充放电决策。购气成本CHP和燃气锅炉的耗气量乘以天然气价格。购气成本把“多发电”和“多产热”的经济代价传导进模型。设备运维成本各设备出力乘以单位运维成本。虽然数值上比购电购气小但不能忽略否则模型可能出现“为了省0.01元电费让设备频繁启停”的不合理行为。需求响应补偿成本可转移负荷量乘以补偿单价用来平衡负荷转移带来的效益。写成目标函数的简化形式是min sum_t (price_elec(t) * P_grid(t) price_gas * G_total(t)) * dt sum_t (C_om * P_out(t)) sum_t price_dr * L_shift(t)日前层的时间分辨率是1小时即dt1T24。这个时间粒度足够看清系统全天的运行趋势又不至于让模型规模失控。3.2 约束落进YALMIP的具体写法代码层面我用YALMIP建模。变量定义是最关键的一步我习惯按设备分组定义方便检查。核心变量包括P_chp sdpvar(1, 24); % CHP电出力 H_chp sdpvar(1, 24); % CHP热出力 P_gb sdpvar(1, 24); % 燃气锅炉热出力 P_eb sdpvar(1, 24); % 电锅炉耗电 P_grid sdpvar(1, 24); % 电网购电 P_es_c sdpvar(1, 24); % 蓄电池充电功率 P_es_d sdpvar(1, 24); % 蓄电池放电功率 SOC_es sdpvar(1, 25); % SOC多一个点存初末状态 L_in sdpvar(1, 24); % 转入负荷 L_shift sdpvar(1, 24); % 转出负荷蓄电池“不能同时充放电”的约束我用两组0-1变量加一个大M实现z_c binvar(1, 24); z_d binvar(1, 24); Constraints [Constraints, z_c z_d 1]; Constraints [Constraints, 0 P_es_c 150 * z_c]; Constraints [Constraints, 0 P_es_d 150 * z_d];注意SOC变量定义成25个点因为递推式里SOC(1)是初始状态SOC(25)是24小时结束后的状态这样写递推时下标不容易越界。负荷转移的守恒约束Constraints [Constraints, sum(L_shift) sum(L_in)]; Constraints [Constraints, 0 L_shift 0.2 * P_load_base]; Constraints [Constraints, 0 L_in 0.2 * P_load_base];其中0.2是峰值负荷的可转移比例上限实际需求响应项目中这个比例需要根据用户合同和负荷特性来定。3.3 一组日前调度结果长什么样用前面那套参数跑出来的日前计划曲线特征其实非常有规律。这里给出一组示意结果时段电负荷需求光伏出力CHP出力购电功率蓄电池动作谷段 23:00-7:00200-300 kW0400-500 kW较低满功率充电峰段 10:00-15:00600-700 kW100-150 kW500 kW高满功率放电平段 15:00-18:00400-500 kW80-120 kW400-450 kW中等浅充浅放谷段电价0.3元/kWh时蓄电池会持续充电电锅炉也会在有余电时启动把热量存进蓄热罐。峰段电价1.0元/kWh时蓄电池放电、CHP满发购电虽然在峰段高但被压到了最低必要水平。整体看日前层的决策就是“谷蓄峰放”“气电热联供”的组合拳分时电价带来的削峰填谷效果非常明显。需求响应的作用在这个阶段已经嵌入峰时段一部分可转移负荷会平移到谷时段。比如原本10点-12点有120 kW的工业洗涤负荷在需求响应约束下会有一部分被转移到凌晨2点-4点这直接削掉了峰段购电压力。4. 日内层用滚动窗口对冲预测误差4.1 滚动优化“滚”起来的完整流程日内滚动优化的流程简单讲就是“预测未来一段优化未来一段只执行眼前一步”。我用伪代码把完整流程写出来for k 0 : 15min : 总时长 - 15min 1. 读取当前时刻系统实际状态SOC、蓄热罐容量、设备当前出力 2. 更新未来4小时16个时段的风电/光伏/负荷超短期预测 3. 求解当前时刻起的16时段优化模型 4. 只执行第1个时段的控制指令各设备目标出力 5. 等待15分钟进入下一轮 end这个循环从当天0点一直滚到24点总共96次。每一次求解都在做同一件事以当前状态为初值以最新预测为输入算出一个局部最优控制序列然后“只见树木不见森林”地只取第一步。损失一点全局最优性换来的是对预测误差的持续修正能力——这个交换在工程上是值得的。4.2 窗口长度与预测更新的选择滚动窗口长度我建议取4小时也就是16个15分钟时段。为什么是4小时窗口太短比如1到2小时虽然求解速度快但模型“目光短浅”看不到未来电价变化和负荷高峰储能可能在窗口末尾把电放光了刚好错过下一个时段的峰价。窗口太长比如8到12小时一是超短期预测的精度优势发挥不出来二是求解规模变大三是不必要的约束会让日内决策被远处的错误预测带偏。预测更新方面日内层用的是“超短期预测”数据。以风电为例日前短期预测的误差随提前时间增大而明显增加而提前1到4小时的风电预测精度会好很多。换句话说日内层用到的输入数据本身就更可信这是滚动优化能改善运行效果的一个重要原因。4.3 怎么样让日内结果不跟日前计划“打架”日内滚动优化的目标函数设计是很多新手容易搞砸的地方。如果只设“当前窗口运行成本最小”那么日内层可能会为了省一点成本把日前定好的机组出力大幅度调整造成设备出力振荡也会让运行结果失去可解释性。反过来如果严格跟踪日前计划又失去了滚动修正的意义。我的做法是“双向加权”目标函数里同时包含运行成本项和偏差惩罚项。偏差惩罚指各设备出力与日前计划对应值的偏差绝对值之和乘上一个权重系数。权重设置经验是让偏差惩罚在数量级上略大于可能节省的运行成本具体做法先跑一次纯日前优化记下各类成本的量级然后取偏差惩罚系数为购电电价的一半到一倍。这样日内层在“偏离计划”和“节省成本”之间就有了一个合理的折中。另外日内层的决策边界也要注意它不应该重新决定机组启停。日前层定了启停计划日内层只调整出力大小、储能充放电功率和购电功率。如果日内层允许改启停状态等于否定了日前层的全局经济性安排而且会增加大量0-1变量拖慢求解。我见过有人在这个问题上纠结很久最后发现把日内模型的0-1变量全部去掉只留下连续变量调出力求解又快、结果又稳。5. Matlab实现里那些文档不会写明的坑5.1 建模工具链YALMIP加求解器的正确组合Matlab下做优化调度我强烈建议用YALMIP建模配Gurobi或者CPLEX求解。YALMIP的语法接近数学表达式写约束时能把注意力集中在模型本身而不是程序化表达。Gurobi和CPLEX的学术授权申请很方便速度在MILP这类问题上也足够快。版本兼容性值得提醒YALMIP是开源项目更新很快Matlab版本升级后建议同步更新到最新版YALMIP。我用R2023a配最新YALMIP和Gurobi 11.0稳定跑过很多轮没有出过问题。如果碰到YALMIP函数报错先考虑是不是YALMIP版本太旧。求解器配置也踩过坑如果电脑上同时装过多个求解器YALMIP默认调用的可能不是你想要的。稳妥做法是在optimize调用时显式指定options sdpsettings(solver, gurobi, verbose, 0); options.gurobi.MIPGap 0.01; % 设置MIP间隙 options.gurobi.TimeLimit 60; % 单次求解时限 optimize(Constraints, Objective, options);MIPGap设成0.01即1%的间隙对工程场景足够。不要默认设0否则整数变量一多求解时间可能翻好几倍。5.2 代码骨架与调试顺序代码结构我一般分成四个模块数据输入、日前模型、日内模型、结果导出。数据输入模块统一管理设备参数、预测曲线、电价表方便更换场景。日前模型和日内模型用函数封装便于单独调试。调试顺序很重要。我建议先不跑完整48个时段日前24日内96个点而是先用5个时段的小规模问题测试模型是否自洽约束有没有写漏、变量有没有放错位置、目标函数有没有算对。小规模问题几秒钟就能出结果出错了也容易定位。跑通了再扩到24时段、96时段的完整算例。一个小技巧在跑滚动优化之前先单独跑一遍“开环日内优化”也就是从0点一次性求解未来96个时段不滚动。如果开环结果和日前结果差距不大说明模型本身没问题如果开环结果已经出现异常就不要急着跑滚动先回头检查建模。5.3 求解速度和规模的平衡点整个两阶段模型的计算负担主要在日内滚动——96次优化每次求解一个16时段的MILP/LP。如果日内模型保留0-1变量96次MILP加起来时间可观。我的经验是日内层尽量设计成LP问题只调整连续变量把整数决策全部留给日前层。这个设计不仅是逻辑上的“日前管启停、日内管出力”也是性能上的关键选择。如果确实需要在日内层保留部分整数变量比如储能充放电状态切换可以给求解器设置合理的MIPGap和TimeLimit但更建议优先想办法用连续变量表达。比如储能充放电逻辑可以在日前层已经确定当天充放电模式的前提下把日内层的充放电状态固定为日前结果只允许功率大小线形调整。5.4 三个常见报错和完整排查链路报错一YALMIP提示“No suitable solver”或“No solver found”这个报错90%的可能是求解器路径没配好。Gurobi安装后需要在Matlab里运行gurobi_setup添加路径然后YALMIP才能在solver列表里找到它。排查链路先在命令行输入yalmiptest它会列出当前可用的求解器如果没有Gurobi检查Matlab路径如果显示Gurobi存在但仍报错检查license环境变量或者直接用sdpsettings(solver,gurobi)指定。报错二储能结果“同时充电又放电”这是新手最容易遇到的物理常识问题。原因是充放电两个变量没有用整数状态隔离开模型为了同时满足能量平衡和降低目标函数可能会让P_es_c和P_es_d同时为正。虽然目标函数里充放电成本不同会让模型“看起来有的赚”但物理上根本不可能。排查链路先检查约束里是否有z_c z_d 1再看充放电功率上限是否绑定了这些状态变量最后检查SOC递推式里的效率系数是否写反写反也会导致模型出现“边充边放”的最优解。报错三模型返回“Infeasible problem”无解问题的排查最考验耐心我的办法是“二分排除”。第一步把目标函数去掉只求满足约束判断问题是否本身可解第二步把约束按类别注释掉比如先去掉末端SOC约束再去掉蓄热罐约束逐步缩小范围。根据我的经验最常见的无解原因有两个一是末端SOC设得太严比如要求SOC(25)0.2但容量和充放电功率限制导致最后几个时段根本来不及充到这个水平二是可转移负荷的上下限和守恒约束互相矛盾比如限制峰段只能转出10%又限制谷段只能转入15%但实际需要转移的量比这个范围大得多。先把这些约束放宽再逐步收紧基本都能定位到问题。还有一次我遇到一个特别隐蔽的无解原因电锅炉用电进了电力平衡但热功率输出只加了上限没加下限而蓄热罐初始容量又设得特别低导致系统无法满足凌晨时段的供热需求。这类跨能源品种的耦合约束在单独看电平衡或者热平衡时都是对的合在一起才会暴露问题所以排查时一定要把电和热两条平衡线放在一起检查。最后再分享一点个人体会。这个项目最核心的收获不是把模型跑通而是理解“调度的艺术在于留有余地”日前层给的计划不应该是一条被“精确最优”框死的曲线日内层的修正也不应该把每个偏差都强行拉回计划值。权重设置、约束松紧、窗口长度这些看似“调参”的工作其实都在表达你对系统不确定性的态度。初做的时候别急着堆复杂度先把一个最简单的两阶段框架跑通再逐步加入需求响应、更多的设备耦合、甚至鲁棒优化的不确定性集你会更容易看清楚每个模块到底在解决什么问题。