2021年深圳杯数学建模
C题 配电网可靠性和故障软自愈研究
原题再现:
配电网的可靠性是保障高质量供电的重要指标。在配电网终端负荷已知和各设备单元可靠性已知的条件下,整体配电网的可靠性依赖于配电网连续时空的拓扑信息,尤其在配电网发展、运营由"静态"模式转为"动态"模式后更是如此。
配电业务主要包含 "规划"、"建设"、"运方"、"营销"、"配网(运维)"、"调控"等六大专业环节,简称"规、建、运"/"营、配、调",前三环节为"发展"领域,负责新增电网业务,后三者为"运营"领域,负责存量电网业务。当各个环节的信息系统协同性有问题时,即使额外增加数据实测、校验、纠错等成本开销,也难以保证"运维"环节得到准确、全面的配电网连续时空的拓扑信息。
"业务过程内生电网拓扑信息"是新建电网业务在"链式"跨专业业务协同工作流程中、由业务人员在各自岗位上完成自身业务内容的过程中"自然"形成的拓扑结构信息(既不要来自业务外信息系统、人员的电网图形信息支持,也不需业务人员录入岗位业务内容外的辅助信息)。
目前,已有电网基层单位实现了"业务过程内生电网拓扑信息系统",它以连续转化的"未来"、"当下"、"过去"时态形式,在"规建运"、"营配调"各业务环节实现贯通的"信息时空"中跨专业协作生成、流转、演化。对于电网企业追求内部业务"数据一个源,电网一张图,业务一条线"的"数字化"转型工作来说,"业务过程内生电网拓扑信息"既是具体的技术"目标",同时又是保障"目标"得以实现的管理"手段":它以"内生"的连续时空迭代纠错能力,保证电网拓扑数据"自主"收敛于实体电网拓扑,而不再需要以额外的数据纠错处理作业,去消除"外化"于业务流程的拓扑数据处理带来的数据失真问题。"业务过程内生电网拓扑信息"作为"活的"电网"数字映像",具有了数字化"平行系统"信息技术特征,与电网实时数据耦合后,能够在线展现电网实时拓扑结构及运行状态。
配电网故障自愈是事故恢复的快速自动化方式,需要自动化监测和控制设备的投入。在现有自动化设备基础上,利用准确的配电网连续时空拓扑信息,尽量实现"软自愈",能够降低故障自动恢复的成本。
任务:
1、 建立一种依赖于连续时空电网拓扑的配电网可靠性评估模型,并举例说明配电网拓扑信息差异引起的可靠性评估差异。
2、 建立一种依赖于连续时空电网拓扑的配电网故障检测模型,并举例说明配电网拓扑信息差异引起的故障检测差异。
3、 图1、2给出了两个10kv电网故障示意场景,请收集资料研究设计 "业务过程内生电网拓扑信息"条件下电网故障"软自愈"方案,实现同类线路故障的自动判断、自动隔离和负荷转移恢复供电的算法,并估算与"硬自愈"方案的成本差异。
请根据图中说明,考虑提交完备算法(决赛阶段将提供同类线路实际故障场景数据验证算法),并分析自己算法的优缺点:
算法输入数据是流过每个开关的电流值(电流值代表线路的负荷。如果电流值降为0,代表停电、损失负荷了;如果电流值瞬间增大、超过开关跳闸的阀值,代表有故障、开关跳闸隔离故障);
算法输出结果是开关动作系列,即通过电流值的变化判断故障点在哪里,跳开距离故障点最近的开关,把停电范围控制在最小范围,然后合上无故障段的联络开关,把停电的无故障段恢复供电。

