☰
遗传算法求解动态经济调度:Python实现与爬坡/网损处理
2026/10/10 12:47:23 网站建设 项目流程

做经济调度研究的人大多会碰过这种情况:单机成本模型很简单,约束一加就出事。尤其是爬坡约束和输电损耗同时出现以后,等约束优化已经不太容易处理,很多传统解法要么收敛慢,要么直接把约束简化掉,算出来一套看似可行、现场却跑不了的结果。这个项目做的就是一件事:用遗传算法把动态经济调度问题直接怼进去,机组爬坡速率、线路损耗、负荷曲线一起考虑,最后用Python把整个流程跑通。文章把模型公式、算法设计、代码实现、参数坑都写清楚,适合正在做电力系统优化课题、或者想用遗传算法解决实际约束优化问题的读者参考。

1. 先把问题说清楚:这到底在优化什么

1.1 经济调度是电力系统的“算账问题”

电网调度每天的日常工作之一,就是决定哪些机组带多少负荷。发电机组数量一多,谁多发、谁少发、谁停机、谁启动,这些问题没法靠拍脑袋。经济调度(Economic Dispatch,ED)做的事情就是:在满足负荷需求、机组出力上下限、备用容量等约束的前提下,让发电总成本最低。

单时段的ED非常简单,甚至可以手算。可一旦我们面对24小时甚至96个调度时段,问题就变成动态经济调度(Dynamic Economic Dispatch,DED)。DED比单时段版本难很多,核心原因就是机组出力在相邻时段之间不是随便跳的,这也就是爬坡约束的由来。

本项目的目标函数可以写成:

min Σₜ Σᵢ (aᵢ + bᵢ·Pᵢₜ + cᵢ·Pᵢₜ²)

其中i是机组编号,t是时段编号,a、b、c是机组成本系数,P是出力。这个二次函数是电力系统调度最经典的机组成本模型,虽然实际中有更精细的分段线性或阀点效应模型,但二次函数已经足够说明遗传算法的处理方式。

约束条件则是这几条:

  1. 功率平衡:所有机组出力总和,等于负荷需求加上输电损耗。
  2. 机组出力上下限:每台机组都有最小稳定出力和最大出力。
  3. 爬坡约束:相邻时段出力变化不能超过机组允许的爬坡速率。
  4. 输电损耗:损耗不是固定值,它随着机组出力变化,是出力的非线性函数。

很多人初看会觉得这不过是个带约束的数学规划问题。但当机组数和时段数增加,再加上输电损耗的非线性耦合,整个搜索空间会变得非常复杂,传统解析法和部分梯度法很难在合理时间内找到全局较优解。

1.2 爬坡约束不是“学术矫情”,是设备物理极限

爬坡约束这个概念,刚接触的人容易低估它。一台大型火电机组,从50%负荷升到100%,锅炉燃烧、汽轮机热应力、辅机出力都有一个响应过程。你让它在15分钟间隔里从200MW直接跳到400MW,机组很可能直接触发保护动作,甚至造成设备寿命损伤。

在数学上,爬坡约束写作:

|Pᵢₜ - Pᵢₜ₋₁| ≤ Rampᵢ

Rampᵢ就是机组i在单位时段内允许的出力变化量。如果调度时段是1小时,Rampᵢ的单位就是MW/h。这个约束最大的麻烦在于它把不同时段“粘”在了一起。单时段优化可以各算各的,多时段优化就必须把24个时段当成一个整体来看,解空间维度一下子从“机组数×1”变成“机组数×24”。

本项目里给每台机组设置了不同的爬坡能力,比如某台机组每小时只能变化60MW,另一台能变100MW。这样的设置更贴近实际:不同厂家、不同容量、不同燃料类型的机组,响应能力天然不同。

如果忽略了爬坡约束,经济性最优解很可能要求某台机组在凌晨低负荷时段大幅降出力,第二天早上又大幅升出力。这种剧烈变化恰恰是现场最不希望看到的。所以,爬坡约束才是让“算出来的最优”变成“能执行的最优”的关键一步。

