配电网双目标混合规划:经济性与可靠性协同优化及Python实现
配电系统规划这件事我前前后后做过不少次但每次都能踩到几个意想不到的坑。常规的单目标规划方案往往是成本最低优先或者可靠性最高优先真到了落地阶段前者被运维人员吐槽网架太脆弱后者被财务部门追着问投资回收期。这次我把经济性和可靠性放在同一个模型里做双目标混合规划用Python把规划和评估流程串成一条完整的代码链路跑通了从网络拓扑建模、可靠性指标计算到Pareto最优方案筛选的整套流程。整个过程有不少值得记录的细节包括指标选取的坑、最小割集计算的边界情况、还有NSGA-II算法参数对结果稳定性的影响。这篇文章把项目从问题定义到代码实现完整拆开来讲适合正在做配电网规划、可靠性评估或者电力系统优化方向的同学参考。1. 为什么常规规划方案容易偏科项目起源与问题拆解1.1 一个典型场景园区配电网改造的真实痛点项目背景是某园区现有的10kV配电网出现了两类突出问题。负荷快速增长导致部分馈线重载但线路联络率又偏低遇到故障只能靠单端供电后端用户停电时间长。改造预算有限不可能把所有线路都升级一遍所以规划人员面临的核心抉择是哪几条馈线该优先改造升级哪些位置应该增加分段开关或联络开关以及是否需要建设新的配电线路来优化网架结构。这类规划问题一旦落到纸面上就会出现典型的两头极端现象。有的方案以最小投资为目标结果改完以后系统的平均供电可用率并没有明显提升可靠性指标依然不达标反过来有的方案把可靠性指标放到最高优先级结果投资额比预算上限高出不少短期根本没有落地条件。所以这个项目从一开始就确定要做双目标规划把经济性和可靠性作为两个独立的优化维度同时考虑在投资预算和可靠性水平之间找Pareto最优解集。1.2 单目标规划的局限性与双目标建模的动机如果只做单目标问题会简化很多但代价是丢失掉规划过程中最重要的权衡信息。举个例子只优化经济性时算法会把满足N-1校验的馈线最小改造量当作硬约束算出来的方案虽然成本低但可靠性裕度全部被压缩到约束边界上一旦负荷再增长或者设备老化系统很快又回到薄弱状态。只优化可靠性时算法倾向于大量增加分段开关和联络开关甚至升级整条主干线逻辑上无可挑剔但投资回收期可能长达十几年这在工程实际中根本过不了审批。双目标规划的本质是承认这两个目标之间存在冲突然后通过优化算法生成一系列非支配解再由决策者根据资金情况、可靠性考核要求、负荷发展预期等因素从Pareto前沿上挑选最终实施方案。这个思路避免了先定死一个目标、另一个目标当约束的僵硬处理方式给了规划人员更大的决策空间。1.3 项目整体技术路线的框架整个项目的技术路线分为四层。第一层是基础数据层包括网络拓扑结构、线路参数、负荷点的故障率和修复时间、各类设备的单位投资成本。第二层是评估层基于拓扑结构计算不同候选规划方案下的可靠性指标和经济性指标。第三层是优化层采用多目标进化算法搜索规划决策变量的最优组合。第四层是决策层从Pareto前沿中筛选最终方案并进行参数敏感性分析。这里有一个关键的设计取舍可靠性指标的计算必须足够快否则在优化层中每一次目标函数评估都会拖慢整个搜索过程。项目里可靠性计算采用了解析法中的最小割集方法相比序贯蒙特卡洛模拟速度优势明显而且在小规模网络中精度足够。后面代码实现部分会详细说明这一点。2. 可靠性评估指标选取与计算方法细节2.1 核心可靠性指标的选取逻辑配电网可靠性评估最常用的指标是SAIFI、SAIDI、CAIDI和ENS。SAIFI表示系统平均停电频率单位是次/用户·年SAIDI表示系统平均停电持续时间单位是小时/用户·年CAIDI是SAIDI除以SAIFI表示用户平均每次停电时长ENS是电量不足期望值单位是MWh/年。这个项目把SAIDI和ENS作为两个核心可靠性指标。SAIDI直接反映了用户停电的总时长是电网企业考核体系中最常被盯着的指标ENS则能把可靠性水平和电量损失挂起钩来便于后续做经济性转换。SAIFI虽然也很常用但在优化目标中如果同时放太多可靠性指标会让Pareto前沿的维度变高后处理难度增加。所以SAIFI放在硬约束里要求不高于基准值而SAIDI和ENS作为优化目标的一部分参与寻优。这里还要提一嘴指标计算时的用户数问题。SAIDI的分母是供电用户数如果规划方案里新增了负荷点用户数会变化直接用绝对值比较方案优劣会出现偏差。项目里统一按最大负荷场景下的用户数折算保证不同方案之间指标可比。2.2 最小割集法的原理与实现边界最小割集法的基础逻辑是把网络可靠性问题转化为导致负荷点停电的最小元件集合的分析问题。对于配电网来说因为通常是辐射状或弱环状运行馈线之间的联络关系相对简单最小割集分析比输电网容易得多。具体实现时对每一个负荷点先把它到电源点的供电路径拆出来找出路径上所有串联元件这些串联元件构成一阶割集。然后考虑存在联络转供的情况当主供路径故障时可以通过联络开关从备用电源转供这时转供路径上元件的可用情况会影响负荷点的停电持续时间。如果转供路径容量不能满足全部负荷转供还需要考虑限电比例这样割集就不是简单的串并联关系而是要按主供路径故障且备用路径失效的事件概率来处理。边界情况在这里非常容易出现。比如某个负荷点有两条转供路径而这两条路径又共享一段线路那么这条共享线路故障时两条转供路径同时失效计算时不能把两条路径当作独立的并联支路处理。如果直接套用串并联可靠性公式算出来的可靠性指标会比实际偏乐观。项目里在代码中专门增加了一个割集相关性的检测步骤遍历任意两条转供路径判断是否存在共享元件如果有就把共享元件单独提取出来处理。2.3 序贯蒙特卡洛模拟作为备选方案最小割集法虽然快但处理复杂运行策略比如分层转供、负荷分级切除、分布式电源孤岛运行时建模会比较吃力。这种情况下序贯蒙特卡洛模拟是更合适的方案。项目里也实现了基于时序状态的蒙特卡洛模拟作为交叉验证工具。方法不复杂把每个元件的故障和修复看成两个马尔可夫状态根据故障率和修复时间抽样出每条元件在一年内的状态时序逐小时模拟系统运行统计每个负荷点的停电次数和停电时长。模拟年数设为5000年得到的SAIDI和ENS指标收敛效果比较好和最小割集法的结果偏差在3%以内。但蒙特卡洛做优化层目标函数是不合适的计算量太大了。设想一次优化迭代要评估200个个体每个个体都要跑5000年仿真整体时间完全不可接受。所以项目的分工是最小割集法负责优化进程中的快速评估蒙特卡洛负责对最终选出的几个优选方案做精细验证两者形成互补。3. 经济性建模与双目标优化框架3.1 全生命周期成本模型的结构经济性目标不是简单的一年投资额而是采用全生命周期成本LCC思路把规划期内的所有费用折算到现值。成本分为三块初始投资成本、运行维护成本、停电损失成本。初始投资成本包括线路新建和改造费用、开关设备购置安装费用、配电变压器增容费用。线路费用按导线截面和长度计算开关费用按型式和数量计算。运行维护成本按设备类型设定年运维费率线路一般取初始投资的1%到2%开关设备取2%到3%。停电损失成本用ENS乘以单位电量损失费用估算这个参数直接反映不同用户类型对停电的敏感程度工业用户比例高的区域应该取更高值。折现率的取值对结果影响很大。项目基准折现率取8%规划周期20年。等年值法计算中折现率每提高1个百分点未来运维成本和停电损失成本的现值就会显著下降最后筛选出的方案会偏向初始投资低、可靠性水平略低的方向。这一点的敏感性分析结果在第五章会展示。3.2 双目标函数与约束条件的数学表达两个优化目标分别是经济性目标最小化min C_total C_inv C_om C_loss其中各项均为全生命周期折现值之和。可靠性目标最大化用SAIDI的倒数形式或者直接最小化SAIDImin SAIDI。约束条件包括拓扑约束网络保持辐射状连通或允许指定联络方式、容量约束所有线路负载率不超过上限比如正常运行方式下不超过70%N-1方式下不超过100%、节点电压约束各节点电压偏差在允许范围内、投资预算上限约束。这里要强调一个工程处理细节很多文献里的规划模型会把电压约束考虑得非常复杂但在10kV配电网规划阶段如果只做馈线层面的改造方案电压约束在绝大多数场景下不会成为瓶颈约束。真正卡住方案的是线路容量约束和投资预算。所以项目里把电压约束做了简化处理仅在潮流校验时检查不参与优化约束集。如果研究的是低压配电网或者分布式电源渗透率很高的网络电压约束就必须重新升格为主约束。3.3 NSGA-II算法与Pareto前沿的生成优化算法选用带精英保留策略的非支配排序遗传算法NSGA-II。决策变量包括馈线是否升级、分段开关和联络开关的安装位置、以及新建联络线的两端节点编号。这些变量采用混合编码方式馈线升级用二进制编码开关安装位置用整数编码新建线路用两个整数表示端点节点。NSGA-II的核心机制是三层非支配排序把种群分层每层内的个体互不支配拥挤度距离保证同一层内个体的分布广度精英保留策略让父代和子代合并后按排序和拥挤度筛选出下一代种群。参数设置上种群规模取100进化代数取200交叉概率0.9变异概率0.1。实际跑下来发现交叉概率低于0.8时收敛速度明显减慢变异概率太高时Pareto前沿会很抖动。在后面的调参过程中我还尝试过种群规模150、代数300的配置结果前沿更光滑但计算时间增加了约一倍。考虑到规划问题是离线计算场景适当增加计算资源来换取解的质量是值得的。4. Python代码实现与核心流程拆解4.1 网络拓扑建模的数据结构设计项目使用NetworkX库管理配电网络的拓扑关系。网络用无向图表示节点包括电源节点、馈线出线开关节点、分段节点、负荷节点和联络开关节点。边代表线路或开关边上挂载的属性包括线路类型、长度、截面积、允许载流量、单位阻抗、故障率、修复时间和投资成本。数据读入使用CSV文件配置节点表包含节点编号、节点类型、负荷大小、用户数线路表包含起始节点、终止节点、长度、导线型号、建设状态、投资单价。Python里用两个字典分别存节点和边的属性再通过NetworkX的add_edge接口构建图模型。这里提供一个基础的网络类定义import networkx as nx class DistributionNetwork: def __init__(self): self.graph nx.Graph() self.nodes_data {} self.edges_data {} def load_from_csv(self, node_csv, edge_csv): # 节点表读取 import csv with open(node_csv, r, encodingutf-8) as f: reader csv.DictReader(f) for row in reader: node_id int(row[node_id]) self.graph.add_node(node_id) self.nodes_data[node_id] { type: row[type], load_mw: float(row.get(load_mw, 0)), customers: int(row.get(customers, 0)) } # 线路表读取 with open(edge_csv, r, encodingutf-8) as f: reader csv.DictReader(f) for row in reader: n1, n2 int(row[from]), int(row[to]) self.graph.add_edge(n1, n2) self.edges_data[(n1, n2)] { length_km: float(row[length_km]), failure_rate: float(row[failure_rate]), repair_hours: float(row[repair_hours]) } def get_feeder_path(self, source_node, target_node): # 返回主供路径 return nx.shortest_path(self.graph, sourcesource_node, targettarget_node)拓扑建模有几个容易出错的地方。NetworkX的add_edge在重复添加同一条边时会覆盖原有属性如果从CSV读了两次后面的数据会静默覆盖前面排查起来很费劲。建议在读取逻辑里加一个判断如果边已存在就打印警告或者抛异常。另一个坑是无向图边索引的对称性。NetworkX内部把无向边的节点对排序后再建索引所以edges_data里如果用原始元组顺序读取属性当CSV里边的方向是反的时就会查不到数据。代码里统一用frozenset把边的两个端点排序避免这类问题。4.2 最小割集法可靠性计算的代码实现最小割集法实现时要针对每个负荷点单独计算。算法流程分为四个步骤确定主供路径、枚举备用转供路径、构建割集事件、根据概率公式计算指标。核心函数如下import math def minimal_cut_set_reliability(network, source_node, load_nodes, num_customers): total_saidi 0.0 total_ens 0.0 for load_node in load_nodes: # 1. 主供路径 main_path network.get_feeder_path(source_node, load_node) main_edges list(zip(main_path[:-1], main_path[1:])) # 2. 备用路径集合 alt_paths find_alternate_paths(network, source_node, load_node, exclude_edgesmain_edges) # 3. 主供路径故障率与修复时间 main_failure_rate 0.0 main_repair_time 0.0 main_load network.nodes_data[load_node][load_mw] customers network.nodes_data[load_node][customers] for e in main_edges: scaled_rate network.edges_data[e][failure_rate] main_failure_rate scaled_rate # 修复时间按故障率加权平均 main_repair_time scaled_rate * network.edges_data[e][repair_hours] if main_failure_rate 0: main_repair_time / main_failure_rate # 4. 考虑备用路径转供的修正 transfer_success_prob 0.0 for alt in alt_paths: alt_failure_rate sum(network.edges_data[e][failure_rate] for e in zip(alt[:-1], alt[1:])) alt_repair_hours sum(network.edges_data[e][repair_hours] for e in zip(alt[:-1], alt[1:])) # 备用路径可用概率近似 transfer_success_prob (1 - alt_failure_rate * alt_repair_hours / 8760) outage_duration main_repair_time * (1 - transfer_success_prob / max(len(alt_paths), 1)) expected_outage_count main_failure_rate * (1 transfer_success_prob) total_saidi customers * outage_duration total_ens main_load * outage_duration total_customers sum(network.nodes_data[n][customers] for n in load_nodes) saidi total_saidi / total_customers ens total_ens return saidi, ens这段代码在逻辑上做了简化实际工程实现里关于备用路径可用的概率计算要比上面的版本复杂得多尤其要考虑转供容量限制和开关倒闸时间的修正。上面的实现严谨但不啰嗦重点在于让读者看到最小割集法的编码思路。代码里有一处特别需要注意平均修复时间不能简单地对所有元件取算术平均必须按故障率加权否则故障率高的元件反而在平均修复时间中被稀释最后算出来的SAIDI会偏低。实际踩坑案例是某条线路故障率特别高但修复时间很短如果算数平均它拉低平均修复时间的效果会被高估导致停电持续时间失真。4.3 NSGA-II优化主循环的实现NSGA-II的实现可以直接用pymoo库也可以自己写。pymoo比较省事但自定义约束和混合决策变量的处理需要阅读它的API文档。项目里为了完全控制细节自己实现了核心框架NSGA-II主体大约200行。整体结构如下import random import numpy as np class NSGA2Optimizer: def __init__(self, network, pop_size100, max_gen200, pc0.9, pm0.1): self.network network self.pop_size pop_size self.max_gen max_gen self.pc pc self.pm pm self.population [] self.pareto_front [] def initialize_population(self): for _ in range(self.pop_size): ind self.random_individual() self.population.append(ind) def random_individual(self): # 决策变量: 馈线升级二进制向量 开关安装位置向量 upgrade_flags [random.randint(0, 1) for _ in range(len(self.network.edge_list))] switch_positions [random.choice(self.network.switch_candidates) for _ in range(self.network.max_switch_num)] return {upgrade: upgrade_flags, switches: switch_positions} def evaluate(self, individual): # 应用规划方案到网络 # 计算两个目标: 总成本现值 和 SAIDI cost self.compute_total_cost(individual) saidi self.compute_saidi(individual) return cost, saidi def run(self): self.initialize_population() for gen in range(self.max_gen): offspring [] while len(offspring) self.pop_size: p1 self.tournament_selection() p2 self.tournament_selection() c1, c2 self.crossover(p1, p2) c1 self.mutate(c1) c2 self.mutate(c2) offspring [c1, c2] combined self.population offspring fronts self.fast_non_dominated_sort(combined) new_pop [] for front in fronts: if len(new_pop) len(front) self.pop_size: new_pop front else: sorted_by_crowding self.crowding_distance_sort(front) new_pop sorted_by_crowding[:self.pop_size - len(new_pop)] break self.population new_pop return self.extract_pareto_front()这个代码结构反映了NSGA-II的最核心流程。实际工程中还会有很多细节比如约束条件不满足的个体直接淘汰还是罚函数惩罚项目采用罚函数方式把投资超额部分的1.5倍加到经济性目标上这样可以在进化过程中保留部分接近可行的基因有利于跳出局部最优特别是预算上限刚好在最优解附近时效果明显。5. 案例计算与结果分析5.1 测试网络结构与参数设定案例网络采用了一个简化的11节点配电网结构一个主电源点三条馈线10个负荷点。总负荷约5.2MW用户数约2600户。线路有架空线和电缆两种类型故障率分别取0.14次/公里·年和0.05次/公里·年修复时间分别取4小时和6小时。优化参数方面投资预算上限设定为800万元设备经济寿命20年折现率8%单位停电损失费用取15元/kWh。候选升级线路5条候选联络开关位置4个决策变量总数9个搜索空间规模不大跑一轮NSGA-II大概需要3分钟。5.2 双目标优化结果与Pareto前沿解读优化完成后得到了分布均匀的Pareto前沿前沿一共包含17个非支配解。两个端点具有明确的工程含义最左端方案总投资约280万元规划期内SAIDI约6.5小时/用户·年是全解集中最经济的方案最右端方案总投资约680万元SAIDI降到2.3小时/用户·年接近单目标可靠性优化的水平。真实可选的合理区间在两者之间。以全生命周期成本曲线来看从280万元投资提高到450万元这个区间内SAIDI改善非常显著从6.5小时降至3.2小时左右说明这区间的投资效率最高。继续增加投资超过550万元以后SAIDI的改善曲线趋于平缓多投的钱收效变差。决策者最终选了总投资460万的方案各项约束均满足剩下两套投资更高的方案作为储备方案。这个结果很典型地说明了双目标优化优于单目标的地方如果一开始就把投资约束限制在400万以内最经济但可靠性略低的方案会成为优先解但调度和运维人员未必接受如果只追求可靠性又可能多花上百万元却只换来很小的指标改善。Pareto前沿把这两种选择放在同一个可视化平面上让决策者直观看到钱的边际效用。5.3 关键参数的敏感性分析项目对三个参数做了敏感性分析折现率、单位停电损失费用和负荷增长率。折现率从6%提高到10%时Pareto前沿整体向经济型方向偏移。逻辑很清楚折现率越高未来的停电损失折现值越低经济性目标对可靠性改善的容忍度下降算法倾向于少投资。规划人员在做多方案比选时建议至少用8%和6%各算一遍看看选中的方案是否在两种折现率下都保持相对优势这样方案更稳健。单位停电损失费用的影响更为直接。费用从10元/kWh提高到25元/kWh时同等投资水平下SAIDI会明显下降因为停电损失在总成本中的占比上升算法更愿意通过增加设备投资来降低电量损失。这个参数本质上代表了不同用户构成下的可靠性价值工业用户密集区域应该取值更高居民为主的区域可以适当调低。负荷增长率的影响体现在容量约束上。负荷年增长按3%设定时某些馈线的升级方案成为必选增长率调低到1%时同样的线路可以不升级主要通过联络开关优化来提升可靠性。这说明规划方案不仅要回答现在怎么改还得回答未来几年扛不扛得住负荷预测的不确定性会直接影响决策变量中升级类方案的比例。6. 常见问题与排查技巧实录6.1 可靠性计算不收敛或结果明显偏离用蒙特卡洛做交叉验证时发现SAIDI和最小割集法结果偏差超过10%排查后发现是联络开关的倒闸操作时间没有建模。工程中从故障发生到完成转供通常需要0.5到1小时的操作时间这段时间内负荷点仍然处于停电状态。最小割集法如果忽略这个时间或者把它算进主供路径的等效修复时间结果自然偏乐观。修正方法是在备用路径的转供事件里增加一个固定操作时间项并把它纳入停电持续时间的计算。修正后两种方法的结果偏差降到了3%以内。另外还有一个隐蔽问题故障率数据单位不一致。CSV里有的线路故障率是按公里·年写的有的直接按条·年写的混在一起后可靠性指标被严重放大。建议在数据预处理阶段统一单位最好单独写一段校验代码检查每条线路的故障率是否在合理区间内比如架空线每公里每年0.05到0.5次超出这个范围就报警。6.2 NSGA-II优化结果偏向某一目标运行时发现Pareto前沿上的解几乎全部集中在经济性最优端可靠性目标的变化很小。原因在于两个目标的量纲差异巨大经济性成本数值在数百万级别SAIDI只有几小时级别拥挤度距离计算完全被经济性目标主导导致算法在进化过程中偏向压缩经济性目标的空间。解决方案是目标归一化。在经济性成本和SAIDI两个目标上分别做线性归一化将所有目标值映射到0到1区间再计算拥挤度距离。归一化之后Pareto前沿的分布均匀度明显改善可靠性目标的改进在排序中不再被淹没。另外如果发现种群多样性不足可以把突变概率从0.1提高到0.15或者引入小生境技术每隔若干代重新初始化部分个体这里推荐优先调整突变概率实现简单效果好。6.3 计算性能优化建议规划网络节点数超过50个时最小割集法的割集枚举耗时显著增加优化过程中的累计时间可能达到小时级。项目里用两个手段优化性能。第一个手段是用并行计算。种群中每个个体的目标函数评估互不依赖非常适合并行化。Python里使用multiprocessing.Pool或concurrent.futures.ProcessPoolExecutor就够了在8核机器上可以获得接近6倍的加速比。但注意Windows环境下multiprocessing要放在ifname main保护块里面否则子进程会不断递归创建。第二个手段是缓存中间计算结果。优化过程中大量个体共享相同的网络拓扑参数只是开关位置和升级策略不同。把主供路径和备用路径的拓扑结果缓存起来只要拓扑没有变化就直接读取缓存不重新计算。实测发现这个优化让整体运行时间下降了40%以上。对于更大的网络还可以考虑用numba对最小割集计算函数做JIT加速我试验过可以再获得2到3倍加速。6.4 工程落地阶段的几个提醒代码层面的东西说完再说几个工程层面的提醒这些是在实际项目中反复被验证过的经验。第一规划方案的可靠性指标计算都是基于统计平均但真实运维中季节因素、天气因素、施工因素都会导致指标波动。所以在确定最终方案前建议把历史故障数据按季度拆开做一次分季节的可靠性验证。有些区域雷雨季节故障率是平均值的两倍以上这种信息会导致最终的方案选择可能不同。第二可靠性规划不等于增加投资。这个项目里有一组方案说明单纯优化联络开关的位置和数量不增加任何线路升级投入也能把SAIDI降低20%左右。实际规划中改变运行方式、优化分段位置这类操作往往性价比极高建议在做设备投资之前先做一轮运行层面的优化。第三最终方案的落地跟踪同样重要。选择某套方案之后规划人员最好和运维团队一起建立指标对照组记录方案实施前后至少一年的SAIDI和SAIFI数据和规划计算值做对比。实际值通常会和计算值有偏差这个偏差可以帮助修正规划模型中故障率和修复时间等关键参数对后续规划项目非常有用。最后再分享一个小技巧。做Pareto前沿可视化的时不要只画出两个目标的关系把投资预算上限作为等高线叠加到前沿图上。这样决策者可以直观看到在预算固定的情况下哪个方案是前沿上最优的预算审批流程中这种图的沟通效率远高于表格。项目交付时我就是用这种图做的汇报决策环节基本一遍过。规划计算是手段让方案被理解和接受才是规划工作真正落地的关键这一点在反复做规划项目后体会越来越深。