3
0

2 Коммиты b7f198fd21 ... 4d4c1334de

Автор SHA1 Сообщение Дата
  zhanghao 4d4c1334de 1. 添加了龙亭动态异常诊断代码 2.代码整理 3 недель назад
  zhanghao 0eaaae7914 1. 添加龙亭动态异常诊断模型 3 недель назад

+ 38 - 0
models/Dynamic_anomaly_diagnosis/README.md

@@ -0,0 +1,38 @@
+# 双膜工艺(UF-RO)动态异常诊断系统
+
+基于“双重异常量化”与“强化学习(PPO)因果溯源”的水处理过程智能诊断引擎。
+本项目旨在为水厂的超滤-反渗透(UF-RO)双膜系统提供从数据清洗、异常预警到根因定位的端到端解决方案。
+
+## 🌟 架构设计特点:高内聚,低耦合
+本项目采用了**“核心算法引擎 + 水厂独立工作空间(Workspace)”**的插件化解耦架构:
+- **通用引擎层**:所有水厂共享一套核心数据处理、图构建与强化学习寻路算法。
+- **水厂工作空间**:每个水厂(如 `xishan`, `longting`)拥有独立的文件目录,
+                   实现了配置、数据集、专家知识库与模型权重的完全物理隔离,
+                   极大地提升了系统的可迁移性与可维护性。
+
+---
+
+## 📂 目录结构说明
+
+```text
+项目根目录/
+├── 核心引擎层 (通用代码)
+│   ├── data_processing.py      # Layer 1: 时序数据预处理与双重异常量化(绝对阈值+动态MAD)
+│   ├── causal_structure.py     # Layer 2: 基于工艺层级与设备约束的物理因果图构建
+│   ├── rl_tracing.py           # Layer 3: 基于 Actor-Critic 架构的 PPO 强化学习根因溯源
+│   ├── config.py               # 动态配置加载器(基于相对路径与 YAML 解析)
+│   ├── main.py                 # 模型训练与评估主入口
+│   └── test.py                 # 在线诊断与实时测试接口
+│
+├── xishan/                     # 🏆 锡山水厂专属工作空间
+│   ├── config.yaml               # 锡山专属配置文件(路径、算法超参数、传感器列表)
+│   ├── sensor_threshold.xlsx     # 锡山传感器物理阈值与层级定义表
+│   ├── abnormal_link.xlsx        # 锡山专家历史异常链路知识库(用于BC预训练)
+│   ├── ppo_tracing_model.pth     # 训练生成的锡山专属 PPO 模型权重
+│
+│
+└── longting/                   # 🏆 龙亭水厂专属工作空间
+    ├── config.yaml               
+    ├── sensor_threshold.xlsx     
+    ├── abnormal_link.xlsx        
+    ├── ppo_tracing_model.pth     

+ 35 - 2
models/Dynamic_anomaly_diagnosis/causal_structure.py

@@ -1,5 +1,8 @@
 # -*- coding: utf-8 -*-
-"""causal_structure.py: 第二层 - 物理因果结构构建"""
+"""
+causal_structure.py: 第二层 - 物理因果结构构建
+该模块负责将专家知识库(如工艺层级划分、设备归属)转化为图结构(邻接矩阵)。
+"""
 import numpy as np
 import pandas as pd
 from config import config
@@ -20,35 +23,65 @@ class CausalStructureBuilder:
         raise ValueError(f"错误: 未找到列名包含 '{keyword}' 的列")
 
     def build(self):
+        """
+        核心构建逻辑:基于规则生成传感器之间的有向连接关系(邻接矩阵)
+        返回值包含:传感器列表、索引映射字典、邻接矩阵(adj_matrix)
+        """
+        # 初始化 N x N 的全零矩阵,0 表示无连接,1 表示有连接
         adj_matrix = np.zeros((self.num_sensors, self.num_sensors), dtype=int)
         nodes_info = {}
+        
+        # 1. 遍历解析所有节点的属性字典
         for _, row in self.df.iterrows():
             d_val = row[self.col_device]
+            
+            # 清洗设备名:处理空值、NaN 等异常输入
             dev_id = str(d_val).strip() if pd.notna(d_val) and str(d_val).strip() != '' else None
+           
+            # 清洗层级号:如果层级未定义或填写错误,赋予 -1 表示该节点不参与因果溯源
             try: l_val = int(row[self.col_layer])
             except: l_val = -1
+            
             nodes_info[row['ID']] = {'layer': l_val, 'device': dev_id}
             
         count_edges = 0
+        
+        # 2. 嵌套循环对比每一对传感器,判断它们之间是否存在“因果通路”
         for i, src_name in enumerate(self.sensor_list):
             src_node = nodes_info.get(src_name)
+            
+            # 如果起始节点没有定义有效层级,则跳过
             if not src_node or src_node['layer'] == -1: continue
             src_l, src_d = src_node['layer'], src_node['device']
             
             for j, dst_name in enumerate(self.sensor_list):
+                # 排除自身到自身的连接(防止图遍历时陷入死循环)
                 if i == j: continue 
+            
                 dst_node = nodes_info.get(dst_name)
+                
+                # 如果目标节点没有定义有效层级,则跳过
                 if not dst_node or dst_node['layer'] == -1: continue
                 dst_l, dst_d = dst_node['layer'], dst_node['device']
                 
+                # ==================== (A) 层级约束 (Layer Constraint) ====================
+                # 水厂工艺是从上游传导到下游的。溯源方向是由下到上。
+                # dst_l == src_l: 允许在同层级(例如同一环节的不同传感器)平移寻找
+                # dst_l == src_l - 1: 允许向上一级(上游环节)寻找原因。绝不允许越级或向下游找。
                 is_layer_valid = (dst_l == src_l) or (dst_l == src_l - 1)
                 if not is_layer_valid: continue
                     
+                # ==================== (B) 设备约束 (Device Constraint) ====================
+                # 如果两个传感器明确归属于不同的具体设备(如一个是 RO1 膜,一个是 RO2 膜),
+                # 则判定它们之间物理隔离,不存在因果关系,切断连接。
                 is_dev_valid = True
                 if (src_d is not None) and (dst_d is not None):
                     if src_d != dst_d: is_dev_valid = False
                 
+                # 如果同时满足 层级约束 和 设备约束,则判定为有效通路
                 if is_dev_valid:
                     adj_matrix[i, j] = 1
                     count_edges += 1
-        return {"sensor_list": self.sensor_list, "sensor_to_idx": self.id_to_idx, "adj_matrix": adj_matrix}
+        return {"sensor_list": self.sensor_list, "sensor_to_idx": self.id_to_idx, "adj_matrix": adj_matrix}
+    
+    

+ 81 - 91
models/Dynamic_anomaly_diagnosis/config.py

@@ -1,98 +1,88 @@
 # -*- coding: utf-8 -*-
-"""config.py: 参数文件"""
+"""config.py: 纯相对路径动态配置加载器"""
 import os
+import yaml
 
 class Config:
-    # ---------------- 路径配置 ----------------
-    # 项目根目录
-    BASE_DIR = os.path.dirname(os.path.abspath(__file__))
-    # 传感器时序数据文件存放目录
-    DATASET_SENSOR_DIR = os.path.join(BASE_DIR, "datasets_xishan")
-    # 训练好的模型权重保存目录
-    MODEL_SAVE_DIR = os.path.join(BASE_DIR, "models")
-    # 最终结果报表保存目录
-    RESULT_SAVE_DIR = os.path.join(BASE_DIR, "results")
-    
-    # 阈值配置文件名 (包含传感器阈值、层级One_layer、设备Device等元数据)
-    THRESHOLD_FILENAME = "sensor_threshold.xlsx"
-    # 专家知识库文件名 (包含已知的历史异常链路)
-    ABNORMAL_LINK_FILENAME = "abnormal_link.xlsx"
-    # 传感器数据文件的命名前缀 (如 data_process_1.csv)
-    SENSOR_FILE_PREFIX = "data_process_"
-    # 最终生成的测试评估报告文件名
-    TEST_RESULT_FILENAME = "Final_Test_Report.xlsx" 
-    
-    # Excel中用于识别关键列的关键字
-    KEYWORD_LAYER = 'One_layer' # 用于构建因果图层级的列名关键字
-    KEYWORD_DEVICE = 'Device'   # 用于设备一致性约束的列名关键字
-    
-    # ---------------- 数据处理参数 ----------------
-    # 要读取的文件编号范围 (例如读取 data_process_1 到 data_process_119)
-    SENSOR_FILE_NUM_RANGE = (1, 119)
-    # 原始数据的采样间隔 (秒)
-    ORIGINAL_SAMPLE_INTERVAL = 4
-    # 降采样后的目标间隔 (秒),用于减少数据量加速计算
-    TARGET_SAMPLE_INTERVAL = 20
-    # 一个检测窗口的时间长度 (分钟)
-    WINDOW_DURATION_MIN = 40
-    # 每个窗口包含的数据点数 = (40分钟 * 60秒) / 20秒 = 120点
-    POINTS_PER_WINDOW = int((WINDOW_DURATION_MIN * 60) / TARGET_SAMPLE_INTERVAL)
-    # 滑动窗口的步长 (点数),设为窗口的一半以增加样本覆盖率
-    WINDOW_STEP = POINTS_PER_WINDOW // 2
-    # 窗口有效性阈值:窗口内非空数据比例需超过此值(60%)才会被处理,否则视为无效窗口
-    VALID_DATA_RATIO = 0.6
-    # 窗口异常判定阈值 (用于判断根因节点的异常程度是否足够高)
-    WINDOW_ANOMALY_THRESHOLD = 0.2
-    # 训练集与测试集的划分比例 (0.8 表示前80%的时间段用于训练,后20%用于测试)
-    TRAIN_TEST_SPLIT = 0.8
-    # 诱发变量的触发阈值
-    TRIGGER_SCORE_THRESH = 0.5
-    
-    # ---------------- 异常量化得分权重与动态MAD参数 ----------------
-    # 绝对阈值异常得分权重
-    ABSOLUTE_SCORE_WEIGHT = 0.6
-    # 动态MAD异常得分权重
-    DYNAMIC_SCORE_WEIGHT = 0.4
-    
-    # 动态MAD滚动窗口大小 (360 = 2小时)
-    MAD_HISTORY_WINDOW = 360
-    # 动态MAD判定阈值 
-    MAD_THRESHOLD = 3.0
-    
-    # ---------------- 诱发变量列表 ----------------
-    # 定义哪些传感器是“诱发变量” 
-    TRIGGER_SENSORS = [
-        "UF1Per", "UF2Per", "UF3Per", "UF4Per",
-        "C.M.RO1_DB@DPT_1", "C.M.RO2_DB@DPT_1", "C.M.RO3_DB@DPT_1", "C.M.RO4_DB@DPT_1",
-        "C.M.RO1_DB@DPT_2", "C.M.RO2_DB@DPT_2", "C.M.RO3_DB@DPT_2", "C.M.RO4_DB@DPT_2",
-        "RO1_CSFlow", "RO2_CSFlow", "RO3_CSFlow", "RO4_CSFlow",
-        "RO1HSL", "RO2HSL", "RO3HSL", "RO4HSL",
-        "RO1_TYL", "RO2_TYL", "RO3_TYL", "RO4_TYL"
-    ]
+    def __init__(self):
+        self._config_data = {}
+        self.PLANT_NAME = ""
+        self.PLANT_DIR = "" 
+        
+    def load(self, plant_name: str):
+        """传入水厂名称 (如 'longting'),自动挂载该水厂所有相对路径"""
+        self.PLANT_NAME = plant_name
+        self.PLANT_DIR = f"./{plant_name}"
+        
+        yaml_path = f"{self.PLANT_DIR}/config.yaml"
+        if not os.path.exists(yaml_path):
+            raise FileNotFoundError(f"找不到配置文件: {yaml_path}")
+            
+        with open(yaml_path, 'r', encoding='utf-8') as f:
+            self._config_data = yaml.safe_load(f)
+            
+        self._parse_config()
+        self._init_directories()
 
-    # ---------------- 强化学习核心参数 ----------------
-    # 生成的异常链路最小长度限制 (防止路径过短)
-    MIN_PATH_LENGTH = 3
-    # 生成的异常链路最大长度限制 (防止路径过长发散)
-    MAX_PATH_LENGTH = 6
-    
-    # 网络结构参数
-    EMBEDDING_DIM = 64  # 节点的嵌入向量维度
-    HIDDEN_DIM = 256    # 隐藏层维度
-    
-    # PPO (Proximal Policy Optimization) 算法超参数
-    PPO_LR = 3e-4             # 学习率
-    PPO_GAMMA = 0.90          # 折扣因子
-    PPO_EPS_CLIP = 0.2        # PPO更新时的截断范围,防止策略更新幅度过大
-    PPO_K_EPOCHS = 10         # 每次数据采集后,网络更新的循环次数
-    PPO_BATCH_SIZE = 64       # 训练批次大小
-    
-    # 训练轮次设置
-    BC_EPOCHS = 50000         # 行为克隆 (Behavior Cloning) 轮次
-    RL_EPISODES = 5000        # 强化学习 (PPO) 轮次
-    
-    # 自动创建所需的目录结构
-    for d in [MODEL_SAVE_DIR, DATASET_SENSOR_DIR, RESULT_SAVE_DIR]:
-        os.makedirs(d, exist_ok=True)
+    def _parse_config(self):
+        files = self._config_data.get('files', {})
+        
+        # 1. 目录路径
+        self.DATASET_SENSOR_DIR = f"{self.PLANT_DIR}/datasets"
+        self.RESULT_SAVE_DIR = f"{self.PLANT_DIR}/results"
+        self.MODEL_SAVE_DIR = self.PLANT_DIR  # 模型保存在水厂根目录
+        
+        # 2. 完整文件相对路径 
+        self.THRESHOLD_FILENAME = f"{self.PLANT_DIR}/{files.get('threshold_filename', 'sensor_threshold.xlsx')}"
+        self.ABNORMAL_LINK_FILENAME = f"{self.PLANT_DIR}/{files.get('abnormal_link_filename', 'abnormal_link.xlsx')}"
+        self.MODEL_FILE_PATH = f"{self.PLANT_DIR}/{files.get('model_filename', 'ppo_tracing_model.pth')}"
+        self.TEST_RESULT_FILENAME = files.get('test_result_filename', 'Final_Test_Report.xlsx') # 这个留给 pd.ExcelWriter 处理
+        self.SENSOR_FILE_PREFIX = files.get('sensor_file_prefix', 'data_process_')
 
+        # 3. 传感器与关键字
+        sensors = self._config_data.get('sensors', {})
+        self.KEYWORD_LAYER = sensors.get('keyword_layer', 'One_layer')
+        self.KEYWORD_DEVICE = sensors.get('keyword_device', 'Device')
+        self.TRIGGER_SENSORS = sensors.get('trigger_sensors', [])
+
+        # 4. 数据处理参数
+        data = self._config_data.get('data_processing', {})
+        self.SENSOR_FILE_NUM_RANGE = tuple(data.get('sensor_file_num_range', (1, 10)))
+        self.ORIGINAL_SAMPLE_INTERVAL = data.get('original_sample_interval', 4)
+        self.TARGET_SAMPLE_INTERVAL = data.get('target_sample_interval', 20)
+        self.WINDOW_DURATION_MIN = data.get('window_duration_min', 40)
+        
+        # 衍生变量
+        self.POINTS_PER_WINDOW = int((self.WINDOW_DURATION_MIN * 60) / self.TARGET_SAMPLE_INTERVAL)
+        self.WINDOW_STEP = self.POINTS_PER_WINDOW // 2
+        
+        self.VALID_DATA_RATIO = data.get('valid_data_ratio', 0.6)
+        self.WINDOW_ANOMALY_THRESHOLD = data.get('window_anomaly_threshold', 0.2)
+        self.TRAIN_TEST_SPLIT = data.get('train_test_split', 0.8)
+        self.TRIGGER_SCORE_THRESH = data.get('trigger_score_thresh', 0.5)
+        self.ABSOLUTE_SCORE_WEIGHT = data.get('absolute_score_weight', 0.6)
+        self.DYNAMIC_SCORE_WEIGHT = data.get('dynamic_score_weight', 0.4)
+        self.MAD_HISTORY_WINDOW = data.get('mad_history_window', 360)
+        self.MAD_THRESHOLD = data.get('mad_threshold', 3.0)
+
+        # 5. 强化学习参数
+        rl = self._config_data.get('rl_params', {})
+        self.MIN_PATH_LENGTH = rl.get('min_path_length', 3)
+        self.MAX_PATH_LENGTH = rl.get('max_path_length', 6)
+        self.EMBEDDING_DIM = rl.get('embedding_dim', 64)
+        self.HIDDEN_DIM = rl.get('hidden_dim', 256)
+        self.PPO_LR = float(rl.get('ppo_lr', 3e-4))
+        self.PPO_GAMMA = rl.get('ppo_gamma', 0.90)
+        self.PPO_EPS_CLIP = rl.get('ppo_eps_clip', 0.2)
+        self.PPO_K_EPOCHS = rl.get('ppo_k_epochs', 10)
+        self.PPO_BATCH_SIZE = rl.get('ppo_batch_size', 64)
+        self.BC_EPOCHS = rl.get('bc_epochs', 20000)
+        self.RL_EPISODES = rl.get('rl_episodes', 20)
+
+    def _init_directories(self):
+        """确保当前水厂的数据和结果目录存在"""
+        os.makedirs(self.DATASET_SENSOR_DIR, exist_ok=True)
+        os.makedirs(self.RESULT_SAVE_DIR, exist_ok=True)
+
+# 实例化全局单例
 config = Config()

+ 23 - 10
models/Dynamic_anomaly_diagnosis/data_processing.py

@@ -1,5 +1,10 @@
 # -*- coding: utf-8 -*-
-"""data_processing.py: 第一层 - 异常量化表征 """
+"""
+data_processing.py: 第一层 - 异常量化表征 
+该模块负责将海量、原始、可能有缺失值的传感器时序数据,
+转化为系统可理解的、标准化的 0~1 异常得分矩阵。
+包含数据降采样、双重异常评估、滑动窗口聚合等核心逻辑。
+"""
 import pandas as pd
 import numpy as np
 import os
@@ -51,15 +56,14 @@ def _process_single_file_task(file_idx, file_path, sensor_list, target_interval)
 def _calculate_window_chunk(start_indices, values, win_len, valid_ratio, threshold_val=0.95):
     """
     窗口计算任务块(运行在子进程中)
-    处理一批窗口的 nanquantile 计算
+    将连续的时序点打包成窗口(例如 40分钟=120个点),计算该窗口内的综合异常程度。
     """
     chunk_results = []
     
     for start in start_indices:
         win_data = values[start : start + win_len, :]
         
-        # 向量化计算有效性
-        # axis=0 沿时间轴统计
+        # 向量化计算窗口内的数据有效性(非 NaN 数据的比例)
         valid_counts = np.sum(~np.isnan(win_data), axis=0)
         valid_ratios = valid_counts / win_len
         
@@ -67,17 +71,18 @@ def _calculate_window_chunk(start_indices, values, win_len, valid_ratio, thresho
         valid_mask = valid_ratios >= valid_ratio
         
         if np.any(valid_mask):
-            # 并行化
+            # 取窗口内的 95 分位数作为该窗口的代表异常分,过滤掉偶发的极端孤立噪点
             quantile_scores = np.nanquantile(win_data[:, valid_mask], threshold_val, axis=0)
             win_res[valid_mask] = quantile_scores
             
+        # 限制分数在 0~1 之间    
         chunk_results.append(np.clip(win_res, 0, 1))
         
     return chunk_results
 
 class DataAnomalyProcessor:
     def __init__(self):
-        self.threshold_path = os.path.join(config.BASE_DIR, config.THRESHOLD_FILENAME)
+        self.threshold_path = config.THRESHOLD_FILENAME
         self.threshold_df = self._load_thresholds()
         self.sensor_list = self.threshold_df['ID'].tolist()
         self.threshold_dict = self.threshold_df.set_index('ID').to_dict('index')
@@ -114,6 +119,7 @@ class DataAnomalyProcessor:
         return df
 
     def _calculate_point_score_vectorized(self, series, sensor_name):
+        """双重异常得分计算(绝对阈值 + 动态MAD)"""
         # 向量化计算逻辑
         if sensor_name not in self.threshold_dict:
             return pd.Series(0.0, index=series.index, dtype=np.float32)
@@ -121,7 +127,7 @@ class DataAnomalyProcessor:
         info = self.threshold_dict[sensor_name]
         vals = series.astype(np.float32).copy()
         
-        # 1. 硬阈值掩码(在硬阈值外的数据视为无效/缺失)
+        # 1. 物理硬阈值过滤:超出物理极限的数据视为无效/传感器故障,直接置为 NaN
         mask_invalid = (vals < info['Hard_min']) | (vals > info['Hard_max'])
         vals[mask_invalid] = np.nan
         
@@ -133,6 +139,7 @@ class DataAnomalyProcessor:
         abs_scores = pd.Series(0.0, index=vals.index, dtype=np.float32)
         direction = str(info['Direction']).strip().lower()
         
+        # 计算偏低异常:当值低于 Good_min 但高于 Hard_min 时,采用线性插值计算异常度 (0~1)
         if direction in ['low', 'both']:
             mask_low = (vals < info['Good_min']) & (vals >= info['Hard_min'])
             denom = info['Good_min'] - info['Hard_min']
@@ -140,7 +147,8 @@ class DataAnomalyProcessor:
                 abs_scores[mask_low] = (info['Good_min'] - vals[mask_low]) / denom
             else:
                 abs_scores[mask_low] = 1.0
-                
+        
+        # 计算偏高异常:当值高于 Good_max 但低于 Hard_max 时
         if direction in ['high', 'both']:
             mask_high = (vals > info['Good_max']) & (vals <= info['Hard_max'])
             denom = info['Hard_max'] - info['Good_max']
@@ -148,13 +156,15 @@ class DataAnomalyProcessor:
                 abs_scores[mask_high] = (vals[mask_high] - info['Good_max']) / denom
             else:
                 abs_scores[mask_high] = 1.0
-
+        
+        # 根据报警时间赋予时间权重,报警要求越快,异常得分放大比例越高
         alarm_t = max(info['Alarm_time'], 1.0)
         time_weight = 1.0 + (30.0 / alarm_t) 
         abs_scores = (abs_scores * time_weight).clip(0, 1)
         
         # ==================== (B) 计算动态 MAD 得分 ====================
-        # 使用 rolling 计算滚动窗口内的中位数 (Median)
+        # MAD (中位数绝对偏差) 能在不依赖人为阈值的情况下,敏锐捕捉数据的“异常突降/突增”
+        # # 获取近期历史窗口的中位数作为“动态基线”
         rolling_median = vals.rolling(
             window=config.MAD_HISTORY_WINDOW, 
             min_periods=1
@@ -182,6 +192,9 @@ class DataAnomalyProcessor:
         return final_scores
 
     def process(self):
+        """
+        主执行流水线:文件读取 -> 降采样 -> 异常打分 -> 窗口聚合 -> 数据集切分
+        """
         print(f">>> [Step 1] 数据处理启动 | 检测到 CPU 核心数: {multiprocessing.cpu_count()}")
         
         # 1. 并行读取与降采样

+ 0 - 158
models/Dynamic_anomaly_diagnosis/input_format.txt

@@ -1,158 +0,0 @@
-index
-C.M.FT_ZJS@out
-C.M.LT_JSC@out
-UF_bump1_n
-UF_bump2_n
-UF_bump3_n
-UF_bump4_n
-C.M.UF_GSB1_fre@out
-C.M.UF_GSB2_fre@out
-C.M.UF_GSB3_fre@out
-C.M.UF_GSB4_fre@out
-C.M.UF_GSB1_A@out
-C.M.UF_GSB2_A@out
-C.M.UF_GSB3_A@out
-C.M.UF_GSB4_A@out
-UF_bump_avg
-water_in
-C.M.UF_PT_ZJS@out
-C.M.UF_Tur_ZJS@out
-C.M.RO_TT_ZJS@out
-PFTMP
-C.M.UF1_FT_JS@out
-C.M.UF2_FT_JS@out
-C.M.UF3_FT_JS@out
-C.M.UF4_FT_JS@out
-C.M.UF1_JSF_kd@out
-C.M.UF2_JSF_kd@out
-C.M.UF3_JSF_kd@out
-C.M.UF4_JSF_kd@out
-C.M.UF1_PT_JS@out
-C.M.UF2_PT_JS@out
-C.M.UF3_PT_JS@out
-C.M.UF4_PT_JS@out
-UF1_FluxF
-UF2_FluxF
-UF3_FluxF
-UF4_FluxF
-UF1Per
-UF2Per
-UF3Per
-UF4Per
-C.M.UF1_DB@press_PV
-C.M.UF2_DB@press_PV
-C.M.UF3_DB@press_PV
-C.M.UF4_DB@press_PV
-C.M.UF1_PT_CS@out
-C.M.UF2_PT_CS@out
-C.M.UF3_PT_CS@out
-C.M.UF4_PT_CS@out
-C.M.UF_PT_ZCS@out
-C.M.UF_FT_ZCS@out
-C.M.UF_Cl_ZCS@out
-C.M.UF_ORP_ZCS@out
-C.M.UF_PH_ZCS@out
-C.M.UF_Tur_ZCS@out
-RO_TotalFlow
-C.M.RO_Cond_ZJS@out
-C.M.RO_ORP_ZJS@out
-C.M.RO_PH_ZJS@out
-C.M.LT_HYJ1@out
-C.M.LT_HYJ2@out
-C.M.LT_ZGJ@out
-C.M.LT_SJJ@out
-RO1_GYB_n
-RO2_GYB_n
-RO3_GYB_n
-RO4_GYB_n
-C.M.RO1_GYB_fre@out
-C.M.RO2_GYB_fre@out
-C.M.RO3_GYB_fre@out
-C.M.RO4_GYB_fre@out
-C.M.RO1_GYB_A@out
-C.M.RO2_GYB_A@out
-C.M.RO3_GYB_A@out
-C.M.RO4_GYB_A@out
-C.M.RO1_GYBF_kd@out
-C.M.RO2_GYBF_kd@out
-C.M.RO3_GYBF_kd@out
-C.M.RO4_GYBF_kd@out
-C.M.RO1_FT_JS@out
-C.M.RO2_FT_JS@out
-C.M.RO3_FT_JS@out
-C.M.RO4_FT_JS@out
-C.M.RO1_PT_JS@out
-C.M.RO2_PT_JS@out
-C.M.RO3_PT_JS@out
-C.M.RO4_PT_JS@out
-C.M.RO1_DB@DPT_1
-C.M.RO2_DB@DPT_1
-C.M.RO3_DB@DPT_1
-C.M.RO4_DB@DPT_1
-C.M.RO1_PT_DJ1@out
-C.M.RO2_PT_DJ1@out
-C.M.RO3_PT_DJ1@out
-C.M.RO4_PT_DJ1@out
-RO1_DJB_n
-RO2_DJB_n
-RO3_DJB_n
-RO4_DJB_n
-C.M.RO1_DJB_fre@out
-C.M.RO2_DJB_fre@out
-C.M.RO3_DJB_fre@out
-C.M.RO4_DJB_fre@out
-C.M.RO1_DJB_A@out
-C.M.RO2_DJB_A@out
-C.M.RO3_DJB_A@out
-C.M.RO4_DJB_A@out
-C.M.RO1_PT_DJ2@out
-C.M.RO2_PT_DJ2@out
-C.M.RO3_PT_DJ2@out
-C.M.RO4_PT_DJ2@out
-C.M.RO1_DB@DPT_2
-C.M.RO2_DB@DPT_2
-C.M.RO3_DB@DPT_2
-C.M.RO4_DB@DPT_2
-C.M.RO1_PT_NS@out
-C.M.RO2_PT_NS@out
-C.M.RO3_PT_NS@out
-C.M.RO4_PT_NS@out
-C.M.RO1_FT_CS2@out
-C.M.RO2_FT_CS2@out
-C.M.RO3_FT_CS2@out
-C.M.RO4_FT_CS2@out
-RO1_SPK
-RO2_SPK
-RO3_SPK
-RO4_SPK
-C.M.RO1_FT_NS@out
-C.M.RO2_FT_NS@out
-C.M.RO3_FT_NS@out
-C.M.RO4_FT_NS@out
-RO1_CSFlow
-RO2_CSFlow
-RO3_CSFlow
-RO4_CSFlow
-RO1_FluxF
-RO2_FluxF
-RO3_FluxF
-RO4_FluxF
-C.M.RO1_PT_CS@out
-C.M.RO2_PT_CS@out
-C.M.RO3_PT_CS@out
-C.M.RO4_PT_CS@out
-C.M.RO1_Cond_CS@out
-C.M.RO2_Cond_CS@out
-C.M.RO3_Cond_CS@out
-C.M.RO4_Cond_CS@out
-RO1HSL
-RO2HSL
-RO3HSL
-RO4HSL
-RO1_TYL
-RO2_TYL
-RO3_TYL
-RO4_TYL
-C.M.RO_PT_ZCS@out
-RO_TCHFlow
-C.M.RO_Cond_ZCS@out

BIN
models/Dynamic_anomaly_diagnosis/longting/abnormal_link.xlsx


+ 55 - 0
models/Dynamic_anomaly_diagnosis/longting/config.yaml

@@ -0,0 +1,55 @@
+# longting/config.yaml
+project:
+  plant_name: "longting"
+
+files:
+  threshold_filename: "sensor_threshold.xlsx"
+  abnormal_link_filename: "abnormal_link.xlsx"
+  model_filename: "ppo_tracing_model.pth"
+  test_result_filename: "Final_Test_Report.xlsx"
+  sensor_file_prefix: "data_process_"
+
+data_processing:
+  sensor_file_num_range: [1, 11]
+  original_sample_interval: 4
+  target_sample_interval: 20
+  window_duration_min: 40
+  valid_data_ratio: 0.6
+  window_anomaly_threshold: 0.2
+  train_test_split: 0.8
+  trigger_score_thresh: 0.5
+  absolute_score_weight: 0.6
+  dynamic_score_weight: 0.4
+  mad_history_window: 360
+  mad_threshold: 3.0
+
+sensors:
+  keyword_layer: "One_layer"
+  keyword_device: "Device"
+  trigger_sensors:
+    - "UF1Per"
+    - "UF2Per"
+    - "ns=3;s=RO1_1D_YC"
+    - "ns=3;s=RO2_1D_YC"
+    - "ns=3;s=RO1_2D_YC"
+    - "ns=3;s=RO2_2D_YC"
+    - "ns=3;s=PUBLIC_BY_REAL_1"
+    - "ns=3;s=PUBLIC_BY_REAL_2"
+    - "ns=3;s=1#RO_SDCSFLOW_O"
+    - "ns=3;s=2#RO_SDCSFLOW_O"
+    - "ROHSL"
+    - "RO1_TYL"
+    - "RO2_TYL"
+
+rl_params:
+  min_path_length: 3
+  max_path_length: 6
+  embedding_dim: 64
+  hidden_dim: 256
+  ppo_lr: 0.0003
+  ppo_gamma: 0.90
+  ppo_eps_clip: 0.2
+  ppo_k_epochs: 10
+  ppo_batch_size: 64
+  bc_epochs: 20000
+  rl_episodes: 20000

BIN
models/Dynamic_anomaly_diagnosis/longting/ppo_tracing_model.pth


BIN
models/Dynamic_anomaly_diagnosis/longting/sensor_threshold.xlsx


+ 18 - 11
models/Dynamic_anomaly_diagnosis/main.py

@@ -1,12 +1,22 @@
 # -*- coding: utf-8 -*-
 """main.py: 主运行文件"""
-from data_processing import DataAnomalyProcessor
-from causal_structure import CausalStructureBuilder
-from rl_tracing import RLTrainer
+import argparse
+from config import config
 
 def main():
+    parser = argparse.ArgumentParser(description="水厂诊断模型训练")
+    parser.add_argument('-p', '--plant', type=str, required=True, help="水厂名称(对应文件夹名),例如: longting")
+    args = parser.parse_args()
     
-    # 1. 数据层 (返回切分好的数据)
+    print(f"[*] 正在初始化工作空间: {args.plant}")
+    config.load(args.plant)
+
+    # 在 config 初始化完成后,再导入后面的通用逻辑
+    from data_processing import DataAnomalyProcessor
+    from causal_structure import CausalStructureBuilder
+    from rl_tracing import RLTrainer
+    
+    # 1. 数据层
     processor = DataAnomalyProcessor()
     train_scores, test_scores, threshold_df = processor.process()
     
@@ -15,18 +25,15 @@ def main():
     causal_graph = builder.build()
     
     # 3. 强化学习层
-    # 初始化传入训练集
     trainer = RLTrainer(causal_graph, train_scores, threshold_df)
-    
-    # 3.1 训练阶段
-    trainer.pretrain_bc()   # 学习已有的
-    trainer.train_ppo()     # 探索未知的
+    trainer.pretrain_bc()   
+    trainer.train_ppo()     
     trainer.save_model()
     
-    # 3.2 评估阶段 (使用测试集)
+    # 4. 评估阶段
     trainer.evaluate(test_scores)
     
-    print("\n[Success] 所有任务执行完毕!")
+    print(f"\n[Success] {args.plant} 水厂训练与评估完毕!模型保存在: {config.MODEL_FILE_PATH}")
 
 if __name__ == "__main__":
     main()

+ 0 - 44
models/Dynamic_anomaly_diagnosis/output_format.txt

@@ -1,44 +0,0 @@
-输入按照input_format.txt文件中的变量输入(跟原来一样,不变),输入为过去2h的数据(1800条数据),40min的数据也能正常运行,但是动态范围计算可能存在问题
-输出分为三种情况:输入数据时间不足(至少大于40min);无异常;存在异常,给出异常的诱发变量,异常路径和根因变量
-{'status': 'warning', 'message': '数据时长不足: 6.6min < 40min'}
-{'status': 'normal', 'message': '系统运行正常'}
-{
-  "status": "abnormal",
-  "results": [
-    {
-      "trigger": "RO3_TYL",
-      "path": "RO3_TYL -> C.M.RO3_Cond_CS@out -> C.M.RO3_PT_NS@out -> C.M.RO3_PT_DJ1@out",
-      "root_cause": "C.M.RO3_PT_DJ1@out",
-      "details": [
-        {
-          "node": "RO3_TYL",
-          "name": "RO3脱盐率",
-          "anomaly_score": 0.5178,
-          "is_abnormal": true,
-          "deviation": "当前值: 94.78 | 物理工况: 偏低 2.8% (物理允许下限: 97.5) | 动态趋势: 平稳波动 (近期基线: 94.73, 动态区间: [94.59, 94.87])"
-        },
-        {
-          "node": "C.M.RO3_Cond_CS@out",
-          "name": "RO3产水电导",
-          "anomaly_score": 0.4,
-          "is_abnormal": true,
-          "deviation": "当前值: 106.80 | 物理工况: 正常 (物理范围: [10.0, 250.0]) | 动态趋势: 平稳波动 (近期基线: 107.43, 动态区间: [105.12, 109.73])"
-        },
-        {
-          "node": "C.M.RO3_PT_NS@out",
-          "name": "RO3二段浓水压力",
-          "anomaly_score": 0.4,
-          "is_abnormal": true,
-          "deviation": "当前值: 0.96 | 物理工况: 正常 (物理范围: [0.5, 1.05]) | 动态趋势: 平稳波动 (近期基线: 0.96, 动态区间: [0.96, 0.96])"
-        },
-        {
-          "node": "C.M.RO3_PT_DJ1@out",
-          "name": "RO3一段浓水压力",
-          "anomaly_score": 0.4,
-          "is_abnormal": true,
-          "deviation": "当前值: 0.90 | 物理工况: 正常 (物理范围: [0.02, 1.0]) | 动态趋势: 平稳波动 (近期基线: 0.89, 动态区间: [0.89, 0.90])"
-        }
-      ]
-    }
-  ]
-}

+ 70 - 9
models/Dynamic_anomaly_diagnosis/rl_tracing.py

@@ -1,5 +1,10 @@
 # -*- coding: utf-8 -*-
-"""rl_tracing.py: 强化学习链路级异常溯源"""
+"""
+rl_tracing.py: 强化学习链路级异常溯源
+基于 PPO (Proximal Policy Optimization) 的 Actor-Critic 架构。
+结合了专家经验的“行为克隆 (Imitation Learning)”与“自主探索 (Reinforcement Learning)”,
+实现从“诱发变量”逆流而上寻找“根因变量”的智能寻路。
+"""
 import torch
 import torch.nn as nn
 import torch.optim as optim
@@ -13,6 +18,10 @@ from config import config
 
 # ----------------- 1. 环境 -----------------
 class CausalTracingEnv:
+    """
+    强化学习交互环境。
+    定义了 AI 智能体的状态(State)、动作空间(Action Space)以及奖励机制(Reward Function)。
+    """
     def __init__(self, causal_graph, window_scores, threshold_df, expert_knowledge=None):
         self.sensor_list = causal_graph['sensor_list']
         self.map = causal_graph['sensor_to_idx']
@@ -20,10 +29,11 @@ class CausalTracingEnv:
         self.adj = causal_graph['adj_matrix']
         self.scores = window_scores
         
+        # 专家历史异常链路知识库
         self.expert_knowledge = expert_knowledge if expert_knowledge else {}
         self.num_sensors = len(self.sensor_list)
         
-        # 解析属性
+        # 解析每个传感器的层级 (Layer) 和归属设备 (Device) 属性,用于限制非法动作
         self.node_props = {} 
         col_one_layer = self._find_col(threshold_df, config.KEYWORD_LAYER)
         col_device = self._find_col(threshold_df, config.KEYWORD_DEVICE)
@@ -40,6 +50,7 @@ class CausalTracingEnv:
             d_val = str(d_val).strip() if pd.notna(d_val) and str(d_val).strip() != '' else None
             self.node_props[idx] = {'one_layer': l_val, 'device': d_val}
 
+        # 初始化回合状态变量
         self.current_window_idx = 0
         self.current_node_idx = 0
         self.prev_node_idx = 0
@@ -55,11 +66,16 @@ class CausalTracingEnv:
         return None
 
     def reset(self, force_window_idx=None, force_trigger=None):
+        """
+        重置环境,开启新的一轮寻路 (Episode)。
+        随机选取一个发生异常的时间窗口和触发报警的传感器作为起点。
+        """
         if force_window_idx is not None:
             self.current_window_idx = force_window_idx
             t_name = force_trigger
         else:
             found = False
+            # 尝试随机寻找一个存在触发变量异常的时间窗口
             for _ in range(100):
                 w_idx = np.random.randint(len(self.scores))
                 win_scores = self.scores[w_idx]
@@ -67,6 +83,7 @@ class CausalTracingEnv:
                 for t_name in config.TRIGGER_SENSORS:
                     if t_name in self.map:
                         idx = self.map[t_name]
+                        # 只有当诱发变量得分超过触发阈值,才将其作为候选起点
                         if win_scores[idx] > config.TRIGGER_SCORE_THRESH:
                             candidates.append(t_name)
                 if candidates:
@@ -77,12 +94,14 @@ class CausalTracingEnv:
             if not found:
                 self.current_window_idx = np.random.randint(len(self.scores))
                 t_name = list(self.map.keys())[0]
-
+                
+        # 初始化路径状态
         self.current_node_idx = self.map.get(t_name, 0)
         self.trigger_node_idx = self.current_node_idx
         self.prev_node_idx = self.current_node_idx
         self.path = [self.current_node_idx]
         
+        # 加载对应的专家知识作为本回合的目标(用于计算奖励)
         self.target_roots = set()
         self.current_expert_paths = []
         if self.current_node_idx in self.expert_knowledge:
@@ -93,6 +112,10 @@ class CausalTracingEnv:
         return self._get_state()
     
     def _get_state(self):
+        """
+        获取当前状态观测值 (Observation)。
+        将离散的 ID 信息与连续的异常分数/层级信息打包,供神经网络提取特征。
+        """
         curr_score = self.scores[self.current_window_idx, self.current_node_idx]
         prev_score = self.scores[self.current_window_idx, self.prev_node_idx]
         curr_layer = self.node_props[self.current_node_idx]['one_layer'] / 20.0
@@ -103,6 +126,11 @@ class CausalTracingEnv:
         )
     
     def get_valid_actions(self, curr_idx):
+        """
+        动作掩码 (Action Masking) 机制。
+        根据因果图和业务规则,告诉 AI 当前这一步可以走向哪些邻居节点。
+        """
+        # 从邻接矩阵获取物理相邻的节点
         neighbors = np.where(self.adj[curr_idx] == 1)[0]
         curr_props = self.node_props[curr_idx]
         curr_l, curr_d = curr_props['one_layer'], curr_props['device']
@@ -111,6 +139,8 @@ class CausalTracingEnv:
             if n in self.path: continue 
             tgt_props = self.node_props[n]
             tgt_l, tgt_d = tgt_props['one_layer'], tgt_props['device']
+            
+            # 双重保险:再次校验层级和设备约束
             if curr_l != 0 and tgt_l != 0:
                 if not ((tgt_l == curr_l) or (tgt_l == curr_l - 1)): continue
             if (curr_d is not None) and (tgt_d is not None):
@@ -119,6 +149,10 @@ class CausalTracingEnv:
         return np.array(valid)
     
     def step(self, action_idx):
+        """
+        AI 执行一步动作,环境返回新的状态和获得的奖励 (Reward)。
+        奖励函数 (Reward Function) 是整个 AI 的价值观,决定了它的行为倾向。
+        """
         prev = self.current_node_idx
         self.prev_node_idx = prev
         self.current_node_idx = action_idx
@@ -129,7 +163,8 @@ class CausalTracingEnv:
         reward = 0.0
         done = False
         
-        # 奖励机制 (Imitation > Root > Gradient)
+        # [奖励 1:模仿专家经验] 
+        # 如果走到了历史记录过的异常节点上,给予正反馈
         in_expert_nodes = False
         for e_path in self.current_expert_paths:
             if action_idx in e_path:
@@ -137,29 +172,39 @@ class CausalTracingEnv:
                 break
         
         if in_expert_nodes: reward += 2.0
-        else: reward -= 0.2
+        else: reward -= 0.2    # 探索未知节点的轻微惩罚,避免随意瞎走
             
+        # [奖励 2:命中最终根因] 
+        # 成功找到了真正的罪魁祸首,给予巨额奖励并结束本回合
         if action_idx in self.target_roots:
             reward += 10.0
             done = True
         
+        # [奖励 3:异常梯度导向] 
+        # 鼓励 AI 顺着异常分越来越高的方向走(异常传导衰减原理)
         score_prev = self.scores[self.current_window_idx, prev]
         diff = score_curr - score_prev
         if diff > 0: reward += diff * 3.0
         else: reward -= 0.5
             
+        # [惩罚 1:路径过长] 防止发散
         if len(self.path) >= config.MAX_PATH_LENGTH:
             done = True
             if action_idx not in self.target_roots: reward -= 5.0
             
+        # [惩罚 2:走入正常区域] 如果走到了异常分很低的节点,说明找错方向了    
         if score_curr < 0.15 and len(self.path) > 3:
             done = True
             reward -= 2.0
             
         return self._get_state(), reward, done, {}
 
-# ----------------- 2. 网络 -----------------
+# ----------------- 2. 神经网络架构 (Actor-Critic) -----------------
 class TargetDrivenActorCritic(nn.Module):
+    """
+    智能体的“大脑”,采用 Actor-Critic 双头输出架构。
+    Actor 负责决定“下一步去哪”(策略),Critic 负责评估“当前局势有多好”(价值)。
+    """
     def __init__(self, num_sensors, embedding_dim=64, hidden_dim=256):
         super().__init__()
         self.node_emb = nn.Embedding(num_sensors, embedding_dim)
@@ -197,7 +242,7 @@ class RLTrainer:
         self.optimizer = optim.Adam(self.model.parameters(), lr=config.PPO_LR)
         
     def _load_expert_data(self):
-        path = os.path.join(config.BASE_DIR, config.ABNORMAL_LINK_FILENAME)
+        path = config.ABNORMAL_LINK_FILENAME
         kb_data = {} 
         bc_data = [] 
         if not os.path.exists(path): return kb_data, bc_data, None
@@ -233,6 +278,11 @@ class RLTrainer:
         return kb_data, bc_data, df
 
     def pretrain_bc(self):
+        """
+        第一阶段:行为克隆 (Behavior Cloning) 预训练。
+        相当于给 AI 上课死记硬背专家已知的异常链路,让它具备基础的业务常识。
+        采用标准的监督学习交叉熵损失。
+        """
         if not self.bc_samples: return
         print(f"\n>>> [Step 3.1] 启动BC预训练 ({config.BC_EPOCHS}轮)...")
         states_int = torch.LongTensor([list(s) for s, a in self.bc_samples])
@@ -252,6 +302,11 @@ class RLTrainer:
             if epoch%100==0: pbar.set_postfix({'Loss': f"{loss.item():.4f}"})
 
     def train_ppo(self):
+        """
+        第二阶段:PPO 强化学习自主探索。
+        AI 在具有不同异常分数分布的真实数据环境中不断试错,
+        发现专家库中未登记的新的潜在异常链路。
+        """
         print(f"\n>>> [Step 3.2] 启动PPO训练 ({config.RL_EPISODES}轮)...")
         pbar = tqdm(range(config.RL_EPISODES), desc="PPO Training")
         rewards_hist = []
@@ -288,6 +343,7 @@ class RLTrainer:
             pbar.set_postfix({'AvgR': f"{np.mean(rewards_hist):.2f}"})
 
     def _update_ppo(self, b_int, b_float, b_act, b_lp, b_rew, b_mask):
+        """PPO 核心公式计算:折扣回报计算、优势函数、Clip 截断防止策略更新幅度过大"""
         returns = []
         R = 0
         for r, m in zip(reversed(b_rew), reversed(b_mask)):
@@ -325,6 +381,11 @@ class RLTrainer:
             self.optimizer.step()
 
     def evaluate(self, test_scores):
+        """
+        第四步:模型验证与评估。
+        使用未见过的测试集数据让 AI 跑全流程,评估诊断准确率和新模式发现能力。
+        并将结果导出为结构化的 Excel 评估报告。
+        """
         print("\n>>> [Step 4] 评估测试集...")
         self.model.eval()
         results = []
@@ -428,7 +489,7 @@ class RLTrainer:
             {"指标": "新发现异常模式数", "数值": cnt_new}
         ]
         
-        save_path = os.path.join(config.RESULT_SAVE_DIR, config.TEST_RESULT_FILENAME)
+        save_path = f"{config.RESULT_SAVE_DIR}/{config.TEST_RESULT_FILENAME}"
         with pd.ExcelWriter(save_path, engine='openpyxl') as writer:
             pd.DataFrame(summary).to_excel(writer, sheet_name='Sheet1_概览指标', index=False)
             pd.DataFrame(results).to_excel(writer, sheet_name='Sheet2_测试集详情', index=False)
@@ -439,5 +500,5 @@ class RLTrainer:
         print("="*50)
 
     def save_model(self):
-        path = os.path.join(config.MODEL_SAVE_DIR, "ppo_tracing_model.pth")
+        path = config.MODEL_FILE_PATH
         torch.save(self.model.state_dict(), path)

+ 18 - 13
models/Dynamic_anomaly_diagnosis/test.py

@@ -4,33 +4,33 @@ import os
 import pandas as pd
 import numpy as np
 import torch
+import argparse
 from config import config
-from data_processing import DataAnomalyProcessor
-from causal_structure import CausalStructureBuilder
-from rl_tracing import RLTrainer, CausalTracingEnv
 
 class WaterPlantDiagnoser:
     def __init__(self):
+        from data_processing import DataAnomalyProcessor
+        from causal_structure import CausalStructureBuilder
+        from rl_tracing import RLTrainer, CausalTracingEnv
         
-        # 1. 初始化数据处理器 (用于加载阈值和计算得分,异常表征逻辑与训练完全一致)
         self.processor = DataAnomalyProcessor()
         self.sensor_list = self.processor.sensor_list
         self.threshold_df = self.processor.threshold_df
         
-        # 2. 构建因果图 (Layer 2)
         self.builder = CausalStructureBuilder(self.threshold_df)
         self.causal_graph = self.builder.build()
         
-        # 3. 加载强化学习模型 (Layer 3)
         dummy_scores = np.zeros((1, len(self.sensor_list)), dtype=np.float32)
         self.trainer = RLTrainer(self.causal_graph, dummy_scores, self.threshold_df)
+        self.CausalTracingEnv = CausalTracingEnv # 缓存类供后面使用
         
-        model_path = os.path.join(config.MODEL_SAVE_DIR, "ppo_tracing_model.pth")
-        if not os.path.exists(model_path):
-            print(f"[Warning] 未找到模型文件: {model_path}。如果是测试环境,请确保已有预训练模型。")
+        # 直接使用 config 里面拼好的路径
+        if not os.path.exists(config.MODEL_FILE_PATH):
+            print(f"[Warning] 未找到模型文件: {config.MODEL_FILE_PATH}。请先执行 main.py 进行训练。")
         else:
-            state_dict = torch.load(model_path, map_location=torch.device('cpu'), weights_only=True)
+            state_dict = torch.load(config.MODEL_FILE_PATH, map_location=torch.device('cpu'), weights_only=True)
             self.trainer.model.load_state_dict(state_dict)
+            print(f"[*] 成功加载模型: {config.MODEL_FILE_PATH}")
         
         self.trainer.model.eval()
         
@@ -128,7 +128,7 @@ class WaterPlantDiagnoser:
 
         # 构建临时环境进行溯源
         env_scores = current_window_scores.reshape(1, -1)
-        temp_env = CausalTracingEnv(self.causal_graph, env_scores, self.threshold_df, self.trainer.expert_knowledge)
+        temp_env = self.CausalTracingEnv(self.causal_graph, env_scores, self.threshold_df, self.trainer.expert_knowledge)
         
         for t_name, t_idx, t_score in active_triggers:
             state_data = temp_env.reset(force_window_idx=0, force_trigger=t_name)
@@ -252,13 +252,18 @@ class WaterPlantDiagnoser:
 if __name__ == "__main__":
     
     # 模拟外部调用机制:每次固定传 2 小时的数据
+    parser = argparse.ArgumentParser(description="水厂在线诊断测试")
+    parser.add_argument('-p', '--plant', type=str, required=True, help="测试的水厂名称")
+    args = parser.parse_args()
+    
+    config.load(args.plant)
+    diagnoser = WaterPlantDiagnoser()
+    
     test_file = os.path.join(config.DATASET_SENSOR_DIR, f"{config.SENSOR_FILE_PREFIX}1.csv")
     
     if os.path.exists(test_file):
         print(">>> 正在启动在线诊断引擎测试 (模式:读2小时,查末尾40分钟)...")
-        diagnoser = WaterPlantDiagnoser()
         
-        # 假设原始数据采样率为 4 秒一次
         # 2小时 = 7200秒 = 1800 行原始数据
         CHUNK_SIZE = 1800 
         

BIN
models/Dynamic_anomaly_diagnosis/xishan/abnormal_link.xlsx


+ 67 - 0
models/Dynamic_anomaly_diagnosis/xishan/config.yaml

@@ -0,0 +1,67 @@
+# xishan/config.yaml
+project:
+  plant_name: "xishan"
+
+files:
+  # 纯文件名,系统会自动在 ./xishan/ 下寻找
+  threshold_filename: "sensor_threshold.xlsx"
+  abnormal_link_filename: "abnormal_link.xlsx"
+  model_filename: "ppo_tracing_model.pth"
+  test_result_filename: "Final_Test_Report.xlsx"
+  sensor_file_prefix: "data_process_"
+
+data_processing:
+  sensor_file_num_range: [1, 119]
+  original_sample_interval: 4
+  target_sample_interval: 20
+  window_duration_min: 40
+  valid_data_ratio: 0.6
+  window_anomaly_threshold: 0.2
+  train_test_split: 0.8
+  trigger_score_thresh: 0.5
+  absolute_score_weight: 0.6
+  dynamic_score_weight: 0.4
+  mad_history_window: 360
+  mad_threshold: 3.0
+
+sensors:
+  keyword_layer: "One_layer"
+  keyword_device: "Device"
+  trigger_sensors:
+    - "UF1Per"
+    - "UF2Per"
+    - "UF3Per"
+    - "UF4Per"
+    - "C.M.RO1_DB@DPT_1"
+    - "C.M.RO2_DB@DPT_1"
+    - "C.M.RO3_DB@DPT_1"
+    - "C.M.RO4_DB@DPT_1"
+    - "C.M.RO1_DB@DPT_2"
+    - "C.M.RO2_DB@DPT_2"
+    - "C.M.RO3_DB@DPT_2"
+    - "C.M.RO4_DB@DPT_2"
+    - "RO1_CSFlow"
+    - "RO2_CSFlow"
+    - "RO3_CSFlow"
+    - "RO4_CSFlow"
+    - "RO1HSL"
+    - "RO2HSL"
+    - "RO3HSL"
+    - "RO4HSL"
+    - "RO1_TYL"
+    - "RO2_TYL"
+    - "RO3_TYL"
+    - "RO4_TYL"
+
+rl_params:
+  min_path_length: 3
+  max_path_length: 6
+  embedding_dim: 64
+  hidden_dim: 256
+  ppo_lr: 0.0003
+  ppo_gamma: 0.90
+  ppo_eps_clip: 0.2
+  ppo_k_epochs: 10
+  ppo_batch_size: 64
+  bc_epochs: 20000
+  rl_episodes: 20000

+ 0 - 0
models/Dynamic_anomaly_diagnosis/models/ppo_tracing_model.pth → models/Dynamic_anomaly_diagnosis/xishan/ppo_tracing_model.pth


BIN
models/Dynamic_anomaly_diagnosis/xishan/sensor_threshold.xlsx