1.3 输电损耗为什么不能忽略

很多入门教材在做经济调度时都会做“忽略网损”的假设。这确实省事,但实际系统里,电能在输电线路上会产生热量损耗,尤其是负荷重、送电阻离远的情况下,网损占总负荷的比例可能达到5%-8%。忽略掉这部分,功率平衡本身就是错的:机组发出的电减去损耗,才是真正送到负荷端的功率。

输电损耗最简单的建模方式是用B系数公式:

P_L,t = Σᵢ Σⱼ Pᵢₜ·Bᵢⱼ·Pⱼₜ + Σᵢ Bᵢ₀·Pᵢₜ + B₀₀

其中B矩阵是损耗系数矩阵,B₀是线性项系数,B₀₀是常数项。这个公式最早来自经典的网损系数法,用系统运行点附近的线性化潮流推导得到。虽然精度不如完整潮流计算,但在经济调度这种需要反复求解的场景里,B系数模型计算速度极快,精度也够用,非常适合作为遗传算法适应度函数的一部分。

注意B系数公式里的平方项和交叉项,意味着每台机组出力的变化会通过损耗影响所有机组的功率平衡。这导致一个问题:某一台机组出力改变,损耗变了,其他机组的平衡出力也要跟着变。这就是典型的非线性耦合,也是为什么很多传统方法在这个问题上会碰壁。

2. 为什么选择遗传算法来处理这个难题

2.1 传统方法的痛点

单时段经济调度是凸二次规划,用拉格朗日乘子法加迭代修正就能解,速度快、稳定性也不错。到了多时段动态经济调度,情况就变了。

最直接的做法是把所有时段的变量都展开,变成一个大规模约束优化问题。理论上可以用序列二次规划、内点法等求解。但这类方法对初值敏感,而且一旦目标函数非凸——比如考虑机组阀点效应、禁止运行区、启停变量——很容易陷入局部最优。另一个现实问题是,很多科研人员和工程师不是优化算法专家,让他们去调内点法的收敛参数,成本太高。

启发式算法在工程界之所以流行,不是因为它们保证找到全局最优,而是因为它们在面对复杂约束时非常“皮实”。你不需要推导问题特殊的梯度信息,只要能把适应度函数写出来,算法就会自己朝好的方向搜索。遗传算法作为启发式算法里最成熟的一支,实现简单、灵活性高,天然适合这种“前两天还在改约束条件,今天就要出结果”的研究场景。

2.2 遗传算法怎么帮你“试”出好解

遗传算法(GA)模拟自然选择的过程。一个“个体”就是一组完整调度方案;一组个体就是“种群”。每一代里,适应度好的个体被选出来进行交叉和变异,产生下一代;适应度差的个体自然被淘汰。循环几十上百代以后,种群整体会朝着低成本、少违规的方向进化。

GA对应到本项目有几个关键设计:

  • 编码方式:用浮点数数组表示每个机组在每个时段的出力。
  • 适应度函数:总发电成本加上各项约束的惩罚项。
  • 选择算子:锦标赛选择,在种群中随机挑几个个体,留下最优的。
  • 交叉算子:把两个父代个体的部分时段出力互换,产生新个体。
  • 变异算子:随机挑某个时段、某个机组,加一个小的随机扰动。

GA最大的优势在于它不需要目标函数可导。你的成本函数可以是二次函数,也可以是分段函数;约束可以写成等式,也可以写成不等式;甚至可以是某种仿真程序的输出结果。这给了建模极大的自由度。当前项目把爬坡约束和损耗同时加进适应度函数里,GA都能统一处理。

2.3 一个容易出效果的建模技巧:松弛机组

