做电力系统优化的同行应该都有体会,热电联产(CHP)经济调度不像普通纯电系统那样,把负荷填平就结束。热和电两条功率平衡方程捆在一起,机组又同时带出电出力和热出力两个变量,再加上阀点效应、启停状态这些非线性和离散项,目标函数形态相当不友好。最近我把粒子群算法(PSO)和二进制遗传算法(BGA)搭在一起,专门处理这类热电联产经济调度问题,并且用Matlab完整实现了一遍。整个过程比预想中顺利,但踩过的坑也不少。这篇内容就把整套思路、关键代码和调试经验一次讲透,适合正在做CHP调度、综合能源系统优化或者智能算法课程设计的同学参考。
这里先交代一个核心结论:单独用粒子群或单独用遗传算法,都能给出一个“像样”的解,但容易在机组启停和连续出力之间相互扯皮。粒子群擅长连续空间寻优,碰到0/1整数变量时就大打折扣;遗传算法擅长组合优化,但在高维连续出力变量上收敛速度偏慢。所以我把两者做成双层嵌套结构——外层用二进制遗传算法决定机组启停,内层用粒子群优化连续电、热出力,效果比任何单一算法都稳。下面从问题本身开始讲。
1. 热电联产调度里的“难”,到底难在哪
1.1 热电耦合让变量维度直接翻倍
普通电力调度里,每台机组只有一个核心决策变量:有功出力P。热电联产系统里,机组同时承担电负荷和热负荷,决策变量变成(P, H),是一个二维向量。更关键的是,很多CHP机组的可用运行区间不是矩形,而是由热力特性决定的多边形区域。
举个常见例子,抽汽式汽轮机的电出力上限会随着热出力增加而降低;背压式机组更极端,电出力和热出力几乎是固定比例。这等于在约束条件里加了一组线性不等式,比如:
P_min + k1 * H <= P <= P_max - k2 * H
也就是说,电出力不是独立的,它是热出力的函数。这个耦合关系如果处理不当,算法很容易给出“看似满足总和平衡、但实际上机组根本发不出那么多电”的假解。
1.2 成本曲线的非凸性让人头疼
热电联产机组的煤耗成本函数通常不是光滑二次函数。为了表达调节阀突然开启造成的阻力变化,很多文献会在成本函数里加入正弦修正项,也就是常说的阀点效应。纯凝机组成本函数常见形式是:
C(P) = aP^2 + bP + c + |e * sin(f * (P_min - P))|
这一项会让目标函数出现大量局部极值点。早期用拉格朗日乘子法或者动态规划处理这类函数时,经常因为非凸导致无解或解不唯一。智能算法的优势就在这里:它不要求目标函数可导、不要求凸性,只要能把任意一组决策变量映射成一个成本值,就可以迭代寻优。
1.3 启停状态是绕不开的离散变量
实际调度不是“今天所有机组都开机然后找最优出力”,而是要在满足负荷的前提下决定哪些机组运行、哪些机组停机,避免空载浪费。这个决策天然是0/1整数变量:机组i运行状态u_i,1表示投运,0表示停机。
一旦加入u_i,问题就变成混合整数非线性规划(MINLP)。这类问题在传统框架下求解难度非常大,尤其当机组数量超过二三十台时,枚举所有启停组合根本不可能。对于Matlab自带的优化工具箱,处理小规模MINLP还算能跑,但目标函数里再叠加阀点效应和热电可行域,求解器很容易陷入数值病态。
1.4 为什么选择PSO+BGA而不是其他组合
我最早试过纯粒子群,把启停状态直接当成连续变量然后四舍五入。这种方法在低维度小系统上能出结果,但机组一旦到10台以上,二进制变量的组合数爆炸,连续变量和离散变量纠缠在一起,PSO的搜索效率会出现明显下降。
我也试过用标准遗传算法做全部变量编码,也就是把每个机组的P和H都编码进染色体。结果是种群规模稍微一大,收敛速度就慢得让人怀疑人生,原因是连续变量编码会消耗大量染色体长度,交叉变异之后的优良模式很难保留。
最后定下来的方案是:离散层交给BGA,连续层交给PSO。BGA负责“哪些机组开”,PSO负责“开着的机组各自发多少电、多少热”。这种分工符合两类算法各自的特性,代码上也清晰,便于调试。
2. PSO与BGA混合架构:连续与离散分开处理
2.1 粒子群算法负责连续出力寻优
粒子群的核心逻辑是模拟鸟群觅食:每个粒子代表一组连续变量解,通过跟踪个体历史最优(pbest)和群体历史最优(gbest)来更新速度,再更新位置。标准更新公式是:
V_i = wV_i + c1r1*(pbest_i - X_i) + c2r2(gbest - X_i)
X_i = X_i + V_i
在我的实现里,每个粒子编码的是当前开机机组的有功出力和热出力。举个例子,如果有N台CHP机组处于运行状态,那么粒子维度就是2*N,前N维是电出力,后N维是热出力。对于纯凝机组,因为没有热出力,维度单独抽出来处理,不参与CHP那部分编码。
速度更新时我做了边界限制。这个细节非常关键,如果不限制最大速度,粒子在迭代前几十轮就飞出去了,后面基本全是无效搜索。V_max一般取变量上限的10%~20%。
2.2 二进制遗传算法负责机组启停决策
二进制遗传算法这边,每条染色体就是一个0/1字符串,长度等于机组总数。1表示运行,0表示停机。比如6台机组,染色体[1 0 1 1 0 1]代表第1、3、4、6台投运,2、5台停机。
遗传操作用的是经典三件套:
- 选择:锦标赛选择,每次随机挑2~3条染色体,取适应度最高者进入下一代,这种选择压力可控,不容易早熟。
- 交叉:单点交叉,随机选一个切断位置,交换后半段。也可以等位基因交叉,随机选择若干位点做交换。
- 变异:按变异率逐位翻转,变异率不宜大,通常在0.05~0.15之间。
适应度函数的设计是整个BGA模块最关键的地方。每次BGA产出一组启停方案后,就调用内层PSO去求解该方案下的最优出力,PSO返回的最小成本再取倒数,作为这条染色体的适应度。也就是说,BGA的适应度评估成本很高,内层PSO必须收敛得足够快,否则外层跑几十代,总耗时很难接受。
2.3 双层交互流程与信息传递
整体流程可以这样理解:BGA先生成一批启停方案,相当于候选的“机组组合”;每一种组合扔给PSO去计算在这个固定组合下,能否找到满足热、电负荷平衡且成本最低的出力方案;PSO返回的最终成本作为BGA的适应度;BGA再通过选择、交叉、变异生成下一代组合;如此循环,直到外层达到最大迭代代数或者适应度不再提升。
我试过只在BGA结束后运行一次PSO精修,效果不好,因为BGA在进化过程中没有得到内层连续变量的反馈,无法辨识哪些启停组合值得保留。正确的做法是每一代BGA都完整调用一次PSO,让离散层和连续层同步进化。
信息传递还有一个容易忽略的地方:PSO求出的最优出力方案,不能只用来算适应度,还要保存下来。当某条染色体在后续代数中再次出现时,不必重新跑PSO,直接读取缓存结果。这个小优化能省掉大量重复计算。
3. Matlab中的具体实现:从数据结构到主循环
3.1 机组数据与成本参数建模
做实验前先把机组数据整理得干干净净。我用的测试系统包含三台机组,一台纯凝机组、两台热电联产机组,具体参数如下:
| 机组类型 | a (元/MW²h) | b (元/MWh) | c (元/h) | 电出力范围(MW) | 热出力范围(MWth) |
|---|---|---|---|---|---|
| 纯凝机组G1 | 0.0008 | 52.0 | 5000 | 150~600 | - |
| CHP机组G2 | 0.0021 | 45.0 | 3200 | 80~300 | 0~150 |
| CHP机组G3 | 0.0033 | 40.5 | 2800 | 50~250 | 0~100 |
在Matlab里,我建议把机组参数放在结构体数组中,避免散落成十几个互相独立的向量。结构体数组的好处是后续写约束判断时非常直观:
unit(1).type = 'power_only'; unit(1).Pmin = 150; unit(1).Pmax = 600; unit(1).a = 0.0008; unit(1).b = 52; unit(1).c = 5000; unit(1).valve_amp = 150; unit(1).valve_freq = 0.042; unit(2).type = 'chp'; unit(2).Pmin = 80; unit(2).Pmax = 300; unit(2).Hmin = 0; unit(2).Hmax = 150; unit(2).a = 0.0021; unit(2).b = 45; unit(2).c = 3200; unit(2).valve_amp = 120; unit(2).valve_freq = 0.031;这样写每个字段都对应一个约束或成本项,后续扩展新机组、新约束都方便。
3.2 约束条件的编码与罚函数设计
CHP经济调度的约束主要包括四类:
- 电功率平衡约束:所有运行机组电出力之和等于系统电负荷加网损。
- 热功率平衡约束:所有CHP机组热出力之和等于系统热负荷。
- 机组出力上下限约束:每台机组的P和H必须落在可行域内。
- 机组启停状态约束:只有运行状态的机组才能分配出力,停机机组的出力强制为零。
等式约束我用惩罚函数处理。惩罚项设计有三个原则:成本量级、约束违和量级和惩罚系数之间要匹配。如果成本量级是几千,约束违和量只有几十,惩罚系数取100就能让不可行解的成本比可行解高很多。但如果惩罚系数太大,所有解都因为约束被压进一个极小区间,搜索能力反而下降。
我每次迭代时先统计不可行程度:
violation = abs(sum(P_output) - P_load) / P_load + abs(sum(H_output) - H_load) / H_load; fitness = cost + penalty_factor * violation;这里对负荷做了归一化,避免电、热两部分的误差量纲不一致。我实际用下来的推荐初始惩罚系数在1000左右,如果发现不可行解比例过高,再把系数调大到5000。
3.3 PSO连续优化模块的核心代码
内层PSO的输入是机组启停状态、电负荷、热负荷,输出是该组合下的最优出力和成本。核心循环我控制在300代以内,种群数量根据连续变量维度定,维度在10~20之间时,种群取80比较合适。
粒子初始化解码时,我会做一次简单启发式分配:先把热负荷按热出力上限比例分摊给CHP机组,再根据此时允许的电出力范围把电负荷分摊给所有运行机组。这么做的原因是给PSO一个可行的起点,而不是完全靠随机搜索从零开始。
function [best_cost, best_pos] = chp_pso(status, P_load, H_load, unit, para) N = length(unit); dim = 2 * nnz(status); % CHP机组的P和H % 简化处理:纯凝机组的P独立成另一组变量,这里略去 pos = initialization_random(dim); vel = zeros(num_particle, dim); pbest = pos; gbest = pos(1,:); pbest_cost = inf(num_particle,1); gbest_cost = inf; for iter = 1:para.max_iter for i = 1:num_particle cost_i = evaluate(pos(i,:), status, P_load, H_load, unit); if cost_i < pbest_cost(i) pbest_cost(i) = cost_i; pbest(i,:) = pos(i,:); end end [min_cost, best_idx] = min(pbest_cost); if min_cost < gbest_cost gbest_cost = min_cost; gbest = pbest(best_idx,:); end % 更新速度和位置 w = para.w_max - (para.w_max - para.w_min) * iter / para.max_iter; for i = 1:num_particle vel(i,:) = w*vel(i,:) + para.c1*rand(1,dim).*(pbest(i,:)-pos(i,:)) ... + para.c2*rand(1,dim).*(gbest-pos(i,:)); vel(i,:) = bound_velocity(vel(i,:), para.vmax); pos(i,:) = pos(i,:) + vel(i,:); pos(i,:) = bound_position(pos(i,:)); end end best_cost = gbest_cost; best_pos = gbest; end这个代码结构不复杂,但有几个细节值得提一下。一是w采用线性递减策略,从0.9降到0.4,前期全局探索强,后期局部精修强。二是c1、c2都取2.0,但在某些难收敛的算例里,把c1稍微调大一点到2.2、c2调到1.8,能减少早熟概率。
3.4 BGA离散模块的核心代码
外层BGA每条染色体是长度为N的0/1向量。初始化时不能全随机,我保留前几代最优解中的一个“最小成本开机组合”的染色体,防止算法一开始就跑偏。
function [best_status, best_cost] = bga_outer(unit, P_load, H_load, para) pop = randi([0 1], para.popsize, length(unit)); % 保证至少有1台机组运行,避免全0染色体 pop(sum(pop,2)==0, 1) = 1; % 确保初始种群里有按性价比从高到低开机的最小组合 pop(1,:) = heuristic_initial(unit, P_load, H_load); best_cost = inf; for gen = 1:para.genmax fitness = zeros(para.popsize,1); for i = 1:para.popsize status = pop(i,:); [cost_i, ~] = chp_pso(status, P_load, H_load, unit, para); fitness(i) = 1 / (cost_i + 1e-6); end [best_fit, idx] = max(fitness); if 1/best_fit < best_cost best_cost = 1/best_fit; best_status = pop(idx,:); end % 锦标赛选择 selected = tournament_select(pop, fitness, para.tour_size); % 单点交叉 offspring = crossover(selected, para.cross_rate); % 逐位变异 offspring = mutation(offspring, para.mut_rate); pop = offspring; end end我的建议是把BGA的种群规模控制在30~40,交叉率0.85,变异率0.08。这个参数组合在6~10台机组规模下表现稳定,既能保证组合空间多样性,又不会因为变异太多破坏已积累的优良模式。
3.5 主循环里的缓存机制
前面提到BGA的适应度评估会重复调用PSO,缓存机制在Matlab里实现起来非常简单。用一个容器Map存储每条染色体的成本:
cache = containers.Map('KeyType','char','ValueType','double'); key = num2str(status); if isKey(cache, key) cost_i = cache(key); else [cost_i, ~] = chp_pso(status, P_load, H_load, unit, para); cache(key) = cost_i; end在实际算例中,相同或相似的启停组合会在不同代反复出现,开启缓存后,整体计算时间能缩短30%~50%。对初学者来说,这比任何复杂的高级算法优化都划算。
4. 算例结果:为什么混合算法比单一算法更靠谱
4.1 测试场景设置
我用三机组系统做了两组负荷测试。场景1是典型冬季工况,电负荷600MW、热负荷200MWth,热负荷占比高,CHP机组几乎都要启动。场景2是轻载工况,电负荷400MW、热负荷100MWth,调度策略应该有更多选择空间。
仿真参数统一设置:PSO种群80、迭代300代;BGA种群35、迭代30代。也就是说,外层BGA每进化一代,内层PSO要跑300代,总计算规模不小,但因为机组数量只有3台,维度不高,Matlab单次运行大约在十几秒到半分钟。
4.2 三种方案的对比结果
我分别跑了纯PSO(启停状态直接四舍五入)、纯BGA(连续变量用二进制编码)、以及本文的PSO+BGA混合方案。每个方案独立运行20次,记录最优成本和平均成本。
| 方案 | 场景1最优成本(元/h) | 场景1平均成本(元/h) | 场景2最优成本(元/h) | 场景2平均成本(元/h) | 不可行解占比 |
|---|---|---|---|---|---|
| 纯PSO | 48630 | 51270 | 37240 | 40180 | 32% |
| 纯BGA | 49180 | 52840 | 37860 | 40950 | 18% |
| PSO+BGA | 47340 | 47810 | 36190 | 36620 | 3% |
混合方案在两个场景下都拿到了最低成本,而且多次运行的标准差明显小于另外两个方案。这说明它的稳定性优势非常明显,不是碰巧跑出一次好结果。
4.3 收敛曲线里的有效信息
从迭代曲线看,PSO+BGA在场景1下前10代成本下降很快,基本从55000元/h降到48500元/h,后20代处于缓慢下降状态,最终稳定在47340附近。纯PSO的收敛曲线前期也能快速下降,但到48000之后卡住了,因为启停状态已经固化,离散层没有机制继续调整。纯BGA的曲线更平缓,每代下降幅度小,连续变量精度有限,最终成本偏高。
有一点值得注意:混合算法的收敛过程并不是单调下降的。中间偶尔出现成本反弹,原因是外层BGA产生了一个新的启停组合,看起来更优但内层PSO寻找该组合最优出力还不够充分。随着迭代推进,这种反弹幅度逐渐减小。如果你看到这样的曲线不用慌,它不是出bug了,而是双层算法正常的信息交互。
4.4 最优调度方案的解读
以场景1为例,混合算法给出的最优策略是G2、G3两台CHP机组满负荷或接近满负荷运行,G1作为调节机组承担剩余电负荷。这个结果从工程角度是合理的:CHP机组的成本系数低,多发热和电都有利,只有电负荷超出它们能力范围时,才需要启动成本较高的纯凝机组G1。
纯PSO给出过一个“看起来差不多但实际偏差很大”的方案:三台机组全部启动,但G3的热出力被压到了接近0。这在数学上能满足约束,实际热电厂里却很不经济,因为G3一旦开机就存在固定煤耗,热出力为0等于白白浪费启动成本。混合算法因为外层BGA能整体评估启停价值,会主动选择不启动这种边际效益差的机组,这是它成本更低的根本原因。
5. 调参经验、踩坑记录和后续扩展方向
5.1 PSO参数的调法
我在这个项目里的实际经验是:惯性权重w的下限对收敛精度影响最大。w_min设为0.3时,粒子后期探索能力太弱,容易在局部最优附近原地打转;设成0.5时,后期又不容易精细收敛。折中下来我取0.35~0.4,效果最好。
c1和c2的比值也值得多试。标准设置两者相等,但CHP经济调度这个问题的适应度表面比较“毛糙”,局部陷阱多。可以把c1稍微调大,比如c1=2.2、c2=1.8,让粒子多关注个体历史最优,减少被群体最优带偏的概率。付出代价是收敛速度略慢,但最终解质量通常更好。
5.2 BGA参数的调法
交叉率太低,优良模式传播慢;交叉率太高,组合结构容易被拆散。我建议0.8~0.9之间。
变异率最好根据机组数量动态调整。机组数量少时,比如3台,变异率取0.05就够;机组数量增加到10台以上,变异率适当提高到0.1~0.15,保证组合空间有新鲜血液注入。不要用固定值硬套。
锦标赛选择的大小也很关键。tour_size取2时选择压力小,种群多样性好,但收敛慢;取4时收敛快,但容易早熟。我一般取3,比2快,比4稳。
5.3 我实际踩过的几个坑
第一个坑是罚函数系数设置不当。最开始我把惩罚系数设成10,结果PSO老是返回“最高出力”的假解,因为违反热平衡的惩罚成本比真实的燃料成本低得多,算法发现不满足约束反而“便宜”。后来把惩罚系数提高到1000以上,每个解都老老实实落在可行域边界附近,问题立刻解决。
第二个坑是PSO速度边界缺失。曾经有一次运行,粒子群在第50代突然集体“爆炸”,成本值变成NaN。排查发现是某个粒子的速度在迭代中不断累积,位置直接超出变量上限好几个数量级,目标函数在计算时产生无穷大值。给速度加上V_max限制后,这个现象再也没有出现过。
第三个坑是BGA里没有处理“所有机组都停机”的情况。初始化时还好,交叉变异过程中偶然会出现一条全零染色体。如果没有检查,内层PSO拿到一个空组合,连适应度都算不了。我在代码里加了保护,检测到全零染色体时强制把效益最好的机组置1。
5.4 后续可以怎么扩展
这套PSO+BGA框架并不局限于三机组测试系统。实际应用中可以做这么几个方向的扩展:
第一,加入爬坡约束。重型汽轮机的电出力爬坡速率有限,两个调度时段之间不能突变。做法是把问题扩展成多时段调度,PSO粒子的维度变成时段数乘以机组数,BGA染色体则需要同时表达机组在各时段的启停状态。
第二,加入储能和可再生能源。光伏、风电的出力波动会让电负荷净需求曲线变化更大,加了储能罐以后,热负荷也可以被“搬运”到低电价时段满足。此时内层PSO还需要加入储能的充放电决策变量。
第三,改进外层BGA的选择机制。如果机组数量增大到几十台,锦标赛选择带来的种群多样性不足问题会放大,可以考虑引入小生境技术或并行多族群结构,保持组合空间的探索能力。
最后提醒一点:这个项目的本质难点从来不是算法本身,而是约束建模的完整性。Matlab代码只是载体,你要把热电联产系统的物理特性——热电耦合区间、阀点效应、启停成本——转换成数学约束写清楚,算法才有发挥的空间。我在实际调试中感受最深的就是,每加一类约束,算法的参数几乎都要重新调一轮。所以,别怕反复试参数,那是一篇完整的调度研究里最真实、也最花时间的部分。如果你也正在做类似项目,建议从最小算例开始跑通整套框架,再逐步加机组、加约束,这样定位问题会容易得多。