使用Graph进行故障自动定位

link.json 正是标准的图(Graph)结构拓扑数据

1. 为什么说它是拓扑?

在电力系统中,拓扑就是描述“谁连着谁”的地图。

这比单纯的数据库表更高级,因为它直接描述了电网的物理连接模型。


2. 关键挑战:ID 匹配(Data Mapping)

在使用这份数据前,必须注意一个关键的数据清洗问题:

解决办法: 在代码中解析拓扑时,需要把 SWITCH_ 等前缀去掉,只保留数字部分,以便和 MySQL 里的 SOE 数据对应上。


3. Perl 模块

针对 link.json,编写 Perl 模块。它真实解析 JSON 拓扑,构建内存中的电网地图,然后结合 MySQL 的故障信号进行精准定位。

需要安装额外的 CPAN 模块:

cpan install JSON Graph

代码:Grid::TopologyFaultLocator.pm

package 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 清洗和关联),也就拥有了一套完整的配网故障定位引擎