我建议在GA里引入“松弛机组”的概念。原理很简单:功率平衡约束是等式约束,如果直接用罚函数处理,算法很容易钻空子,找到一个成本很低但功率不平衡的“假最优解”。与其让GA自己摸索平衡条件,不如在设计阶段就把等式消掉。

具体做法是:选一台机组作为平衡机组,其他机组的出力由基因编码决定;平衡机组的出力根据功率平衡反算出来。这样每次评价个体时,不需要额外判断平衡约束是否满足,天然就满足。代价是平衡机组的出力范围以及爬坡约束需要额外检查,一旦反算出的出力越界,就要给惩罚。

这个想法和实际电网调度中“平衡节点”的概念很像,也是本项目代码最核心的部分。很多初学者直接对全变量编码,然后寄希望于罚函数,结果遗传算法跑几百代还在找可行解,原因往往就在这里:没有利用问题结构消掉等式约束。

3. Python代码实现:从模型到可运行结果

3.1 数据准备与模型参数定义

先给一个具体算例。三台发电机,24个时段,负荷曲线包含夜间低谷和白天高峰。机组参数如下表:

机组abcPmin/MWPmax/MW爬坡速率/MW·h⁻¹
G15008.00.0045030060
G24007.00.0035040080
G33006.00.00250500100

其中G3作为松弛机组。负荷曲线取一组典型的互联网流传的实测形状数据,凌晨负荷低到580MW,傍晚高峰到1080MW。三台机组总容量是1200MW,考虑损耗后峰值仍有足够裕度。

输电损耗B矩阵取3×3对称矩阵,对角线是自损耗系数,非对角线是交叉损耗系数,数值在10⁻⁵到10⁻⁴量级。这个量级对应实际系统中总损耗约占总出力的2%-4%。

3.2 适应度函数怎么把约束“揉”进去

遗传算法的核心评价函数包含三部分:发电成本、机组越限惩罚、爬坡越限惩罚。

发电成本部分直接按二次函数计算,全部时段、全部机组求和:

cost = Σᵢₜ aᵢ + bᵢ·Pᵢₜ + cᵢ·Pᵢₜ²

越限惩罚处理两种情况。一是松弛机组反算出的出力超出上下限;二是爬坡约束被破坏。惩罚量用线性惩罚乘以一个较大的权重:

penalty = Σ (越限量 × 惩罚系数)

为什么用线性惩罚而不是二次惩罚?我的经验是线性惩罚在GA里更稳定。二次惩罚虽然对大幅越限的惩罚更狠,但在小幅越限时几乎不起作用,导致算法倾向于“稍微违约一点”来降低成本;线性惩罚让每个单位越限都有明确代价,配合合适的权重系数,搜索方向更清晰。

功率平衡因为用了松弛机组反算,所以不需要惩罚项,这是整个代码实现里的最大简化点。

3.3 核心算法代码

