简介:这份资源聚焦电力系统与复杂分布式网络中的连锁故障(级联失效)问题,面向电力系统、网络服务及可靠性研究方向的初学者与工程人员,帮助理解故障如何在相互依赖的组件间链式传播并最终导致系统瘫痪。包内共15个文件,以MATLAB脚本(.m)为主体,辅以两个MATLAB App(.mlapp与.mlappinstall)、一份PDF用户手册和一份README说明,压缩包约1.21MB,涵盖潮流计算、故障选择、图结构检查与可视化绘图等模块。资源围绕慢过程与快过程两阶段演化机理展开,提供仿真建模、故障传播分析与鲁棒性评估的脚本工具,读者可据此复现连锁故障场景、识别系统薄弱节点,并参考冗余设计、故障隔离与监控预警等应对思路。目前已有202人学习,适合作为电力与网络可靠性仿真的入门实践素材。
1. 连锁故障可视化:从黑匣子到可解释的级联推演
电力调度中心的大屏上,一条 500kV 线路跳闸后,潮流转移在 300ms 内引发相邻三条线路过载,随后保护装置连锁动作,8 秒内切掉整个区域负荷——这是典型的连锁故障(cascading failures)。事后复盘时,调度员面对 SCADA 系统里密密麻麻的告警条目,根本说不清“第一块多米诺骨牌”是怎么推倒后续所有环节的。可视化要解决的正是这个问题:把级联过程中的拓扑变化、潮流转移、保护动作时序,用一张可交互的图讲清楚。它适合电力系统分析工程师、复杂网络研究者,以及任何需要向非技术决策者解释“为什么一次小故障会演变成大停电”的从业者。核心诉求不是画得好看,而是让故障传播路径可追溯、关键节点可识别、阻断策略可验证。
2. 连锁故障可视化的数据底座:拓扑、时序与状态量
2.1 为什么邻接矩阵不够用:从静态图到时序超图
多数人第一反应是用邻接矩阵表示电网拓扑,然后跑个 PageRank 找关键节点。但连锁故障的本质是时序过程:线路 A 在 t1 时刻断开,导致线路 B 在 t2 时刻过载,线路 C 在 t3 时刻因保护误动而跳闸。静态图只能告诉你“谁连着谁”,无法表达“谁在什么时候因为谁而失效”。
我一般会构建一个时序超图:节点是物理设备(线路、变压器、发电机),超边是一次故障事件,超边携带时间戳和触发原因。这样可视化的横轴是时间,纵轴是拓扑层级,每条超边用带箭头的曲线连接源节点和目标节点,箭头颜色表示故障类型(过载、保护误动、电压崩溃)。
常见做法是用 Python 的networkx加matplotlib做原型,但networkx对动态图支持有限。更可靠的是用graph-tool或igraph的 temporal 扩展,或者直接上plotly的dash做交互式时间轴。下面是一个最小可复现的时序超图构建代码:
import networkx as nx import plotly.graph_objects as go from datetime import datetime, timedelta # 定义故障事件序列:每条记录是 (时间偏移秒, 源节点, 目标节点, 故障类型) events = [ (0.0, "L1", "L2", "overload"), (0.3, "L2", "L3", "overload"), (0.5, "L3", "T1", "protection_misoperation"), (1.2, "T1", "G1", "voltage_collapse"), (2.0, "G1", "Bus_A", "load_shedding"), ] # 构建时序超图:节点是设备,边带时间戳和类型 G = nx.DiGraph() for t, src, dst, ftype in events: G.add_edge(src, dst, time=t, type=ftype) # 用 plotly 画时间轴上的拓扑传播图 fig = go.Figure() for t, src, dst, ftype in events: fig.add_trace(go.Scatter( x=[t, t+0.1], y=[src, dst], mode="lines+markers+text", text=[src, dst], textposition="top center", line=dict(color={"overload": "orange", "protection_misoperation": "red", "voltage_collapse": "purple", "load_shedding": "black"}[ftype], width=2), name=f"{src}->{dst} ({ftype})" )) fig.update_layout( title="连锁故障时序超图", xaxis_title="时间偏移 (秒)", yaxis_title="设备节点", showlegend=True ) fig.show()这段代码的逻辑是:每条故障事件作为一条有向边,边的 x 坐标是时间偏移,y 坐标是设备名称。这样一眼就能看出故障是从 L1 开始,经过 L2、L3、T1、G1,最终导致 Bus_A 切负荷。参数说明:events列表里的时间偏移单位是秒,实际工程中可能到毫秒级;ftype决定颜色映射,你可以按自己的故障分类体系扩展。
注意:如果节点数超过 50,plotly 的散点图会变得难以阅读,此时应改用桑基图(Sankey)或和弦图(Chord Diagram),把时间维度用动画帧表达。
2.2 状态量采集:从 SCADA 到 PMU 的数据对齐
可视化要可信,数据必须对齐。SCADA 的刷新率是 2-4 秒,PMU 是 30-60 帧/秒,两者时间戳对不齐是常态。我踩过的坑是:直接用 SCADA 的告警时间做排序,结果发现保护动作记录比 PMU 录波晚了 800ms,导致故障传播路径看起来是“反向”的。
解决方法是做时间窗对齐:以 PMU 的 GPS 同步时钟为基准,把 SCADA 告警映射到最近的 PMU 时间窗内。具体步骤:
- 提取 PMU 录波文件的绝对时间戳(通常是 IEEE C37.118 格式)。
- 提取 SCADA 告警的 SOE(Sequence of Events)时间戳。
- 用线性插值把 SCADA 时间戳对齐到 PMU 的 10ms 网格上。
- 对齐后,用 PMU 的电压/电流相量验证 SCADA 告警的合理性。
import numpy as np import pandas as pd # 模拟 PMU 时间戳(10ms 间隔)和 SCADA 告警时间戳 pmu_times = pd.date_range("2024-01-01 00:00:00", periods=1000, freq="10ms") scada_times = pd.to_datetime([ "2024-01-01 00:00:00.003", "2024-01-01 00:00:00.307", "2024-01-01 00:00:00.512", ]) # 对齐:找到每个 SCADA 时间最近的 PMU 时间索引 aligned_indices = [] for st in scada_times: idx = np.argmin(np.abs(pmu_times - st)) aligned_indices.append(idx) # 输出对齐后的时间差 for st, idx in zip(scada_times, aligned_indices): print(f"SCADA {st} -> PMU index {idx}, offset {(pmu_times[idx] - st).total_seconds()*1000:.1f} ms")参数说明:freq="10ms"是 PMU 的典型采样间隔,实际中可能是 1ms 或 20ms,取决于装置型号。np.argmin找最近邻,如果时间差超过半个采样周期,应该标记为“对齐失败”,不要强行插值。
提示:对齐后的数据要存成 Parquet 或 HDF5,不要用 CSV——时序数据用 CSV 读写会丢精度,而且列数一多就慢得没法用。
3. 用 PyViz 生态搭一个可交互的连锁故障推演面板
3.1 选型:为什么是 Panel + HoloViews 而不是 Dash
Dash 很流行,但它的回调机制在连锁故障场景下有个致命问题:每次时间轴滑动都要重新计算整个图布局,节点一多就卡。Panel + HoloViews 的组合更适合,因为 HoloViews 的DynamicMap可以只更新变化的数据点,而不是重绘整个图。
我一般会这样搭:
- 数据层:Pandas DataFrame 存事件表,列包括
time,source,target,type,severity。 - 可视化层:HoloViews 的
Graph元素画拓扑,Curve画时间轴上的故障累积曲线。 - 交互层:Panel 的
IntSlider控制时间窗口,Select控制故障类型过滤。 - 布局层:Panel 的
Row和Column把拓扑图和时间轴并排。
下面是一个可运行的最小面板:
import panel as pn import holoviews as hv import pandas as pd import numpy as np hv.extension('bokeh') pn.extension() # 构造模拟数据:10 个节点,20 条故障事件 np.random.seed(42) nodes = [f"N{i}" for i in range(10)] events = [] for i in range(20): src = np.random.choice(nodes) dst = np.random.choice([n for n in nodes if n != src]) t = np.random.uniform(0, 10) ftype = np.random.choice(["overload", "protection", "voltage"]) events.append({"time": t, "source": src, "target": dst, "type": ftype}) df = pd.DataFrame(events).sort_values("time") # 定义动态图:根据时间窗口过滤事件 def fault_graph(time_window): filtered = df[df["time"] <= time_window] if filtered.empty: return hv.Graph([]) # 构造边列表 edges = [(row["source"], row["target"], row["type"]) for _, row in filtered.iterrows()] # 用 HoloViews Graph 画拓扑 return hv.Graph(edges, vdims='type').opts( width=600, height=400, title=f"故障传播拓扑 (t <= {time_window:.1f}s)", edge_color='type', cmap={'overload': 'orange', 'protection': 'red', 'voltage': 'purple'} ) # 时间滑块 time_slider = pn.widgets.FloatSlider(name='时间窗口 (秒)', start=0, end=10, step=0.1, value=5) # 绑定动态图 dynamic_graph = pn.bind(fault_graph, time_slider) # 布局 dashboard = pn.Column( pn.pane.Markdown("## 连锁故障推演面板"), time_slider, dynamic_graph ) dashboard.servable()逻辑说明:fault_graph函数接收时间窗口参数,过滤出该时刻之前的所有故障事件,然后用hv.Graph画拓扑。pn.bind把滑块的值绑定到函数上,滑块一动,图就更新。参数说明:time_slider的step=0.1是时间分辨率,实际中可以根据 PMU 采样率调整到 0.01;cmap里的颜色映射要和你的故障分类一致。
注意:HoloViews 的
Graph元素要求边列表的每个元素是(source, target)或(source, target, vdim)。如果你的节点有坐标,可以用hv.Graph的nodes参数指定布局,否则它会自动用弹簧布局,节点一多就乱。
3.2 把保护动作时序叠到拓扑上:双轴联动技巧
光有拓扑图不够,调度员需要同时看到“拓扑上谁挂了”和“时间轴上谁先挂”。我一般会在 Panel 里放两个并排的图:左边是拓扑图,右边是时间轴上的故障累积曲线。两者共享一个时间滑块,滑动时同时更新。
时间轴曲线用hv.Curve画,x 轴是时间,y 轴是累计故障数。关键技巧是:在曲线上用hv.VLine标记当前时间窗口的位置,这样拓扑图和时间轴就联动了。
# 累计故障曲线 def cumulative_faults(time_window): filtered = df[df["time"] <= time_window] if filtered.empty: return hv.Curve([]) # 按时间排序,计算累计数 filtered = filtered.sort_values("time") filtered["cumulative"] = range(1, len(filtered) + 1) curve = hv.Curve(filtered, 'time', 'cumulative').opts( width=400, height=400, title="累计故障数", xlim=(0, 10), ylim=(0, len(df)) ) # 加一条竖线标记当前时间 vline = hv.VLine(time_window).opts(color='red', line_dash='dashed') return curve * vline # 绑定 dynamic_curve = pn.bind(cumulative_faults, time_slider) # 并排布局 dashboard = pn.Row( pn.Column(time_slider, dynamic_graph), pn.Column(dynamic_curve) ) dashboard.servable()参数说明:xlim和ylim要固定,否则每次更新曲线时坐标轴会跳,看起来眼花。hv.VLine的line_dash='dashed'是为了和曲线区分开,实际中可以用更醒目的颜色。
提示:如果故障事件超过 1000 条,
hv.Curve的渲染会变慢。此时应该用hv.Dataset加datashader做栅格化,或者直接降采样到每秒一个点。
4. 避坑:连锁故障可视化里最容易翻车的 4 个地方
4.1 现象:拓扑图节点重叠成一团,根本看不清传播路径
原因:用了默认的弹簧布局(spring layout),节点斥力和引力参数没调,节点一多就挤在一起。或者用了networkx的spring_layout但没设k参数(最优节点间距)。
解决:对于电网拓扑,不要用弹簧布局,用地理布局——每个节点有经纬度坐标,直接按坐标画。如果没有地理坐标,用graphviz的neato布局,它比弹簧布局稳定。在 HoloViews 里,可以用hv.Graph的nodes参数传入(x, y)坐标:
# 假设 nodes_df 有 node_id, x, y 三列 nodes_df = pd.DataFrame({ "node_id": nodes, "x": np.random.uniform(0, 10, len(nodes)), "y": np.random.uniform(0, 10, len(nodes)) }) # 构造节点位置字典 pos = {row["node_id"]: (row["x"], row["y"]) for _, row in nodes_df.iterrows()} # 在 hv.Graph 里指定节点位置 graph = hv.Graph(edges, nodes=nodes_df).opts(node_position='x_y')4.2 现象:时间轴对齐后,故障顺序还是错的
原因:SCADA 的 SOE 时间戳分辨率是 1ms,但 PMU 的绝对时间戳可能因为 GPS 失锁而漂移。如果 PMU 时钟没同步,对齐就是白做。
解决:先检查 PMU 的TIME_SYNC标志位,如果为 0,说明时钟未同步,该段数据不可用于时序分析。然后做交叉验证:用 SCADA 的断路器变位信号和 PMU 的电流突变时刻对比,如果偏差超过 20ms,就要怀疑 PMU 时钟问题。
4.3 现象:面板滑动时卡顿,浏览器内存飙升
原因:每次滑块更新都重新创建hv.Graph对象,旧的图形对象没被回收。Panel 的bind默认不清理旧对象。
解决:用pn.cache装饰器缓存计算结果,或者改用hv.streams做增量更新。更彻底的办法是:把数据预处理成ColumnDataSource,用 Bokeh 的CustomJS做前端过滤,完全不经过 Python 回调。
# 用 pn.cache 缓存 @pn.cache def fault_graph_cached(time_window): # 同样的逻辑 ...4.4 现象:故障类型颜色映射混乱,同一类型在不同图里颜色不一样
原因:每次画图时cmap字典的顺序或键不一致,或者用了自动颜色分配。
解决:把颜色映射定义成全局常量,所有图共用:
FAULT_COLORS = { "overload": "#FFA500", "protection": "#FF0000", "voltage": "#800080", "load_shedding": "#000000" }然后在所有opts里引用这个字典,不要重新定义。
5. 进阶:用桑基图做故障传播的“能量流”归因
桑基图(Sankey Diagram)在连锁故障可视化里被低估了。它能把“哪个节点贡献了多少故障传播”用流量宽度表达出来,比拓扑图更适合向管理层汇报。我一般用 Plotly 的Sankey做,因为它支持交互式悬停,而且能直接导出 HTML。
关键技巧是:把故障传播路径拆成“源-中间-目标”三层。源层是初始故障设备,中间层是传播经过的设备,目标层是最终受影响的负荷。流量宽度用故障严重程度(比如过载倍数或切负荷量)加权。
import plotly.graph_objects as go # 定义三层节点 labels = ["L1", "L2", "L3", "T1", "G1", "Bus_A", "L1->L2", "L2->L3", "L3->T1", "T1->G1", "G1->Bus_A"] # 源层索引 0-5,中间层索引 6-10 source = [0, 1, 2, 3, 4, 6, 7, 8, 9] target = [6, 7, 8, 9, 10, 1, 2, 3, 4] value = [10, 8, 6, 4, 2, 10, 8, 6, 4] fig = go.Figure(go.Sankey( node=dict(label=labels, pad=15, thickness=20), link=dict(source=source, target=target, value=value) )) fig.update_layout(title="连锁故障传播桑基图", font_size=12) fig.show()参数说明:source和target是节点索引,value是流量宽度。实际中value可以用过载电流的标幺值或切负荷的 MW 数。注意:桑基图的节点不能有环,所以要把时序路径展开成有向无环图。
注意:如果故障路径有分支(一个节点同时触发多个下游),桑基图会自动分配流量,但你需要手动确保
value的总和一致,否则图会看起来“漏流量”。
我自己的习惯是:每次做完一个连锁故障复盘,先用桑基图给调度处看“能量流”,再用时序超图给保护处看“动作时序”,最后用 Panel 面板给所有人做交互式推演。这三张图的数据源必须是同一个 DataFrame,否则口径对不上,开会时会被问住。希望帮到你。
本文还有配套的精品资源,点击获取