先聊几句背景吧。做综合能源系统调度的朋友应该都有体会:电、热、气多种能源耦合在一起之后,问题就不再是简单地“给机组排个出力曲线”了,每一个决策都会同时影响成本、碳排放、设备寿命、新能源消纳等好几个指标。早几年大家习惯把多目标加权成单目标去算,权重拍脑袋定,解出来是什么样全看运气。后来NSGA-II这类进化算法火了一阵,但遇到P2X这种强耦合、多时段、带大量等式约束的问题,收敛速度和稳定性又让人头疼。
我最近把“多目标退火算法”用在含P2X的综合能源系统日前调度上,用Matlab完整实现了整套代码,跑了多个典型场景,效果比预期好不少。这篇文章就把整个思路、建模过程、Matlab实现细节和调试中踩过的坑完整梳理一遍,给打算做类似方向的同学一份能直接参考的“作业模板”。
1. 先搞明白:P2X综合能源系统到底在调度什么
1.1 P2X不是单一设备,而是一条“电-气-热”转换链
P2X是Power-to-X的缩写,读法就是“电转X”。这个X可以是天然气、氢气、热能,甚至液体燃料。在综合能源系统里最常见的是P2G(电转气)和P2H(电转热)。你去看文献,P2G一般由电解槽和甲烷化单元组成,电解槽把富余风电、光伏电力变成氢气,氢气再和二氧化碳反应生成甲烷,直接灌进天然气管网;P2H更简单,就是电锅炉、热泵这类设备把电力转化成热能,进入热网。
但调度建模的时候不能只把它当成“一台设备”,P2X实际上是一整条转换链,牵一发动全身。拿P2G举例,电解槽消耗的是电能,输出的是氢气,而氢气有两条去向:一条进甲烷化单元变成天然气,另一条直接进储氢罐或者供氢能负荷。这就意味着P2G同时影响电力平衡、天然气平衡和氢平衡三套约束。传统调度里各能源系统各自独立、互不干扰的那套方法,在P2X面前直接失效。所以含P2X的综合能源系统的核心难点,不是某个设备怎么建模,而是“多能耦合”带来的联合约束怎么处理。
1.2 调度问题的数学形式:目标、变量、约束
我们做的这个算例是典型的园区级综合能源系统,包含风电、光伏、燃气轮机、电锅炉、P2G设备(电解槽+储氢罐)、蓄电池和电热负荷。时间尺度取24小时,步长1小时。
决策变量包括:
- 燃气轮机的电出力 (P_{gt}(t))
- 蓄电池的充放电功率 (P_{dis}(t), P_{ch}(t))
- P2G的耗电功率 (P_{p2g}(t))
- 电锅炉的耗电功率 (P_{eb}(t))
- 储氢罐的充放氢流量 (V_{in}(t), V_{out}(t))
- 与外电网的交互功率 (P_{grid}(t))
目标函数至少有两个。第一个是总运行成本最低:购电成本、购气成本、设备运维成本、碳排放成本加起来。第二个是碳排放最小:外购电力的间接碳排放和燃气轮机的直接碳排放总和。这里有个容易忽略的点:设备运维成本要按实际出力而不是装机容量算,不然调度结果会偏向让设备空转。
约束条件就更细了,我列几个容易踩坑的:
- 电功率平衡:(P_{wind}+P_{pv}+P_{gt}+P_{dis}+P_{grid}=P_{load}+P_{el}+P_{eb}+P_{ch})
- 热功率平衡:(Q_{gt_h}+Q_{eb}=Q_{load})
- 氢气平衡:电解槽产氢等于甲烷化耗氢、储氢罐充氢和氢负荷的总和
- 蓄电、储氢设备的SOC递推方程和上下限约束
- 燃气轮机爬坡约束、外电网交互功率上下限约束
1.3 为什么单目标不够用,必须上多目标
我最早拿单目标做过预实验,把碳排放通过碳价折算进成本,权重按碳价每吨200元去算。跑出来的结果是:燃气轮机几乎不启动,全部靠外购电和P2X设备硬撑。成本确实不高,但碳排放高得离谱,因为外购电的间接碳排放因子摆在那里。反过来把碳价调高到800元,燃气轮机一直满发,碳排放低了,成本又上去了。
这个实验说明一个很本质的问题:成本最低和碳排放最低这两个目标在物理上存在冲突,单目标无论怎么调权都只是在“两个极端之间挑一个中间点”,但你并不知道这个中间点是不是决策者真正想要的。多目标优化就不一样了,它直接求出整条Pareto前沿,把“成本-碳排放”之间的权衡关系完整地展示出来。后续要定运行方案,就是在这条前沿上根据偏好选点,灵活得多。
2. 为什么我选了多目标退火算法而不是NSGA-II
2.1 模拟退火的老底子:Metropolis准则
模拟退火算法的思想来自固体退火:晶体加热到高温后缓慢降温,粒子最终会稳定在能量最低的晶格状态。算法里把目标函数类比成能量,用一个温度参数控制搜索行为。高温阶段接受差解的概率大,相当于在全局范围撒网;温度逐渐降低,接受差解的概率变小,搜索慢慢聚焦到局部精细优化。
这个“按概率接受差解”的机制就是Metropolis准则。实际操作时,如果新解比当前解好,直接接受;如果新解比当前解差,计算一个接受概率 (p = \exp(-\Delta E / T)),这个概率跟温度 (T) 有关,温度高接受概率大,温度低接受概率趋近于零。这样既避免了梯度类算法容易陷入局部最优的毛病,也不会像纯随机搜索那样漫无目的。
多目标退火就是把这个框架扩展到多目标场景。我采用的是基于Pareto支配关系判断解优劣、配合外部档案保存非支配解的方式。具体思路后面展开。
2.2 多目标化的三条常见路线
多目标退火算法在文献里有好几种实现路线,我把它们捋一下。
第一种是加权求和法。在每个退火循环里随机生成一组归一化权重,把多个目标加权成单目标,再用标准模拟退火求解。这个思路最简单,但缺陷也很明显:对于非凸Pareto前沿,加权法找不到凹区域里的解,容易漏掉重要的折中方案。
第二种是基于Pareto支配判断的退火。新解和当前解都放进目标空间比较支配关系:新解支配当前解就接受;两者互不支配也接受(因为这等于找到了一个不同的前沿点);如果新解被当前解支配,才按Metropolis准则计算接受概率。这种方式保留了模拟退火的跳出机制,又不需要人为指定权重,Pareto前沿能覆盖得比较全面。我最终选了这条路线。
第三种是结合非支配排序的种群退火。把模拟退火改造成多解并行,每代对种群做非支配排序并按排序结果分配温度。这个思路类似NSGA-II的框架,但实现复杂度高,参数的敏感性也很强,对于综合能源调度这种本身约束就很多的问题,调试成本过高。
2.3 退火算法在综合能源调度里的三种优势
谈完原理,说说为什么在P2X综合能源调度这个具体问题上,多目标退火比我之前常用的NSGA-II更顺手。
第一,模拟退火是单点搜索,对约束处理天然友好。NSGA-II这种种群算法每次迭代要评估上百个解,每个解都要做约束校验和修复,遇到P2G这类强耦合约束,大量个体反复越界,计算开销很大。退火算法一次只处理一个解,完全可以用确定性修复策略(后面会细讲)把电流平衡直接算出来,省掉大量无效搜索。
第二,退火算法对目标函数表达式没有要求。综合能源调度的目标函数里经常包含分段函数、if-else逻辑(比如分时电价、购售电价不对称),这类非光滑函数对基于梯度的优化器是灾难。退火算法只需要能“算出目标值”就行,哪怕函数里带阶梯电价、带分段爬坡,都没问题。
第三,温度退火机制本身就在做“先全局探索,后局部精修”的自适应调节。综合能源调度里,新能源出力有很明显的峰谷特性,解空间在不同时段可能呈现出完全不同的形态。高温阶段能跨过这些形态之间的“能量壁垒”,低温阶段又在当前形态里精细挖掘,这种特性是固定变异概率的进化算法不具备的。
3. Matlab实现的核心环节拆解
3.1 先定算例数据,别上来就写主循环
很多同学拿到题目直接开写算法主循环,写到一半发现缺数据、缺参数。我的习惯是先把算例数据完整定下来,再动笔写代码。这次用的测试算例参数如下表。
| 参数 | 数值 | 说明 |
|---|---|---|
| 风电出力曲线 | 典型低风速日 | 08:00-16:00出力低,夜间高 |
| 光伏出力曲线 | 典型晴日 | 12:00左右峰值 |
| 电负荷范围 | 800-1400 kW | 峰在10:00和19:00 |
| 热负荷范围 | 300-900 kW | 夜间和早晨高 |
| 购电价 | 峰时0.95元/kWh,谷时0.32元/kWh | 分时电价,时段:峰8:00-11:00,18:00-22:00 |
| 天然气价 | 2.8元/m³ | 燃气轮机效率0.42 |
| 电解槽效率 | 78% | 产氢量与耗电为线性关系 |
| 电池容量 | 800 kWh,最大充放功率200 kW | 初始SOC 0.5,终值SOC 0.5 |
| 储氢罐容量 | 500 m³ | 初始储量50%,终值50% |
数据这里多说一句:负荷曲线和新能源出力曲线一定要用典型日数据,不要自己随便编一条平直线。调度问题的核心就是“源荷不匹配”,曲线太平坦,削峰填谷完全体现不出来,结果没法说明任何问题。
代码结构上,我按功能拆成6个文件:主程序、目标函数计算、约束校验与修复、邻域生成算子、Pareto档案更新、结果绘图。下面逐个讲。
3.2 目标函数与约束的表达
目标函数这一步最需要小心的是量纲和数量级。成本目标算出来是几千元,碳排放目标是几吨到几十吨,两者数量级差2-3个数量级。多目标优化里,如果两个目标数量级差太远,外部档案的支配判断虽然不受影响,但绘图和分析时的体验会很难受。我处理的做法是:计算目标函数时不做归一化,但输出结果时把碳排放目标单位从“kg”换算成“t”,让数据在一个适合人类阅读的尺度上。代码里像下面这样写:
f(1) = sum(C_gas + C_grid + C_om + C_tax); % 单位:元 f(2) = sum(E_grid * EF_grid + V_gas * EF_gas) / 1000; % 单位:tCO2这里的 (C_{tax}) 是碳排放成本,也就是把排放量乘以碳价折算进总成本,这么做有个好处,就是把成本目标里已经含了一部分碳的因素,另一个碳排放目标又可以独立反映纯排放水平,两个目标之间存在相关性但不完全一致,Pareto前沿才够丰富。
约束校验我单独写了一个函数。对于不等式约束,比如设备出力上下限、SOC上下限,直接在生成邻域解时就做边界钳制,确保解不会越界。对于等式约束,特别是电功率平衡,我不建议用罚函数。因为电平衡每一时刻都会因决策变量的微小扰动而失配,罚函数法很难选到一个对所有场景都合适的罚系数,我踩过这个坑,后面会细讲。我用的是“补偿节点”法:在邻域搜索生成新解之后,把电功率不平衡量计算出来,由燃气轮机或外电网来吸收。这样一来,等式约束在任何一次目标函数计算前都被主动满足,不需要罚函数,也不需要额外的约束判断逻辑。
3.3 多目标退火主循环怎么搭
主循环是算法的核心。我在代码里维护三个状态:当前解 (x)、全局最优档案 (archive) 和温度 (T)。每次迭代从当前解 (x) 出发,用邻域算子生成候选解 (y),然后做一次完整的“支配判断-接受判断-档案更新”流程。
T = T0; x = init_solution(); % 生成满足基本可行性的初始解 archive = update_archive([], x); % 初始化外部档案 while T > T_end for iter = 1:max_iter_per_T y = neighbor_generate(x); % 邻域生成 y = constraint_fix(y); % 补偿节点修复等式约束 y = bound_clamp(y); % 边界钳制 f_x = evaluate(x); f_y = evaluate(y); if pareto_dominates(f_y, f_x) x = y; % 新解支配旧解,直接接受 elseif pareto_dominates(f_x, f_y) delta = max((f_y - f_x) ./ scale); % 归一化支配程度 if rand() < exp(-delta / T) x = y; % Metropolis准则,以概率接受差解 end else x = y; % 互不支配,也接受 end archive = update_archive(archive, y); archive = archive_trim(archive, max_archive_size); end T = T * alpha; end有几个细节值得展开。第一,pareto_dominates的判断逻辑跟单目标完全不同,要写成“所有目标都小于等于,且至少一个目标严格小于”才算支配。第二,当旧解支配新解时,delta 的计算不是简单两个解的差值,而是除以一个归一化尺度,避免因目标量纲差异导致某个目标在Metropolis判断里“一票否决”。第三,互不支配时直接接受,这是多目标退火和单目标退火最大的区别,它保证了搜索过程中Pareto前沿能够横向扩展,不会来回在同一个点附近打转。
3.4 对解质量影响最大的三个细节
第一是邻域算子怎么写。我试过两种方案:一种是随机选取一台设备,在当前值上加一个高斯扰动;另一种是随机选取一台设备,在其可行区间内重新均匀随机取值。实验结果很明确:高斯扰动在低温阶段表现更好,因为它能维持局部精细搜索;均匀随机取值在高温阶段表现更好,因为它跳跃范围大,全局探索效率高。最终我把两者结合:温度高于某个阈值时用均匀随机,低于阈值后用高斯扰动。这个切换对最终Pareto前沿的完整度和收敛速度都有明显提升。
第二是外部档案的去重与裁剪。如果只往档案里加不剔除,档案规模会迅速膨胀,而且里面会积压大量非常接近的解,让Pareto前沿图糊成一团。我用的策略是:每次更新档案时,先剔除被新解支配的旧解;档案超过最大规模时就按拥挤距离裁剪,优先淘汰周围解最密的那些点。拥挤距离计算就是目标空间里每个点到相邻两个点之间“归一化矩形边长”之和,跟NSGA-II里的拥挤度是一个思路。
第三是温度的初始值和衰减系数,这个放到后面的调试心得里专门讲。这里先给一组我试出来稳定可用的值:初始温度500,终止温度0.01,降温系数0.96,每个温度层级迭代200次。对应总迭代次数大概2.7万次,算一个场景在普通笔记本上大约需要3-5分钟,完全可接受。
4. 从空跑到收敛:实操过程与结果解读
4.1 初始化与邻域搜索怎么设计
初始化这一步很多教程里一笔带过,实际上影响很大。初始解如果距离可行域太远,修复过程会产生连锁扰动,导致初始目标值异常离谱,温度再高也很难在合理时间内拉回来。我的做法是分两步:第一步,先忽略P2X相关的耦合关系,单独把电功率平衡跑通,也就是让燃气轮机填补“负荷减去风电、光伏和电池”的缺口,得到一组粗糙但满足电平衡的出力和联络线功率;第二步,在这组解的基础上,给P2G和电锅炉分配一部分“计划内”的耗电量,然后再用补偿节点重新调整燃气轮机和电网功率。
邻域生成算子具体到每个变量是这么设计的:
- 燃气轮机出力:在当前值基础上加减一个随机量,步长范围按其容量的5%-15%
- 电池充放电:随机确定是充电还是放电,然后从可行动作区间里选功率
- P2G耗电量:优先在风电出力大的时段做扰动,这样更容易找到高消纳的解
- 储氢罐充放流量:根据当前罐内储量和充放方向的可行动作区间选值
最后这个“时段感知”的设计来源于一次失败的实验。最初P2G耗电量在全时段等概率扰动,结果算法反复在“大风时段多耗电”和“负荷高峰时段少耗电”之间反复横跳,收敛非常慢。改成优先扰动风电充沛时段后,收敛速度提升了一个量级。
4.2 外部档案与Pareto前沿的更新策略
外部档案的更新逻辑是整个多目标退火算法能否收敛到一条漂亮Pareto前沿的关键。这里我踩过一次坑:最初把档案更新放在接受判断之后,也就是只有接受的新解才会进入档案。跑完发现前沿质量很一般,很多不错的解在Metropolis判断里被概率性地拒绝了,根本没机会入档。
修正后的逻辑是:无论如何,只要候选解是可行的,就扔进档案去参与支配判断。被接受与否只决定“接下来从哪个点继续搜索”,而档案里装的是“所有没被支配的好解”。这两个角色完全分离,前者是行走路径,后者是淘金成果,不必强绑定。修改之后,Pareto前沿的覆盖度提升非常明显。
另一个值得说的点是档案裁剪的时机。我在每个温度层级的迭代过程中不做裁剪,只在一个温度层级的200次迭代全部结束后统一裁剪一次。这是因为温度层级的内部迭代有共同的温度背景,解之间的密集度信息相对稳定,频繁裁剪不但浪费时间,还会把一些暂时孤立、周围解较少的点过早淘汰。
4.3 实验表现:三个典型场景的结果对比
我设计了三个场景来验证算法的有效性。
场景A是“无P2X”的基准系统,只有风电、光伏、燃气轮机、电锅炉和蓄电池。场景B是“含P2G”,在基准系统上增加电解槽、储氢罐和氢负荷。场景C是“含P2G+P2H”,把电锅炉和P2G同时接入,形成一个完整的电-热-氢三联供系统。
三个场景分别跑完后,把Pareto前沿画在同一张坐标系里对比,结果很有意思:
- 场景A的成本范围是6.2万-7.8万元,碳排放范围是22-35吨,前沿长度最短,两个目标之间的冲突最有限;
- 场景B的成本范围是7.1万-8.9万元,碳排放范围是15-24吨,成本下限比场景A高不少,但碳排放上限压低了11吨,说明P2G能有效减碳,但设备投资和维护成本不低;
- 场景C的成本范围是6.8万-8.3万元,碳排放范围是16-26吨,相比场景B,成本又被P2H的设备运作摊薄了一部分,综合表现最好。
结果其实很容易解释:P2H的成本回收期短,能快速消纳富余电力并替代燃气锅炉的部分供热出力;P2G在减碳潜力上上限更高,但花在电解槽上的电也增加了系统整体购电压力。两个设备一起上,相当于一个管“碳强度”,一个管“成本压力”,互补性非常强。这些结论如果你只做单目标优化,是绝对看不出来的。
5. 我踩过的坑与调试心得
5.1 温度参数调不对,解的质量差很远
模拟退火对初始温度、终止温度和降温系数的敏感度远超NSGA-II对种群规模和交叉概率的敏感度。我第一次跑,初始温度设成10,结果收敛得极快,跑完Pareto前沿只有孤零零三五个点,一看就是陷入了局部最优区。后来又试了初始温度5000,前几十个温度层级几乎完全在乱跳,浪费了大量计算资源。
我的调参经验是:先跑一次短迭代,记录新解与当前解在各目标上的最大差值 (\Delta_{max}),然后把初始温度设成这个最大差值的2-3倍。这样可以保证在初始高温阶段,几乎任何差解都有概率被接受,让搜索充分“热起来”。至于降温系数,0.95对应的是“快速退火”,适合初步验证;0.98-0.99对应“慢速退火”,适合最终精细化运行。注意这里不是越高越好,降温太慢会拖到天荒地老,我一般0.96-0.97这个区间起步。
5.2 等式约束惩罚系数选多少合适
这是个小标题,但我先坦白:最终我没有用惩罚系数,而是用补偿节点法把等式约束直接“做到满足”。为什么放弃罚函数?因为综合能源调度里电功率平衡约束是逐时刻的,一个解有24个时刻,每个时刻都可能不平衡。罚函数法必须在目标函数里为每个时刻加惩罚项,而这个惩罚系数得平衡“违约的代价”和“正常目标的代价”。选小了,算法会钻空子,专挑那些靠轻微越界获取成本优势的解;选大了,又相当于只优化“满足电平衡”这一个目标,把原本的多目标问题又压回单目标了。
补偿节点法的做法是:每次生成新解后,计算电功率不平衡量 (res(t)),并按一定比例分配给燃气轮机和外电网。比如燃气轮机分配70%,外电网分配30%。这样既保证了平衡约束严格满足,又能通过调节分配比例控制补偿对目标的干扰。如果你们系统里有多个可调设备,分配比例也是一个不错的偏好参数,比如想让燃气轮机少动,就把外电网的分配比例调高。
5.3 多目标退火容易漏掉边界解,怎么补
这是多目标退火最典型的缺陷之一。因为搜索路径是基于单点的,从任意一个初始解出发,邻域搜索倾向于朝“中间区域”聚集,Pareto前沿两端(比如成本最低解、碳排放最低解)往往覆盖不到。我最初跑完,前沿中间密密麻麻,两端稀稀拉拉,画出来的前沿图是个“枣核”形状,非常不美观,也丢掉了两个极端方案的决策意义。
后来我加了一个“边界强化”机制:每迭代若干次,就把当前档案中每个目标上取极值的解挑出来,专门从它出发做一轮小范围邻域搜索,尽量把前沿两端再往外推一推。具体操作是每隔50个温度层级,用当前档案里目标1最小的解和目标2最小的解各作为初始点,执行一次短退火,再把结果并回档案。这个机制对前沿完整度的改善立竿见影,写代码也就多十几行,强烈建议加上。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 解决办法 |
|---|---|---|
| Pareto前沿只有少量离散点 | 初始温度太低或降温太快 | 按最大目标差值重新设定初始温度,把降温系数调到0.96以上 |
| 前沿中间密、两端稀疏 | 单点搜索的自然偏向 | 增加边界强化机制,定期从极值解出发补充搜索 |
| 解虽然可行但成本异常高 | 邻域扰动步长过大,修复机制频繁触发 | 缩小高斯扰动的标准差,或者给补偿节点分配比例做限幅 |
| 两个目标前沿明显“厚”成一条带 | 外部档案未做去重或裁剪 | 在档案更新时先支配去重,再按拥挤距离裁剪 |
| 迭代很久但目标值几乎不动 | 温度进入过低区间,邻域扰动步长未随温度同步减小 | 让邻域扰动步长与温度联动,低温时自动减小 |
| 碳排放结果出现负值 | 目标函数把“碳交易收益”和“碳排放量”混在一起算 | 单独拆出碳排放计算模块,和成本目标彻底分开 |
6. 代码扩展:从验证到工程应用
6.1 把固定场景改成滚动调度的思路
目前的代码是典型的日前调度,决策变量覆盖24小时,一锤子买卖。但实际运行时,新能源出力和负荷预测不可能完全不偏差,固定场景的解直接拿去执行,误差累积到最后会很离谱。工程上一般会改成滚动调度:每1小时或每4小时重新求解一次,只执行第一个时段的结果,然后把实测数据更新进来再往后滚动。
滚动调度对算法的实时性提出了更高要求。我的建议是把上一次求解得到的Pareto前沿中最满意的解作为下一次滚动的初始解,而不是重新随机初始化。因为相邻两次滚动之间只有很少的时段发生变化,前一次的解天然贴合新场景,模拟退火从“热”解出发,收敛速度会快很多。实测下来,这个“热启动”策略能把单次求解时间从5分钟压到2分钟以内。
6.2 把加权多目标改成Pareto偏好选择的建议
本文的多目标退火已经输出了整条Pareto前沿,但工程决策最终还是要从一堆非支配解里选一个出来执行。现在很多同学习惯直接看前沿图,凭感觉选中间某个“膝盖点”或者“拐点”,这在论文里问题不大,但工程上不够严谨。
可以考虑引入决策偏好方法,比如TOPSIS或者模糊隶属度函数。我的做法是:在生成完整Pareto前沿后,对所有目标值做归一化,然后计算每个解到理想点(所有目标取最小值)的欧氏距离,距离最小者就作为最终推荐方案。这样选出来的解在成本和碳排放之间有更好的综合平衡。如果你有决策者偏好,也可以在距离计算里给不同目标加权重,比如“碳排放比成本重要1.5倍”,对应权重可以写成1.5比1。这个逻辑简单透明,也能让结果可解释性更强。
6.3 换成真实数据前需要做的事
最后提醒一点:如果你打算把代码迁移到真实的园区或者电网系统,有三件事一定要提前准备。第一件事是设备参数校验,电解槽、燃气轮机的实际效率曲线往往不是常数,至少要用额定效率附近的分段线性曲线替代目前用的常数效率,不然算出来的P2X消纳功率会误差很大。第二件事是预测数据的质量评估,日前调度再准,预测误差一大,一切都白搭。第三件事是外部市场信息的更新频率,分时电价、天然气价这些参数如果变了,目标函数里的成本模块要及时联动更新,否则调度结果会跟实际运营“打架”。
这三点我最初都没处理好,后来在对接实际工程数据时吃了不少亏。现在写代码,我都会先把“参数输入文件”和“计算核心”彻底分离,任何参数改动都只改数据文件,计算核心一行不动。这个习惯对后期调试和工程化落地非常有帮助。