import numpy as np # 机组参数 N_GEN = 3 A = np.array([500.0, 400.0, 300.0]) B_COST = np.array([8.0, 7.0, 6.0]) C_COST = np.array([0.004, 0.003, 0.002]) Pmin = np.array([50.0, 50.0, 50.0]) Pmax = np.array([300.0, 400.0, 500.0]) Ramp = np.array([60.0, 80.0, 100.0]) T = 24 # 24小时负荷序列 load = np.array([ 700, 650, 620, 600, 580, 600, 650, 750, 850, 950, 1000, 1050, 1080, 1050, 1000, 980, 950, 900, 850, 800, 750, 720, 700, 680 ], dtype=float) # 输电损耗B系数矩阵 B = np.array([ [0.00006, 0.00002, 0.00001], [0.00002, 0.00005, 0.00001], [0.00001, 0.00001, 0.00003] ]) B0 = np.array([0.0001, 0.0002, 0.0003]) B00 = 0.0 # 选择3号机作为松弛机组,索引为2 SLACK = 2 def loss_value(P): """根据B系数公式计算每时段网损""" loss = np.einsum('ti,ij,tj->t', P, B, P) loss = loss + P @ B0 + B00 return loss def decode(genes): """ 将基因解码为完整的三机组出力矩阵。 genes只包含两台非松弛机组的出力,形状为(T, 2)。 松弛机组G3的出力通过功率平衡反算。 """ P = np.zeros((T, N_GEN)) P[:, 0] = genes[:, 0] P[:, 1] = genes[:, 1] P3 = load - P[:, 0] - P[:, 1] P[:, SLACK] = P3 # 网损和P3相互耦合,做固定点迭代,通常几步就收敛 for _ in range(10): loss = loss_value(P) P[:, SLACK] = load + loss - P[:, 0] - P[:, 1] return P def calc_cost(P): """总发电成本:Σ(a + b*P + c*P^2)""" return float(np.sum(A + B_COST * P + C_COST * P * P)) def calc_penalty(P): """约束越限惩罚""" pen = 0.0 w_limit = 8000.0 w_ramp = 5000.0 # 所有机组上下限越限 for i in range(N_GEN): pen += np.sum(np.maximum(0, Pmin[i] - P[:, i]) * w_limit) pen += np.sum(np.maximum(0, P[:, i] - Pmax[i]) * w_limit) # 爬坡越限 for i in range(N_GEN): diff = np.abs(P[1:, i] - P[:-1, i]) pen += np.sum(np.maximum(0, diff - Ramp[i]) * w_ramp) return float(pen) def fitness(genes): P = decode(genes) return calc_cost(P) + calc_penalty(P) def init_individual(): """ 初始化个体时利用爬坡限制随机游走。 保证G1/G2从出生起就尽量不违反爬坡约束。 """ genes = np.zeros((T, 2)) for i in range(2): genes[0, i] = np.random.uniform(Pmin[i], Pmax[i]) for t in range(1, T): prev = genes[t - 1, i] val = prev + np.random.uniform(-Ramp[i], Ramp[i]) genes[t, i] = np.clip(val, Pmin[i], Pmax[i]) return genes def tournament_select(pop, fits): idx = np.random.choice(len(pop), 3, replace=False) best_idx = idx[np.argmin(fits[idx])] return pop[best_idx] def crossover(p1, p2): """单点交叉,随机选一个时段点作为分界""" if np.random.rand() < 0.8: point = np.random.randint(1, T) c1 = np.concatenate([p1[:point], p2[point:]], axis=0) c2 = np.concatenate([p2[:point], p1[point:]], axis=0) return c1, c2 return p1.copy(), p2.copy() def mutate(ind): """对随机时段、随机机组的基因加一个小扰动""" new_ind = ind.copy() i = np.random.randint(0, 2) t = np.random.randint(0, T) delta = np.random.uniform(-0.15, 0.15) * (Pmax[i] - Pmin[i]) new_ind[t, i] = np.clip(new_ind[t, i] + delta, Pmin[i], Pmax[i]) return new_ind def ga_run(pop_size=80, generations=300, mutation_rate=0.15): pop = [init_individual() for _ in range(pop_size)] best_fit_history = [] best_ind_history = [] for gen in range(generations): fits = np.array([fitness(ind) for ind in pop]) best_fit_history.append(fits.min()) best_ind_history.append(pop[np.argmin(fits)].copy()) new_pop = [] while len(new_pop) < pop_size: parent1 = tournament_select(pop, fits) parent2 = tournament_select(pop, fits) child1, child2 = crossover(parent1, parent2) if np.random.rand() < mutation_rate: child1 = mutate(child1) if np.random.rand() < mutation_rate: child2 = mutate(child2) new_pop.append(child1) new_pop.append(child2) # 精英保留:把上一代最好的个体直接放进下一代 new_pop[0] = best_ind_history[gen] pop = new_pop[:pop_size] final_fits = np.array([fitness(ind) for ind in pop]) best_idx = np.argmin(final_fits) P_best = decode(pop[best_idx]) return P_best, best_fit_history