整体求解过程概述(摘要)
针对问题一,为了刻画配电网由"静态断面"转向"动态运行"后拓扑状态对可靠性评估的影响,本文首先将设备属性、开关状态、负荷、地理位置与业务事件统一表达为连续时空属性图 D_PG(t)。鉴于附件未提供设备级故障台账和实测负荷序列,本文构造含 2 个电源、8 个负荷节点、9 条主干支路和 3 条联络线的受控算例,并对故障率、修复时间、遥控能力、容量和阻抗进行量纲校验、状态编码和一致性预处理。随后引入故障模式与影响分析、图可达性判别和线性化 DistFlow 校核,构建负荷点可靠性指标与系统 SAIFI、SAIDI、CAIDI、ASAI、ENS 模型。计算得到连续时空拓扑下 SAIDI 为 0.527 h/(户·a)、ENS 为 4.214 MWh/a;当拓扑退化为静态或联络信息缺失时,相应指标增至 0.798 和 6.364,表明拓扑时效性会显著改变故障影响范围与恢复时间估计。
针对问题二,在问题一形成的拓扑图模和开关映射基础上进一步研究故障检测,避免重复构造相同的网络数据。为了从开关电流中提取具有机理含义的故障证据,本文按照"稳健基线估计---变点检测---上游过流特征---下游失流特征---拓扑一致性残差"的流程构造四类时序特征,并依据独立高斯测量误差假设建立拓扑贝叶斯最大后验判别函数。为验证判别函数的合理性,本文将其与正则化 Logistic、RBF-SVM、随机森林和梯度提升模型进行五折分层交叉验证。受控样本中前三类模型准确率均达到 1.000,梯度提升为 0.989;进一步的陈旧拓扑映射和噪声扰动实验表明,模型性能下降主要来源于拓扑标签错配而非一般测量噪声,由此验证了连续时空拓扑在故障定位中的信息增益。
针对问题三,基于问题二输出的故障区段后验概率,本文构建"故障判断---最小隔离---联络转供---安全校核---拓扑回写"的软自愈闭环。考虑到配电网恢复问题同时具有离散开关决策和连续潮流约束,引入以失供负荷、越限量、低电压惩罚及开关次数为目标的组合优化模型,并通过枚举联络开关状态与线性化 DistFlow 进行求解。在变电站侧故障和线路中段故障两个场景中,模型分别给出"分 K1、合 KT1"和"分 K3、合 KT2"的先分后合动作序列,恢复负荷分别为 7.36 MW 与 8.40 MW。以 20 个可控开关、10 年期为例,软自愈全寿命净现值为 301.8 万元,低于硬自愈的 457.9 万元;通信丢包实验进一步说明确认重发和本地降级机制能够提升动作成功率。
综上,本文以连续时空拓扑作为贯穿三个问题的统一信息底座,形成了"数据理解与探索---拓扑预处理---可靠性建模---故障反演---恢复优化---闭环验证"的递进式建模体系。模型的主要优势是物理意义清晰、可解释性强、能够显式处理拓扑版本差异并兼顾可靠性、准确性、鲁棒性和经济性;不足在于题目未提供真实设备参数和故障序列,数值结论属于受控仿真而非现场鉴定值。所附 Python 程序可直接替换台账与电流数据,为决赛实际场景验证及工程部署提供可复现基础。
模型假设:
1. 在单次故障分析窗口内,故障率服从指数分布且不同设备的一阶故障相互独立;同一时刻发生两个及以上独立永久故障的概率可忽略。
2. 正常运行和恢复后的 10 kV 网络保持辐射状,每个带电连通分量至多包含一个上级电源;联络开关闭合后若形成环网,则必须同步分开一个分段开关。
3. 负荷在故障定位的秒级窗口内近似不变,在可靠性年尺度计算中用给定平均负荷表示;用户数用于 SAIFI、SAIDI 加权,负荷功率用于 ENS 计算。
4. 电流测量误差经标幺化后近似服从零均值高斯分布;通信时延短于保护动作与转供控制允许时间,丢包可通过确认重发降低。
5. 恢复潮流采用线性化 DistFlow,忽略无功和高阶损耗;在应急恢复阶段允许短时最低电压 0.895 p.u.、最大负载率 1.35,长期运行应恢复到更严格限值。
6. 成本参数为受控工程假设,单位为万元,不代表具体厂商报价;比较关注成本结构、规模效应和敏感性,而非给出采购结算价。
问题分析:
问题一分析
针对含时变开关状态的连续时空配电网可靠性评估建模需求,本题属于图论可达性分析与故障模式影响分析相结合的解析建模问题。传统静态拓扑仅记录固定网架,无法反映检修、转供带来的网络结构变化,会误判故障停电时长与缺供电量;核心思路是构建带时间戳的时空属性图统一存储设备、开关、负荷时序信息,对每条支路单独模拟故障场景,通过图连通性判定失电负荷,再结合联络开关转供能力、线性 DistFlow 潮流校核计算可恢复负荷规模,最终输出 SAIFI、SAIDI、ENS 等标准化可靠性指标,并对比静态、动态两类拓扑下指标差异,量化拓扑时效性对可靠性评估结果的影响,为故障定位与自愈优化提供网架基础数据。
问题二分析
基于问题一搭建的时空拓扑网络,本题属于时序特征提取与拓扑感知贝叶斯故障定位建模问题。故障发生时上下游开关会呈现过流、下游失流差异化电流特征,但老旧拓扑映射关系会造成故障区段判别系统性偏差;建模时先对开关电流时序做稳健基线预处理,提取峰值倍数、电流跌落率四类故障特征,依托当前实时拓扑构建故障签名先验,建立高斯噪声假设下的贝叶斯最大后验判别模型,同时引入 Logistic、SVM、随机森林多类机器学习模型交叉验证定位精度,通过拓扑错配、测量噪声两组对照实验,分别量化拓扑信息失真、量测误差对故障识别准确率的负面影响,设置置信拒识机制规避误判引发的自动合闸风险。
问题三分析
依托问题二输出的故障区段后验概率,本题属于带潮流、辐射安全约束的开关组合多目标优化(软自愈)与全寿命成本对比问题。自愈控制需严格遵循 "先隔离、后转供" 安全操作顺序,同时兼顾恢复负荷最大化、开关操作次数最少、电压过载惩罚最小多重目标;先构建最小故障隔离边界,枚举联络开关可行组合并通过 DistFlow 校验电气可行性,求解两类典型故障场景的最优开关动作序列,再搭建含初始投资、运维折现的净现值成本模型,分别测算软自愈、硬自愈两套方案长期投入,开展开关规模、设备单价单因素敏感性分析,额外搭建通信丢包仿真验证降级保护策略的鲁棒性,从经济性、安全性、泛化性多维度对比两套自愈方案优劣。
模型的建立与求解整体论文缩略图

