link.json 正是标准的图(Graph)结构拓扑数据。
在电力系统中,拓扑就是描述“谁连着谁”的地图。
nodes(节点): 对应具体的物理设备(如开关、刀闸、变压器)。
"id": "SWITCH_55000001096700" 就是一个开关设备。links(连线): 对应连接设备的导线或逻辑连接点。
"source": "POLESITE_...", "target": "SWITCH_..." 描述了电从杆塔流向开关。这比单纯的数据库表更高级,因为它直接描述了电网的物理连接模型。
在使用这份数据前,必须注意一个关键的数据清洗问题:
SOE数据 (soe_data.txt): 设备ID是纯数字,例如 25000847621400。
拓扑数据 (link.json): 设备ID带有前缀,例如 SWITCH_55000001096700。
解决办法: 在代码中解析拓扑时,需要把 SWITCH_ 等前缀去掉,只保留数字部分,以便和 MySQL 里的 SOE 数据对应上。
针对 link.json,编写 Perl 模块。它真实解析 JSON 拓扑,构建内存中的电网地图,然后结合 MySQL 的故障信号进行精准定位。
需要安装额外的 CPAN 模块:
cpan install JSON Graph
Grid::TopologyFaultLocator.pmpackage Grid::TopologyFaultLocator;
use strict;
use warnings;
use DBI;
use JSON;
use Data::Dumper;
use List::Util qw(first);
# -------------------------------------------------------------------------
# 构造函数
# -------------------------------------------------------------------------
sub new {
my ($class, %args) = @_;
my $self = {
dsn => $args{dsn},
user => $args{user},
password => $args{password},
topology_file => $args{topology_file}, # 传入 link.json 路径
dbh => undef,
graph => {}, # 内存拓扑图
nodes_map => {}, # ID到节点详情的映射
};
bless $self, $class;
$self->_connect_db();
$self->_load_topology_json(); # 初始化时加载 JSON
return $self;
}
# -------------------------------------------------------------------------
# 1. 解析 link.json 构建拓扑
# -------------------------------------------------------------------------
sub _load_topology_json {
my $self = shift;
open(my $fh, '<', $self->{topology_file}) or die "无法打开拓扑文件: $!";
my $json_text = do { local $/; <$fh> };
close($fh);
my $data = decode_json($json_text);
# A. 构建节点映射 (清洗 ID)
foreach my $node (@{ $data->{nodes} }) {
my $raw_id = $node->{id};
# 清洗ID: 去除 "SWITCH_", "BREAKER_" 等前缀,只留数字
# 注意:需确认 link.json 和 soe_data 的数字部分是否真的能对上
my ($clean_id) = $raw_id =~ /(\d+)/;
$self->{nodes_map}->{$clean_id} = {
raw_id => $raw_id,
name => $node->{name},
type => $node->{type}
};
}
# B. 构建邻接表 (无向图 -> 有向树)
# 这里的 links source/target 描述了连接。
# 为了定位,我们需要知道电流方向。通常默认从 list 顺序或特定 Source 节点开始。
# 这里简单构建双向连接,后续通过 BFS 确定层级。
foreach my $link (@{ $data->{links} }) {
my ($src_id) = $link->{source} =~ /(\d+)/;
my ($tgt_id) = $link->{target} =~ /(\d+)/;
next unless ($src_id && $tgt_id);
push @{ $self->{graph}->{$src_id} }, $tgt_id;
push @{ $self->{graph}->{$tgt_id} }, $src_id;
}
}
# -------------------------------------------------------------------------
# 2. 核心功能:结合拓扑与SOE定位
# -------------------------------------------------------------------------
sub locate_fault {
my ($self, %args) = @_;
my $feeder_id = $args{feeder_id}; # 需提供线路的电源点ID (变电站出口开关)
my $event_time = $args{event_time};
# 第一步:从 MySQL 获取该时刻的所有故障信号
my $soe_signals = $self->_fetch_soe_from_db($event_time);
# 第二步:基于拓扑进行广度优先搜索 (BFS),模拟电流流向
# 我们需要找到一条路径:电源 -> 开关A(过流) -> 开关B(过流) -> 开关C(无过流)
my $root_node = $feeder_id; # 假设 feeder_id 就是根节点ID (或需转换)
unless ($self->{nodes_map}->{$root_node}) {
return { error => "拓扑中未找到根节点 (Feeder/Substation Switch): $root_node" };
}
my @queue = ($root_node);
my %visited = ($root_node => 1);
my $last_fault_device = undef; # 最后一个感受到故障电流的设备
my $first_healthy_device = undef; # 第一个没感受到故障的下游设备
# 遍历拓扑树
while (my $current_id = shift @queue) {
my $node_info = $self->{nodes_map}->{$current_id};
my $has_fault = $self->_check_fault_signal($current_id, $soe_signals);
if ($has_fault) {
$last_fault_device = $node_info;
# 继续向下游搜索
if ($self->{graph}->{$current_id}) {
foreach my $neighbor (@{ $self->{graph}->{$current_id} }) {
unless ($visited{$neighbor}) {
$visited{$neighbor} = 1;
push @queue, $neighbor;
}
}
}
} else {
# 当前设备没有过流,但它的上游有过流 -> 故障点就在这之间!
if ($last_fault_device) {
$first_healthy_device = $node_info;
# 找到边界后,通常可以停止该分支的搜索,但为了严谨可能需要检查所有分支
last;
}
}
}
# 第三步:输出结论
if ($last_fault_device && $first_healthy_device) {
return {
status => "LOCATED",
segment => $last_fault_device->{name} . " ---[故障]--- " . $first_healthy_device->{name},
msg => "上游开关过流,下游开关正常,故障位于两者之间。"
};
} elsif ($last_fault_device) {
return {
status => "END_OF_LINE",
segment => $last_fault_device->{name} . " 之后",
msg => "末端故障,所有下级设备均未反馈(或无下级设备)。"
};
} else {
return { status => "UNKNOWN", msg => "根节点未检测到过流信号,可能非主干故障或数据缺失。" };
}
}
# -------------------------------------------------------------------------
# 辅助:查库判断某设备是否有过流
# -------------------------------------------------------------------------
sub _check_fault_signal {
my ($self, $dev_id, $signals) = @_;
# 检查该 ID 是否在信号列表中,且包含 "过流", "速断", "零序" 等关键字
if (exists $signals->{$dev_id}) {
foreach my $sig_name (@{ $signals->{$dev_id} }) {
return 1 if ($sig_name =~ /过流|速断|零序|Overcurrent/);
}
}
return 0;
}
sub _fetch_soe_from_db {
my ($self, $time_str) = @_;
# 简单查前后 30 秒
my $sql = "SELECT dev_gid, name FROM soe_data WHERE soe_time BETWEEN ? AND ? + INTERVAL 30 SECOND";
# 注意:这里假设 dev_gid 和 link.json 清洗后的 ID 一致
my $sth = $self->{dbh}->prepare($sql);
$sth->execute($time_str, $time_str);
my %signals;
while (my $row = $sth->fetchrow_hashref) {
push @{ $signals{$row->{dev_gid}} }, $row->{name};
}
return \%signals;
}
sub _connect_db {
my $self = shift;
$self->{dbh} = DBI->connect($self->{dsn}, $self->{user}, $self->{password},
{ RaiseError => 1, mysql_enable_utf8mb4 => 1 });
}
1;
use Grid::TopologyFaultLocator;
my $locator = Grid::TopologyFaultLocator->new(
dsn => 'dbi:mysql:database=smart_dms_db',
user => 'root',
password => '123456',
topology_file => 'link.json' # 加载你提供的 JSON
);
# 假设你知道变电站出来的第一个开关 ID (根节点)
# 注意:这个 ID 必须是去掉前缀后的数字,且存在于 link.json 中
my $result = $locator->locate_fault(
feeder_id => '55000001096700',
event_time => '2025-09-20 17:31:00'
);
print "定位结果: " . $result->{segment} . "\n";
link.json 完美补全了静态数据库 sql.txt 缺失的“地图”功能。将它与 soe_data.txt 结合(通过 Perl 脚本做 ID 清洗和关联),也就拥有了一套完整的配网故障定位引擎。