3.4 运行结果解读

P_best, his = ga_run() loss = loss_value(P_best) cost = calc_cost(P_best) print("总发电成本:", cost) print("总网损电量:", loss.sum()) print("平均网损率:", loss.sum() / load.sum() * 100, "%") # 查看约束违反情况 for i in range(N_GEN): over_lim = np.maximum(P_best[:, i] - Pmax[i], 0).sum() + np.maximum(Pmin[i] - P_best[:, i], 0).sum() diff = np.abs(P_best[1:, i] - P_best[:-1, i]) over_ramp = np.maximum(diff - Ramp[i], 0).sum() print(f"机组{i+1} 越限总量: {over_lim:.4f} MW,爬坡越限总量: {over_ramp:.4f} MW")

我这边跑出来的典型结果是成本在几十万元量级,网损率在3%左右,而且末代个体基本能保证爬坡越限总量很小。收敛曲线会呈现快速下降后进入平台期的形态——前80代成本下降明显,后面主要是继续调整约束越限部分的小量改进。

从出力结果看,夜间低负荷时段三台机组都在较低水平运行,白昼高峰时段G2和G3出力明显抬升,G1在尖峰时段才被推到较高位置。这正是带了爬坡约束后的典型调度形态:机组出力变化是连续的、光滑的,而不是在经济最优的“跳变解”。

4. 遗传算法跑起来之后的常见坑与排查思路

4.1 早熟收敛:种群还没搜索够就抱团取暖

GA最常见的现象是前几十代收敛很快,后面几乎没有改进。这未必是坏事,如果目标函数是凸的,快速收敛说明算法很快锁定了好区域。但在带强约束的问题里,快速收敛往往是“假收敛”:种群里的个体高度相似,大家都落在某个违规惩罚较小的局部区域,而不是真正的经济最优区。

我常用的解决办法有三个。

第一,提高变异率。不要让变异只发生在单个时段,可以设计一种“连续变异”:随机选一个时间段区间,对区间内所有时段的基因都加一个趋势性偏移。这样变异既能改变局部细节,也能影响整段爬坡曲线。

第二,增大种群规模。48维搜索空间不算大,但带复杂约束的问题很容易形成大量局部陷阱,种群太小根本覆盖不过来。建议从60起步,多试80、100,观察曲线是否还有明显下降空间。

第三,调整选择压力。锦标赛规模越大,选择压力越强,收敛越快,但也更容易早熟。我这里用3是很温和的设定,如果发现早熟,可以降到2试试。

4.2 罚函数系数总在对与错之间摇摆

惩罚系数是这类方法的灵魂,也是最难调的部分。罚得轻了,GA会给出明显违约的结果,因为违约带来的成本降低大于被罚的数量;罚得重了,算法会倾向于满足约束,但也可能因此错过一些合理的候选解,收敛变得很慢。

我的经验是:先把约束越限的惩罚权重设为发电成本量级的100-1000倍。本算例里,单台机组小时成本大约几千元,越限1MW如果只罚100元,等于让算法“鼓励”越限;罚到5000-10000元,越限1MW的代价和正常发电成本拉开量级,合理。

调参技巧是:跑一次,打印每个约束的越限总量和惩罚量。如果一个约束越限几乎为零但总成本很高,说明罚重了;如果某个越限频繁出现,说明对应权重偏低,或者搜索空间根本不好找可行解。

4.3 爬坡越限总是消不掉怎么办

如果最后结果的成本很低,但爬坡越限总量一直不为零,问题通常不在权重,而在搜索结构。

本项目的代码里,初始化时用了随机游走,这能保证G1和G2的初始爬坡基本满足,但单点交叉会直接打断爬坡连续性。父代1的前半段和父代2的后半段拼接后,交界处很可能出现一个很大的出力跳变。这个跳变虽然在适应性评估时会被罚,但GA需要很多代才能修复这种“结构性伤害”。

