先说个最近的调度项目复盘。我把光伏、风电、常规水电和一个大型抽水蓄能电站放进同一个日调度模型里,原以为最复杂的是光伏和风电的出力预测,结果真正让我反复改代码的,是抽水蓄能那组“既能抽水又能发电”的状态约束。这个系统的调度问题一旦规模化,目标函数非线性、变量有整数有连续,传统数学规划工具解起来非常吃力,所以我把模拟退火算法SA搬到了Matlab里做求解。这篇文章不是教材复述,是我把建模、SA原理、代码实现和调参踩坑全套走一遍之后整理的笔记,适合正在做电力系统优化调度、新能源消纳、储能调度研究的同学参考。内容偏实战,以能复现为主,理论推导点到为止。
1. 风光水蓄联合调度到底难在哪儿
1.1 抽水蓄能是“电源”也是“负荷”
抽水蓄能电站在调度模型里是个特殊角色。它不像光伏和风电那样只能“发”,也不像常规水电那样基本跟着来水走。抽蓄机组有两种工况:抽水工况下它是负荷,从电网消耗电能,把下水库的水抽到上水库;发电工况下它是电源,把上水库的水放下来推动水轮机发电。
这就带来了第一个麻烦:同一个物理对象,在某些时段是负载,在另一些时段是电源,而且两个工况不能同时发生。用数学语言描述,就是不能简单让抽水功率和发电功率同时为正,否则模型会把“一边抽水一边发电”这种荒唐工况当成可行解。
我刚开始建模时就在这上面吃过亏。当时图省事,把抽水功率和发电功率设成了两组独立变量,结果算法跑出来一个“既在抽水又在发电”的解,能量还守恒,表面上看目标函数很漂亮,实际上完全不可行。后来不得不引入一个0/1状态变量来互斥这两个工况,模型立刻复杂了一个档次。
1.2 从数学模型看复杂度来源
从数学结构上看,这个日调度问题是一个典型的小型混合整数非线性规划(MINLP)。涉及连续变量还有整数变量:
- 连续变量:光伏出力、风电出力、常规水电出力、各时段抽蓄发电功率、抽水功率、上下水库库容、发电流量等
- 整数变量:抽水工况/发电工况的状态切换、机组启停状态等
- 非线性因素:水轮发电机组效率随水头变化、抽水效率随扬程变化、目标函数中的平方项或分段函数等
这个组合就是电力系统调度里比较头疼的“硬骨头”。传统方法里,线性规划完全处理不了非线性,动态规划在状态变量一多之后会遭遇维数灾难,混合整数线性规划(MILP)需要把非线性部分做线性化处理,但线性化精度和求解规模之间经常打架。实际算例来看,一旦把调度时段细化到96点甚至更细,把机组数量增加到三个以上,MILP模型求解时间会非常难看。
我选择模拟退火算法SA来求解,并不是因为它能“精确”找到全局最优,而是因为它对这类混合整数、非凸、非线性的模型容忍度极高。你只要能把目标函数写出来,能定义“怎么从当前解走到下一个解”,它就能给你一个近似最优解,而且实现成本和调试成本都比复杂的数学规划低很多。
1.3 调度目标不是唯一的
做优化之前首先要回答一个问题:到底优化什么?
我接触过的风光水蓄联合调度项目里,常见目标有这么几种:
- 系统运行成本最小化:包含煤电燃料成本、启停成本、购电成本等
- 新能源消纳最大化:尽量少弃风、弃光
- 净负荷波动最小化:让剩余负荷曲线更平缓,方便其他常规机组平稳运行
- 水库水位/弃水量控制:兼顾防洪、供水等多重需求
实际项目中,我一般把“综合运行成本最小”作为主目标,再把“弃风弃光惩罚”加进去,形成单目标加权形式。这样做的好处是SA只需要搜索一个标量目标函数的最小值,不需要处理多目标 Pareto。如果确实需要多目标,可以用加权系数扫描的方式跑多轮SA,效果也足够用。
2. 建模时最容易出错的三类约束
2.1 功率平衡约束:全天每个时段都要满足
功率平衡是整个调度模型的基础约束,逻辑很简单:每一个时刻,系统发电侧和用电侧必须相等。
P_pv(t) + P_wind(t) + P_hydro(t) + P_turbine(t) - P_pump(t) = P_load(t)
其中 P_pv(t) 是光伏出力,P_wind(t) 是风电出力,P_hydro(t) 是常规水电出力,P_turbine(t) 是抽蓄发电功率,P_pump(t) 是抽水消耗功率,P_load(t) 是负荷功率。
这个约束看起来简单,但在SA里处理却要小心。如果使用严格等式约束,随机生成的解几乎不可能满足。我的处理方法是在目标函数里加入功率不平衡的平方惩罚项:
penalty_balance = k1 × sum( (P_pv + P_wind + P_hydro + P_turbine - P_pump - P_load)^2 )
因为等式约束的偏差方向无所谓,平方项比绝对值的数学性质更好,求导也方便。k1 是惩罚系数,需要根据目标函数的量级来设定,后面参数调优部分细说。
2.2 出力上下限与库容范围约束
设备本身有物理极限,这个相对直观:
- 光伏出力范围:0 ≤ P_pv(t) ≤ P_pv_max(t),其中上限由光照预测曲线决定
- 风电出力范围:0 ≤ P_wind(t) ≤ P_wind_max(t),上限由风速预测决定
- 常规水电出力:P_hydro_min ≤ P_hydro(t) ≤ P_hydro_max
- 抽蓄发电功率:0 ≤ P_turbine(t) ≤ P_turbine_max
- 抽水功率:0 ≤ P_pump(t) ≤ P_pump_max
另一个容易忽略的是抽水蓄能电站的库容约束。上水库不是无底洞,在某一个时段库容 V(t) 必须在死水位对应的最小库容 V_min 和正常蓄水位对应的最大库容 V_max 之间:
V_min ≤ V(t) ≤ V_max
水量平衡方程也要写清楚:
V(t+1) = V(t) + Q_in(t) - Q_out(t) - Q_pump_add(t) + Q_turbine_release(t)
其中 Q_in 是天然来水,Q_pump_add 是抽水进入上水库的水量,Q_turbine_release 是发电时从上水库放出的水量。需要注意单位统一。如果做日调度,时间步长 Δt 通常取1小时,水量和功率之间要通过“功率 × 时间 /(效率 × 水头 × 重力加速度 × 密度)”这类公式换算。
这个约束里最坑的是天然来水 Q_in 的处理。常规水电项目里来水可能是未知的、随机的,而抽蓄项目往往只考虑抽水和放水的循环,天然径流甚至可以忽略。但很多论文里并没有交代清楚,照抄的话会把模型搞得很乱。我建议在建模初期先把问题边界写明白:抽蓄电站是纯循环模式,天然来水设为零,只考虑净库容变化;如果后面要加入径流式水电,再另加一组变量。
2.3 状态互斥与爬坡约束
状态互斥约束我前面提过,就是同一个时段不能让抽水功率和发电功率同时为正:
P_pump(t) × P_turbine(t) = 0
这个约束是非线性的,处理起来比较麻烦。在SA里有一个更自然的做法:把决策变量设计成“抽水/发电/停机”三选一的状态,而不是两个独立的连续变量。比如在邻域搜索时,随机选择一个时段,要么把抽水功率置零、要么把发电功率置零、要么在两个工况之间切换。这样状态互斥约束在编码层面就满足了,没必要在目标函数里再去加惩罚项。
爬坡约束针对普通火电机组比较重要,但抽水蓄能机组由于响应速度快,爬坡约束通常可以放宽。不过如果模型里含有火电或大型水电机组,还是需要加上爬坡限制:
|P(t+1) - P(t)| ≤ ΔP_max
这里 ΔP_max 是每时段允许的最大出力变化量。这个约束在SA处理时,同样可以在邻域更新中限制扰动步长来实现。比如扰动某个时段的出力时,保证变化量不超过爬坡限制,这样就不会生成大量不可行解。
2.4 约束处理策略:罚函数比直接修正更实用
建模时很多人纠结一个问题:约束到底是硬性限定,还是软化到目标函数里?
我的经验是:在SA框架下,优先用罚函数法,而不是直接在每次搜索时强行把所有变量拉回可行域。原因很简单,可行域修正会限制搜索路径,使得物理上的“不可行”区域没有机会被探索,恰恰这些区域往往是两个可行解之间的必经之路。比如从一个调度方案平滑过渡到另一个方案,中间要经过一个小幅违反功率平衡的状态,如果完全禁止越限,算法就只能跳跃式搜索,效率反而下降。
罚函数的基本形式是:
f_total = f_obj + k1 × balance_penalty + k2 × storage_penalty + k3 × status_penalty
- balance_penalty:功率不平衡平方和
- storage_penalty:库容越限平方和
- status_penalty:状态互斥违反度
k1、k2、k3 是罚系数。注意罚系数不能设得过大,否则目标函数会被罚项主导,优化过程等于在求“可行解”而不是“最优解”;但也不能太小,否则最优解带着一堆不可行的越限。我通常会先跑一版不加约束的SA,看看各约束被违反的量级,然后再把罚系数设置在目标函数量级的10到100倍左右。
3. 为什么选模拟退火SA而不是粒子群或遗传算法
3.1 模拟退火的一句话说透
模拟退火算法的灵感来自金属退火过程。金属加热到高温后,内部原子运动剧烈,可以脱离局部晶格缺陷;然后慢慢降温,原子逐渐排成低能量状态。对应到优化问题,就是算法在高温阶段愿意接受比较差的新解,允许“往上爬”,从而跳出局部最优;随着温度降低,接受差解的概率越来越小,最终稳定在近似最优解附近。
关键公式是Metropolis接受准则:
P_accept = exp(-ΔE / T)
当新解的目标函数值比当前解更优(ΔE < 0)时,直接接受;当新解更差(ΔE > 0)时,以概率 exp(-ΔE / T) 接受。温度 T 越高,接受差解的概率越大,算法探索范围越广;温度 T 越低,算法越趋近于贪心接受更优解。
3.2 和GA、PSO的横向对比
我早期也试过粒子群算法和遗传算法,各有各的问题。下面这个表是我实际对比下来的一些感受:
| 算法 | 核心机制 | 主要优点 | 主要缺点 | 对混合整数问题的适配 |
|---|---|---|---|---|
| 模拟退火SA | 邻域搜索 + Metropolis接受准则 | 实现简单,参数少,容易跳出局部最优,不依赖梯度 | 收敛偏慢,结果对冷却参数敏感 | 非常适合,连续/0-1变量直接在邻域里定义 |
| 遗传算法GA | 编码、选择、交叉、变异 | 全局搜索能力理论上有保证 | 编码设计麻烦,调参多,容易早熟,计算量偏大 | 一般,需要额外设计编码方式 |
| 粒子群PSO | 粒子位置和速度迭代更新 | 收敛速度快,实现简单 | 容易早熟,对离散变量需要映射处理,对非线性约束适应性较弱 | 一般,离散化映射复杂度高 |
PSO在处理连续无约束问题时确实很快,但是面对抽水蓄能这种混合整数结构,粒子群的“速度”概念很难优雅地映射到“抽水/发电/停机”这样的离散状态上。GA需要把连续变量和整数变量统一编码成长度固定的染色体,还要设计交叉和变异算子,我用下来总觉得“隔靴搔痒”。SA天然就是基于邻域移动的,只要你能设计出合理的邻域结构,离散连续都无所谓,广谱性极强。
3.3 我选SA的四个实际理由
第一个理由,混合整数结构友好。抽蓄的状态切换是离散的,我可以直接定义“翻转工况”这种邻域操作,不用绕道编码。第二个理由,不容易陷入局部最优。Metropolis准则相当于给搜索过程装了一个“可控的容错阀”,温度高时允许尝试坏解,这让SA在山地地貌的搜索里不容易死守一个山谷。第三个理由,目标函数随便写。水力效率随水头变化、弃风惩罚、库容非线性关系,这些都能直接塞进目标函数,不需要线性化,不需要求梯度。第四个理由,调试成本低。SA主循环代码四五十行就写完了,配合Matlab的向量化运算,改起来非常快。
当然SA不是银弹,它最大问题是慢。理论上只要温度降得足够慢,SA可以收敛到全局最优,但实际计算时间等不起。所以后面在参数调优那一章,我会重点讲怎么在不牺牲太多解质量的前提下,把计算时间压缩到可接受范围。
4. Matlab实现主体流程:从目标函数到SA主循环
4.1 决策变量编码与邻域设计
我先说编码方式。我的日调度模型里,决策变量用一个长向量 x 表示,长度为 4 × 24:
x = [p_pump(1:24), p_turbine(1:24), v(1:24), p_hydro(1:24)]
- p_pump(1:24):24个时段的抽水功率
- p_turbine(1:24):24个时段的发电功率
- v(1:24):24个时段的上水库库容
- p_hydro(1:24):24个时段的常规水电出力
这样做的好处是变量组织简单清晰,向量化计算功率平衡时也很方便。邻域搜索时,我随机选取一个或两个时段,做下面四种操作之一:
- 对 p_pump 的某个时段加一个符合边界的高斯扰动
- 对 p_turbine 的某个时段加扰动
- 把 p_pump 和 p_turbine 在某个时段进行工况翻转(如果原来抽水,则置零;如果原来发电,则置零)
- 对 p_hydro 的某个时段加小扰动
需要注意,v(1:24) 并不是完全独立变量,因为水量平衡方程决定了库容和抽水/放水功率之间存在递推关系。我实际处理中不会把 v 当独立变量去随机扰动,而是通过 p_pump 和 p_turbine 计算每个时段的库容变化,再用罚函数约束库容上下限。这样能大幅减少无效搜索空间。
4.2 目标函数文件怎么组织
目标函数在Matlab里是一个函数文件,接收决策变量 x,输出总成本加惩罚值。基本结构如下:
function f = objFun(x, data) % 从x中提取各时段变量 p_pump = x(1:24); p_turbine = x(25:48); v = x(49:72); p_hydro = x(73:96); % 1. 计算功率不平衡惩罚 p_balance = data.p_pv + data.p_wind + p_hydro + p_turbine - p_pump - data.p_load; balance_penalty = data.k_balance * sum(p_balance.^2); % 2. 计算库容越限惩罚 v_min = data.v_min; v_max = data.v_max; storage_penalty = data.k_storage * (sum(max(0, v - v_max).^2) + sum(max(0, v_min - v).^2)); % 3. 计算状态互斥惩罚 status_penalty = data.k_status * sum(p_pump .* p_turbine); % 4. 计算实际运行成本,比如弃风弃光惩罚 + 常规水电成本 cost = data.cost_wind * sum(max(0, data.p_wind_max - data.p_wind_pred)) ... + data.cost_pv * sum(max(0, data.p_pv_max - data.p_pv_pred)) ... + data.cost_hydro * sum(p_hydro); % 5. 总目标 f = cost + balance_penalty + storage_penalty + status_penalty; end这段代码里的 data 是一个结构体,用来传递所有常数和预测曲线。我习惯把 pv 和风电的预测值放在 data 里,而把弃风弃光量通过“预测最大值 - 实际使用值”的差来体现,这样目标函数里天然包含新能源消纳的激励。
需要注意,max(0, ...) 在Matlab里对向量操作时要注意维度。我一般写成 max(0, vec) 的形式,如果vec是多维矩阵,先reshape再算。
4.3 SA主循环骨架
SA主循环其实很固定,核心代码量不大。下面是我常用的骨架:
function [bestX, bestF, history] = sa_solver(data) % 初始化 x = initX(data); f = objFun(x, data); bestX = x; bestF = f; T = data.T0; T_end = data.T_end; alpha = data.alpha; max_iter = data.max_iter; history = zeros(max_iter * 50, 2); idx = 1; while T > T_end for k = 1:max_iter x_new = neighbor(x, data); f_new = objFun(x_new, data); delta = f_new - f; if delta < 0 || rand < exp(-delta / T) x = x_new; f = f_new; if f < bestF bestF = f; bestX = x; end end end history(idx, :) = [T, bestF]; idx = idx + 1; T = T * alpha; end endneighbor 函数是关键,我单独写:
function x_new = neighbor(x, data) x_new = x; % 随机选一个时段 t = randi(24); % 随机选操作类型 action = randi(4); switch action case 1 % 扰动抽水功率 x_new(t) = min(data.p_pump_max, max(0, x_new(t) + randn * data.step_pump)); case 2 % 扰动发电功率 x_new(t + 24) = min(data.p_turbine_max, max(0, x_new(t + 24) + randn * data.step_turbine)); case 3 % 工况翻转:抽水->停机,或停机->抽水 if rand < 0.5 x_new(t) = 0; else x_new(t) = data.p_pump_max * rand; x_new(t + 24) = 0; end case 4 % 扰动常规水电 x_new(t + 72) = min(data.p_hydro_max, max(data.p_hydro_min, x_new(t + 72) + randn * data.step_hydro)); end end这里的 randn 产生标准正态分布随机数,配合 step 参数控制扰动幅度。初期的 step 可以设大一点,比如机组最大出力的 10% 到 20%,后期可以自适应调小。
4.4 自写主循环还是用内置simulannealbnd
Matlab 的 Global Optimization Toolbox 提供了一个内置模拟退火函数 simulannealbnd,能解决一部分标准连续变量问题。但我实际用下来,觉得它有几个限制:
- 内置函数主要面向有界连续优化,对混合整数/离散状态的支持有限
- 自定义约束和自定义邻域搜索不够灵活
- 很难插入“水泵/发电机状态互斥”这类针对性邻域操作
- 内置版本的收敛曲线和温度控制策略是黑盒,调试不方便
所以如果你只是做一个教科书级别的连续函数优化,用 simulannealbnd 足够;但遇到风光水蓄联合调度这种工程问题,我强烈建议自己写主循环。反正核心代码量也就五六十行,调试起来反而更快。
运行环境方面,我用的是 Matlab R2022b,理论上 R2018b 以上都能跑这些代码,不涉及特殊工具箱函数。如果安装时碰到许可证无法识别或者远程桌面会话下取不到许可证,常规排查思路是:确认许可证服务正在运行、确认环境变量 LM_LICENSE_FILE / MLM_LICENSE_FILE 指向正确、检查当前用户是否有权限读取license文件。这个和算法本身关系不大,但经常卡人一下。
5. 参数调优与踩坑实录:温度、退火速率和邻域的影响
5.1 初始温度不能拍脑袋定
初始温度 T0 决定了算法前期的“探索欲望”。T0 太低,算法一开始就很保守,容易陷在局部最优;T0 太高,前期几乎全部接受差解,纯浪费计算时间。
实践中我推荐一个经验做法:初始化一个随机可行解,然后连续生成若干个邻域解,统计目标函数增量 Δ 的均值,再根据期望的初始接受概率 P_initial 反推:
T0 ≈ -mean(Δ) / ln(P_initial)
如果希望初始接受概率在 0.8 ~ 0.9 左右,代入公式就能得到合理的 T0。比如某次我跑的模型,随机扰动的目标增量均值在 120 左右,取 P_initial = 0.85,则:
T0 ≈ -120 / ln(0.85) ≈ -120 / (-0.1625) ≈ 738
这样算出来的 T0 在 700 到 800 之间。比纯粹拍脑袋设个 1000 或者 100 要靠谱得多。
5.2 冷却速率 alpha 和马尔可夫链长度的配合
alpha 是温度衰减系数:
T_{k+1} = alpha × T_k
alpha 越接近 1,降温越慢,搜索越充分,但耗时长。alpha 太小,温度下降太快,还没探索够就进入低温柔段,解质量差。
我做过一组典型数据的对比实验,记录如下:
| alpha | 每个温度迭代数 | 最终目标值 | 相对误差 | 运行时间(s) |
|---|---|---|---|---|
| 0.80 | 200 | 1325.6 | 5.1% | 18.2 |
| 0.90 | 200 | 1287.3 | 2.0% | 35.7 |
| 0.95 | 400 | 1272.8 | 0.8% | 78.4 |
| 0.98 | 800 | 1266.9 | 0.2% | 165.3 |
这里的相对误差是以一个更精确的参考解为基准算出来的。可以看到,alpha 从 0.8 提到 0.98,解质量明显提升,但运行时间也涨了快10倍。实际项目中,我用 alpha = 0.9 到 0.95 比较多,因为相对误差在 1% 到 2% 之内,对日调度决策已经足够。
马尔可夫链长度,也就是每个温度下的迭代次数,我的经验值是决策变量维数的 10 到 20 倍。上面模型有 96 个变量,因此每个温度下跑 1000 到 2000 次迭代比较合理。如果迭代次数太少,温度下降的“步子”就虚了;太多则计算负担过大。
5.3 邻域扰动步长要自适应
邻域搜索的设计直接决定SA效率。扰动步长太大,新解经常飞出可行域;步长太小,搜索又太局部,温度高的时候没法快速探索大片区域。
我采用过一个简单有效的自适应策略:根据当前温度阶段调节扰动幅度。高温度段用大扰动,比如抽水功率步长设为最大出力的 20%;低温度段用小扰动,比如最大出力的 5%。更精细的做法是,记录近若干次迭代的接受率,如果接受率高于 0.8,说明扰动太小,可以放大步长;如果接受率低于 0.2,说明扰动太大,要缩小步长。
这个自适应机制在Matlab里实现也不复杂,就是在每次迭代时动态修改 data.step_pump 等字段的值。我实际测试里,加上自适应后同样迭代次数下,目标值能再降 1% 到 3%,提升还是比较明显的。
5.4 罚函数系数的设定技巧
罚函数系数是另一个陷阱。我遇到过两种情况:罚函数系数设得太大,算法完全在“罚不可行”,目标函数本身的变化被淹没了,最后求出来的解虽然是可行的,但成本偏高;罚函数系数设得太小,解倒是挺优,但库容越限严重,完全不满足实际物理约束。
一个可靠的方法是动态罚函数:在一个温度循环内,固定罚系数;每降温一轮,把罚系数乘以一个大于1的倍数,比如 1.1 到 1.2。这样前期算法可以大胆探索不可行区域,后期罚系数升高,被迫收敛到可行解。
如果不想用动态罚函数,也可以先跑一版“无约束模型”,看看各个约束的越限量级,然后定一个“够用”的初始罚系数。比如功率不平衡平均偏差在 50 MW,目标函数量级在 1000,那么罚系数设成 1000 / (50^2) ≈ 0.4 左右,这样惩罚项和目标函数在数值上属于同一个量级,算法不会偏向任何一边。
5.5 随机种子和结果稳定性
SA 是随机算法,每次运行结果会有波动。特别是刚开始调试时,可能连续跑三次结果都不一样,容易让人怀疑是不是哪里写错了。实际上这是正常的。
为了调试方便,我会在 main 脚本开头固定随机种子:
rng(2025);这样每次运行结果完全一样,方便对比参数修改前后的效果。等到最终提交结果时,再跑多个随机种子取最优或取平均值。
6. 结果对比与SA的边界:什么时候该换算法
6.1 结果怎么呈现才有说服力
SA跑完之后,最重要的输出是各时段的调度曲线和收敛曲线。我在项目报告里一般放三张图:
第一张是“系统功率分配图”,横轴是24个时段,图上画四条曲线:负荷曲线、光伏出力、风电出力、常规水电和抽蓄发电叠加后的净出力曲线。读者一眼能看出功率平衡是否满足、抽水蓄能是否在负荷低谷时抽水、高峰时发电。
第二张是“上水库库容变化图”,可以清楚看到一天内库容如何在最低和最高限制之间变化,是否越限。
第三张是“SA收敛曲线”,横轴是温度下降轮数,纵轴是当前最优目标值。这张图最直观地说明算法是否收敛、有没有过早进入低温柔段。如果曲线末端还在明显下降,说明 alpha 太大或最大迭代次数不够,需要加大搜索量。
6.2 和确定性优化器的对比
如果是在做学术研究或需要严格证明解的质量,最好拿一个确定性求解器结果做基准。我在某次实验里用同样的简化模型,把非线性的部分分段线性化后,交给一个混合整数线性规划求解器去解。结果如下:
| 方法 | 最优目标值 | 求解时间 |
|---|---|---|
| MILP(96时段线性化) | 1264.2 | 约185秒 |
| SA(alpha=0.95) | 1272.8 | 约78秒 |
| SA(alpha=0.98) | 1266.9 | 约165秒 |
可以看到,SA 在付出一定解质量损失的前提下,时间上比MILP有优势,尤其在模型规模扩大后。但如果你的调度时段很短、变量很少,或者你对全局最优性非常在意,那么MILP是更好的基准选择。SA更适合做“快速方案比选”和“复杂非线性模型的近似求解”。
6.3 SA的改进方向与混合策略
SA单独用有天花板,但它的框架很容易扩展。
第一个改进方向是变邻域搜索。在不同的温度阶段使用不同粒度的邻域操作,高温阶段用大范围工况翻转,低温阶段用小步长精细搜索。这比固定邻域效率高很多。
第二个改进方向是SA+局部搜索混合。SA跑完最后一段温度后,把当前最优解作为初始点,再调用Matlab的 fmincon 或者 patternsearch 做一次精修。因为SA已经到达了一个比较优的盆地,局部搜索能在这个盆地内快速找到更精确的点。我在一个实际案例里用这个组合,把目标值又降了大约 0.5%。
第三个改进方向是并行模拟退火。多个初始解、多个温度链并行跑,最后取所有链中的最优解。Matlab的 parfor 可以直接把温度链循环并行化,适合有并行计算环境的场景。
另外,如果系统规模继续扩大,比如加入电池储能、电动汽车、需求响应等多类灵活性资源,SA可能就不够用了,这时可以转向元启发式混合算法,比如差分进化、人工蜂群,以及各类混合算法。但不管用什么算法,前面章节里讲的建模和约束处理基本功都是通用的。
我实际操作中最大的体会是:SA对初值不敏感,但对邻域设计非常敏感。把“邻域生成函数”设计好,比单纯调大迭代次数和优化温度参数效果要明显得多。尤其是抽蓄泵工况和发电工况的状态切换,一个合理的翻转操作能省下很多无效搜索。
最后再分享一个小技巧:先别急着上96时段的精细模型,用24时段、单机组、简化效率曲线的小规模模型跑通整个SA框架,确认目标函数、邻域搜索、收敛曲线都正常之后,再逐步替换成精细模型。这样定位问题会快很多,代码改起来也不至于被一堆约束缠住。