简介:这份资源面向电力系统研究人员、配电网规划工程师及韧性电网方向学者,围绕台风等极端灾害下配电网全过程韧性提升问题,复现了基于Wasserstein距离与CVaR理论的多类型韧性资源分布鲁棒机会约束规划方法。内容涵盖不确定性模糊集构建、预防-应急-抢修双层三阶段模型、混合整数二阶锥规划转化与求解,并给出完整Python代码及逐段解释,便于读者理解建模思路并动手复现。资源包共1个PDF文件,约790KB,集中呈现论文复现说明、数学模型推导与代码实现,结构紧凑,适合按章节顺序研读。目前已有147人学习。读者可从中获得从理论框架到代码落地的完整链路,包括模糊集与CVaR处理尾部风险的技巧、三阶段决策变量的定义方式、求解与可视化流程,以及不同策略效果对比的案例分析,对开展配电网韧性规划、分布式能源与储能协同配置等研究具有直接参考价值。
1. 台风季配电网规划:为什么“韧性”不能只算设备加固费
每年台风季,沿海城市配电网的抢修队伍几乎连轴转。杆塔倒伏、线路跳闸、开关站进水,这些场景对一线运维来说并不陌生。但真正让规划岗头疼的,不是灾后修得多快,而是灾前这笔钱怎么花才不冤——全换成地下电缆当然韧性高,可投资回收期长到让财务皱眉;只做常规N-1校验,一场十七级风圈过境,多个变电站同时失压,N-1准则直接失效。
这就是“全过程韧性”要解决的问题:不是单看某个设备扛不扛得住,而是把灾害演进拆成预防、抵抗、恢复三个阶段,分别量化资源投入的效果。而多类型资源,指的是储能、可中断负荷、移动发电车、联络开关这些手段各有各的响应时间和空间约束,不能简单加总。分布鲁棒机会约束规划,则是在台风路径和强度不确定的前提下,既不假设已知概率分布,也不做最坏情况的绝对保守决策,而是用Wasserstein距离构造一个围绕经验分布的模糊集,把机会约束嵌进去,再用CVaR把尾部风险折算成可优化的线性项。这套组合拳在论文里逻辑自洽,但复现时参数一改,结果可能天差地别。下面按我实际跑通这套模型的顺序,把每个环节拆开讲。
2. 从台风路径到模糊集:Wasserstein距离和CVaR怎么接进配电网模型
2.1 为什么不用鲁棒优化或随机规划,偏要选分布鲁棒
常规鲁棒优化把台风强度设成区间上限,所有资源按最坏情况配置,结果往往是储能容量翻倍、联络线截面加大,投资高到评审会直接打回。随机规划则要求知道台风路径和强度的联合概率分布,实际中样本量少则几十条历史台风记录,多则几百条,拟合出来的分布尾部误差极大,机会约束的置信水平根本兜不住。
分布鲁棒机会约束的折中逻辑是:我不信你给的经验分布,但我也不跑到无穷保守,而是以经验分布为中心,用Wasserstein距离画一个半径ε的球,所有落在球内的分布都算“可能”。机会约束要求在这个球内最坏的分布下,系统失负荷概率仍低于阈值。这样既保留了数据驱动,又给尾部风险留了缓冲。Wasserstein距离的好处是它对样本扰动的敏感度有理论界,且和CVaR有对偶关系,能把无限维的分布优化转成有限维的线性规划。
2.2 机会约束转CVaR:把概率约束变成可求解的线性项
机会约束原始形式是 ( \mathbb{P}(g(x,\xi) \leq 0) \geq 1-\alpha ),其中ξ是台风随机变量,x是资源规划决策。直接处理概率不可导,常规做法是用CVaR近似:当CVaR_{α}(g(x,ξ)) ≤ 0时,机会约束成立。更精确地说,对于任意分布P,( \mathbb{P}(g \leq 0) \geq 1-\alpha ) 等价于 ( \text{CVaR}_{\alpha}(g) \leq 0 )。而分布鲁棒版本进一步要求模糊集内最坏分布的CVaR也≤0。
Wasserstein模糊集下的最坏CVaR有对偶形式,最终可以写成:
[ \inf_{\lambda \geq 0} \left{ \lambda \varepsilon + \frac{1}{N}\sum_{i=1}^{N} \max(0, g(x,\xi_i) - \lambda \cdot d(\xi_i, \xi_0)) \right} ]
其中ε是Wasserstein半径,N是样本数,d是样本到参考点的距离。这个形式是凸的,可以用现成求解器处理。实际写代码时,我一般把max项用辅助变量线性化,避免直接写max导致求解器报错。
2.3 配电网全过程韧性的三阶段指标怎么定义
预防阶段看的是灾前资源预布局是否合理,指标用“关键负荷供电裕度”,即台风登陆前24小时,重要负荷(医院、水厂、通信基站)的备用电源可支撑时长。抵抗阶段看灾害发生时的网络连通性,用“失负荷期望”除以总负荷,但要注意这里的时间粒度要细到小时级,因为台风过境通常6到12小时,粗粒度会平滑掉峰值。恢复阶段看灾后恢复速度,用“负荷恢复曲线下面积”与理想恢复曲线的比值,越接近1越好。
这三个指标不能各自独立优化,否则预防阶段多投储能,抵抗阶段可能因为储能放电深度限制反而降低支撑时长。我的做法是把三阶段指标加权成一个综合韧性指数,权重根据当地台风频率和负荷重要性调整。沿海高发区抵抗阶段权重给到0.5,内陆低发区预防阶段权重可以到0.4。
2.4 多类型资源的建模差异:储能、可中断负荷、移动发电车
储能的核心参数是额定功率、容量、充放电效率、SOC上下限。在韧性模型里,还要加一个“灾前预留SOC”变量,因为台风预警后储能可能被调度到满充状态,但满充会加速老化,需要在目标函数里加退化成本项。
可中断负荷不是简单切负荷,而是分等级:一级负荷不可中断,二级负荷可中断但需提前通知,三级负荷可即时中断。建模时用0-1变量表示中断状态,但要注意通知时间约束,提前通知时间越长,可中断容量越大。
移动发电车的关键是位置和路径。灾前预布局时,发电车停在哪个变电站,决定了灾后能多快接入关键负荷。我一般把候选位置设为配电网节点,用运输时间矩阵约束接入时间,运输时间由路网距离和台风风速共同决定,风速超过25m/s时运输时间乘1.5倍。
2.5 最小可复现算例:IEEE 33节点加台风场景生成
复现时不要一上来就上真实城市配电网,先用IEEE 33节点系统跑通。台风场景生成用蒙特卡洛抽样:路径用随机游走模拟,强度用对数正态分布,每次抽样得到一条台风轨迹和对应的风速-时间序列。然后根据风速计算线路故障概率,用状态抽样得到故障场景集。
import numpy as np from scipy.stats import lognorm def generate_typhoon_scenarios(n_scenarios, n_nodes, base_wind=30): """ 生成台风场景:每条场景包含各节点风速和线路故障状态 n_scenarios: 场景数,建议先设50跑通 n_nodes: 配电网节点数,IEEE33就是33 base_wind: 基准风速 m/s """ scenarios = [] for _ in range(n_scenarios): # 台风路径随机游走,影响各节点风速 path_offset = np.cumsum(np.random.randn(n_nodes) * 0.5) wind_speed = base_wind * np.exp(-0.1 * np.abs(path_offset)) # 风速超过35m/s线路故障概率显著上升 fault_prob = 1 / (1 + np.exp(-(wind_speed - 35) / 3)) line_status = (np.random.rand(n_nodes) > fault_prob).astype(int) scenarios.append({ 'wind': wind_speed, 'line_status': line_status }) return scenarios # 跑50个场景看分布 scenarios = generate_typhoon_scenarios(50, 33) wind_all = np.array([s['wind'] for s in scenarios]) print(f"风速均值 {wind_all.mean():.1f} m/s, 最大 {wind_all.max():.1f} m/s")这段代码的逻辑是先模拟台风路径偏移,再折算成各节点风速,最后用Sigmoid函数把风速映射成故障概率。参数base_wind根据当地历史台风调整,沿海强台风区可以设到35,内陆设25。n_scenarios先跑50看计算时间,如果求解器能在5分钟内收敛,再增加到200。注意故障概率函数不是唯一选择,也可以用威布尔分布拟合历史故障数据,但Sigmoid在风速阈值附近更平滑,数值稳定性好。
3. 分布鲁棒机会约束的代码实现:从对偶形式到求解器调用
3.1 模糊集半径ε的选取:不能拍脑袋,也不能全信理论界
Wasserstein半径ε决定了模糊集的大小。ε=0退化成随机规划,ε→∞变成最坏情况鲁棒优化。理论上有有限样本下的置信界公式,但那个界通常偏大,实际用会过于保守。我的经验做法是:先用理论界算一个上界,然后在这个上界和0之间取0.3到0.5倍作为初始值,跑一遍看失负荷概率是否接近目标置信水平。如果实际失负荷概率远低于目标,说明ε偏大,可以调小;如果高于目标,ε偏小。
具体操作:设目标机会约束置信水平1-α=0.95,即允许5%的失负荷概率。跑完模型后,用测试场景集(不参与训练的场景)验证实际失负荷频率。如果实际频率是2%,说明模型过于保守,ε可以降到原来的0.7倍;如果实际频率是8%,ε需要增大到1.3倍。迭代两三轮基本能收敛。
3.2 对偶转化后的线性化技巧:辅助变量和big-M
对偶形式里的max项直接写进求解器会报错,因为大多数求解器不支持max函数。标准做法是引入辅助变量t_i ≥ g(x,ξ_i) - λ·d(ξ_i,ξ_0),且t_i ≥ 0,目标函数里最小化λ·ε + (1/N)Σt_i。这样就把max转成了线性约束。
但要注意big-M的选取。如果g(x,ξ)的量级是兆瓦级,big-M设太小会导致约束失效,设太大会让松弛问题病态。我一般先跑一次不带big-M的模型,看g(x,ξ)的最大值,然后big-M取这个最大值的1.5倍。如果求解器报“numerical trouble”,优先检查big-M是不是过大。
import cvxpy as cp import numpy as np def solve_dro_cvar(n_scenarios, n_resources, g_values, distances, epsilon): """ 求解分布鲁棒CVaR近似 g_values: 每个场景下的g(x,ξ)值,形状(n_scenarios,) distances: 每个场景到参考分布的距离,形状(n_scenarios,) epsilon: Wasserstein半径 """ N = n_scenarios lam = cp.Variable(nonneg=True) t = cp.Variable(N, nonneg=True) # 目标:lambda*epsilon + 平均t objective = cp.Minimize(lam * epsilon + cp.sum(t) / N) constraints = [] for i in range(N): # t_i >= g_i - lambda * d_i constraints.append(t[i] >= g_values[i] - lam * distances[i]) prob = cp.Problem(objective, constraints) prob.solve(solver=cp.ECOS) return lam.value, t.value, prob.value # 模拟数据测试 np.random.seed(42) g_vals = np.random.randn(50) * 10 + 20 # 模拟g值 dists = np.random.rand(50) * 0.5 lam_val, t_val, obj_val = solve_dro_cvar(50, 4, g_vals, dists, epsilon=0.1) print(f"lambda={lam_val:.3f}, 目标值={obj_val:.3f}")这段代码用cvxpy搭建对偶问题,ECOS求解器适合小规模凸问题。参数epsilon先设0.1跑通,再根据验证结果调整。g_values在实际模型中不是预先算好的,而是决策变量x的函数,所以需要把这段嵌到主优化问题里,用x表示g。如果求解规模大,ECOS可能慢,可以换MOSEK或Gurobi,但要注意许可证。
3.3 机会约束置信水平的验证:用测试集算实际失负荷频率
训练完模型后,必须用独立测试集验证。测试集场景数至少是训练集的1/3,且要包含极端场景(比如风速超过历史最大值10%)。验证指标是实际失负荷频率,即测试集中失负荷场景数除以总场景数。如果实际频率低于目标置信水平对应的α,说明模型保守但安全;如果高于α,说明模糊集没覆盖住真实分布,需要增大ε或增加训练样本。
我一般会画一张图:横轴是ε,纵轴是实际失负荷频率和投资成本。随着ε增大,失负荷频率下降但投资成本上升,拐点通常在ε=0.15到0.3之间。选拐点附近的ε,性价比最高。
3.4 求解器选择与收敛性排查:ECOS、MOSEK、Gurobi的实测差异
小规模算例(IEEE 33节点,50场景)用ECOS足够,求解时间在30秒以内。但场景数增加到200、节点数增加到123时,ECOS会明显变慢甚至不收敛。这时候换MOSEK,内点法对凸问题的收敛性更稳,但MOSEK对模型形式要求严格,max项必须线性化干净,否则报“model not convex”。
Gurobi适合混合整数问题,因为配电网里可中断负荷和移动发电车有0-1变量,Gurobi的分支定界比ECOS强。但Gurobi的学术许可证申请需要时间,商用许可证贵。如果只是复现论文,ECOS加MOSEK组合基本够用。
收敛性排查顺序:先检查big-M是否过大,再检查Wasserstein距离计算是否有数值误差(样本距离矩阵如果出现NaN,后面全崩),最后检查CVaR的α是否设得太小(α<0.01时尾部样本太少,对偶问题不稳定)。
4. 复现避坑:Wasserstein距离计算、场景生成和参数标定里的翻车点
4.1 现象:Wasserstein距离矩阵出现NaN,求解器直接报错
原因:样本距离计算时用了欧氏距离,但台风场景里风速和故障状态量纲差几个数量级,风速是几十,故障状态是0或1,直接算距离会导致数值下溢。另外如果两个样本完全相同,距离为0,后续除以距离的步骤会出Inf。
解决:先对每个维度做归一化,风速除以最大风速,故障状态保持0-1。距离用加权欧氏距离,权重按维度重要性设,风速权重0.7,故障状态权重0.3。如果还有0距离,加一个极小值1e-6。
4.2 现象:机会约束置信水平设0.95,但实际失负荷频率高达15%
原因:训练场景太少,经验分布根本没覆盖尾部。50个场景里可能只有2个极端场景,Wasserstein球半径再大也兜不住。另外α设0.05意味着允许5%失负荷,但实际台风频率可能高于5%,模型没考虑台风发生概率本身。
解决:训练场景增加到至少200,且要分层抽样,极端场景占20%。α根据当地台风年发生概率调整,如果每年平均1.5次台风,α可以设0.1,留更多裕度。
4.3 现象:储能容量优化结果比常规规划还小,韧性反而下降
原因:目标函数里储能退化成本权重设太大,优化器为了省退化成本宁愿少投储能。或者灾前预留SOC约束没加,储能被调度到深度放电,灾时没电可用。
解决:退化成本权重先设0,跑一遍看储能容量上限,再逐步增加权重到容量下降10%左右。灾前预留SOC约束必须硬约束,SOC_min在台风预警后提高到0.8。
4.4 现象:移动发电车预布局结果全挤在同一个变电站
原因:运输时间矩阵没考虑路网容量,所有发电车都选最近的变电站,但实际路网可能拥堵。另外目标函数里发电车接入时间权重太低,优化器觉得放哪都一样。
解决:运输时间矩阵加拥堵系数,台风期间主干道拥堵系数1.8。目标函数里接入时间权重提高到和失负荷成本同一量级。还可以加地理分散约束,同一变电站最多停2辆。
4.5 现象:CVaR对偶问题求解时间随场景数指数增长
原因:对偶形式里每个场景一个辅助变量t_i,场景数N=500时变量数500+,约束数500+,内点法复杂度O(N^3)。如果还嵌在主问题里,整体规模更大。
解决:场景削减。用快速前向选择法把500场景削减到100,保留尾部极端场景。削减后再跑,求解时间从小时级降到分钟级。削减时注意保留至少10%的极端场景,否则尾部风险被削掉。
5. 进阶技巧:用场景削减和对偶校准把复现时间压到可接受范围
场景削减是复现这套模型最实用的加速手段。快速前向选择法的逻辑是:先选一个代表性场景,然后每次选一个使Wasserstein距离增量最大的场景,直到达到目标场景数。这样保留的场景在分布上最分散,尾部场景不会被均匀采样稀释。
def fast_forward_selection(scenarios, n_keep): """ 快速前向选择场景削减 scenarios: 原始场景列表,每个场景是字典 n_keep: 保留场景数 """ n = len(scenarios) # 提取特征向量 features = np.array([np.concatenate([s['wind'], s['line_status']]) for s in scenarios]) selected = [0] # 先选第一个 candidates = list(range(1, n)) while len(selected) < n_keep: best_idx = None best_dist = -1 for c in candidates: # 计算候选场景到已选场景集的最小距离 min_dist = min(np.linalg.norm(features[c] - features[s]) for s in selected) if min_dist > best_dist: best_dist = min_dist best_idx = c selected.append(best_idx) candidates.remove(best_idx) return [scenarios[i] for i in selected] # 从200削减到80 reduced = fast_forward_selection(scenarios, 80) print(f"削减后场景数: {len(reduced)}")这段代码的核心是贪心选择距离已选集合最远的场景,保证保留的场景在特征空间里尽量分散。n_keep一般设原始场景数的30%到40%,200场景保留80个,求解时间能降60%以上。注意特征向量要先归一化,否则风速会主导距离计算。
对偶校准是另一个技巧。Wasserstein半径ε和对偶变量λ在最优解处满足互补松弛条件,如果λ=0,说明模糊集约束不起作用,ε可以调小;如果λ很大,说明模糊集约束紧,ε可能偏小。我一般跑完模型后检查λ值,如果λ在0.1到1之间,说明ε选得合理;如果λ>10,ε需要增大;如果λ<0.01,ε可以减小。
最后说一个我踩过的坑:不要一上来就调参,先把模型在50场景、ε=0.1下跑通,确认目标函数值和约束满足,再逐步增加场景数和调整ε。很多复现失败不是模型错,是数值问题在早期没暴露,到大规模时集中爆发。希望帮到你。
本文还有配套的精品资源,点击获取