全部论文请见下方" 只会建模 QQ名片" 点击QQ名片即可
程序代码:
python
from __future__ import annotations
import argparse
import json
import math
import warnings
from dataclasses import dataclass, asdict
from itertools import product
from pathlib import Path
from typing import Dict, List, Sequence, Tuple
import matplotlib as mpl
import matplotlib.pyplot as plt
from matplotlib.patches import FancyArrowPatch, FancyBboxPatch
import networkx as nx
import numpy as np
import pandas as pd
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, confusion_matrix, f1_score
from sklearn.model_selection import StratifiedKFold, cross_val_predict
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.svm import SVC
warnings.filterwarnings("ignore")
SEED = 202108
RNG = np.random.default_rng(SEED)
# 显式注册容器内置中文字体,避免图表中文标题/坐标显示为方框。
from matplotlib.font_manager import FontProperties, fontManager
_CHINESE_FONT = Path("/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc")
if _CHINESE_FONT.exists():
fontManager.addfont(str(_CHINESE_FONT))
_FONT_NAME = FontProperties(fname=str(_CHINESE_FONT)).get_name()
else:
_FONT_NAME = "DejaVu Sans"
mpl.rcParams["font.family"] = _FONT_NAME
mpl.rcParams["font.sans-serif"] = [_FONT_NAME, "DejaVu Sans"]
mpl.rcParams["axes.unicode_minus"] = False
mpl.rcParams["figure.dpi"] = 130
mpl.rcParams["savefig.dpi"] = 240
mpl.rcParams["axes.titleweight"] = "bold"
COLORS = {
"blue": "#2F6BFF", "green": "#2E9D67", "orange": "#F59E0B",
"red": "#DC3C4B", "purple": "#7756A5", "cyan": "#00A6A6",
"dark": "#263238", "gray": "#6B7280", "light": "#EAF0FB"
}
@dataclass(frozen=True)
class LoadNode:
node: str
load_mw: float
customers: int
priority: float
x: float
y: float
@dataclass(frozen=True)
class Edge:
edge_id: str
u: str
v: str
switch: str
closed: bool
tie: bool
failure_rate: float
repair_h: float
capacity_mw: float
resistance_pu: float
length_km: float
remote: bool = True
@dataclass
class ReliabilityOutput:
name: str
SAIFI: float
SAIDI: float
CAIDI: float
ASAI: float
ENS_MWh: float
expected_switch_ops: float
load_point: pd.DataFrame
event_detail: pd.DataFrame
@dataclass
class RestorationPlan:
fault_edge: str
open_switches: Tuple[str, ...]
close_switches: Tuple[str, ...]
restored_mw: float
unserved_mw: float
min_voltage: float
max_loading: float
switch_operations: int
objective: float
def ensure_dir(path: Path) -> Path:
path.mkdir(parents=True, exist_ok=True)
return path
def build_case() -> Tuple[Dict[str, LoadNode], List[Edge], Dict[str, Tuple[float, float]], Dict[str, float]]:
loads = {
"L1": LoadNode("L1", 0.85, 420, 1.10, 1.0, 0.2),
"L2": LoadNode("L2", 1.10, 510, 1.25, 2.1, -0.1),
"L3": LoadNode("L3", 0.70, 350, 1.00, 3.2, 0.25),
"L4": LoadNode("L4", 1.25, 600, 1.40, 4.3, -0.2),
"L5": LoadNode("L5", 0.95, 470, 1.10, 5.5, 0.2),
"L6": LoadNode("L6", 1.30, 640, 1.35, 6.6, -0.15),
"L7": LoadNode("L7", 0.80, 390, 1.00, 7.7, 0.2),
"L8": LoadNode("L8", 1.05, 520, 1.20, 8.8, -0.1),
}
sources = {"SA": 7.0, "SB": 7.5}
pos = {"SA": (0.0, 0.0), "SB": (10.0, 0.0)}
pos.update({k: (v.x, v.y) for k, v in loads.items()})
edges = [
Edge("E1", "SA", "L1", "K1", True, False, 0.050, 4.0, 5.0, 0.0040, 0.7),
Edge("E2", "L1", "L2", "K2", True, False, 0.085, 5.0, 4.5, 0.0050, 0.9),
Edge("E3", "L2", "L3", "K3", True, False, 0.100, 5.5, 4.0, 0.0060, 1.1),
Edge("E4", "L3", "L4", "K4", True, False, 0.120, 6.0, 3.8, 0.0065, 1.2),
Edge("E5", "L4", "L5", "K5", False, True, 0.070, 4.5, 4.0, 0.0045, 0.8),
Edge("E6", "L5", "L6", "K6", True, False, 0.110, 5.8, 4.2, 0.0060, 1.0),
Edge("E7", "L6", "L7", "K7", True, False, 0.095, 5.2, 4.6, 0.0055, 1.0),
Edge("E8", "L7", "L8", "K8", True, False, 0.080, 4.8, 5.0, 0.0050, 0.9),
Edge("E9", "L8", "SB", "K9", True, False, 0.045, 3.8, 5.5, 0.0040, 0.7),
Edge("T1", "L2", "L7", "KT1", False, True, 0.035, 3.0, 2.8, 0.0060, 1.4),
Edge("T2", "L3", "L6", "KT2", False, True, 0.040, 3.2, 3.2, 0.0055, 1.1),
Edge("T3", "L1", "L8", "KT3", False, True, 0.030, 2.8, 2.5, 0.0065, 1.6),
]
return loads, edges, pos, sources
def state_dict(edges: Sequence[Edge], overrides: Dict[str, bool] | None = None) -> Dict[str, bool]:
s = {e.edge_id: e.closed for e in edges}
if overrides:
s.update(overrides)
return s
def make_graph(edges: Sequence[Edge], states: Dict[str, bool], failed: str | None = None) -> nx.Graph:
g = nx.Graph()
for e in edges:
if states.get(e.edge_id, e.closed) and e.edge_id != failed:
g.add_edge(e.u, e.v, edge_id=e.edge_id, switch=e.switch,
capacity=e.capacity_mw, r=e.resistance_pu)
return g
def served_nodes(g: nx.Graph, sources: Dict[str, float], loads: Dict[str, LoadNode]) -> set[str]:
served: set[str] = set()
for s in sources:
if s in g:
served |= set(nx.node_connected_component(g, s))
return served & set(loads)
def valid_radial(g: nx.Graph, sources: Dict[str, float]) -> bool:
for comp in nx.connected_components(g):
ns = [s for s in sources if s in comp]
if len(ns) > 1:
return False
if len(ns) == 1 and g.subgraph(comp).number_of_edges() != len(comp) - 1:
return False
return True
def distflow(g: nx.Graph, loads: Dict[str, LoadNode], sources: Dict[str, float]) -> Tuple[float, float, Dict[str, float], Dict[str, float]]:
"""线性化 DistFlow:返回最小电压、最大负载率、节点电压和支路潮流。"""
volt: Dict[str, float] = {}
flow_by_edge: Dict[str, float] = {}
max_loading = 0.0
for comp in nx.connected_components(g):
srcs = [s for s in sources if s in comp]
if len(srcs) != 1:
continue
root = srcs[0]
t = nx.bfs_tree(g.subgraph(comp), root)
parent = {v: u for u, v in t.edges()}
order = list(nx.topological_sort(t))[::-1]
down = {n: loads[n].load_mw if n in loads else 0.0 for n in t.nodes()}
directed_flow: Dict[Tuple[str, str], float] = {}
for n in order:
if n in parent:
p = parent[n]
down[p] += down[n]
directed_flow[(p, n)] = down[n]
volt[root] = 1.0
for n in nx.topological_sort(t):
if n == root:
continue
p = parent[n]
d = g.get_edge_data(p, n)
f = directed_flow[(p, n)]
volt[n] = volt[p] - d["r"] * f
flow_by_edge[d["edge_id"]] = f
max_loading = max(max_loading, f / max(d["capacity"], 1e-9))
min_v = min([volt.get(n, 0.0) for n in loads if n in served_nodes(g, sources, loads)], default=0.0)
return min_v, max_loading, volt, flow_by_edge
def restoration_after_failure(
edges: Sequence[Edge], loads: Dict[str, LoadNode], sources: Dict[str, float],
states: Dict[str, bool], failed_edge: str, available_ties: Sequence[str]
) -> Tuple[set[str], int, Dict[str, bool], float, float]:
best_served: set[str] = set()
best_states = dict(states)
best_score = (-1.0, -999.0)
best_ops = 0
best_min_v = 0.0
best_loading = 99.0
ties = [t for t in available_ties if t != failed_edge]
for bits in product([0, 1], repeat=len(ties)):
trial = dict(states)
trial[failed_edge] = False
ops = 0
for tie, b in zip(ties, bits):
if bool(b) != trial.get(tie, False):
trial[tie] = bool(b)
ops += 1
g = make_graph(edges, trial, failed_edge)
if not valid_radial(g, sources):
continue
served = served_nodes(g, sources, loads)
min_v, loading, _, _ = distflow(g, loads, sources)
source_ok = True
for comp in nx.connected_components(g):
srcs = [s for s in sources if s in comp]
if len(srcs) == 1:
p = sum(loads[n].load_mw for n in comp if n in loads)
source_ok &= p <= sources[srcs[0]] + 1e-9
if not source_ok or min_v < 0.895 or loading > 1.35:
continue
weighted = sum(loads[n].load_mw * loads[n].priority for n in served)
score = (weighted, -ops)
if score > best_score:
best_score = score
best_served = served
best_states = trial
best_ops = ops
best_min_v = min_v
best_loading = loading
return best_served, best_ops, best_states, best_min_v, best_loading
def reliability_assessment(
name: str, edges: Sequence[Edge], loads: Dict[str, LoadNode], sources: Dict[str, float],
available_ties: Sequence[str], remote_override: Dict[str, bool] | None = None,
topology_override: Dict[str, bool] | None = None
) -> ReliabilityOutput:
states = state_dict(edges, topology_override)
remote_override = remote_override or {}
load_ids = list(loads)
lam = {n: 0.0 for n in load_ids}
unavailability = {n: 0.0 for n in load_ids}
detail_rows = []
expected_ops = 0.0
for e in edges:
if not states[e.edge_id]:
continue
g0 = make_graph(edges, states, e.edge_id)
immediate = served_nodes(g0, sources, loads)
interrupted = set(load_ids) - immediate
restored, ops, _, min_v, loading = restoration_after_failure(
edges, loads, sources, states, e.edge_id, available_ties
)
expected_ops += e.failure_rate * ops
remote = remote_override.get(e.edge_id, e.remote)
switching_h = 0.083 if remote else 0.50
for n in load_ids:
if n not in interrupted:
continue
duration = switching_h if n in restored else e.repair_h
lam[n] += e.failure_rate
unavailability[n] += e.failure_rate * duration
detail_rows.append({
"edge": e.edge_id, "switch": e.switch, "load": n,
"failure_rate": e.failure_rate, "restored": int(n in restored),
"duration_h": duration, "outage_contribution_h": e.failure_rate * duration,
"min_voltage": min_v, "max_loading": loading
})
n_total = sum(loads[n].customers for n in load_ids)
saifi = sum(lam[n] * loads[n].customers for n in load_ids) / n_total
saidi = sum(unavailability[n] * loads[n].customers for n in load_ids) / n_total
caidi = saidi / saifi if saifi > 0 else 0.0
asai = 1.0 - saidi / 8760.0
ens = sum(unavailability[n] * loads[n].load_mw for n in load_ids)
lp = pd.DataFrame([{
"load": n, "load_mw": loads[n].load_mw, "customers": loads[n].customers,
"priority": loads[n].priority, "lambda_per_year": lam[n],
"outage_h_per_year": unavailability[n],
"availability": 1.0 - unavailability[n] / 8760.0
} for n in load_ids])
return ReliabilityOutput(name, saifi, saidi, caidi, asai, ens, expected_ops, lp, pd.DataFrame(detail_rows))
def dynamic_risk_curve(edges, loads, sources) -> pd.DataFrame:
rows = []
for hour in range(24):
if 8 <= hour < 14:
overrides = {"E3": False, "E5": True}
mode = "检修转供"
else:
overrides = None
mode = "基础方式"
r = reliability_assessment(mode, edges, loads, sources, ["T1", "T2", "T3"],
topology_override=overrides)
stale = reliability_assessment("静态图模", edges, loads, sources, ["T1"])
rows.append({
"hour": hour, "mode": mode, "true_ENS": r.ENS_MWh,
"stale_ENS": stale.ENS_MWh, "true_SAIDI": r.SAIDI,
"stale_SAIDI": stale.SAIDI
})
return pd.DataFrame(rows)
def topology_perturbation_mc(edges, loads, sources, repeats=90) -> pd.DataFrame:
truth = reliability_assessment("真实", edges, loads, sources, ["T1", "T2", "T3"])
rows = []
for p in np.linspace(0.0, 0.30, 7):
for _ in range(repeats):
override: Dict[str, bool] = {}
for e in edges:
if RNG.random() < p:
override[e.edge_id] = not e.closed
ties = [e.edge_id for e in edges if e.tie and override.get(e.edge_id, e.closed) is False]
try:
r = reliability_assessment("扰动", edges, loads, sources, ties, topology_override=override)
rows.append({
"topology_error_rate": p,
"SAIFI_relative_error": abs(r.SAIFI - truth.SAIFI) / max(truth.SAIFI, 1e-9),
"SAIDI_relative_error": abs(r.SAIDI - truth.SAIDI) / max(truth.SAIDI, 1e-9),
"ENS_relative_error": abs(r.ENS_MWh - truth.ENS_MWh) / max(truth.ENS_MWh, 1e-9)
})
except Exception:
pass
return pd.DataFrame(rows)
def sensitivity_grid(edges, loads, sources) -> pd.DataFrame:
rows = []
for fail_mul in np.linspace(0.6, 1.5, 7):
for switch_min in np.linspace(3, 30, 7):
adjusted = [Edge(
e.edge_id, e.u, e.v, e.switch, e.closed, e.tie,
e.failure_rate * fail_mul, e.repair_h, e.capacity_mw,
e.resistance_pu, e.length_km, remote=(switch_min <= 8)
) for e in edges]
r = reliability_assessment("敏感性", adjusted, loads, sources, ["T1", "T2", "T3"])
rows.append({"failure_multiplier": fail_mul, "switching_minutes": switch_min,
"SAIDI": r.SAIDI, "ENS": r.ENS_MWh})
return pd.DataFrame(rows)
# --------------------------- 故障检测 ---------------------------
def fault_signature_templates(edges: Sequence[Edge], loads: Dict[str, LoadNode]) -> Tuple[List[str], Dict[str, np.ndarray]]:
switches = [e.switch for e in edges if not e.tie]
templates: Dict[str, np.ndarray] = {}
for k, e in enumerate([e for e in edges if not e.tie]):
vec = np.zeros(len(switches) * 4)
for j in range(len(switches)):
upstream = j <= k
downstream = j >= k
vec[4*j + 0] = 4.0 if upstream else 1.0
vec[4*j + 1] = 0.05 if downstream else 1.0
vec[4*j + 2] = 0.95 if downstream else 0.0
vec[4*j + 3] = 0.95 if downstream else 0.02
templates[e.switch] = vec
return switches, templates
def fault_dataset(n_per_class=80, noise_levels=(0.02, 0.05, 0.09)) -> Tuple[pd.DataFrame, pd.Series]:
loads, edges, _, _ = build_case()
switches, templates = fault_signature_templates(edges, loads)
rows, labels = [], []
for label, template in templates.items():
for i in range(n_per_class):
noise = noise_levels[i % len(noise_levels)]
sample = template.copy()
sample += RNG.normal(0, noise, sample.size)
# 负荷波动和传感器增益偏差
gains = RNG.normal(1.0, 0.04, len(switches))
for j, g in enumerate(gains):
sample[4*j:4*j+2] *= g
sample = np.clip(sample, 0, None)
row = {"noise": noise}
for j, sw in enumerate(switches):
for q, feat in enumerate(["peak_ratio", "post_ratio", "drop_ratio", "zero_prob"]):
row[f"{sw}_{feat}"] = sample[4*j + q]
rows.append(row)
labels.append(label)
return pd.DataFrame(rows), pd.Series(labels, name="fault_switch")
def train_fault_models(X: pd.DataFrame, y: pd.Series) -> Tuple[pd.DataFrame, Dict[str, np.ndarray], Dict[str, object]]:
features = [c for c in X.columns if c != "noise"]
models = {
"拓扑特征Logit": Pipeline([("scale", StandardScaler()), ("clf", LogisticRegression(max_iter=3000, C=2.0))]),
"RBF-SVM": Pipeline([("scale", StandardScaler()), ("clf", SVC(C=6, gamma="scale"))]),
"随机森林": RandomForestClassifier(n_estimators=180, max_depth=12, min_samples_leaf=2,
random_state=SEED, class_weight="balanced_subsample"),
"梯度提升": GradientBoostingClassifier(random_state=SEED, n_estimators=100, max_depth=3),
}
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=SEED)
rows, preds, fitted = [], {}, {}
for name, model in models.items():
pred = cross_val_predict(model, X[features], y, cv=cv, n_jobs=1)
rows.append({"model": name, "accuracy": accuracy_score(y, pred),
"macro_f1": f1_score(y, pred, average="macro")})
preds[name] = pred
model.fit(X[features], y)
fitted[name] = model
return pd.DataFrame(rows), preds, fitted
def noise_robustness(templates: Dict[str, np.ndarray], repeats=100) -> pd.DataFrame:
labels = list(templates)
rows = []
for sigma in np.linspace(0.01, 0.22, 9):
correct_map = 0
correct_stale = 0
for _ in range(repeats):
true = labels[int(RNG.integers(0, len(labels)))]
x = np.clip(templates[true] + RNG.normal(0, sigma, templates[true].size), 0, None)
# MAP/最近模板:等价于独立高斯误差下的最大似然
pred = min(labels, key=lambda k: np.mean((x - templates[k])**2))
# 陈旧拓扑错误地将 K5 与 K6 的模板交换,模拟开关映射错误
stale = dict(templates)
if "K5" in stale and "K6" in stale:
stale["K5"], stale["K6"] = stale["K6"], stale["K5"]
pred_stale = min(labels, key=lambda k: np.mean((x - stale[k])**2))
correct_map += pred == true
correct_stale += pred_stale == true
rows.append({"noise_sigma": sigma, "real_time_topology": correct_map/repeats,
"stale_topology": correct_stale/repeats})
return pd.DataFrame(rows)
# --------------------------- 恢复优化 ---------------------------
def enumerate_restoration(
edges: Sequence[Edge], loads: Dict[str, LoadNode], sources: Dict[str, float],
fault_edge: str, tie_ids: Sequence[str], load_factor: float = 1.0
) -> RestorationPlan:
scaled = {n: LoadNode(v.node, v.load_mw * load_factor, v.customers, v.priority, v.x, v.y)
for n, v in loads.items()}
base = state_dict(edges)
base[fault_edge] = False
candidates: List[RestorationPlan] = []
for bits in product([0, 1], repeat=len(tie_ids)):
trial = dict(base)
close = []
for t, b in zip(tie_ids, bits):
trial[t] = bool(b)
if b:
close.append(t)
g = make_graph(edges, trial, fault_edge)
if not valid_radial(g, sources):
continue
served = served_nodes(g, sources, scaled)
min_v, loading, _, _ = distflow(g, scaled, sources)
restored = sum(scaled[n].load_mw for n in served)
total = sum(v.load_mw for v in scaled.values())
unserved = total - restored
ops = 1 + len(close)
objective = 1000*unserved + 250*max(0, loading-1) + 300*max(0, 0.95-min_v) + 3*ops
candidates.append(RestorationPlan(
fault_edge=fault_edge, open_switches=(fault_edge,), close_switches=tuple(close),
restored_mw=restored, unserved_mw=unserved, min_voltage=min_v,
max_loading=loading, switch_operations=ops, objective=objective
))
return min(candidates, key=lambda x: x.objective)
def action_sequence(plan: RestorationPlan, edges: Sequence[Edge]) -> pd.DataFrame:
edge_map = {e.edge_id: e for e in edges}
rows, step = [], 1
for eid in plan.open_switches:
rows.append({"step": step, "action": "分闸", "switch": edge_map[eid].switch,
"reason": "隔离故障区段,满足先分后合的安全约束"})
step += 1
for eid in plan.close_switches:
rows.append({"step": step, "action": "合闸", "switch": edge_map[eid].switch,
"reason": "联络转供,恢复无故障失电负荷"})
step += 1
rows.append({"step": step, "action": "校核", "switch": "-",
"reason": "检查径向性、源容量、支路负载率与节点电压"})
return pd.DataFrame(rows)
def cost_model(n_switches: np.ndarray, years=10, discount=0.05) -> pd.DataFrame:
annuity = sum(1 / (1 + discount)**y for y in range(1, years+1))
rows = []
for n in n_switches:
hard_capex = 50 + n * 12.0
soft_capex = 180 + n * 2.2
hard_om = hard_capex * 0.075 * annuity
soft_om = soft_capex * 0.045 * annuity
rows.append({
"switches": int(n), "hard_capex": hard_capex, "soft_capex": soft_capex,
"hard_om_npv": hard_om, "soft_om_npv": soft_om,
"hard_total_npv": hard_capex + hard_om,
"soft_total_npv": soft_capex + soft_om
})
return pd.DataFrame(rows)