做热电联产规划的人都知道选址定容是前期最折磨人的一环既要考虑热源靠近热负荷中心又要兼顾电力并网和燃料供应不仅要算当下投资还要把几十年的运行成本折进来更麻烦的是候选站址和容量档位一多组合爆炸式的方案数量根本不是人工枚举能扛住的。我这次用遗传算法Genetic AlgorithmGA在Matlab里写了一套热电联产选址定容程序目标就是把这道优化难题变成一套可复现、可扩展的求解流程。文章会从问题建模、GA方案设计、Matlab实现细节一直讲到调试实战和工程落地坑点。不管你是刚到电力设计院、综合能源规划岗的新人还是已经在做能源系统优化的工程师希望这份基于常见实践总结的完整拆解能帮你少踩几个坑。1. 项目背景与问题定义1.1 为什么聚焦热电联产选址定容热电联产系统Combined Heat and PowerCHP最显著的特点就是“热电耦合”。一台机组选在哪个位置、装多大容量会同时影响电网侧和热网侧的拓扑结构离负荷太远供热管道投资和沿程热损耗都会暴涨离负荷太近又可能受制于地块、环保和消防间距要求。更难受的是热负荷和电负荷并不同步冬季采暖期热需求大夏季可能很弱容量选大了机组长期低负荷运行效率掉得厉害容量选小了又会在寒潮期供热不足。所以选址定容本质上是一个“混合整数、多约束、非线性”的组合优化问题。传统的枚举法、动态规划在候选点超过8到10个、容量档位超过三档之后计算量会滚雪球式增长。遗传算法恰好擅长处理这类复杂空间搜索问题——它不需要梯度信息能处理离散变量和连续变量混合的情况还能通过适应度函数灵活嵌入各种工程经验约束这也是我最终选择GA作为核心求解器的原因。1.2 问题数学化决策变量、目标函数和约束要写程序先把脑子里的工程问题翻译成数学模型。我用“候选站址集”的方式来描述规划区域内预先给出若干允许建站的地点编号从1到N每个地点有对应的最大可用土地面积、环境容量和接入电网热网的条件。决策变量分为两类选址变量( s_i \in {0, 1} )表示第i个候选点是否建站定容变量( c_i \in {0, 1, 2, \dots, K} )表示第i个候选点选择的装机容量档位其中0表示不建站( c_i )只有当( s_i 1 )时有效。目标函数采用规划期中常见的“年总费用最小化”包括四个部分建设投资折算费用机组本体、厂房、接入工程费用按折旧年限平摊到每年运行维护费用主要包括发电电耗、燃料费以及固定运维费轮机启停和部分负荷效率损失费用这是CHP区别于普通发电机组的重点用热电比和负荷率修正曲线来近似热网投资费用从候选站址到各热力站的管道建设成本管道长度按曼哈顿距离或实际路网距离近似。约束条件我重点考虑了六类电力供应约束所有建站机组的发电容量之和加上上级电网购电功率必须大于等于区域总电负荷和备用容量热力供应约束各热力站的供热缺口( Q_{h, deficit} )必须为零同时每个机组的供热功率小于其额定供热能力供热半径约束单个CHP站点的供热半径不能超过管网经济输送距离超过就认为该站点无法覆盖某个热负荷块热电比约束机组实际运行热电比应在技术允许区间内避免纯凝运行或背压工况超限环境保护约束污染物排放总量不超过上限换算成机组燃料消耗量的限值逻辑约束同一候选点不能同时出现“选站但容量档位为0”或“不选站但容量档位非0”的情况。这些式子写起来不难但放到遗传算法里就需要仔细设计编码和适应度函数了。2. 遗传算法求解方案设计2.1 遗传算法在选址定容问题里的直观解释我之前看到有人问“在运输中遗传算法应该怎么解释”其实选址定容和运输问题的思路完全一样你有一张地图、一批货热负荷和电负荷、一批可能的仓库位置候选站址要决定仓库开在哪、建多大。遗传算法的做法就是把每一个“建哪几个站、各自多大容量”的方案看成一条染色体然后用“优胜劣汰”的方式不断进化。打个生活化的比方假设你有100个方案第一代方案大多是随手瞎凑的有的站建在湖边、有的容量小得离谱适应度很低。你按“省钱可靠”的标准给每个方案打分分高的更大概率被选中作为“父母”然后让父母方案互相交换部分决策变量交叉偶尔随机改变某些站的容量变异产生下一代。这样迭代几十上百代最后剩下的方案会非常接近真实最优解。整个过程不需要把解空间枚举一遍只需要评估足够多的样本就能收敛。2.2 编码与解码二进制选址 离散定容编码方式直接决定算法的搜索效率和程序复杂度。我在这个项目里采用“二进制选址基因 整数定容基因”的两段式染色体。设候选点数量为( N )容量档位数量为( L )。一条染色体的总长度为( N N \times \lceil \log_2 L \rceil )位。例如N10L8那么前10位是开关基因后30位是10个定容基因每个3位二进制总共40位。定容基因用二进制数表示容量档位解码时再把档位映射到实际容量值比如档位08MW、档位112MW、档位216MW...。这种编码的优点很明显交叉和变异操作可以统一处理二进制位Matlab的向量化运算也方便缺点是“不选站”时定容基因会冗余表达需要在解码阶段用一个掩码向量做乘法消除影响。解码的核心逻辑是% siteFlag: 1xN 的逻辑向量表示是否建站 % capGene: 1xN 的整数向量表示容量档位 % capLevel: 1xL 的向量存储各档位对应的实际容量(MW) if siteFlag(i) 1 cap(i) capLevel(capGene(i) 1); else cap(i) 0; end这段代码看起来简单但实际运行时需要注意Matlab的索引从1开始容量档位0要映射到capLevel(1)另外定容基因解码后如果超出L范围要用mod函数或饱和截断处理否则数组越界报错。2.3 适应度函数与约束处理适应度函数是GA的“指挥棒”。我一开始直接套目标函数求倒数作为适应度结果发现很多不可行解比如热力供应不足成本很低GA反而偏向这些解。后来我采用“外部罚函数法”先算目标费用再对违反约束的程度给予惩罚罚项最终适应度取倒数的同时把罚项放大。具体公式是fitness(rawCost, violationVector) 1 / (rawCost sum(penaltyFactor .* violationVector) 1);这里的violationVector包含六个分量电力缺口量、热力缺口量、供热半径越界总长度、热电比越界量、排放超限比例、选址逻辑矛盾数。penaltyFactor需要手工调整。我的经验是先给一个较大初值比如1e5然后观察收敛曲线。如果早期种群适应度全被罚项主导就适当降低罚项如果迭代结束仍有不可行解残留就提高罚项。这个参数需要反复打磨是整个程序调参的重头戏。2.4 GA参数与算子选择下面是我在这个工程里用到的实测参数配置表列出来供你参考参数项设置值说明种群规模200候选点越多种群越大10个候选点200个个体足够最大代数200实际跑下来一般在120代左右收敛交叉率0.85均匀交叉每对父母按位随机取父方或母方基因变异率0.01每个基因位独立变异概率防止过高的变异率破坏优秀方案精英保留个数10每代直接把适应度最高的10个个体复制到下一代防止最优解被交叉变异破坏选择算子随机联赛选择每次从种群中随机抽2个个体取适应度高者进入交配池重复N次终止条件连续30代最佳适应度提升量小于1e-6避免浪费计算时间现在来说说为什么这些参数这样取。交叉率0.85是经验值太高会让优秀基因快速被打散太低又导致搜索太慢变异率0.01虽然看起来小但配合200个个体、30个有效基因位每代大约会产生60个基因变动足够探索新区域。精英保留策略是很多人容易忽略的点——没有它即使找到最优解也可能在下一次交叉后丢失这个坑我踩过。3. Matlab程序实现与代码核心3.1 程序框架与流程控制整个程序我用一个主函数控制流程子函数各自独立方便维护和复用。主函数叫ga_chp_siting_design.m它的逻辑如下加载输入数据候选点坐标、热负荷块坐标、各负荷块热/电负荷时序曲线、容量档位表、电价和天然气价格初始化GA参数包括种群规模、最大代数、交叉率、变异率、惩罚系数生成初始种群用随机数生成选址基因和定容基因保证至少有一个候选点被选中进入进化循环对每个个体解码调用calc_cost计算总费用调用check_constraints判断约束违反量计算适应度用精英保留和联赛选择构造新种群执行交叉、变异记录每代最佳个体和目标函数收敛历史结果输出把最优方案写入Excel并调用plot_result画出站点布局和收敛曲线。这个流程听起来很简单但真正写的时候我强烈建议你从一开始就把“输入数据字典”统一管理。我曾经犯过一个低级错误候选点热负荷和电负荷向量顺序不一致导致GA找到一个看似很优、实际却张冠李戴的方案浪费了两天调试时间。3.2 各模块代码要点先看目标函数模块它是我这个程序里最复杂的部分。示例代码如下function totalCost calc_cost(decodedChrom, inputData) % decodedChrom: 结构体, 包含 siteFlag, cap, hrRad 等字段 % inputData: 结构体, 包含价格、负荷、候选点坐标等 capexAnnual sum(inputData.capCostPerMW .* decodedChrom.cap) ... / inputData.depreciationYears; fuelCost sum(inputData.fuelPrice .* decodedChrom.fuelConsume); opeCost sum(decodedChrom.cap .* inputData.omPerMW); heatPipeCost sum(calc_heat_pipe_cost(decodedChrom, inputData)); totalCost capexAnnual fuelCost opeCost heatPipeCost; end这里calc_heat_pipe_cost需要根据每个热负荷块与最近建站点的距离来计算管道投资和热损失。你可能觉得余热供热效率不会变但在实际项目中距离超过2km的热力管道热损失已经非常可观了所以在适应度中体现出来是合理且必要的。然后是约束检查模块我把它单独拎出来而不是塞进目标函数里便于单独调试function violationVec check_constraints(decodedChrom, inputData) violationVec zeros(1, 6); % 电力不足 violationVec(1) max(0, inputData.sumMaxElecLoad - sum(decodedChrom.cap)); % 热力不足 violationVec(2) max(0, inputData.sumMaxHeatLoad - sum(decodedChrom.heatCap)); % 供热半径越界 violationVec(3) sum(calc_radius_exceed(decodedChrom, inputData)); % 热电比越界 violationVec(4) max(0, abs(decodedChrom.heatPowerRatio - inputData.techRatioLimit) - 0.1); end写这段代码时有一个细节特别容易出错Matlab的max(0, ...)如果传入向量返回的也是逐元素的向量导致violationVec(1)被赋值成一个向量后面sum( 向量 )会神秘出错。正确的写法是外层再加一个max(0, sum(...))或者在比较前先用sum聚合成标量。遇到这种“运行时错误”你排查半天大概率是这类维度问题。3.3 真实算例10个候选点、4个负荷区的输入结构与结果为了验证程序正确性我设计了一个不算复杂但足够说明问题的算例规划区域有4个热负荷块用坐标表示候选站址10个容量档位5档8MW、12MW、16MW、20MW、25MW。电价0.5元/kWh天然气2.6元/Nm³折旧期20年。输入数据举例如下对象X坐标(m)Y坐标(m)热负荷(MW)电负荷(MW)热负荷块A2003001815热负荷块B8009002622热负荷块C150012003227热负荷块D22006001411候选点分布在热负荷块周围相距约300-600米。程序跑完200代最优方案选择了3个站点容量分别为16MW、20MW和25MW——总装机容量61MW热负荷缺口为0电力缺口为0总年费用约4300万元。这个结果让我欣慰的点是GA自动放弃了靠近热负荷块C的候选点虽然它区位极佳但周围没有足够的电网并网容量罚函数把它压下去了。这就说明罚函数设计得当的时候模型确实能“看到”工程约束的相互制约关系而不是闷头找最小成本方案。3.4 结果可视化与敏感性分析程序里我写了一个plot_result函数输出三张图站点布局图底图是候选点和负荷块的位置用不同颜色标记选中的站点同时画出热力管道连接线目标函数收敛曲线横轴为代数纵轴为最佳年费用/亿生成一条快速下降后趋平的曲线容量分配饼图展示各选中站点容量占比。收敛曲线是最先值得看的。如果看到曲线在20代内就彻底停滞多半是参数设置出了问题而不是已经找到了全局最优解。配合敏感性分析我会固定其他参数分别调整容量档位数和候选点数量观察目标函数值的跳跃变化这可以帮助设计人员判断多加一个候选点到底能带来多大收益如果收益小于规划协调成本这个候选点就可以从名单里删除。4. 常见问题、调试与避坑指南4.1 GA不收敛或早熟的4个典型原因在跑这个程序的过程中我踩过不少GA算法本身的坑最典型的是这四类第一类种群规模太小。候选点10个、容量档位5档时理论上解空间就有 ( 10! ) 量级。如果种群只有20个初期很难覆盖多样性的方案后期非常容易陷入局部最优。解决方式很简单把种群加到200以上同时配合均匀交叉。第二类适应度函数尺度过于平坦。当惩罚系数设置得过大时所有不可行解的适应度都趋近于0可行解和不可行解之间没有梯度选择算子无法有效区分个体。我调试时发现收敛曲线像一条直线把罚项从1e5降到1e3曲线立刻就出现了正常下降。第三类变异率过高或过低。变异率超过0.05后优秀基因被打散收敛曲线呈锯齿状且最后稳定不下来变异率低于0.005时局部搜索能力不足。建议用自适应变异率算法前期变异率大一些0.05后期逐渐减小到0.005附近。第四类缺少精英保留。这是一个非常隐蔽的问题。我最早实现时没加精英保留有一代找到了最优解下一代的交叉操作却把这个解的基因给破坏了导致最佳适应度发生“回跳”。加入精英保留后回跳现象立刻消失。4.2 Matlab程序运行中的报错处理在调试Matlab程序时其他工程师常问“写二叉树程序时为什么总是报运行时错误”“npm无法识别”这类和环境配置或语言语法有关的问题实际上Matlab项目中最常见的运行时报错反而集中在两类错误一未定义函数或变量。这类错误通常是因为子函数不在Matlab搜索路径上。我的做法是在主程序文件头部用addpath(genpath(pwd))把当前目录及子目录全部加入路径同时用clear all; close all; clc;清空历史变量避免残留变量干扰。错误二矩阵维度不一致。交叉、变异算子的位操作很容易让染色体长度发生错位。建议在每个子函数入口加assert(chromLength expectedLength)一旦维度不对立刻报错避免错误传递到目标函数里变成莫名其妙的数值错误。下面给出一份通用排查顺序表现象第一步检查第二步检查第三步检查遗传代数跑完仍有不可行解检查罚项系数大小检查约束检查函数是否使用sum聚合检查染色体解码后容量是否被正确掩码目标函数值异常大/出现NaN检查输入负荷数据是否包含NaN检查目标函数里是否除零如容量为0时计算热损检查罚项是否乘了过大的系数导致溢出收敛曲线变成水平线检查种群多样性是否过早丢失检查交叉率是否过低检查选择算子是否退化成随机选择运行速度极慢检查目标函数是否用了大量循环检查是否每次都重新计算基础矩阵考虑向量化sum和矩阵乘法4.3 性能优化技巧当候选点数量增加到30个、容量档位增加到10档之后每代200个个体的评估次数高达4000次如果目标函数里还嵌套了复杂的管网水力计算程序会慢到让人崩溃。我优化的第一个思路是“近似评估”在初始进化阶段用简化的线性热网损耗模型代替详细的管网水力计算只在最后20代切换到精确模型做最终筛选。第二个思路是“向量化”把200个个体的目标函数计算改成矩阵运算避免for循环逐个体调用calc_cost。我把代码重构后耗时从每代18秒降到了4秒左右。第三个思路是“并行计算”Matlab的parfor可以轻松并行评估种群个体但注意交叉和变异算子必须保持确定性不能使用随机数在并行环境里混乱。这里有一份优化前后的耗时对比供你参考方案单代评估耗时总耗时(200代)说明原始for循环18s3600s每代200个体逐个调用目标函数向量化评估4.2s840s用矩阵乘法处理所有个体向量化 parfor1.1s220s8核并行注意随机数流设定简化模型 向量化0.5s100s前180代用近似热网模型4.4 工程落地注意事项这个程序跑出来的结果最终要拿到项目评审会上去。下面几条工程落地的注意事项是基于我在实际项目里的教训总结的你一定要重视。第一负荷数据必须做时序处理。不能用单一最大负荷做选型否则冬季和夏季的负荷差异会让机组容量选择完全失调。建议至少按“典型日电负荷热负荷曲线”做多场景校核。本程序示例中只用最大负荷是为了快速验证流程工程上建议改成8760小时生产模拟。第二备用容量不能省。CHP机组每年都有计划检修和故障停机如果选址定容不考虑备用实际运行时一旦主力机组跳机整个区域的热力供应都会崩掉。建议在电力约束和热力约束里分别加入5%-10%的备用系数。第三供热半径与经济性要联动考虑。有不少团队把供热半径简单设为一个固定值比如2km但实际经济性评估显示当燃料价格高、保温性能好时供热半径适当扩大反而更经济。所以更好的做法是把供热半径作为一个软化约束通过罚项动态调整。第四结果可解释性很重要。GA是一个启发式算法评审专家经常会问“为什么选这个站而不选另一个”。我建议程序输出除了最优解之外还要给出前5个近似最优方案并用风电、热电比、单位投资成本等指标做对比表格。这样在项目论证时会更有说服力。写到这里我其实想再强调一遍遗传算法不是万能药但它很适合热电联产选址定容这种“离散变量多、约束复杂、非凸”的问题。只要你能把工程约束翻译成合理的编码和适应度函数用Matlab搭起这套程序并不难。我个人实操时的最大体会是这里的核心不在一百行的GA框架而在于约束处理和数据可靠性。曾有一次程序跑出一个返直觉的最优解后来发现是输入文件里某个热负荷块的发电功率被误填成了负数GA算法再智能拿到错误数据也只能给出错误答案。如果你正准备搭自己的程序我建议你先从“3个候选点、3个容量档位”的超简案例开始跑通完整流程后再逐步扩展。等程序稳定了还可以往两个方向扩展一是把目标函数从单目标变成多目标同时优化成本和碳排放二是引入多场景鲁棒优化让选址方案在冬季、夏季、过度季都站得住脚。这已经超出“一个简单GA程序”的范围了但恰恰是真实工程中最有价值的部分。
阅读完成 · 觉得有帮助?