解决方向有几个:

  • 改用均匀交叉:每个时段独立选择来自哪个父代,而不是在时间轴上切一刀。均匀交叉对连续变量的破坏更小。
  • 加入启发式修复:交叉或变异之后,检查每个机组相邻时段是否越限,如果越限就把越限方向的出力拉回来。这是一种拉马克式修复,能大幅提升解可行性。
  • 增加对爬坡约束的权重同时,也给变异操作加一个“爬坡感知”:变异时优先选相邻时段,让变异后的偏移在爬坡允许范围内。

实际项目中,我最后用了“变异后修复”的做法,代价是计算量增加一点,但可行性提升非常明显。

4.4 为什么代码换一组数据就跑不出好结果

GA的性能严重依赖问题规模、约束结构和数据量级。换机组数、换时段数、换负荷曲线之后,原先调好的惩罚权重基本都要重调。

这里有一个重要原则:在写适应度函数时,尽量把成本项和惩罚项都做“归一化”。比如发电成本除以一个基准成本,越限惩罚也除以一个量纲统一后的数值。这样换数据后参数仍有参考性。如果直接把5000、8000这种绝对权重写死在里面,遇到成本量级差100倍的新算例,整个算法又要推倒重来。

另外,时段粒度变细以后,比如从1小时间隔变成15分钟间隔,爬坡约束的允许变化量要除以4,但遗传算法里的变异步长并不会自动缩放。这也是换参数后经常崩溃的原因。

5. 这个项目还能怎么扩展

5.1 从单目标走向多目标:成本和排放一起优化

当前版本只优化发电成本。现实调度中,碳排放、污染物排放、机组启停次数、燃料消耗量都可以作为优化目标。可以把模型改写成多目标优化,用NSGA-II这类算法同时得到一组Pareto前沿解,再交给决策者选择。

从代码结构上看,只需要把适应度函数改成返回一个目标向量,然后替换选择算子为基于支配关系的锦标赛选择。其他部分,包括解码、松弛机组、爬坡惩罚,几乎不用动。这个扩展非常适合作为课题的下一个阶段。

5.2 和商业求解器、数学规划方法做对比

遗传算法不是万能钥匙。机组数和时段数非常大时,比如上百台机组、96个时段,纯GA的搜索效率会明显下降,因为浮点数编码的搜索空间太大了。此时可以考虑先用拉格朗日松弛把问题拆成分机组子问题,再用动态规划求解每个单机组的时序优化问题,或者直接用商业求解器处理二次约束规划模型。

我建议把这套Python代码当成“原型验证工具”,而不是最终的生产求解器。它的价值在于快速验证模型和约束的合理性,给后面的精确求解算法提供一个高质量初值。事实上,用GA结果当初始解,再交给局部优化算法做精修,这种“粗搜+精修”的组合策略在工程上非常有效。

5.3 从离线优化到滚动在线调度

上面的代码是离线一次性优化24小时。实际调度场景里,负荷预测每天都在更新,机组状态也在实时变化。真正能落地的版本,应该是“滚动优化”:每4小时或者每小时更新一次负荷预测,重新跑一次优化,只执行未来一个时段的调度决策,然后滚动前移。

这个扩展对算法的实时性提出了要求。GA作为一种慢速算法,直接做小时级在线调度有点吃力,但可以结合历史最优解热启动、限制种群规模、缩短迭代代数等方法,在可接受的时间内输出一个次优解,再交给人工确认。作为研究项目,先把离线版本跑通,再逐步加滚动窗口,是合理的推进路径。

我个人在完成这个项目后最深的体会是:遗传算法这部分代码,真正的门槛不在算法本身,而在你把约束梳理清楚、把编码设计合理的那一步。松弛机组、爬坡感知初始化、线性惩罚这三个小设计,比后面调几千代参数都管用。如果你正在做类似的经济调度优化,建议先把模型公式写漂亮,再动手写GA,顺序反了的调试成本会非常感人。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询