Просмотр исходного кода

Merge remote-tracking branch 'company/dev' into dev

junc_WHU 4 месяцев назад
Родитель
Сommit
73a96536ed
100 измененных файлов с 1643 добавлено и 6426 удалено
  1. 37 0
      models/Dynamic_anomaly_diagnosis/README.md
  2. 35 2
      models/Dynamic_anomaly_diagnosis/causal_structure.py
  3. 81 91
      models/Dynamic_anomaly_diagnosis/config.py
  4. 23 10
      models/Dynamic_anomaly_diagnosis/data_processing.py
  5. 0 158
      models/Dynamic_anomaly_diagnosis/input_format.txt
  6. BIN
      models/Dynamic_anomaly_diagnosis/longting/abnormal_link.xlsx
  7. 55 0
      models/Dynamic_anomaly_diagnosis/longting/config.yaml
  8. BIN
      models/Dynamic_anomaly_diagnosis/longting/ppo_tracing_model.pth
  9. BIN
      models/Dynamic_anomaly_diagnosis/longting/sensor_threshold.xlsx
  10. 18 11
      models/Dynamic_anomaly_diagnosis/main.py
  11. 0 44
      models/Dynamic_anomaly_diagnosis/output_format.txt
  12. 70 9
      models/Dynamic_anomaly_diagnosis/rl_tracing.py
  13. 18 13
      models/Dynamic_anomaly_diagnosis/test.py
  14. BIN
      models/Dynamic_anomaly_diagnosis/xishan/abnormal_link.xlsx
  15. 67 0
      models/Dynamic_anomaly_diagnosis/xishan/config.yaml
  16. 0 0
      models/Dynamic_anomaly_diagnosis/xishan/ppo_tracing_model.pth
  17. BIN
      models/Dynamic_anomaly_diagnosis/xishan/sensor_threshold.xlsx
  18. 0 627
      models/anomaly_detection/README.md
  19. 0 255
      models/anomaly_detection/detection.py
  20. BIN
      models/anomaly_detection/isolation_forest_models.pkl
  21. BIN
      models/anomaly_detection/scaler.pkl
  22. BIN
      models/anomaly_detection/three_sigma_model.pkl
  23. 0 60
      models/causal-inference/args.py
  24. 0 227
      models/causal-inference/data_preprocessor.py
  25. 0 262
      models/causal-inference/data_trainer.py
  26. BIN
      models/causal-inference/features_scaler.joblib
  27. 0 226
      models/causal-inference/gat.py
  28. 0 313
      models/causal-inference/main.py
  29. BIN
      models/causal-inference/policy.optimizer.pth
  30. BIN
      models/causal-inference/policy.pth
  31. 0 188
      models/causal-inference/rl_optimizer.py
  32. BIN
      models/causal-inference/targets_scaler.joblib
  33. 40 0
      models/prediction_models/20min/README.md
  34. 92 0
      models/prediction_models/20min/anzhen/config.yaml
  35. 0 0
      models/prediction_models/20min/anzhen/edge_index.pt
  36. 0 0
      models/prediction_models/20min/anzhen/id_list.xlsx
  37. 0 0
      models/prediction_models/20min/anzhen/input_format.txt
  38. 0 0
      models/prediction_models/20min/anzhen/model.pth
  39. 0 0
      models/prediction_models/20min/anzhen/output_format.txt
  40. 0 0
      models/prediction_models/20min/anzhen/scaler.pkl
  41. 74 0
      models/prediction_models/20min/config.py
  42. 145 0
      models/prediction_models/20min/data_preprocessor.py
  43. 121 0
      models/prediction_models/20min/data_trainer.py
  44. 44 0
      models/prediction_models/20min/gat_lstm.py
  45. 81 0
      models/prediction_models/20min/jianding/config.yaml
  46. 0 0
      models/prediction_models/20min/jianding/edge_index.pt
  47. 0 0
      models/prediction_models/20min/jianding/id_list.xlsx
  48. 0 0
      models/prediction_models/20min/jianding/input_format.txt
  49. 0 0
      models/prediction_models/20min/jianding/model.pth
  50. 0 0
      models/prediction_models/20min/jianding/output_format.txt
  51. 0 0
      models/prediction_models/20min/jianding/scaler.pkl
  52. 118 0
      models/prediction_models/20min/lankao/config.yaml
  53. 0 0
      models/prediction_models/20min/lankao/edge_index.pt
  54. BIN
      models/prediction_models/20min/lankao/id_list.xlsx
  55. 64 0
      models/prediction_models/20min/lankao/input_format.txt
  56. BIN
      models/prediction_models/20min/lankao/model.pth
  57. 2 0
      models/prediction_models/20min/lankao/output_format.txt
  58. BIN
      models/prediction_models/20min/lankao/scaler.pkl
  59. 131 0
      models/prediction_models/20min/longting/config.yaml
  60. BIN
      models/prediction_models/20min/longting/edge_index.pt
  61. 0 0
      models/prediction_models/20min/longting/id_list.xlsx
  62. 0 0
      models/prediction_models/20min/longting/input_format.txt
  63. 0 0
      models/prediction_models/20min/longting/model.pth
  64. 0 0
      models/prediction_models/20min/longting/output_format.txt
  65. 0 0
      models/prediction_models/20min/longting/scaler.pkl
  66. 57 0
      models/prediction_models/20min/main.py
  67. 84 0
      models/prediction_models/20min/predict.py
  68. 119 0
      models/prediction_models/20min/yancheng/config.yaml
  69. BIN
      models/prediction_models/20min/yancheng/edge_index.pt
  70. BIN
      models/prediction_models/20min/yancheng/id_list.xlsx
  71. 65 0
      models/prediction_models/20min/yancheng/input_format.txt
  72. BIN
      models/prediction_models/20min/yancheng/model.pth
  73. 2 0
      models/prediction_models/20min/yancheng/output_format.txt
  74. BIN
      models/prediction_models/20min/yancheng/scaler.pkl
  75. 0 51
      models/prediction_models/anzhen/args.py
  76. 0 221
      models/prediction_models/anzhen/data_preprocessor.py
  77. 0 165
      models/prediction_models/anzhen/data_trainer.py
  78. 0 54
      models/prediction_models/anzhen/gat_lstm.py
  79. 0 59
      models/prediction_models/anzhen/main.py
  80. 0 271
      models/prediction_models/anzhen/predict.py
  81. 0 51
      models/prediction_models/jianding/args.py
  82. 0 211
      models/prediction_models/jianding/data_preprocessor.py
  83. 0 165
      models/prediction_models/jianding/data_trainer.py
  84. 0 54
      models/prediction_models/jianding/gat_lstm.py
  85. 0 59
      models/prediction_models/jianding/main.py
  86. 0 258
      models/prediction_models/jianding/predict.py
  87. 0 51
      models/prediction_models/longting/args.py
  88. 0 257
      models/prediction_models/longting/data_preprocessor.py
  89. 0 169
      models/prediction_models/longting/data_trainer.py
  90. 0 54
      models/prediction_models/longting/gat_lstm.py
  91. 0 59
      models/prediction_models/longting/main.py
  92. 0 305
      models/prediction_models/longting/predict.py
  93. 0 384
      models/pressure-predictor/20分钟TMP预测模型源码/20min_predict.py
  94. 0 261
      models/pressure-predictor/20分钟TMP预测模型源码/README.md
  95. 0 55
      models/pressure-predictor/20分钟TMP预测模型源码/args.py
  96. 0 85
      models/pressure-predictor/20分钟TMP预测模型源码/data_export.py
  97. 0 276
      models/pressure-predictor/20分钟TMP预测模型源码/data_preprocessor.py
  98. 0 256
      models/pressure-predictor/20分钟TMP预测模型源码/data_trainer.py
  99. BIN
      models/pressure-predictor/20分钟TMP预测模型源码/edge_index.pt
  100. 0 99
      models/pressure-predictor/20分钟TMP预测模型源码/gat_lstm.py

+ 37 - 0
models/Dynamic_anomaly_diagnosis/README.md

@@ -0,0 +1,37 @@
+# 双膜工艺(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


+ 0 - 627
models/anomaly_detection/README.md

@@ -1,627 +0,0 @@
-# 异常检测模块 (Anomaly Detection)
-
-> **版本**: 1.0.0  
-> **最后更新**: 2025-11-04  
-> **功能**: 对双膜系统(UF超滤 + RO反渗透)关键运行指标进行实时异常检测
-
----
-
-## 模块概述
-
-异常检测模块用于监测双膜水处理系统的关键运行指标,及时发现设备异常、性能退化等问题。
-
-### 主要特性
-
-- **多模型融合**: 支持孤立森林(Isolation Forest)、3σ统计、One-Class SVM三种检测方法
-- **逐列检测**: 对每个指标独立建模,精准定位异常来源
-- **无需标注**: 基于无监督学习,不需要人工标注异常样本
-- **实时检测**: 支持在线预测,快速识别异常点
-- **批量处理**: 自动读取和合并多批次历史数据
-
-### 应用场景
-
-1. **设备异常预警**: 膜组件堵塞、泵压异常、流量突变
-2. **性能监控**: 膜渗透率下降、产水量异常
-3. **维护决策**: 基于异常频率制定清洗/更换计划
-4. **质量控制**: 确保系统稳定运行
-
----
-
-## 目录结构
-
-```
-models/anomaly_detection/
-├── detection.py                    # 主程序(训练+预测)
-├── README.md                        # 本文档
-├── scaler.pkl                       # 数据归一化器(训练产物)
-├── isolation_forest_models.pkl      # 孤立森林模型(训练产物)
-├── three_sigma_model.pkl            # 3σ统计模型(训练产物)
-└── datasets_export_xishan/          # 数据目录(需自行准备)
-    ├── data_export5_1.csv           # UF渗透率数据
-    ├── data_export8_1.csv           # RO1/RO2压差数据
-    ├── data_export9_1.csv           # RO3/RO4压差数据
-    ├── data_export11_1.csv          # RO产水流量数据
-    └── ... (data_export*_2.csv ~ data_export*_26.csv)
-```
-
----
-
-## 核心功能
-
-### 1. 数据加载与合并
-
-- 批量读取 26 批次历史数据(`data_export*_1.csv ~ data_export*_26.csv`)
-- 自动提取关键指标列并纵向合并
-- 容错处理:文件缺失时跳过并记录日志
-
-### 2. 数据预处理
-
-- **MinMax归一化**: 将所有指标缩放至 [0, 1] 区间
-- **逐列处理**: 每个指标独立归一化,避免量纲影响
-- **保存scaler**: 归一化器保存为 `scaler.pkl`,用于在线预测
-
-### 3. 模型训练
-
-#### 孤立森林 (Isolation Forest)
-- 基于树结构随机分割,异常点更容易被孤立
-- 参数:`n_estimators=100`, `contamination='auto'`
-- 适用场景:单变量离群点检测
-
-#### 3σ统计法 (Three Sigma)
-- 基于正态分布假设,超出 μ±3σ 视为异常
-- 参数:`n_sigma=3`(可调)
-- 适用场景:稳态数据、可解释性要求高
-
-#### One-Class SVM (可选)
-- 基于核函数学习正常数据边界
-- 参数:`nu=0.05`, `kernel='rbf'`
-- 适用场景:复杂边界、非线性分布
-
-### 4. 异常预测
-
-- 输入归一化后的数据
-- 输出预测标签:`-1=异常`, `1=正常`
-- 支持批量预测和实时检测
-
----
-
-## 监测指标
-
-### UF(超滤)指标
-
-| 指标名称 | 含义 | 单位 | 正常范围 |
-|---------|------|------|---------|
-| UF1Per  | UF1膜渗透率 | - | 根据历史数据确定 |
-| UF2Per  | UF2膜渗透率 | - | 根据历史数据确定 |
-| UF3Per  | UF3膜渗透率 | - | 根据历史数据确定 |
-| UF4Per  | UF4膜渗透率 | - | 根据历史数据确定 |
-
-### RO(反渗透)指标
-
-| 指标名称 | 含义 | 单位 | 异常情况 |
-|---------|------|------|---------|
-| C.M.RO1_DB@DPT_1 | RO1一段压差 | kPa | 压差过高→膜污堵 |
-| C.M.RO1_DB@DPT_2 | RO1二段压差 | kPa | 压差过高→膜污堵 |
-| C.M.RO2_DB@DPT_1 | RO2一段压差 | kPa | 压差过高→膜污堵 |
-| C.M.RO2_DB@DPT_2 | RO2二段压差 | kPa | 压差过高→膜污堵 |
-| C.M.RO3_DB@DPT_1 | RO3一段压差 | kPa | 压差过高→膜污堵 |
-| C.M.RO3_DB@DPT_2 | RO3二段压差 | kPa | 压差过高→膜污堵 |
-| C.M.RO4_DB@DPT_1 | RO4一段压差 | kPa | 压差过高→膜污堵 |
-| C.M.RO4_DB@DPT_2 | RO4二段压差 | kPa | 压差过高→膜污堵 |
-| RO1_CSFlow | RO1产水流量 | m³/h | 流量异常→泵/阀问题 |
-| RO2_CSFlow | RO2产水流量 | m³/h | 流量异常→泵/阀问题 |
-| RO3_CSFlow | RO3产水流量 | m³/h | 流量异常→泵/阀问题 |
-| RO4_CSFlow | RO4产水流量 | m³/h | 流量异常→泵/阀问题 |
-
-**共16个监测指标**
-
----
-
-## 技术架构
-
-### 整体流程图
-
-```mermaid
-graph TB
-    subgraph 数据加载阶段
-        A[开始] --> B1[读取data_export5_*.csv]
-        A --> B2[读取data_export8_*.csv]
-        A --> B3[读取data_export9_*.csv]
-        A --> B4[读取data_export11_*.csv]
-        B1 --> C[提取UF1Per/UF2Per/UF3Per/UF4Per]
-        B2 --> D[提取RO1/RO2的DPT_1和DPT_2]
-        B3 --> E[提取RO3/RO4的DPT_1和DPT_2]
-        B4 --> F[提取RO1~RO4的CSFlow]
-        C --> G[纵向合并所有批次数据]
-        D --> G
-        E --> G
-        F --> G
-    end
-    
-    subgraph 数据预处理阶段
-        G --> H[DataFrame: 16个监测指标列]
-        H --> I[MinMaxScaler归一化到0-1区间]
-        I --> J[保存scaler.pkl]
-    end
-    
-    subgraph 模型训练阶段
-        J --> K{逐列训练}
-        K --> L1[孤立森林训练]
-        K --> L2[3σ统计计算]
-        K --> L3[One-Class SVM训练可选]
-        
-        L1 --> M1[遍历16个指标列]
-        M1 --> N1[每列fit一个IsolationForest模型]
-        N1 --> O1[保存isolation_forest_models.pkl]
-        
-        L2 --> M2[遍历16个指标列]
-        M2 --> N2[每列计算mean和std]
-        N2 --> O2[保存three_sigma_model.pkl]
-        
-        L3 --> M3[遍历16个指标列]
-        M3 --> N3[每列fit一个OneClassSVM模型]
-        N3 --> O3[保存one_class_svm_models.pkl]
-    end
-    
-    subgraph 异常检测阶段
-        O1 --> P[加载新数据]
-        O2 --> P
-        O3 --> P
-        P --> Q[使用scaler归一化新数据]
-        Q --> R{选择模型预测}
-        R --> S1[孤立森林预测]
-        R --> S2[3σ预测]
-        R --> S3[One-Class SVM预测]
-        S1 --> T[结果融合可选]
-        S2 --> T
-        S3 --> T
-        T --> U[输出预测结果: -1异常/1正常]
-        U --> V[结束]
-    end
-```
-
-### 详细训练与预测流程
-
-```mermaid
-sequenceDiagram
-    participant User as 用户
-    participant Script as detection.py
-    participant Data as 数据文件
-    participant Model as 模型
-    participant Result as 预测结果
-    
-    Note over User,Result: 训练阶段
-    User->>Script: 运行python detection.py
-    Script->>Data: 批量读取26批次CSV
-    Data-->>Script: 返回DataFrame(N行×16列)
-    Script->>Script: MinMaxScaler归一化
-    Script->>Script: 保存scaler.pkl
-    
-    loop 对每个指标列
-        Script->>Model: 训练IsolationForest
-        Script->>Model: 计算3σ统计量
-        Script->>Model: 训练OneClassSVM(可选)
-    end
-    
-    Script->>Script: 保存所有模型.pkl
-    Script-->>User: 训练完成
-    
-    Note over User,Result: 预测阶段
-    User->>Script: 提供新数据DataFrame
-    Script->>Script: 加载scaler.pkl和模型
-    Script->>Script: 归一化新数据
-    
-    loop 对每个指标列
-        Script->>Model: 调用predict()
-        Model-->>Script: 返回-1或1
-    end
-    
-    Script->>Result: 生成预测DataFrame
-    Result-->>User: 返回异常检测结果
-```
-
-### 关键技术
-
-- **sklearn.ensemble.IsolationForest**: 孤立森林实现
-- **sklearn.svm.OneClassSVM**: 单类SVM实现
-- **sklearn.preprocessing.MinMaxScaler**: 数据归一化
-- **pandas**: 数据处理
-- **joblib**: 模型序列化
-
----
-
-## 数据来源
-
-### 文件命名规则
-
-| 文件模板 | 包含指标 | 数量 |
-|---------|---------|------|
-| `data_export5_{i}.csv` | UF1Per, UF2Per, UF3Per, UF4Per | 4列 |
-| `data_export8_{i}.csv` | RO1/RO2的DPT_1和DPT_2 | 4列 |
-| `data_export9_{i}.csv` | RO3/RO4的DPT_1和DPT_2 | 4列 |
-| `data_export11_{i}.csv` | RO1~RO4的CSFlow | 4列 |
-
-其中 `{i}` 为批次号,范围 `1~26`
-
-### 数据格式要求
-
-```csv
-UF1Per,UF2Per,UF3Per,UF4Per
-0.85,0.87,0.83,0.86
-0.84,0.86,0.82,0.85
-...
-```
-
-- CSV格式,逗号分隔
-- 第一行为列名(必须与代码中定义的列名一致)
-- 数值型数据,缺失值用空或NaN表示
-
----
-
-## 使用指南
-
-### 1. 环境准备
-
-```bash
-# 安装依赖
-pip install numpy pandas scikit-learn joblib matplotlib
-
-# 确认目录结构
-cd models/anomaly_detection/
-ls datasets_export_xishan/  # 确认数据文件存在
-```
-
-### 2. 训练模型
-
-```bash
-# 运行主程序
-python detection.py
-```
-
-**输出示例**:
-```
-开始加载数据...
-成功读取: data_export5_1.csv
-成功读取: data_export8_1.csv
-...
-数据合并完成,总样本数: 125680
-
-开始数据归一化...
-归一化器已保存为 scaler.pkl
-
-开始训练孤立森林模型...
-已训练 UF1Per 的孤立森林模型
-已训练 UF2Per 的孤立森林模型
-...
-孤立森林模型已保存为 isolation_forest_models.pkl
-
-开始训练3σ模型...
-已计算 UF1Per 的3σ统计量
-...
-3σ模型已保存为 three_sigma_model.pkl
-
-所有模型训练和保存完成!
-```
-
-### 3. 使用模型预测(示例代码)
-
-```python
-import pandas as pd
-import joblib
-
-# 加载模型和归一化器
-scaler = joblib.load("scaler.pkl")
-if_model = joblib.load("isolation_forest_models.pkl")
-ts_model = joblib.load("three_sigma_model.pkl")
-
-# 准备新数据(需包含所有16个指标列)
-new_data = pd.DataFrame({
-    'UF1Per': [0.85, 0.82],
-    'UF2Per': [0.87, 0.84],
-    # ... 其他14个指标
-})
-
-# 归一化
-normalized_data = scaler.transform(new_data)
-normalized_df = pd.DataFrame(normalized_data, columns=new_data.columns)
-
-# 孤立森林预测
-if_predictions = if_model.predict(normalized_df)
-print("孤立森林预测:", if_predictions)
-
-# 3σ预测
-ts_predictions = ts_model.predict(normalized_df, n_sigma=3)
-print("3σ预测:", ts_predictions)
-
-# 结果融合(投票法)
-# -1=异常, 1=正常
-final_predictions = (if_predictions + ts_predictions) // 2
-```
-
----
-
-## 模型说明
-
-### 1. 孤立森林 (Isolation Forest)
-
-**原理**: 
-- 通过随机选择特征和分割点构建多棵树
-- 异常点更容易被孤立(需要更少的分割次数)
-- 路径长度越短,异常性越高
-
-**优点**:
-- 无需标注数据
-- 对高维数据有效
-- 计算效率高
-
-**缺点**:
-- 对正常数据密度不均匀的情况敏感
-- contamination参数需要先验知识
-
-**参数说明**:
-
-| 参数 | 值 | 说明 |
-|-----|-----|------|
-| n_estimators | 100 | 树的数量 |
-| contamination | 'auto' | 异常比例(自动估计) |
-| random_state | 42 | 随机种子 |
-
-### 2. 3σ统计法 (Three Sigma)
-
-**原理**:
-- 假设数据服从正态分布
-- 计算均值μ和标准差σ
-- 超出 [μ-3σ, μ+3σ] 范围视为异常(覆盖99.7%正常数据)
-
-**优点**:
-- 简单直观,易于理解和解释
-- 计算速度快
-- 可调整灵敏度(修改n_sigma)
-
-**缺点**:
-- 假设数据正态分布(偏态分布效果差)
-- 对极端异常值敏感
-
-**参数说明**:
-
-| 参数 | 值 | 说明 |
-|-----|-----|------|
-| n_sigma | 3 | 阈值系数(推荐2~4) |
-
-### 3. One-Class SVM (可选)
-
-**原理**:
-- 在高维空间寻找包含正常数据的最小超球面
-- 核函数将数据映射到高维空间
-- 边界外的点视为异常
-
-**优点**:
-- 适合复杂非线性边界
-- 理论基础扎实
-
-**缺点**:
-- 计算复杂度高(大数据集慢)
-- 参数调优困难(nu、gamma)
-- 对数据尺度敏感
-
-**参数说明**:
-
-| 参数 | 值 | 说明 |
-|-----|-----|------|
-| nu | 0.05 | 异常比例上界(5%) |
-| kernel | 'rbf' | 径向基核函数 |
-| gamma | 'scale' | 核系数(自动) |
-
-**启用方法**:
-1. 在 `detection.py` 中取消 `OneClassSVMModel` 类的注释(第166-203行)
-2. 取消 `main()` 函数中相关代码的注释(第227-250行)
-
----
-
-## 配置参数
-
-### 数据路径配置
-
-```python
-# detection.py 第14行
-data_folder = "datasets_export_xishan"
-```
-
-### 文件模板配置
-
-```python
-# detection.py 第22-29行
-file_info = {
-    "data_export5_{}.csv": ["UF1Per", "UF2Per", "UF3Per", "UF4Per"],
-    "data_export8_{}.csv": ["C.M.RO1_DB@DPT_1", "C.M.RO1_DB@DPT_2", 
-                           "C.M.RO2_DB@DPT_1", "C.M.RO2_DB@DPT_2"],
-    "data_export9_{}.csv": ["C.M.RO3_DB@DPT_1", "C.M.RO3_DB@DPT_2", 
-                           "C.M.RO4_DB@DPT_1", "C.M.RO4_DB@DPT_2"],
-    "data_export11_{}.csv": ["RO1_CSFlow", "RO2_CSFlow", "RO3_CSFlow", "RO4_CSFlow"]
-}
-```
-
-### 模型参数调整
-
-**孤立森林**:
-```python
-# detection.py 第98行
-model = IsolationForest(
-    n_estimators=100,        # 增大→更稳定,但训练慢
-    contamination='auto',    # 或设置具体值如0.05
-    random_state=42
-)
-```
-
-**3σ统计**:
-```python
-# 预测时调整
-ts_predictions = ts_model.predict(normalized_df, n_sigma=3)
-# n_sigma=2 → 更敏感(95%覆盖)
-# n_sigma=4 → 更宽松(99.99%覆盖)
-```
-
----
-
-## 输出结果
-
-### 1. 训练产物
-
-| 文件名 | 大小 | 说明 |
-|--------|------|------|
-| scaler.pkl | ~2KB | MinMax归一化器 |
-| isolation_forest_models.pkl | ~500KB | 孤立森林模型(16个) |
-| three_sigma_model.pkl | ~1KB | 3σ统计量(16个指标的μ和σ) |
-
-### 2. 预测结果格式
-
-**DataFrame示例**:
-
-| UF1Per | UF2Per | C.M.RO1_DB@DPT_1 | ... |
-|--------|--------|------------------|-----|
-| 1      | 1      | -1               | ... |
-| 1      | -1     | 1                | ... |
-| 1      | 1      | 1                | ... |
-
-- `1`: 正常
-- `-1`: 异常
-
-### 3. 结果分析
-
-#### 单指标异常统计
-```python
-# 统计每个指标的异常率
-anomaly_rate = (predictions == -1).sum() / len(predictions)
-print(f"异常率: {anomaly_rate * 100:.2f}%")
-```
-
-#### 多模型融合
-```python
-# 投票法:两个模型都判定为异常才认为异常
-consensus = ((if_pred == -1) & (ts_pred == -1)).astype(int)
-consensus = np.where(consensus, -1, 1)
-```
-
-#### 异常时段定位
-```python
-# 找出连续异常的时间段(需要时间戳列)
-anomaly_indices = np.where(predictions == -1)[0]
-print(f"异常点索引: {anomaly_indices}")
-```
-
----
-
-## 常见问题
-
-### Q1: 读取数据时报错"未找到文件"
-
-**原因**: `datasets_export_xishan/` 目录不存在或文件命名不符合规则
-
-**解决**:
-```bash
-# 检查目录
-ls datasets_export_xishan/
-
-# 确认文件命名格式
-# 正确: data_export5_1.csv, data_export8_2.csv
-# 错误: data_export5-1.csv, export5_1.csv
-```
-
-### Q2: One-Class SVM 报错 `NameError: name 'OneClassSVMModel' is not defined`
-
-**原因**: One-Class SVM 默认被注释
-
-**解决**:
-1. 打开 `detection.py`
-2. 取消第166-203行的注释(类定义)
-3. 取消第227-250行的注释(训练和预测代码)
-
-### Q3: 训练时内存不足
-
-**原因**: 数据量过大(26批次 × 数万样本)
-
-**解决**:
-```python
-# detection.py 第208行后添加下采样
-merged_data = load_and_merge_data()
-# 随机采样50%数据
-merged_data = merged_data.sample(frac=0.5, random_state=42)
-```
-
-### Q4: 预测结果全是异常或全是正常
-
-**原因**: 
-- 数据分布与训练数据差异大
-- contamination参数不合理
-
-**解决**:
-```python
-# 调整contamination参数
-model = IsolationForest(
-    n_estimators=100,
-    contamination=0.05,  # 假设5%为异常
-    random_state=42
-)
-```
-
-### Q5: 3σ方法对某些指标效果差
-
-**原因**: 数据不服从正态分布(偏态、双峰)
-
-**解决**:
-```python
-# 查看数据分布
-import matplotlib.pyplot as plt
-df['UF1Per'].hist(bins=50)
-plt.show()
-
-# 考虑数据变换(如log变换)或使用孤立森林
-```
-
-### Q6: 如何评估模型效果?
-
-**方法**:
-1. **人工标注部分样本**: 构建测试集计算准确率、召回率
-2. **业务反馈**: 结合实际异常事件验证
-3. **多模型对比**: 比较IF、3σ、OCSVM的一致性
-
-```python
-from sklearn.metrics import classification_report
-
-# 需要人工标注的真实标签
-y_true = [1, 1, -1, 1, -1, ...]
-y_pred = if_model.predict(test_data)
-
-print(classification_report(y_true, y_pred))
-```
-
-### Q7: 如何集成到在线系统?
-
-**方案**:
-```python
-# 1. 封装为函数
-def detect_anomaly(new_data):
-    """
-    参数: new_data - DataFrame,包含16个指标列
-    返回: predictions - DataFrame,-1=异常, 1=正常
-    """
-    scaler = joblib.load("scaler.pkl")
-    if_model = joblib.load("isolation_forest_models.pkl")
-    
-    normalized = scaler.transform(new_data)
-    normalized_df = pd.DataFrame(normalized, columns=new_data.columns)
-    
-    return if_model.predict(normalized_df)
-
-# 2. 构建API(FastAPI示例)
-from fastapi import FastAPI
-app = FastAPI()
-
-@app.post("/detect")
-def detect(data: dict):
-    df = pd.DataFrame([data])
-    result = detect_anomaly(df)
-    return {"prediction": result.tolist()}
-```
-
-

+ 0 - 255
models/anomaly_detection/detection.py

@@ -1,255 +0,0 @@
-import os
-import pandas as pd
-import numpy as np
-import joblib
-from sklearn.preprocessing import MinMaxScaler
-from sklearn.ensemble import IsolationForest
-from sklearn.svm import OneClassSVM  
-
-# 设置中文字体显示(用于本地可视化时中文不乱码)
-import matplotlib.pyplot as plt
-plt.rcParams["font.family"] = ["SimHei", "WenQuanYi Micro Hei", "Heiti TC"]
-
-# 数据文件夹路径(批量 CSV 存放目录)
-data_folder = "datasets_export_xishan"
-
-# 定义要读取的文件模板与对应列名(逐批次 data_exportX_{i}.csv, i=1..26)
-# 关键字段含义(业务约定):
-# - UF1Per:UF1膜渗透率(UF1Per)
-# - C.M.RO1_DB@DPT_1:RO1一段压差
-# - C.M.RO1_DB@DPT_2:RO1二段压差
-# - RO1_CSFlow:RO1产水流量
-file_info = {
-    "data_export5_{}.csv": ["UF1Per", "UF2Per", "UF3Per", "UF4Per"],
-    "data_export8_{}.csv": ["C.M.RO1_DB@DPT_1", "C.M.RO1_DB@DPT_2", 
-                           "C.M.RO2_DB@DPT_1", "C.M.RO2_DB@DPT_2"],
-    "data_export9_{}.csv": ["C.M.RO3_DB@DPT_1", "C.M.RO3_DB@DPT_2", 
-                           "C.M.RO4_DB@DPT_1", "C.M.RO4_DB@DPT_2"],
-    "data_export11_{}.csv": ["RO1_CSFlow", "RO2_CSFlow", "RO3_CSFlow", "RO4_CSFlow"]
-}
-
-def load_and_merge_data():
-    """加载并合并所有数据文件
-
-    - 按 file_info 中的模板逐列读取各批 CSV(i=1..26),仅选取关心的指标列
-    - 将成功读取的数据 DataFrame 纵向拼接(ignore_index=True)
-    - 返回合并后的 DataFrame;若无任何成功数据则报错
-    """
-    all_data = []
-    
-    # 循环读取每个文件模板和对应的编号1-26
-    for file_template, columns in file_info.items():
-        for i in range(1, 27):
-            # 构建完整的文件路径
-            filename = file_template.format(i)
-            file_path = os.path.join(data_folder, filename)
-            
-            try:
-                # 读取CSV文件的指定列(仅保留关心指标,减小内存开销)
-                df = pd.read_csv(file_path, usecols=columns)
-                all_data.append(df)
-                print(f"成功读取: {filename}")
-            except Exception as e:
-                print(f"读取文件 {filename} 时出错: {e}")
-    
-    # 合并所有数据(纵向堆叠)
-    if not all_data:
-        raise ValueError("没有成功读取任何数据文件")
-    
-    merged_df = pd.concat(all_data, ignore_index=True)
-    print(f"数据合并完成,总样本数: {len(merged_df)}")
-    return merged_df
-
-def normalize_data(df):
-    """对数据进行归一化处理(逐列 Min-Max 到 [0,1])
-
-    - 拟合并转换每一列;保存 scaler 至 `scaler.pkl`
-    - 返回归一化后的 DataFrame 以及 scaler(便于线上/后续反归一化)
-    """
-    scaler = MinMaxScaler()
-    scaled_data = scaler.fit_transform(df)
-    scaled_df = pd.DataFrame(scaled_data, columns=df.columns)
-    
-    # 保存归一化器
-    joblib.dump(scaler, "scaler.pkl")
-    print("归一化器已保存为 scaler.pkl")
-    
-    return scaled_df, scaler
-
-class IsolationForestModel:
-    """孤立森林异常检测模型(逐列一维检测)
-
-    - 特点:无需标注,适合检测孤立点;contamination='auto' 自动估计比例
-    - 输出:predict → -1 表示异常,1 表示正常
-    """
-    def __init__(self):
-        self.models = {}  # 存储每列的模型
-    
-    def fit(self, df):
-        """逐列训练孤立森林模型
-
-        参数:
-        - df: 归一化后的 DataFrame(每列为一个监测指标)
-        """
-        for column in df.columns:
-            # 准备数据(sklearn 需要二维输入)
-            X = df[column].values.reshape(-1, 1)
-            # 训练模型
-            model = IsolationForest(n_estimators=100, contamination='auto', random_state=42)
-            model.fit(X)
-            self.models[column] = model
-            print(f"已训练 {column} 的孤立森林模型")
-        return self
-    
-    def predict(self, df):
-        """预测异常值,-1 表示异常,1 表示正常(逐列)"""
-        results = pd.DataFrame()
-        for column in df.columns:
-            if column not in self.models:
-                raise ValueError(f"没有 {column} 的模型,请先训练")
-            
-            X = df[column].values.reshape(-1, 1)
-            results[column] = self.models[column].predict(X)
-        return results
-    
-    def save(self, filename="isolation_forest_models.pkl"):
-        """保存模型"""
-        joblib.dump(self, filename)
-        print(f"孤立森林模型已保存为 {filename}")
-
-class ThreeSigmaModel:
-    """3σ 异常检测模型(逐列基于均值±nσ 的阈值法)
-
-    - 特点:简单、可解释;可调 n_sigma(默认 3)
-    - 输出:-1 表示异常,1 表示正常
-    """
-    def __init__(self):
-        self.stats = {}  # 存储每列的均值和标准差
-    
-    def fit(self, df):
-        """计算每列的均值和标准差,并缓存统计量"""
-        for column in df.columns:
-            mean = df[column].mean()
-            std = df[column].std()
-            self.stats[column] = (mean, std)
-            print(f"已计算 {column} 的3σ统计量")
-        return self
-    
-    def predict(self, df, n_sigma=3):
-        """预测异常值(-1=异常,1=正常)
-
-        参数:
-        - df: 归一化后的 DataFrame
-        - n_sigma: 阈值宽度因子,默认 3,即 mean ± 3*std
-        """
-        results = pd.DataFrame()
-        for column in df.columns:
-            if column not in self.stats:
-                raise ValueError(f"没有 {column} 的统计量,请先训练")
-            
-            mean, std = self.stats[column]
-            # 计算上下限
-            lower_bound = mean - n_sigma * std
-            upper_bound = mean + n_sigma * std
-            
-            # 判断异常值
-            is_outlier = (df[column] < lower_bound) | (df[column] > upper_bound)
-            # 转换为-1(异常)和1(正常)
-            results[column] = np.where(is_outlier, -1, 1)
-        return results
-    
-    def save(self, filename="three_sigma_model.pkl"):
-        """保存模型"""
-        joblib.dump(self, filename)
-        print(f"3σ模型已保存为 {filename}")
-
-'''
-class OneClassSVMModel:
-    """One-Class SVM 异常检测模型(可选)
-
-    - 对复杂边界更有表达力,但对参数与尺度敏感
-    - 启用前请确保样本量与特征尺度合适
-    """
-    def __init__(self):
-        self.models = {}  # 存储每列的模型
-    
-    def fit(self, df):
-        """逐列训练 One-Class SVM 模型"""
-        for column in df.columns:
-            # 准备数据(需要二维数组)
-            X = df[column].values.reshape(-1, 1)
-            # 训练模型
-            model = OneClassSVM(nu=0.05, kernel='rbf', gamma='scale')
-            model.fit(X)
-            self.models[column] = model
-            print(f"已训练 {column} 的One-Class SVM模型")
-        return self
-    
-    def predict(self, df):
-        """预测异常值,-1 表示异常,1 表示正常"""
-        results = pd.DataFrame()
-        for column in df.columns:
-            if column not in self.models:
-                raise ValueError(f"没有 {column} 的模型,请先训练")
-            
-            X = df[column].values.reshape(-1, 1)
-            results[column] = self.models[column].predict(X)
-        return results
-    
-    def save(self, filename="one_class_svm_models.pkl"):
-        """保存模型"""
-        joblib.dump(self, filename)
-        print(f"One-Class SVM模型已保存为 {filename}")
-'''
-
-def main():
-    # 1. 加载并合并数据(仅读取关心指标列)
-    print("开始加载数据...")
-    merged_data = load_and_merge_data()
-    
-    # 2. 数据归一化(逐列 Min-Max 到 [0,1] 并保存 scaler)
-    print("\n开始数据归一化...")
-    normalized_data, scaler = normalize_data(merged_data)
-    
-    # 3. 训练并保存孤立森林模型(逐列训练)
-    print("\n开始训练孤立森林模型...")
-    if_model = IsolationForestModel()
-    if_model.fit(normalized_data)
-    if_model.save()
-    
-    # 4. 训练并保存3σ模型(逐列计算统计量)
-    print("\n开始训练3σ模型...")
-    ts_model = ThreeSigmaModel()
-    ts_model.fit(normalized_data)
-    ts_model.save()
-    
-    # 5. 训练并保存 One-Class SVM 模型(可选,默认已注释)
-    # 如需启用,请取消下方注释和类定义(第166-203行)的注释
-    # print("\n开始训练One-Class SVM模型...")
-    # ocsvm_model = OneClassSVMModel()
-    # ocsvm_model.fit(normalized_data)
-    # ocsvm_model.save()
-    
-    print("\n所有模型训练和保存完成!")
-    
-    # 使用模型进行预测(示例:随机抽样 100 条)
-    sample_data = normalized_data.sample(min(100, len(normalized_data)))  # 随机取100个样本或全部样本(如果不足100个)
-    
-    # 孤立森林预测
-    if_predictions = if_model.predict(sample_data)
-    print("\n孤立森林预测结果(-1表示异常,1表示正常):")
-    print(if_predictions)
-    
-    # 3σ预测
-    ts_predictions = ts_model.predict(sample_data)
-    print("\n3σ预测结果(-1表示异常,1表示正常):")
-    print(ts_predictions)
-    
-    # One-Class SVM预测(可选,默认已注释)
-    # 如需启用,请取消下方注释
-    # ocsvm_predictions = ocsvm_model.predict(sample_data)
-    # print("\nOne-Class SVM预测结果(-1表示异常,1表示正常):")
-    # print(ocsvm_predictions)
-
-if __name__ == "__main__":
-    main()

BIN
models/anomaly_detection/isolation_forest_models.pkl


BIN
models/anomaly_detection/scaler.pkl


BIN
models/anomaly_detection/three_sigma_model.pkl


+ 0 - 60
models/causal-inference/args.py

@@ -1,60 +0,0 @@
-import torch
-import argparse
-
-def get_args():
-    parser = argparse.ArgumentParser(description='RL-Optimized GAT for time series prediction')
-    
-    # 数据参数
-    parser.add_argument('--data_dir', type=str, default='../datasets_xishan', 
-                       help='Directory for data files')
-    parser.add_argument('--num_files', type=int, default=50, 
-                       help='Number of data files (1 to num_files)')
-    parser.add_argument('--test_ratio', type=float, default=0.2, 
-                       help='Ratio of test data')
-    parser.add_argument('--val_ratio', type=float, default=0.1, 
-                       help='Ratio of validation data')
-    
-    # 模型参数
-    parser.add_argument('--num_features', type=int, default=145, 
-                       help='Number of feature variables')
-    parser.add_argument('--num_targets', type=int, default=47, 
-                       help='Number of target variables')
-    parser.add_argument('--hidden_dim', type=int, default=64, 
-                       help='Default hidden dimension of GAT')
-    parser.add_argument('--num_heads', type=int, default=4, 
-                       help='Default number of attention heads')
-    parser.add_argument('--dropout', type=float, default=0.3, 
-                       help='Default dropout rate')
-    
-    # 训练参数
-    parser.add_argument('--batch_size', type=int, default=128, 
-                       help='Batch size')
-    parser.add_argument('--lr', type=float, default=0.001, 
-                       help='Default learning rate')
-    parser.add_argument('--epochs', type=int, default=100, 
-                       help='Number of epochs for final training')
-    parser.add_argument('--weight_decay', type=float, default=1e-4, 
-                       help='Weight decay')
-    parser.add_argument('--device', type=str, default='cuda' if torch.cuda.is_available() else 'cpu',
-                       help='Device to use for training')
-    parser.add_argument('--grad_clip', type=float, default=1.0,
-                       help='Gradient clipping threshold')
-    parser.add_argument('--patience', type=int, default=20,
-                       help='Patience for early stopping')
-    
-    # 强化学习参数
-    parser.add_argument('--rl_timesteps', type=int, default=5000, 
-                       help='Total timesteps for RL training')
-    parser.add_argument('--rl_max_steps', type=int, default=20, 
-                       help='Max steps per RL episode')
-    parser.add_argument('--rl_eval_episodes', type=int, default=10, 
-                       help='Number of episodes for RL evaluation')
-    
-    # 小波去噪参数
-    parser.add_argument('--wavelet', type=str, default='db4',
-                       help='Wavelet type for denoising')
-    parser.add_argument('--wavelet_level', type=int, default=1,
-                       help='Wavelet decomposition level')
-    
-    args = parser.parse_args()
-    return args

+ 0 - 227
models/causal-inference/data_preprocessor.py

@@ -1,227 +0,0 @@
-import os
-import pandas as pd
-import numpy as np
-import pywt
-import logging
-from sklearn.preprocessing import StandardScaler
-from sklearn.model_selection import train_test_split
-import torch
-import joblib
-from torch.utils.data import TensorDataset, DataLoader
-
-class DataPreprocessor:
-    def __init__(self, args, logger=None):
-        self.args = args
-        self.data_dir = args.data_dir
-        self.num_files = args.num_files
-        self.scaler_features = StandardScaler()
-        self.scaler_targets = StandardScaler()
-        self.logger = logger if logger is not None else self._default_logger()
-        self.features = None  # 保存特征数据用于构建邻接矩阵
-        self.scaler_dir = 'scalers'
-        os.makedirs(self.scaler_dir, exist_ok=True)
-        self.features_scaler_path = os.path.join(self.scaler_dir, 'features_scaler.joblib')
-        self.targets_scaler_path = os.path.join(self.scaler_dir, 'targets_scaler.joblib')
-        
-    def _default_logger(self):
-        """默认日志记录器"""
-        logger = logging.getLogger('DataPreprocessor')
-        logger.setLevel(logging.INFO)
-        console_handler = logging.StreamHandler()
-        console_handler.setLevel(logging.INFO)
-        formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
-        console_handler.setFormatter(formatter)
-        logger.addHandler(console_handler)
-        return logger
-    
-    def load_data(self):
-        """加载所有数据文件并合并"""
-        all_data = []
-        
-        for i in range(1, self.num_files + 1):
-            file_path = os.path.join(self.data_dir, f'data_process_{i}.csv')
-            try:
-                df = pd.read_csv(file_path, index_col=0)
-                df = df.reset_index()  # 将原索引作为第一列
-                all_data.append(df)
-                self.logger.info(f"Loaded file {i}/{self.num_files}")
-            except Exception as e:
-                self.logger.error(f"Error loading file {i}: {e}")
-        
-        combined_df = pd.concat(all_data, ignore_index=True)
-        return combined_df
-    
-    def decompose_time(self, df):
-        """将时间列分解为年、月、日、时、分、秒"""
-        time_col = df.columns[0]
-        df[time_col] = pd.to_datetime(df[time_col])
-        
-        df['year'] = df[time_col].dt.year
-        df['month'] = df[time_col].dt.month
-        df['day'] = df[time_col].dt.day
-        df['hour'] = df[time_col].dt.hour
-        df['minute'] = df[time_col].dt.minute
-        df['second'] = df[time_col].dt.second
-        
-        df = df.drop(columns=[time_col])
-        
-        # 调整列顺序
-        time_features = ['year', 'month', 'day', 'hour', 'minute', 'second']
-        other_features = [col for col in df.columns if col not in time_features]
-        df = df[time_features + other_features]
-        
-        return df
-    
-    def wavelet_denoising(self, data, wavelet='db4', level=1):
-        """对数据进行小波降噪,避免除以零警告"""
-        denoised_data = np.zeros_like(data)
-        epsilon = 1e-10  # 极小值,避免除以零
-        
-        for i in range(data.shape[1]):
-            # 小波分解
-            coeffs = pywt.wavedec(data[:, i], wavelet, level=level)
-            
-            # 计算阈值时避免系数为零
-            sigma = np.median(np.abs(coeffs[-level] + epsilon)) / 0.6745  # 加epsilon
-            original_length = len(data[:, i])
-            threshold = sigma * np.sqrt(2 * np.log(original_length))
-            
-            # 对系数进行阈值处理(手动实现软阈值,避免库函数警告)
-            processed_coeffs = []
-            for c in coeffs[1:]:
-                # 手动计算软阈值:y = sign(x) * max(|x| - threshold, 0)
-                magnitude = np.abs(c)
-                # 避免除以零:给magnitude加epsilon
-                thresholded = np.where(
-                    magnitude > threshold,
-                    np.sign(c) * (magnitude - threshold),
-                    0
-                )
-                processed_coeffs.append(thresholded)
-            
-            coeffs[1:] = processed_coeffs
-            
-            # 小波重构(保持之前的长度对齐处理)
-            reconstructed = pywt.waverec(coeffs, wavelet)
-            # 补充之前的长度对齐逻辑(如果之前已添加)
-            original_length = data[:, i].shape[0]
-            if len(reconstructed) > original_length:
-                reconstructed = reconstructed[:original_length]
-            elif len(reconstructed) < original_length:
-                reconstructed = np.pad(reconstructed, (0, original_length - len(reconstructed)), mode='edge')
-            
-            denoised_data[:, i] = reconstructed
-        
-        return denoised_data
-    
-    def normalize_data(self, features, targets):
-        """归一化数据并保存scaler"""
-        features_scaled = self.scaler_features.fit_transform(features)
-        targets_scaled = self.scaler_targets.fit_transform(targets)
-        
-        # 保存scaler
-        joblib.dump(self.scaler_features, self.features_scaler_path)
-        joblib.dump(self.scaler_targets, self.targets_scaler_path)
-        self.logger.info(f"已保存特征归一化模型到 {self.features_scaler_path}")
-        self.logger.info(f"已保存目标归一化模型到 {self.targets_scaler_path}")
-        
-        return features_scaled, targets_scaled
-    
-    def inverse_transform_targets(self, targets_scaled):
-        """将归一化的目标变量反变换回原始尺度"""
-        return self.scaler_targets.inverse_transform(targets_scaled)
-    
-    def split_data(self, features, targets):
-        """划分训练集、验证集和测试集"""
-        X_train, X_temp, y_train, y_temp = train_test_split(
-            features, targets, test_size=self.args.test_ratio + self.args.val_ratio, 
-            shuffle=False
-        )
-        
-        test_size = self.args.test_ratio / (self.args.test_ratio + self.args.val_ratio)
-        X_val, X_test, y_val, y_test = train_test_split(
-            X_temp, y_temp, test_size=test_size, shuffle=False
-        )
-        
-        return X_train, X_val, X_test, y_train, y_val, y_test
-    
-    def create_dataloaders(self, X_train, X_val, X_test, y_train, y_val, y_test):
-        """创建DataLoader"""
-        X_train = torch.FloatTensor(X_train)
-        y_train = torch.FloatTensor(y_train)
-        X_val = torch.FloatTensor(X_val)
-        y_val = torch.FloatTensor(y_val)
-        X_test = torch.FloatTensor(X_test)
-        y_test = torch.FloatTensor(y_test)
-        
-        train_dataset = TensorDataset(X_train, y_train)
-        val_dataset = TensorDataset(X_val, y_val)
-        test_dataset = TensorDataset(X_test, y_test)
-        
-        train_loader = DataLoader(
-            train_dataset, batch_size=self.args.batch_size, shuffle=True
-        )
-        val_loader = DataLoader(
-            val_dataset, batch_size=self.args.batch_size, shuffle=False
-        )
-        test_loader = DataLoader(
-            test_dataset, batch_size=self.args.batch_size, shuffle=False
-        )
-        
-        return train_loader, val_loader, test_loader
-    
-    def preprocess(self):
-        """完整的预处理流程"""
-        df = self.load_data()
-        self.logger.info(f"Original data shape: {df.shape}")
-        
-        df = self.decompose_time(df)
-        self.logger.info(f"Data shape after time decomposition: {df.shape}")
-        
-        data = df.values
-        data_denoised = self.wavelet_denoising(data)
-        self.logger.info(f"Data shape after wavelet denoising: {data_denoised.shape}")
-        
-        # 保存特征数据用于构建邻接矩阵
-        self.features = data_denoised[:, :self.args.num_features]
-        targets = data_denoised[:, self.args.num_features:self.args.num_features+self.args.num_targets]
-        self.logger.info(f"Features shape: {self.features.shape}, Targets shape: {targets.shape}")
-        
-        features_scaled, targets_scaled = self.normalize_data(self.features, targets)
-        
-        X_train, X_val, X_test, y_train, y_val, y_test = self.split_data(
-            features_scaled, targets_scaled
-        )
-        self.logger.info(f"Train: {X_train.shape}, Val: {X_val.shape}, Test: {X_test.shape}")
-        
-        train_loader, val_loader, test_loader = self.create_dataloaders(
-            X_train, X_val, X_test, y_train, y_val, y_test
-        )
-        
-        return train_loader, val_loader, test_loader, self
-    
-    def create_adjacency_matrix(self):
-        """创建有向图的邻接矩阵(基于特征相关性)"""
-        num_nodes = self.args.num_features
-        adj = torch.zeros((num_nodes, num_nodes))
-        
-        if self.features is None:
-            self.logger.warning("特征数据未初始化,使用默认邻接矩阵")
-            # 默认自连接
-            for i in range(num_nodes):
-                adj[i, i] = 1
-            return adj
-        
-        # 计算特征之间的相关性
-        corr_matrix = np.corrcoef(self.features.T)
-        corr_threshold = 0.3  # 相关性阈值
-        
-        # 基于相关性构建有向边
-        for i in range(num_nodes):
-            adj[i, i] = 1  # 自连接
-            for j in range(num_nodes):
-                if i != j and abs(corr_matrix[i, j]) > corr_threshold:
-                    adj[i, j] = 1
-        
-        self.logger.info(f"邻接矩阵中边的数量: {int(torch.sum(adj))}")
-        return adj

+ 0 - 262
models/causal-inference/data_trainer.py

@@ -1,262 +0,0 @@
-import torch
-import torch.nn as nn
-import torch.optim as optim
-import numpy as np
-import matplotlib.pyplot as plt
-from tqdm import tqdm
-import os
-from sklearn.metrics import mean_absolute_error, mean_squared_error
-import logging
-
-class DataTrainer:
-    def __init__(self, model, args, preprocessor, optimizer=None, scheduler=None, logger=None):
-        self.model = model
-        self.args = args
-        self.preprocessor = preprocessor
-        self.device = args.device
-        self.logger = logger if logger is not None else self._default_logger()
-        
-        self.criterion = nn.MSELoss()
-        self.optimizer = optimizer if optimizer is not None else optim.Adam(
-            model.parameters(),
-            lr=args.lr,
-            weight_decay=args.weight_decay
-        )
-        self.scheduler = scheduler
-        
-        self.train_losses = []
-        self.val_losses = []
-        self.train_mae = []
-        self.val_mae = []
-        self.best_val_loss = float('inf')
-        self.early_stop_counter = 0
-        self.model_save_path = os.path.join('models', 'best_model.pth')
-        self.final_model_path = os.path.join('models', 'final_model.pth')
-        
-        if not os.path.exists('models'):
-            os.makedirs('models')
-        if not os.path.exists('plots'):
-            os.makedirs('plots')
-        
-    def _default_logger(self):
-        logger = logging.getLogger('DefaultLogger')
-        logger.setLevel(logging.INFO)
-        console_handler = logging.StreamHandler()
-        console_handler.setLevel(logging.INFO)
-        formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
-        console_handler.setFormatter(formatter)
-        if not logger.handlers:
-            logger.addHandler(console_handler)
-        return logger
-    
-    @torch.enable_grad()
-    def train_epoch(self, train_loader, adj, max_batches=None):
-        """
-        支持通过 max_batches 限制本 epoch 使用的批次数(用于 RL 快速评估)。
-        """
-        self.model.train()
-        total_loss = 0.0
-        all_outputs = []
-        all_targets = []
-        adj = adj.to(self.device)
-
-        for batch_idx, (data, target) in enumerate(train_loader):
-            if max_batches is not None and batch_idx >= max_batches:
-                break
-            data, target = data.to(self.device), target.to(self.device)
-            data = data.unsqueeze(-1)  # (batch_size, 145, 1)
-            
-            self.optimizer.zero_grad()
-            output = self.model(data, adj)  # (batch_size, 145, 47)
-            output = output.mean(dim=1)     # (batch_size, 47)
-            
-            loss = self.criterion(output, target)
-            loss.backward()
-            if self.args.grad_clip > 0:
-                torch.nn.utils.clip_grad_norm_(self.model.parameters(), self.args.grad_clip)
-            self.optimizer.step()
-            
-            total_loss += loss.item()
-            all_outputs.append(output.detach().cpu().numpy())
-            all_targets.append(target.detach().cpu().numpy())
-        
-        if len(all_outputs) == 0:
-            # 在极小数据/极小 batch 情况下的保护
-            return float('inf'), float('inf')
-        
-        all_outputs = np.vstack(all_outputs)
-        all_targets = np.vstack(all_targets)
-        mae = mean_absolute_error(all_targets, all_outputs)
-        
-        avg_loss = total_loss / (min(len(train_loader), max_batches) if max_batches else len(train_loader))
-        self.train_losses.append(avg_loss)
-        self.train_mae.append(mae)
-        return avg_loss, mae
-    
-    @torch.no_grad()
-    def validate(self, val_loader, adj, max_batches=None):
-        """
-        支持通过 max_batches 限制验证批次数(用于 RL 快速评估)。
-        """
-        self.model.eval()
-        total_loss = 0.0
-        all_outputs = []
-        all_targets = []
-        adj = adj.to(self.device)
-        
-        for batch_idx, (data, target) in enumerate(val_loader):
-            if max_batches is not None and batch_idx >= max_batches:
-                break
-            data, target = data.to(self.device), target.to(self.device)
-            data = data.unsqueeze(-1)  # (batch_size, 145, 1)
-            output = self.model(data, adj)  # (batch_size, 145, 47)
-            output = output.mean(dim=1)     # (batch_size, 47)
-            loss = self.criterion(output, target)
-            total_loss += loss.item()
-            all_outputs.append(output.cpu().numpy())
-            all_targets.append(target.cpu().numpy())
-        
-        if len(all_outputs) == 0:
-            return float('inf'), float('inf')
-        
-        all_outputs = np.vstack(all_outputs)
-        all_targets = np.vstack(all_targets)
-        mae = mean_absolute_error(all_targets, all_outputs)
-        avg_loss = total_loss / (min(len(val_loader), max_batches) if max_batches else len(val_loader))
-        self.val_losses.append(avg_loss)
-        self.val_mae.append(mae)
-        return avg_loss, mae
-    
-    def train(self, train_loader, val_loader, adj, epochs=None):
-        adj = adj.to(self.device)
-        epochs = epochs if epochs is not None else self.args.epochs
-        self.logger.info(f"开始训练,共 {epochs} 个epoch")
-        
-        for epoch in tqdm(range(epochs)):
-            train_loss, train_mae = self.train_epoch(train_loader, adj)
-            val_loss, val_mae = self.validate(val_loader, adj)
-            if self.scheduler is not None:
-                self.scheduler.step(val_loss)
-            
-            # 保存最佳模型
-            if val_loss < self.best_val_loss:
-                self.best_val_loss = val_loss
-                torch.save({
-                    'epoch': epoch,
-                    'model_state_dict': self.model.state_dict(),
-                    'optimizer_state_dict': self.optimizer.state_dict(),
-                    'loss': val_loss,
-                    'args': self.args  # 保存训练参数
-                }, self.model_save_path)
-                self.early_stop_counter = 0
-                self.logger.info(f"已更新最佳模型到 {self.model_save_path}")  # 日志
-            else:
-                self.early_stop_counter += 1
-                if self.early_stop_counter >= self.args.patience:
-                    self.logger.info(f"早停机制触发,在第 {epoch+1} 轮停止训练")
-                    break
-            
-            if (epoch + 1) % 10 == 0:
-                self.logger.info(f'Epoch {epoch+1}/{epochs}, '
-                                 f'Train Loss: {train_loss:.6f}, Train MAE: {train_mae:.6f}, '
-                                 f'Val Loss: {val_loss:.6f}, Val MAE: {val_mae:.6f}')
-        
-        self.plot_losses()
-        self.plot_mae()
-        
-        # 加载最佳模型
-        checkpoint = torch.load(self.model_save_path, map_location=self.device)
-        self.model.load_state_dict(checkpoint['model_state_dict'])
-        self.logger.info(f"加载最佳模型(第 {checkpoint['epoch']+1} 轮,验证损失: {checkpoint['loss']:.6f})")
-        
-        # 保存最终训练完成的模型(加载最佳模型后)
-        torch.save({
-            'model_state_dict': self.model.state_dict(),
-            'optimizer_state_dict': self.optimizer.state_dict(),
-            'best_val_loss': self.best_val_loss,
-            'args': self.args
-        }, self.final_model_path)
-        self.logger.info(f"已保存最终模型到 {self.final_model_path}")
-        
-        return self.model
-    
-    @torch.no_grad()
-    def test(self, test_loader, adj):
-        self.model.eval()
-        total_loss = 0.0
-        all_outputs = []
-        all_targets = []
-        adj = adj.to(self.device)
-        
-        for data, target in test_loader:
-            data, target = data.to(self.device), target.to(self.device)
-            data = data.unsqueeze(-1)
-            output = self.model(data, adj)
-            output = output.mean(dim=1)
-            loss = self.criterion(output, target)
-            total_loss += loss.item()
-            all_outputs.append(output.cpu().numpy())
-            all_targets.append(target.cpu().numpy())
-        
-        all_outputs = np.vstack(all_outputs)
-        all_targets = np.vstack(all_targets)
-        mse = total_loss / len(test_loader)
-        mae = mean_absolute_error(all_targets, all_outputs)
-        rmse = np.sqrt(mean_squared_error(all_targets, all_outputs))
-        
-        all_outputs_original = self.preprocessor.inverse_transform_targets(all_outputs)
-        all_targets_original = self.preprocessor.inverse_transform_targets(all_targets)
-        original_mse = mean_squared_error(all_targets_original, all_outputs_original)
-        original_mae = mean_absolute_error(all_targets_original, all_outputs_original)
-        original_rmse = np.sqrt(original_mse)
-        
-        self.logger.info(f'Test Loss (normalized): MSE={mse:.6f}, MAE={mae:.6f}, RMSE={rmse:.6f}')
-        self.logger.info(f'Test Loss (original scale): MSE={original_mse:.6f}, MAE={original_mae:.6f}, RMSE={original_rmse:.6f}')
-        self.plot_predictions(all_outputs_original, all_targets_original)
-        return {
-            'normalized_mse': mse,
-            'normalized_mae': mae,
-            'normalized_rmse': rmse,
-            'original_mse': original_mse,
-            'original_mae': original_mae,
-            'original_rmse': original_rmse,
-            'predictions': all_outputs_original,
-            'targets': all_targets_original
-        }
-    
-    def plot_losses(self):
-        plt.figure(figsize=(10, 6))
-        plt.plot(self.train_losses, label='Train Loss')
-        plt.plot(self.val_losses, label='Validation Loss')
-        plt.xlabel('Epoch')
-        plt.ylabel('MSE Loss')
-        plt.title('Training and Validation Loss')
-        plt.legend()
-        plt.savefig('plots/loss_curve.png')
-        plt.close()
-    
-    def plot_mae(self):
-        plt.figure(figsize=(10, 6))
-        plt.plot(self.train_mae, label='Train MAE')
-        plt.plot(self.val_mae, label='Validation MAE')
-        plt.xlabel('Epoch')
-        plt.ylabel('MAE')
-        plt.title('Training and Validation MAE')
-        plt.legend()
-        plt.savefig('plots/mae_curve.png')
-        plt.close()
-    
-    def plot_predictions(self, predictions, targets):
-        num_plots = min(3, self.args.num_targets)
-        plt.figure(figsize=(15, 5*num_plots))
-        for i in range(num_plots):
-            plt.subplot(num_plots, 1, i+1)
-            plt.plot(targets[:100, i], label='True Value')
-            plt.plot(predictions[:100, i], label='Predicted Value')
-            plt.xlabel('Time Step')
-            plt.ylabel(f'Target {i+1}')
-            plt.title(f'Prediction vs True Value for Target {i+1}')
-            plt.legend()
-        plt.tight_layout()
-        plt.savefig('plots/prediction_examples.png')
-        plt.close()

BIN
models/causal-inference/features_scaler.joblib


+ 0 - 226
models/causal-inference/gat.py

@@ -1,226 +0,0 @@
-"""
-有向图注意力网络 (Directed Graph Attention Network)
-
-实现基于有向图的注意力机制,用于建模节点间的非对称因果关系。
-与传统GAT不同,本实现分离源节点和目标节点的注意力参数,更适合因果推理任务。
-
-核心特性:
-    - 有向注意力: 源节点和目标节点使用独立的注意力参数
-    - 多头机制: 并行学习多种关系模式
-    - 邻接掩码: 仅在图中存在的边上计算注意力
-
-技术实现:
-    - 框架: PyTorch
-    - 注意力: 加性注意力 (Additive Attention)
-    - 激活函数: LeakyReLU (α=0.2)
-"""
-
-import torch
-import torch.nn as nn
-import torch.nn.functional as F
-
-class GraphAttentionLayer(nn.Module):
-    """
-    有向图注意力层 (Directed Graph Attention Layer)
-
-    实现单层有向图注意力机制,是GAT模型的基本构建块。
-    通过分离源节点和目标节点的注意力参数,支持建模非对称的因果关系。
-
-    核心思想:
-        传统GAT使用对称注意力权重 (A→B 和 B→A 权重相同)
-        有向GAT分离源节点和目标节点参数,学习方向性的因果影响
-
-    注意力计算:
-        e_ij = LeakyReLU(a_src^T·Wh_i + a_dst^T·Wh_j)
-        α_ij = softmax_j(e_ij)
-        h_i' = σ(Σ_j α_ij·Wh_j)
-
-    Args:
-        in_features (int): 输入特征维度
-        out_features (int): 输出特征维度
-        dropout (float): Dropout概率 [0,1]
-        alpha (float): LeakyReLU负斜率,默认0.2
-        concat (bool): 是否使用ELU激活 (True用于中间层,False用于输出层)
-
-    Example:
-        >>> layer = GraphAttentionLayer(1, 64, dropout=0.3, alpha=0.2, concat=True)
-        >>> h = torch.randn(32, 145, 1)  # (batch, nodes, features)
-        >>> adj = torch.ones(145, 145)   # 邻接矩阵
-        >>> output = layer(h, adj)       # (32, 145, 64)
-    """
-    def __init__(self, in_features, out_features, dropout, alpha, concat=True):
-        super(GraphAttentionLayer, self).__init__()
-        self.dropout = dropout
-        self.in_features = in_features
-        self.out_features = out_features
-        self.alpha = alpha
-        self.concat = concat
-        
-        # 特征变换矩阵: 输入特征 → 输出特征空间
-        # Xavier初始化保证前向/反向传播时方差稳定
-        self.W = nn.Parameter(torch.empty(size=(in_features, out_features)))
-        nn.init.xavier_uniform_(self.W.data, gain=1.414)  # gain=1.414 适配LeakyReLU
-
-        # 有向注意力参数 (核心创新)
-        # a_src: 源节点注意力向量 (发出边的权重)
-        # a_dst: 目标节点注意力向量 (接收边的权重)
-        # 分离参数使模型能够学习非对称因果关系
-        self.a_src = nn.Parameter(torch.empty(size=(out_features, 1)))
-        self.a_dst = nn.Parameter(torch.empty(size=(out_features, 1)))
-        nn.init.xavier_uniform_(self.a_src.data, gain=1.414)
-        nn.init.xavier_uniform_(self.a_dst.data, gain=1.414)
-
-        # LeakyReLU: 允许负值有小梯度,防止神经元死亡
-        self.leakyrelu = nn.LeakyReLU(self.alpha)
-        
-    def forward(self, h, adj):
-        """
-        前向传播
-
-        计算流程:
-            1. 线性变换: Wh = h @ W
-            2. 计算源/目标节点注意力分数
-            3. 构建注意力矩阵: e_ij = LeakyReLU(src_i + dst_j)
-            4. 应用邻接掩码 (不存在的边设为-∞)
-            5. Softmax归一化得到注意力权重 α_ij
-            6. 加权聚合邻居特征: h_i' = Σ_j α_ij·Wh_j
-
-        Args:
-            h (Tensor): 输入特征 (batch_size, num_nodes, in_features)
-                例: (32, 145, 1)
-            adj (Tensor): 邻接矩阵 (num_nodes, num_nodes)
-                adj[i,j]=1 表示节点i→j存在有向边
-
-        Returns:
-            Tensor: 输出特征 (batch_size, num_nodes, out_features)
-                经过图注意力聚合后的节点特征
-        """
-        batch_size = h.size(0) 
-        num_nodes = h.size(1)
-
-        # Step 1: 线性变换 (batch, nodes, in_features) → (batch, nodes, out_features)
-        Wh = torch.matmul(h, self.W)
-
-        # Step 2-3: 计算源/目标节点注意力分数
-        a_input_src = torch.matmul(Wh, self.a_src)  # 源节点分数 (信息发送方)
-        a_input_dst = torch.matmul(Wh, self.a_dst)  # 目标节点分数 (信息接收方)
-
-        # Step 4: 构建注意力矩阵 (广播: src[i] + dst[j]^T)
-        # e[i,j] = a_src^T·Wh_i + a_dst^T·Wh_j
-        e = a_input_src + a_input_dst.transpose(1, 2)
-        e = self.leakyrelu(e)
-
-        # Step 5: 应用邻接掩码 (不存在的边设为-9e15,softmax后≈0)
-        zero_vec = -9e15 * torch.ones_like(e)
-        attention = torch.where(adj > 0, e, zero_vec)
-
-        # Step 6: Softmax归一化 (dim=2: 对每个节点的所有邻居归一化)
-        attention = F.softmax(attention, dim=2)
-        attention = F.dropout(attention, self.dropout, training=self.training)
-
-        # Step 7: 加权聚合邻居特征 h_i' = Σ_j α_ij·Wh_j
-        h_prime = torch.matmul(attention, Wh)
-
-        # 中间层使用ELU激活,输出层保持线性
-        if self.concat:
-            return F.elu(h_prime)
-        else:
-            return h_prime
-        
-    def __repr__(self):
-        return self.__class__.__name__ + f'({self.in_features} -> {self.out_features})'
-
-class GAT(nn.Module):
-    """
-    多层图注意力网络 (Multi-layer Graph Attention Network)
-
-    组合多个图注意力层构建完整的GAT模型,采用多头注意力机制从不同视角捕捉节点关系。
-
-    网络结构:
-        输入 → 多头注意力层 (nheads个并行) → 拼接 → 输出注意力层 → 输出
-
-    Args:
-        nfeat (int): 输入特征维度,例: 1
-        nhid (int): 隐藏层维度 (每个注意力头的输出维度),推荐: 32-128
-        noutput (int): 输出维度 (目标变量数量),例: 47
-        dropout (float): Dropout概率 [0,1],例: 0.3
-        alpha (float): LeakyReLU负斜率,例: 0.2
-        nheads (int): 注意力头数量,例: 4
-
-    多头注意力机制:
-        多个独立注意力头并行学习不同关系模式 (直接因果、间接影响、周期性等)
-        最后拼接所有头的输出,形成丰富的特征表示
-
-    维度变化:
-        (batch, 145, 1) → [多头] → (batch, 145, nhid×nheads)
-        → [输出层] → (batch, 145, noutput)
-
-    Example:
-        >>> model = GAT(nfeat=1, nhid=64, noutput=47, dropout=0.3, alpha=0.2, nheads=4)
-        >>> x = torch.randn(32, 145, 1)
-        >>> adj = torch.ones(145, 145)
-        >>> output = model(x, adj)  # (32, 145, 47)
-    """
-    def __init__(self, nfeat, nhid, noutput, dropout, alpha, nheads):
-        super(GAT, self).__init__()
-        self.dropout = dropout
-        
-        # 多头注意力层: 创建 nheads 个独立的图注意力层
-        self.attentions = [
-            GraphAttentionLayer(
-                in_features=nfeat,
-                out_features=nhid,
-                dropout=dropout,
-                alpha=alpha,
-                concat=True  # 中间层使用ELU激活
-            )
-            for _ in range(nheads)
-        ]
-
-        # 注册为子模块,使参数可被自动追踪和优化
-        for i, attention in enumerate(self.attentions):
-            self.add_module(f'attention_{i}', attention)
-
-        # 输出注意力层: 输入维度 = nhid×nheads (拼接后)
-        self.out_att = GraphAttentionLayer(
-            in_features=nhid * nheads,
-            out_features=noutput,
-            dropout=dropout,
-            alpha=alpha,
-            concat=False  # 输出层保持线性
-        )
-        
-    def forward(self, x, adj):
-        """
-        前向传播
-
-        计算流程:
-            1. 输入dropout
-            2. 多头注意力并行计算并拼接
-            3. 中间dropout
-            4. 输出层 + ELU激活
-
-        Args:
-            x (Tensor): 输入特征 (batch_size, num_nodes, nfeat)
-                例: (32, 145, 1)
-            adj (Tensor): 邻接矩阵 (num_nodes, num_nodes)
-                adj[i,j]=1 表示特征i对特征j有因果影响
-
-        Returns:
-            Tensor: 输出特征 (batch_size, num_nodes, noutput)
-                例: (32, 145, 47)
-        """
-        # 输入dropout (防止过拟合)
-        x = F.dropout(x, self.dropout, training=self.training)
-
-        # 多头注意力并行计算 + 拼接
-        # (batch, nodes, nfeat) → nheads × (batch, nodes, nhid) → (batch, nodes, nhid×nheads)
-        x = torch.cat([att(x, adj) for att in self.attentions], dim=2)
-
-        # 中间dropout
-        x = F.dropout(x, self.dropout, training=self.training)
-
-        # 输出层 + ELU激活
-        x = F.elu(self.out_att(x, adj))
-
-        return x

+ 0 - 313
models/causal-inference/main.py

@@ -1,313 +0,0 @@
-"""
-因果推理模型主程序(Causal Inference Main Program)
-
-本程序实现了基于强化学习优化的图注意力网络训练流程,用于工业时间序列预测。
-整个系统分为三个核心阶段:
-    1. 数据预处理阶段: 数据加载、清洗、降噪、归一化、图构建
-    2. RL超参数优化阶段: 使用PPO算法自动搜索最优超参数
-    3. 最终训练评估阶段: 使用最优参数训练模型并在测试集上评估
-
-核心特点:
-    - 自动化超参数优化: 无需手动调参,RL智能体自动寻找最优配置
-    - 有向图注意力: 建模特征间的因果关系,支持非对称影响
-    - 小波降噪预处理: 提升数据质量,增强模型精度
-    - 完善的监控机制: 日志记录、早停、学习率调度、模型保存
-
-技术栈:
-    - PyTorch: 深度学习框架
-    - Stable-Baselines3: 强化学习库(PPO算法)
-    - PyWavelets: 小波变换库
-    - Scikit-learn: 数据预处理
-
-工作流程:
-    main() → 数据预处理 → RL优化超参数 → 训练最终模型 → 测试评估
-
-"""
-
-import torch.optim as optim
-from args import get_args
-from data_preprocessor import DataPreprocessor
-from gat import GAT
-from data_trainer import DataTrainer
-from rl_optimizer import RLOptimizer
-import logging
-import os
-
-def setup_logger(args):
-    """
-    配置日志系统
-    
-    功能:
-        创建并配置日志记录器,同时输出到控制台和文件。
-        日志文件以训练数据文件数量命名,便于区分不同实验。
-    
-    参数:
-        args: 命令行参数对象
-            - args.num_files: 数据文件数量,用于日志文件命名
-    
-    返回:
-        logging.Logger: 配置好的日志记录器
-    
-    日志级别:
-        INFO: 记录关键步骤和指标信息
-        
-    输出位置:
-        - 控制台: 实时查看训练进度
-        - 文件: logs/training_{num_files}.log,便于事后分析
-    
-    日志格式:
-        时间戳 - 记录器名称 - 日志级别 - 消息内容
-        示例: 2025-01-10 10:30:45 - GAT-Training - INFO - 开始训练
-    
-    技术要点:
-        - 自动创建logs目录
-        - 文件和控制台使用相同的格式化器
-        - 避免重复添加处理器
-    """
-    # 创建日志目录(如果不存在)
-    if not os.path.exists('logs'):
-        os.makedirs('logs')
-    
-    # 创建日志记录器
-    logger = logging.getLogger('GAT-Training')
-    logger.setLevel(logging.INFO)
-    
-    # 文件处理器: 将日志写入文件
-    file_handler = logging.FileHandler(f'logs/training_{args.num_files}.log')
-    file_handler.setLevel(logging.INFO) 
-    
-    # 控制台处理器: 将日志输出到终端
-    console_handler = logging.StreamHandler()
-    console_handler.setLevel(logging.INFO)
-    
-    # 格式化器: 定义日志消息的格式
-    formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
-    file_handler.setFormatter(formatter)    
-    console_handler.setFormatter(formatter)
-    
-    # 添加处理器到记录器
-    logger.addHandler(file_handler) # 添加文件处理器
-    logger.addHandler(console_handler) # 添加控制台处理器
-    
-    return logger
-
-def main():
-    """
-    主程序入口
-    
-    功能:
-        协调整个训练流程,包括数据预处理、RL优化、模型训练和测试评估。
-        这是整个系统的控制中心,按顺序执行各个阶段的任务。
-    
-    执行流程:
-        第一阶段: 数据预处理
-            1. 加载50个CSV数据文件
-            2. 时间特征分解(年月日时分秒)
-            3. 小波降噪(db4小波,1层分解)
-            4. 数据归一化(StandardScaler)
-            5. 划分训练集/验证集/测试集(70%/10%/20%)
-            6. 构建有向图邻接矩阵(相关性阈值0.3)
-            
-        第二阶段: RL超参数优化
-            1. 创建GATEnv强化学习环境
-            2. 使用PPO算法训练5000时间步
-            3. 搜索最优超参数(lr, hidden_dim, num_heads, dropout)
-            4. 快速评估策略(1-2个batch)加速收敛
-            5. 选择奖励最高的超参数组合
-            
-        第三阶段: 最终模型训练
-            1. 使用最优超参数创建GAT模型
-            2. 配置Adam优化器和学习率调度器
-            3. 训练最多100轮,早停耐心20轮
-            4. 保存最佳模型和最终模型
-            5. 生成训练曲线图
-            
-        第四阶段: 测试评估
-            1. 加载最佳模型
-            2. 在测试集上评估性能
-            3. 计算归一化和原始尺度的MSE/MAE/RMSE
-            4. 生成预测对比图
-    
-    输出文件:
-        日志文件:
-            - logs/training_{num_files}.log
-        
-        归一化器:
-            - scalers/features_scaler.joblib
-            - scalers/targets_scaler.joblib
-        
-        模型文件:
-            - models/best_model.pth (验证损失最低的模型)
-            - models/final_model.pth (训练完成后的最终模型)
-            - gat_ppo_agent (RL优化器模型)
-        
-        可视化图表:
-            - plots/loss_curve.png (训练/验证损失曲线)
-            - plots/mae_curve.png (训练/验证MAE曲线)
-            - plots/prediction_examples.png (预测vs真实值对比)
-    
-    关键技术:
-        1. RL自动调参: 避免手动网格搜索,智能寻优
-        2. 有向图建模: 捕捉特征间的因果关系
-        3. 小波降噪: 提升数据质量
-        4. 早停机制: 防止过拟合
-        5. 学习率调度: 自适应调整学习率
-    
-    性能优化:
-        - GPU加速: 自动检测并使用CUDA
-        - 梯度裁剪: 防止梯度爆炸
-        - Dropout正则化: 防止过拟合
-        - ReduceLROnPlateau: 验证损失停滞时降低学习率
-    
-    使用示例:
-        >>> python main.py
-        # 使用默认参数训练
-        
-        >>> python main.py --num_files 30 --epochs 50
-        # 自定义参数训练
-    """
-    # ========== 阶段0: 初始化配置 ==========
-    # 获取命令行参数(或使用默认值)
-    args = get_args()
-    
-    # 配置日志系统
-    logger = setup_logger(args)
-    logger.info(f"使用设备: {args.device}")
-    logger.info("=" * 80)
-    logger.info("因果推理模型训练系统启动")
-    logger.info("=" * 80)
-    
-    # ========== 阶段1: 数据预处理 ==========
-    logger.info("\n" + "=" * 80)
-    logger.info("阶段1: 数据预处理")
-    logger.info("=" * 80)
-    
-    # 创建数据预处理器
-    preprocessor = DataPreprocessor(args, logger)
-    
-    # 执行完整的预处理流程
-    # 返回: train_loader(训练数据加载器), val_loader(验证数据加载器), 
-    #       test_loader(测试数据加载器), preprocessor(预处理器对象)
-    train_loader, val_loader, test_loader, preprocessor = preprocessor.preprocess()
-    logger.info("数据预处理完成!")
-    
-    # 创建有向图邻接矩阵
-    # 基于特征相关性构建图结构,相关性>0.3的特征对之间建立有向边
-    adj = preprocessor.create_adjacency_matrix()
-    logger.info(f"邻接矩阵形状: {adj.shape}")
-    logger.info(f"边的数量: {int(adj.sum())}")
-    
-    # ========== 阶段2: RL超参数优化 ==========
-    logger.info("\n" + "=" * 80)
-    logger.info("阶段2: 强化学习超参数优化")
-    logger.info("=" * 80)
-    logger.info("使用PPO算法搜索最优超参数...")
-    
-    # 创建RL优化器
-    # 在环境中评估不同的超参数组合,找到使验证损失最小的配置
-    rl_optimizer = RLOptimizer(args, preprocessor, train_loader, val_loader, adj, logger)
-    
-    # 执行优化,返回最优超参数字典
-    # best_hparams包含: lr(学习率), hidden_dim(隐藏层维度), 
-    #                   num_heads(注意力头数), dropout(dropout率)
-    best_hparams = rl_optimizer.optimize()
-    logger.info(f"最优超参数: {best_hparams}")
-    
-    # ========== 阶段3: 使用最优超参数训练最终模型 ==========
-    logger.info("\n" + "=" * 80)
-    logger.info("阶段3: 训练最终模型")
-    logger.info("=" * 80)
-    logger.info("使用RL优化得到的最优超参数...")
-    
-    # 创建GAT模型,使用最优超参数
-    final_model = GAT(
-        nfeat=1,                          # 输入特征维度(每个节点1维)
-        nhid=best_hparams['hidden_dim'],  # 隐藏层维度(RL优化得到)
-        noutput=args.num_targets,         # 输出维度(47个目标变量)
-        dropout=best_hparams['dropout'],  # Dropout率(RL优化得到)
-        nheads=best_hparams['num_heads'], # 注意力头数(RL优化得到)
-        alpha=0.2                         # LeakyReLU斜率(固定值)
-    ).to(args.device)  # 移动到GPU(如果可用)
-    
-    logger.info(f"模型结构: nfeat=1, nhid={best_hparams['hidden_dim']}, "
-                f"noutput={args.num_targets}, dropout={best_hparams['dropout']}, "
-                f"nheads={best_hparams['num_heads']}")
-    
-    # 配置优化器
-    # Adam优化器: 自适应学习率,使用RL优化得到的学习率
-    optimizer = optim.Adam(
-        final_model.parameters(),
-        lr=best_hparams['lr'],           # 学习率(RL优化得到)
-        weight_decay=args.weight_decay   # L2正则化系数
-    )
-    logger.info(f"优化器: Adam(lr={best_hparams['lr']}, weight_decay={args.weight_decay})")
-    
-    # 配置学习率调度器
-    # ReduceLROnPlateau: 当验证损失停滞时,将学习率降低一半
-    scheduler = optim.lr_scheduler.ReduceLROnPlateau(
-        optimizer, 
-        mode='min',      # 监控指标越小越好(损失函数)
-        factor=0.5,      # 降低因子(新lr = 旧lr * 0.5)
-        patience=10,     # 容忍10轮无改善
-        verbose=True     # 打印学习率变化信息
-    )
-    logger.info("学习率调度器: ReduceLROnPlateau(factor=0.5, patience=10)")
-    
-    # 创建训练器
-    # 负责模型训练、验证、测试和可视化
-    trainer = DataTrainer(
-        model=final_model,
-        args=args,
-        preprocessor=preprocessor,
-        optimizer=optimizer,
-        scheduler=scheduler,
-        logger=logger
-    )
-    
-    # 执行训练
-    # 训练最多100轮,使用早停机制(耐心20轮)
-    # 自动保存最佳模型(验证损失最低)和最终模型
-    logger.info("开始训练循环...")
-    trained_model = trainer.train(train_loader, val_loader, adj)
-    logger.info("模型训练完成!")
-    
-    # ========== 阶段4: 在测试集上评估 ==========
-    logger.info("\n" + "=" * 80)
-    logger.info("阶段4: 测试集评估")
-    logger.info("=" * 80)
-    logger.info("在测试集上评估最终模型性能...")
-    
-    # 测试模型性能
-    # 返回归一化和原始尺度的MSE/MAE/RMSE指标
-    test_results = trainer.test(test_loader, adj)
-    
-    # 打印最终结果摘要
-    logger.info("\n" + "=" * 80)
-    logger.info("训练完成总结")
-    logger.info("=" * 80)
-    logger.info(f"最优超参数: {best_hparams}")
-    logger.info(f"测试集性能(归一化):")
-    logger.info(f"  - MSE:  {test_results['normalized_mse']:.6f}")
-    logger.info(f"  - MAE:  {test_results['normalized_mae']:.6f}")
-    logger.info(f"  - RMSE: {test_results['normalized_rmse']:.6f}")
-    logger.info(f"测试集性能(原始尺度):")
-    logger.info(f"  - MSE:  {test_results['original_mse']:.6f}")
-    logger.info(f"  - MAE:  {test_results['original_mae']:.6f}")
-    logger.info(f"  - RMSE: {test_results['original_rmse']:.6f}")
-    logger.info("=" * 80)
-    logger.info("所有任务完成!")
-    logger.info("=" * 80)
-
-if __name__ == "__main__":
-    """
-    程序入口点
-    
-    直接运行此文件时执行main()函数。
-    支持命令行参数自定义配置,详见args.py。
-    
-    运行方式:
-        python main.py                    # 使用默认参数
-        python main.py --epochs 50        # 自定义训练轮数
-        python main.py --num_files 30     # 自定义数据文件数量
-    """
-    main()

BIN
models/causal-inference/policy.optimizer.pth


BIN
models/causal-inference/policy.pth


+ 0 - 188
models/causal-inference/rl_optimizer.py

@@ -1,188 +0,0 @@
-import torch
-import numpy as np
-import gymnasium as gym
-from gymnasium import spaces
-from stable_baselines3 import PPO
-from stable_baselines3.common.callbacks import BaseCallback
-import torch.optim as optim
-from gat import GAT
-from data_trainer import DataTrainer
-
-class GATEnv(gym.Env):
-    metadata = {'render_modes': ['human'], 'render_fps': 4}
-    
-    def __init__(self, preprocessor, train_loader, val_loader, adj, args, logger):
-        super().__init__()
-        self.preprocessor = preprocessor
-        self.train_loader = train_loader
-        self.val_loader = val_loader
-        # 使用指定设备(支持GPU)
-        self.eval_device = torch.device(args.device)
-        self.adj = adj.to(self.eval_device)
-        self.args = args
-        self.logger = logger
-        
-        self.action_space = spaces.Box(
-            low=np.array([1e-5, 32, 2, 0.1], dtype=np.float32),
-            high=np.array([1e-2, 128, 8, 0.5], dtype=np.float32),
-            shape=(4,),
-            dtype=np.float32
-        )
-        self.observation_space = spaces.Box(
-            low=np.array([1e-5, 32, 2, 0.1, 0], dtype=np.float32),
-            high=np.array([1e-2, 128, 8, 0.5, 100], dtype=np.float32),
-            shape=(5,),
-            dtype=np.float32
-        )
-        self.best_val_loss = float('inf')
-        self.current_step = 0
-        self.max_steps = args.rl_max_steps
-        self.render_mode = None
-    
-    def reset(self, seed=None, options=None):
-        super().reset(seed=seed)
-        self.current_step = 0
-        self.best_val_loss = float('inf')
-        self.current_state = np.array([
-            float(self.args.lr),
-            float(self.args.hidden_dim),
-            float(self.args.num_heads),
-            float(self.args.dropout),
-            10.0
-        ], dtype=np.float32)
-        return self.current_state, {}
-    
-    def step(self, action):
-        self.current_step += 1
-        lr = float(action[0])
-        hidden_dim = int(round(float(action[1])))
-        num_heads = int(round(float(action[2])))
-        dropout = float(action[3])
-        
-        hidden_dim = max(32, min(128, hidden_dim))
-        num_heads = max(2, min(8, num_heads))
-        dropout = max(0.1, min(0.5, dropout))
-        
-        # 在指定设备上构建与评估(支持GPU)
-        model = GAT(
-            nfeat=1,
-            nhid=hidden_dim,
-            noutput=self.args.num_targets,
-            dropout=dropout,
-            nheads=num_heads,
-            alpha=0.2
-        ).to(self.eval_device)
-        optimizer = optim.Adam(model.parameters(), lr=lr, weight_decay=self.args.weight_decay)
-        
-        # 使用指定设备
-        rl_args = self.args
-        trainer = DataTrainer(model, rl_args, self.preprocessor, optimizer, logger=self.logger)
-        val_loss = self._short_evaluate(trainer)
-        
-        reward = 1.0 / (1.0 + val_loss)
-        if val_loss < self.best_val_loss:
-            reward += 0.5
-            self.best_val_loss = val_loss
-        
-        self.current_state = np.array([lr, hidden_dim, num_heads, dropout, val_loss], dtype=np.float32)
-        terminated = self.current_step >= self.max_steps
-        truncated = False
-        return self.current_state, float(reward), terminated, truncated, {}
-    
-    def _short_evaluate(self, trainer):
-        """
-        关键加速:只用极少 batch 做快速近似,保证一个 env.step() 在毫秒到秒级完成。
-        """
-        # 训练 1 个 batch、重复 2 次以产生可用梯度信号
-        for _ in range(2):
-            trainer.train_epoch(self.train_loader, self.adj, max_batches=1)
-        # 验证用 2 个 batch,降低方差
-        val_loss, _ = trainer.validate(self.val_loader, self.adj, max_batches=2)
-        return float(val_loss)
-    
-    def render(self):
-        if self.render_mode == 'human':
-            print(f"[RL] Step: {self.current_step}, Best Val Loss: {self.best_val_loss:.6f}")
-    
-    def close(self):
-        pass
-
-class TrainingCallback(BaseCallback):
-    def __init__(self, verbose=0, print_every=100):
-        super().__init__(verbose)
-        self.print_every = print_every
-    
-    def _on_step(self) -> bool:
-        # BaseCallback.logger 不是 logging.Logger;用 print 或 record。
-        if self.n_calls % self.print_every == 0:
-            # 某些版本下 self.locals 里没有 'rewards' 键,做个健壮保护
-            rew = None
-            try:
-                r = self.locals.get('rewards', None)
-                if r is not None:
-                    rew = float(r[0])
-            except Exception:
-                pass
-            print(f"[RL] timesteps={self.num_timesteps} calls={self.n_calls} reward={rew}")
-        return True
-
-class RLOptimizer:
-    def __init__(self, args, preprocessor, train_loader, val_loader, adj, logger):
-        self.args = args
-        self.preprocessor = preprocessor
-        self.train_loader = train_loader
-        self.val_loader = val_loader
-        self.adj = adj
-        self.logger = logger
-    
-    def optimize(self):
-        env = GATEnv(
-            self.preprocessor, self.train_loader, self.val_loader,
-            self.adj, self.args, self.logger
-        )
-        
-        # 关键:将 PPO rollout 和训练配置调小,避免一次 rollout 等太久
-        model = PPO(
-            "MlpPolicy",
-            env,
-            verbose=1,
-            learning_rate=3e-4,
-            n_steps=32,       # 原来 2048 -> 32
-            batch_size=32,    # 原来 64 -> 32
-            n_epochs=1,       # 原来 10 -> 1
-            gamma=0.99,
-            gae_lambda=0.95,
-            clip_range=0.2,
-            ent_coef=0.01,
-            device=self.args.device  # 使用指定设备(支持GPU)
-        )
-        
-        self.logger.info("开始训练强化学习智能体...")
-        callback = TrainingCallback(verbose=1, print_every=100)
-        model.learn(total_timesteps=self.args.rl_timesteps, callback=callback)
-        model.save("gat_ppo_agent")
-        
-        # 评估并选最优动作
-        self.logger.info("寻找最优超参数组合...")
-        best_reward = -1.0
-        best_action = None
-        eval_env = GATEnv(
-            self.preprocessor, self.train_loader, self.val_loader,
-            self.adj, self.args, self.logger
-        )
-        for _ in range(self.args.rl_eval_episodes):
-            obs, _ = eval_env.reset()
-            action, _ = model.predict(obs, deterministic=True)
-            _, reward, _, _, _ = eval_env.step(action)
-            if reward > best_reward:
-                best_reward = reward
-                best_action = action
-        
-        best_hparams = {
-            'lr': float(best_action[0]),
-            'hidden_dim': int(round(float(best_action[1]))),
-            'num_heads': int(round(float(best_action[2]))),
-            'dropout': float(best_action[3])
-        }
-        self.logger.info(f"\n最优超参数: {best_hparams}")
-        return best_hparams

BIN
models/causal-inference/targets_scaler.joblib


+ 40 - 0
models/prediction_models/20min/README.md

@@ -0,0 +1,40 @@
+# 双膜工艺(UF-RO)多指标动态预测系统
+
+基于时空图注意力与长短期记忆网络(GAT-LSTM)的水处理过程多步预测引擎。
+本项目旨在为水厂的超滤-反渗透(UF-RO)双膜系统提供精准的跨膜压差、段间压差及产水流量等核心工况指标的未来态势感知与预测。
+
+## 🌟 架构设计特点:高内聚,低耦合
+
+本项目彻底废弃了传统的硬编码传参模式,采用了**“核心算法引擎 + 水厂独立工作空间(Workspace)”**的插件化解耦架构:
+- **零硬编码引擎**:所有的特征维度、网络输入/输出大小均由底层引擎根据 YAML 配置自动推演,彻底消除了维度不匹配的 Bug。
+- **水厂工作空间隔离**:每个水厂(如 `lankao`, `yancheng`, `longting` 等)拥有独立的文件目录,实现了配置、数据集、归一化器(Scaler)与模型权重(PTH)的完全物理隔离,极大地提升了系统的可迁移性与可部署性。
+
+---
+
+## 📂 目录结构说明
+
+```text
+预测项目根目录/
+├── 核心引擎层 (通用代码)
+│   ├── data_preprocessor.py    # 时序数据预处理(自动填充、降采样、时间周期特征 Sin/Cos 自动注入)
+│   ├── gat_lstm.py             # 基于 GAT-LSTM 的多分支并行预测网络结构
+│   ├── data_trainer.py         # 模型训练器(包含早停机制、学习率衰减与多指标评估)
+│   ├── config.py               # 动态配置加载器(基于 YAML 自动推导特征维度与标签数量)
+│   ├── main.py                 # 模型训练与评估主入口
+│   └── predict.py              # 面向生产环境的实时预测推理接口
+│
+├── lankao/                     # 🏆 兰考水厂专属预测空间 (示例)
+│   ├── config.yaml               # 兰考专属配置(日期、训练超参数、具体特征列与预测目标列)
+│   ├── edge_index.pt             # 兰考专属的传感器图拓扑结构矩阵
+│   ├── model.pth                 # 训练生成的兰考专属模型权重
+│   ├── scaler.pkl                # 兰考专属的数据归一化器
+│
+├── yancheng/                   # 🏆 盐城水厂专属预测空间
+│   └── ... (结构同上)
+├── longting/                   # 🏆 龙亭水厂专属预测空间
+│   └── ... (结构同上)
+├── jianding/                   # 🏆 建鼎水厂专属预测空间
+│   └── ... (结构同上)
+└── anzhen/                     # 🏆 安镇水厂专属预测空间
+    └── ... (结构同上)
+```

+ 92 - 0
models/prediction_models/20min/anzhen/config.yaml

@@ -0,0 +1,92 @@
+# anzhen/config.yaml
+project:
+  plant_name: "anzhen"
+
+files:
+  dataset_dir: "datasets"
+  file_pattern: "data_process_{}.csv"
+  model_filename: "model.pth"
+  scaler_filename: "scaler.pkl"
+  output_csv_filename: "predictions.csv"
+  edge_index_filename: "edge_index.pt"
+
+data_split:
+  start_files: 1
+  end_files: 17
+  train_start_date: "2024-10-09"
+  train_end_date: "2025-03-24"
+  val_start_date: "2024-10-09"
+  val_end_date: "2025-03-24"
+  test_start_date: "2024-10-09"
+  test_end_date: "2025-03-24"
+
+model_params:
+  seq_len: 10
+  output_size: 5
+  step_size: 5
+  resolution: 60
+  hidden_size: 64
+  num_layers: 1
+  dropout: 0.0
+
+training_params:
+  epochs: 200
+  lr: 0.01
+  batch_size: 512
+  scheduler_step_size: 100
+  scheduler_gamma: 0.9
+  patience: 200
+  min_delta: 1.0e-10
+  device: 1          # 对应 args.device
+  random_seed: 1314
+
+sensors:
+  # 输入传感器列
+  required_columns:
+    - "index"
+    - "AR.1#UF_JSFLOW_O"
+    - "AR.2#UF_JSFLOW_O"
+    - "AR.1#RO_JSFLOW_O"
+    - "AR.2#RO_JSFLOW_O"
+    - "AR.1#UF_JSPRESS_O"
+    - "AR.2#UF_JSPRESS_O"
+    - "AR.1#RO_JSPRESS_O"
+    - "AR.2#RO_JSPRESS_O"
+    - "AR.1#RO_EDJSPRESS_O"
+    - "AR.1#RO_SDJSPRESS_O"
+    - "AR.2#RO_EDJSPRESS_O"
+    - "AR.2#RO_SDJSPRESS_O"
+    - "AR.ZJS_TEMP_O"
+    - "AR.ZJS_ZD_O"
+    - "AR.RO_JSDD_O"
+    - "AR.RO_JSORP_O"
+    - "AR.RO_JSPH_O"
+    - "AR.1#UF_V_FB_O"
+    - "AR.2#UF_V_FB_O"
+    - "AR.1#UFBWB_FRE_FB_O"
+    - "AR.2#UFBWB_FRE_FB_O"
+    - "AR.1#RODJB_FRE_FB_O"
+    - "AR.1#ROGYB_FRE_FB_O"
+    - "AR.1#RODJB_CZ_O"
+    - "AR.1#ROGYB_CZ_O"
+    - "AR.2#RODJB_CZ_O"
+    - "AR.2#ROGYB_CZ_O"
+    - "AR.ROGSB_FRE_FB_O"
+    - "AR.UFGSB_FRE_FB_O"
+    - "AR.V_UF1_TJV_KD_FB"
+    - "AR.V_UF2_TJV_KD_FB"
+    - "AR.CS_LEVEL_O"
+    - "AR.UF_CSLEVEL_O"
+    - "AR.UF1_SSD_KMYC"
+    - "AR.UF2_SSD_KMYC"
+    - "AR.RO1_2D_YC"
+    - "AR.PUBLIC_BY_REAL_1"
+    - "1#RO_CSFLOW"
+  
+  # 最终预测目标列
+  target_columns:
+    - "AR.UF1_SSD_KMYC"
+    - "AR.UF2_SSD_KMYC"
+    - "AR.RO1_2D_YC"
+    - "AR.PUBLIC_BY_REAL_1"
+    - "1#RO_CSFLOW"

+ 0 - 0
models/prediction_models/anzhen/edge_index.pt → models/prediction_models/20min/anzhen/edge_index.pt


+ 0 - 0
models/prediction_models/anzhen/传感器信息表.xlsx → models/prediction_models/20min/anzhen/id_list.xlsx


+ 0 - 0
models/prediction_models/anzhen/input_format.txt → models/prediction_models/20min/anzhen/input_format.txt


+ 0 - 0
models/prediction_models/anzhen/model.pth → models/prediction_models/20min/anzhen/model.pth


+ 0 - 0
models/prediction_models/anzhen/output_format.txt → models/prediction_models/20min/anzhen/output_format.txt


+ 0 - 0
models/prediction_models/anzhen/scaler.pkl → models/prediction_models/20min/anzhen/scaler.pkl


+ 74 - 0
models/prediction_models/20min/config.py

@@ -0,0 +1,74 @@
+# config.py
+import os
+import yaml
+
+class Config:
+    def __init__(self):
+        self.PLANT_NAME = ""
+        self.PLANT_DIR = ""
+
+    def load(self, plant_name: str):
+        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:
+            cfg = yaml.safe_load(f)
+
+        # 1. 路径挂载
+        files = cfg.get('files', {})
+        self.DATA_DIR = f"{self.PLANT_DIR}/{files.get('dataset_dir', 'datasets')}"
+        self.FILE_PATTERN = files.get('file_pattern', 'data_process_{}.csv')
+        self.MODEL_PATH = f"{self.PLANT_DIR}/{files.get('model_filename', 'model.pth')}"
+        self.SCALER_PATH = f"{self.PLANT_DIR}/{files.get('scaler_filename', 'scaler.pkl')}"
+        self.OUTPUT_CSV_PATH = f"{self.PLANT_DIR}/{files.get('output_csv_filename', 'predictions.csv')}"
+        self.EDGE_INDEX_PATH = f"{self.PLANT_DIR}/{files.get('edge_index_filename', 'edge_index.pt')}"
+
+        # 2. 数据划分
+        split = cfg.get('data_split', {})
+        self.START_FILES = split.get('start_files', 1)
+        self.END_FILES = split.get('end_files', 10)
+        self.TRAIN_START_DATE = split.get('train_start_date')
+        self.TRAIN_END_DATE = split.get('train_end_date')
+        self.VAL_START_DATE = split.get('val_start_date')
+        self.VAL_END_DATE = split.get('val_end_date')
+        self.TEST_START_DATE = split.get('test_start_date')
+        self.TEST_END_DATE = split.get('test_end_date')
+
+        # 3. 模型与超参数
+        mp = cfg.get('model_params', {})
+        self.SEQ_LEN = mp.get('seq_len', 10)
+        self.OUTPUT_SIZE = mp.get('output_size', 5)
+        self.STEP_SIZE = mp.get('step_size', 5)
+        self.RESOLUTION = mp.get('resolution', 60)
+        self.HIDDEN_SIZE = mp.get('hidden_size', 64)
+        self.NUM_LAYERS = mp.get('num_layers', 1)
+        self.DROPOUT = mp.get('dropout', 0.0)
+
+        # 4. 传感器特征推导 (核心:自动计算特征维度)
+        sensors = cfg.get('sensors', {})
+        self.REQUIRED_COLUMNS = sensors.get('required_columns', [])
+        self.TARGET_COLUMNS = sensors.get('target_columns', [])
+        
+        self.LABELS_NUM = len(self.TARGET_COLUMNS)
+        # 业务特征(不含index) + 4维手动注入的时间编码
+        self.FEATURE_NUM = (len(self.REQUIRED_COLUMNS) - 1) + 4
+
+        # 5. 训练参数
+        tp = cfg.get('training_params', {})
+        self.EPOCHS = tp.get('epochs', 200)
+        self.LR = tp.get('lr', 0.01)
+        self.BATCH_SIZE = tp.get('batch_size', 512)
+        self.SCHEDULER_STEP_SIZE = tp.get('scheduler_step_size', 100)
+        self.SCHEDULER_GAMMA = tp.get('scheduler_gamma', 0.9)
+        self.PATIENCE = tp.get('patience', 200)
+        self.MIN_DELTA = float(tp.get('min_delta', 1e-10))
+        self.DEVICE_ID = tp.get('device', 1)
+        self.RANDOM_SEED = tp.get('random_seed', 1314)
+
+        os.makedirs(self.DATA_DIR, exist_ok=True)
+
+config = Config()

+ 145 - 0
models/prediction_models/20min/data_preprocessor.py

@@ -0,0 +1,145 @@
+# data_preprocessor.py
+import os
+import torch
+import joblib
+import numpy as np
+import pandas as pd
+from tqdm import tqdm
+from sklearn.preprocessing import MinMaxScaler
+from torch.utils.data import DataLoader, TensorDataset
+from concurrent.futures import ThreadPoolExecutor
+from config import config
+
+class DataPreprocessor:
+    """数据预处理类"""
+
+    @staticmethod
+    def load_and_process_data(data):
+        data['date'] = pd.to_datetime(data['date'])
+        time_interval = pd.Timedelta(minutes=(4 * config.RESOLUTION / 60))
+        window_time_span = time_interval * (config.SEQ_LEN + 1)
+
+        val_start_date = pd.to_datetime(config.VAL_START_DATE)
+        test_start_date = pd.to_datetime(config.TEST_START_DATE)
+        
+        adjusted_val_start = val_start_date - window_time_span
+        adjusted_test_start = test_start_date - window_time_span
+        
+        train_mask = (data['date'] >= pd.to_datetime(config.TRAIN_START_DATE)) & \
+                     (data['date'] <= pd.to_datetime(config.TRAIN_END_DATE))
+        val_mask = (data['date'] >= adjusted_val_start) & \
+                   (data['date'] <= pd.to_datetime(config.VAL_END_DATE))
+        test_mask = (data['date'] >= adjusted_test_start) & \
+                    (data['date'] <= pd.to_datetime(config.TEST_END_DATE))
+
+        train_data = data[train_mask].reset_index(drop=True).drop(columns=['date'])
+        val_data = data[val_mask].reset_index(drop=True).drop(columns=['date'])
+        test_data = data[test_mask].reset_index(drop=True).drop(columns=['date'])
+    
+        train_supervised = DataPreprocessor.create_supervised_dataset(train_data, 1)
+        val_supervised = DataPreprocessor.create_supervised_dataset(val_data, 1)
+        test_supervised = DataPreprocessor.create_supervised_dataset(test_data, config.STEP_SIZE)
+        
+        train_loader = DataPreprocessor.load_data(train_supervised, shuffle=True)
+        val_loader = DataPreprocessor.load_data(val_supervised, shuffle=False)
+        test_loader = DataPreprocessor.load_data(test_supervised, shuffle=False)
+        
+        return train_loader, val_loader, test_loader, data
+    
+    @staticmethod
+    def read_and_combine_csv_files():
+        def read_file(file_count):
+            file_name = config.FILE_PATTERN.format(file_count)
+            file_path = os.path.join(config.DATA_DIR, file_name)
+            try:
+                df = pd.read_csv(file_path)
+                return df[config.REQUIRED_COLUMNS]
+            except KeyError as e:
+                print(f"文件 {file_name} 中缺少列: {e}")
+                raise
+        
+        file_indices = list(range(config.START_FILES, config.END_FILES + 1))
+        
+        with ThreadPoolExecutor(max_workers=os.cpu_count()) as executor:
+            results = list(tqdm(executor.map(read_file, file_indices),
+                                total=len(file_indices), desc="正在读取文件"))
+        
+        all_data = pd.concat(results, ignore_index=True)
+        all_data = all_data[config.REQUIRED_COLUMNS]
+        
+        chunk = all_data.iloc[::config.RESOLUTION, :].reset_index(drop=True)
+        chunk = DataPreprocessor.process_date(chunk)
+        chunk = DataPreprocessor.scaler_data(chunk)
+        return chunk
+    
+    @staticmethod
+    def process_date(data):
+        data = data.rename(columns={'index': 'date'})
+        data['date'] = pd.to_datetime(data['date'])
+    
+        minute_of_day = data['date'].dt.hour * 60 + data['date'].dt.minute
+        day_of_year = data['date'].dt.dayofyear
+        
+        time_features = ['minute_sin', 'minute_cos', 'day_year_sin', 'day_year_cos']
+        data['minute_sin'] = np.sin(2 * np.pi * minute_of_day / 1440)
+        data['minute_cos'] = np.cos(2 * np.pi * minute_of_day / 1440)
+        data['day_year_sin'] = np.sin(2 * np.pi * day_of_year / 366)
+        data['day_year_cos'] = np.cos(2 * np.pi * day_of_year / 366)
+        
+        other_columns = [col for col in data.columns if col not in ['date'] + time_features]
+        return data[['date'] + time_features + other_columns]
+    
+    @staticmethod
+    def scaler_data(data):
+        date_col = data[['date']]
+        data_to_scale = data.drop(columns=['date'])
+
+        scaler = MinMaxScaler(feature_range=(0, 1))
+        scaled_data = scaler.fit_transform(data_to_scale)
+        joblib.dump(scaler, config.SCALER_PATH)
+
+        scaled_data = pd.DataFrame(scaled_data, columns=data_to_scale.columns)
+        return pd.concat([date_col.reset_index(drop=True), scaled_data], axis=1)
+    
+    @staticmethod
+    def create_supervised_dataset(data, step_size):
+        data = pd.DataFrame(data)
+        cols, col_names = [], []
+        feature_columns = data.columns.tolist()
+
+        for col in feature_columns:
+            for i in range(config.SEQ_LEN - 1, -1, -1):
+                cols.append(data[[col]].shift(i))
+                col_names.append(f"{col}(t-{i})")
+        
+        target_columns = feature_columns[-config.LABELS_NUM:]
+        for i in range(1, config.OUTPUT_SIZE + 1):
+            for col in target_columns:
+                cols.append(data[[col]].shift(-i))
+                col_names.append(f"{col}(t+{i})")
+
+        dataset = pd.concat(cols, axis=1)
+        dataset.columns = col_names
+        dataset = dataset.iloc[::step_size, :]
+        dataset.dropna(inplace=True)
+        return dataset
+
+    @staticmethod
+    def load_data(dataset, shuffle):
+        n_features_total = config.FEATURE_NUM * config.SEQ_LEN
+        n_labels_total = config.OUTPUT_SIZE * config.LABELS_NUM
+
+        X = dataset.values[:, :n_features_total]
+        y = dataset.values[:, n_features_total:n_features_total + n_labels_total]
+    
+        X = X.reshape(X.shape[0], config.SEQ_LEN, config.FEATURE_NUM)
+        device = torch.device(f"cuda:{config.DEVICE_ID}" if torch.cuda.is_available() else "cpu")
+        
+        X = torch.tensor(X, dtype=torch.float32).to(device)
+        y = torch.tensor(y, dtype=torch.float32).to(device)
+
+        dataset_tensor = TensorDataset(X, y)
+        generator = torch.Generator()
+        generator.manual_seed(config.RANDOM_SEED)
+        
+        return DataLoader(dataset_tensor, batch_size=config.BATCH_SIZE, shuffle=shuffle, generator=generator)

+ 121 - 0
models/prediction_models/20min/data_trainer.py

@@ -0,0 +1,121 @@
+# data_trainer.py
+import torch
+import joblib
+import numpy as np
+import pandas as pd
+from sklearn.metrics import r2_score
+from datetime import datetime, timedelta
+from sklearn.preprocessing import MinMaxScaler
+from config import config
+
+class Trainer:
+    def __init__(self, model, data):
+        self.model = model
+        self.data = data
+        self.device = torch.device(f"cuda:{config.DEVICE_ID}" if torch.cuda.is_available() else "cpu")
+        self.best_val_loss = float('inf')
+        self.best_model_state = None
+
+    def train_full_model(self, train_loader, val_loader, optimizer, criterion, scheduler):
+        counter = 0
+        for epoch in range(config.EPOCHS):
+            self.model.train()
+            running_loss = 0.0
+            
+            for inputs, targets in train_loader:
+                optimizer.zero_grad()
+                outputs = self.model(inputs)
+                loss = criterion(outputs, targets)
+                loss.backward()
+                optimizer.step()
+                running_loss += loss.item()
+            
+            train_loss = running_loss / len(train_loader)
+            val_loss = self.validate_full(val_loader, criterion) if val_loader else 0.0
+            print(f'Epoch {epoch+1}/{config.EPOCHS}, Train Loss: {train_loss:.6f}, Val Loss: {val_loss:.6f}')
+
+            if val_loader:
+                if val_loss < (self.best_val_loss - config.MIN_DELTA):
+                    self.best_val_loss = val_loss
+                    counter = 0
+                    self.best_model_state = self.model.state_dict()
+                else:
+                    counter += 1
+                    if counter >= config.PATIENCE:
+                        print(f"早停触发")
+                        break
+                        
+            scheduler.step()
+            torch.cuda.empty_cache()
+
+        if self.best_model_state:
+            self.model.load_state_dict(self.best_model_state)
+        return self.model
+
+    def validate_full(self, val_loader, criterion):
+        self.model.eval()
+        total_loss = 0.0
+        with torch.no_grad():
+            for inputs, targets in val_loader:
+                outputs = self.model(inputs)
+                loss = criterion(outputs, targets)
+                total_loss += loss.item()
+        return total_loss / len(val_loader)
+
+    def save_model(self):
+        torch.save(self.model.state_dict(), config.MODEL_PATH)
+        print(f"模型已保存到:{config.MODEL_PATH}")
+            
+    def evaluate_model(self, test_loader):
+        self.model.eval()
+        scaler = joblib.load(config.SCALER_PATH)
+        predictions, true_values = [], []
+        
+        with torch.no_grad():
+            for inputs, targets in test_loader:
+                predictions.append(self.model(inputs).cpu().numpy())
+                true_values.append(targets.cpu().numpy())
+    
+        predictions = np.concatenate(predictions, axis=0)
+        true_values = np.concatenate(true_values, axis=0)
+    
+        predictions = predictions.reshape(-1, config.LABELS_NUM)
+        true_values = true_values.reshape(-1, config.LABELS_NUM)
+    
+        column_scaler = MinMaxScaler(feature_range=(0, 1))
+        column_scaler.min_ = scaler.min_[-config.LABELS_NUM:] 
+        column_scaler.scale_ = scaler.scale_[-config.LABELS_NUM:] 
+        
+        true_values = column_scaler.inverse_transform(true_values)
+        predictions = column_scaler.inverse_transform(predictions)
+    
+        start_datetime = datetime.strptime(config.TEST_START_DATE, "%Y-%m-%d")
+        time_interval = timedelta(minutes=(4 * config.RESOLUTION / 60))
+        date_times = [start_datetime + i * time_interval for i in range(len(predictions) // config.OUTPUT_SIZE)]
+        
+        # 扩展时间以便和输出形状对齐
+        date_times = np.repeat(date_times, config.OUTPUT_SIZE)
+        
+        results = pd.DataFrame({'date': date_times[:len(predictions)]})
+        metrics_details = []
+        
+        for i, col_name in enumerate(config.TARGET_COLUMNS):
+            results[f'{col_name}_True'] = true_values[:, i]
+            results[f'{col_name}_Predicted'] = predictions[:, i]
+            
+            var_true = true_values[:, i]
+            var_pred = predictions[:, i]
+            
+            mask = var_true != 0
+            if mask.sum() > 0:
+                r2 = r2_score(var_true[mask], var_pred[mask])
+                rmse = np.sqrt(np.mean((var_true[mask] - var_pred[mask]) ** 2))
+                mape = np.mean(np.abs((var_true[mask] - var_pred[mask]) / np.abs(var_true[mask]))) * 100
+                metrics_details.append(f"{col_name}: R2={r2:.4f}, RMSE={rmse:.4f}, MAPE={mape:.4f}%")
+            else:
+                metrics_details.append(f"{col_name}: 无效数据")
+
+        results.to_csv(config.OUTPUT_CSV_PATH, index=False)
+        with open(config.OUTPUT_CSV_PATH.replace('.csv', '_metrics.txt'), 'w') as f:
+            f.write('\n'.join(metrics_details))
+        return metrics_details

+ 44 - 0
models/prediction_models/20min/gat_lstm.py

@@ -0,0 +1,44 @@
+# gat_lstm.py
+import torch
+import torch.nn as nn
+from config import config
+
+class SingleGATLSTM(nn.Module):
+    def __init__(self):
+        super(SingleGATLSTM, self).__init__()
+        self.lstm = nn.LSTM(
+            input_size=config.FEATURE_NUM,
+            hidden_size=config.HIDDEN_SIZE,
+            num_layers=config.NUM_LAYERS,
+            batch_first=True
+        )
+        self.final_linear = nn.Sequential(
+            nn.Linear(config.HIDDEN_SIZE, config.HIDDEN_SIZE),
+            nn.LeakyReLU(0.01),
+            nn.Dropout(config.DROPOUT * 0.4),
+            nn.Linear(config.HIDDEN_SIZE, config.OUTPUT_SIZE)
+        )
+        self._init_weights()
+        
+    def _init_weights(self):
+        for m in self.modules():
+            if isinstance(m, nn.Linear):
+                nn.init.xavier_uniform_(m.weight)
+                if m.bias is not None: nn.init.zeros_(m.bias)
+
+    def forward(self, x):
+        lstm_out, _ = self.lstm(x)
+        last_out = lstm_out[:, -1, :]
+        return self.final_linear(last_out)
+
+class GAT_LSTM(nn.Module):
+    def __init__(self):
+        super(GAT_LSTM, self).__init__()
+        self.models = nn.ModuleList([SingleGATLSTM() for _ in range(config.LABELS_NUM)])
+    
+    def set_edge_index(self, edge_index):
+        self.edge_index = edge_index
+        
+    def forward(self, x):
+        outputs = [model(x) for model in self.models]
+        return torch.cat(outputs, dim=1)

+ 81 - 0
models/prediction_models/20min/jianding/config.yaml

@@ -0,0 +1,81 @@
+# jianding/config.yaml
+project:
+  plant_name: "jianding"
+
+files:
+  dataset_dir: "datasets"
+  file_pattern: "data_process_{}.csv"
+  model_filename: "model.pth"
+  scaler_filename: "scaler.pkl"
+  output_csv_filename: "predictions.csv"
+  edge_index_filename: "edge_index.pt"
+
+data_split:
+  start_files: 1
+  end_files: 24
+  train_start_date: "2024-10-08"
+  train_end_date: "2026-02-13"
+  val_start_date: "2024-10-08"
+  val_end_date: "2026-02-13"
+  test_start_date: "2024-10-08"
+  test_end_date: "2026-02-13"
+
+model_params:
+  seq_len: 10
+  output_size: 5
+  step_size: 5
+  resolution: 60
+  hidden_size: 64
+  num_layers: 1
+  dropout: 0.0
+
+training_params:
+  epochs: 200
+  lr: 0.01
+  batch_size: 512
+  scheduler_step_size: 100
+  scheduler_gamma: 0.9
+  patience: 200
+  min_delta: 1.0e-10
+  device: 1          # 对应 args.device
+  random_seed: 1314
+
+sensors:
+  # 输入传感器列
+  required_columns:
+    - "index"
+    - "water_out"
+    - "ns=3;s=AI_ROJSLL_OUT"
+    - "ns=3;s=AI_UFCSLL_OUT"
+    - "ns=3;s=RO_1DJSLL_SSD"
+    - "ns=3;s=RO_2DJSLL_SSD"
+    - "ns=3;s=RO_NS_SSD"
+    - "ns=3;s=AI_JYCSLL1_OUT"
+    - "ns=3;s=AI_RODJYL_OUT"
+    - "ns=3;s=AI_ROJSYL_OUT"
+    - "ns=3;s=AI_UFCSYL_OUT"
+    - "ns=3;s=AI_JYCIPPH_OUT"
+    - "ns=3;s=AI_JYCSDD_OUT"
+    - "ns=3;s=AI_UFCSZD_OUT"
+    - "ns=3;s=AI_ROCSDD_OUT"
+    - "ns=3;s=AI_UFJSORP_OUT"
+    - "ns=3;s=AI_UFJSPH_OUT"
+    - "ns=3;s=AI_UFJSYW_OUT"
+    - "ns=3;s=AI_JYROCSYW_OUT"
+    - "ns=3;s=AI_JYSYW_OUT"
+    - "ns=3;s=AI_RODJB_FR_OUT"
+    - "ns=3;s=AI_ROGSB_FR_OUT"
+    - "ns=3;s=AI_ROGYB_FR_OUT"
+    - "ns=3;s=AI_UFFXB_FR_OUT"
+    - "ns=3;s=AI_UFCSB_FR_OUT"
+    - "ns=3;s=UF_TMP"
+    - "ns=3;s=RO_CHA1YL_SSD"
+    - "ns=3;s=RO_CHA2YL_SSD"
+    - "ns=3;s=RO_ZCS_SSD"
+  
+  # 最终预测目标列
+  target_columns:
+    - "ns=3;s=UF_TMP"
+    - "ns=3;s=RO_CHA1YL_SSD"
+    - "ns=3;s=RO_CHA2YL_SSD"
+    - "ns=3;s=RO_ZCS_SSD"

+ 0 - 0
models/prediction_models/jianding/edge_index.pt → models/prediction_models/20min/jianding/edge_index.pt


+ 0 - 0
models/prediction_models/jianding/传感器信息表.xlsx → models/prediction_models/20min/jianding/id_list.xlsx


+ 0 - 0
models/prediction_models/jianding/input_format.txt → models/prediction_models/20min/jianding/input_format.txt


+ 0 - 0
models/prediction_models/jianding/model.pth → models/prediction_models/20min/jianding/model.pth


+ 0 - 0
models/prediction_models/jianding/output_format.txt → models/prediction_models/20min/jianding/output_format.txt


+ 0 - 0
models/prediction_models/jianding/scaler.pkl → models/prediction_models/20min/jianding/scaler.pkl


+ 118 - 0
models/prediction_models/20min/lankao/config.yaml

@@ -0,0 +1,118 @@
+# lankao/config.yaml
+project:
+  plant_name: "lankao"
+
+files:
+  dataset_dir: "datasets"
+  file_pattern: "data_process_{}.csv"
+  model_filename: "model.pth"
+  scaler_filename: "scaler.pkl"
+  output_csv_filename: "predictions.csv"
+  edge_index_filename: "edge_index.pt"
+
+data_split:
+  start_files: 1
+  end_files: 10
+  train_start_date: "2025-11-28"
+  train_end_date: "2026-02-20"
+  val_start_date: "2025-11-28"
+  val_end_date: "2026-02-20"
+  test_start_date: "2025-11-28"
+  test_end_date: "2026-02-20"
+
+model_params:
+  seq_len: 10
+  output_size: 5
+  step_size: 5
+  resolution: 60
+  hidden_size: 64
+  num_layers: 1
+  dropout: 0.0
+
+training_params:
+  epochs: 200
+  lr: 0.01
+  batch_size: 512
+  scheduler_step_size: 100
+  scheduler_gamma: 0.9
+  patience: 200
+  min_delta: 1.0e-10
+  device: 1  # GPU ID
+  random_seed: 1314
+
+sensors:
+  # 输入传感器
+  required_columns:
+    - "index"
+    - "ns=3;s=1#RO_CSDD_O"
+    - "ns=3;s=1#RO_CSPRESS_O"
+    - "ns=3;s=1#RO_EDCSFLOW_O"
+    - "ns=3;s=1#RO_EDJSPRESS_O"
+    - "ns=3;s=1#RO_EDNSPRESS_O"
+    - "ns=3;s=1#RO_JSFLOW_O"
+    - "ns=3;s=1#RO_JSPRESS_O"
+    - "ns=3;s=1#RO_NSFLOW_O"
+    - "ns=3;s=1#RO_SDCSFLOW_O"
+    - "ns=3;s=1#RO_SDJSPRESS_O"
+    - "ns=3;s=1#RO_SDNSPRESS_O"
+    - "ns=3;s=1#RODJB_CUR_FB_O"
+    - "ns=3;s=1#RODJB_CZ_O"
+    - "ns=3;s=1#RODJB_FRE_FB_O"
+    - "ns=3;s=1#ROGYB_CUR_FB_O"
+    - "ns=3;s=1#ROGYB_CZ_O"
+    - "ns=3;s=1#ROGYB_FRE_FB_O"
+    - "ns=3;s=1#UF_CSPRESS_O"
+    - "ns=3;s=1#UF_JSFLOW_O"
+    - "ns=3;s=1#UF_JSPRESS_O"
+    - "ns=3;s=1#UF_V_FB_O"
+    - "ns=3;s=1#UFBWB_CUR_FB_O"
+    - "ns=3;s=1#UFBWB_FRE_FB_O"
+    - "ns=3;s=2#RO_CSDD_O"
+    - "ns=3;s=2#RO_CSPRESS_O"
+    - "ns=3;s=2#RO_EDCSFLOW_O"
+    - "ns=3;s=2#RO_EDJSPRESS_O"
+    - "ns=3;s=2#RO_EDNSPRESS_O"
+    - "ns=3;s=2#RO_JSFLOW_O"
+    - "ns=3;s=2#RO_JSPRESS_O"
+    - "ns=3;s=2#RO_NSFLOW_O"
+    - "ns=3;s=2#RO_SDCSFLOW_O"
+    - "ns=3;s=2#RO_SDJSPRESS_O"
+    - "ns=3;s=2#RO_SDNSPRESS_O"
+    - "ns=3;s=2#RODJB_CUR_FB_O"
+    - "ns=3;s=2#RODJB_CZ_O"
+    - "ns=3;s=2#RODJB_FRE_FB_O"
+    - "ns=3;s=2#ROGYB_CUR_FB_O"
+    - "ns=3;s=2#ROGYB_CZ_O"
+    - "ns=3;s=2#ROGYB_FRE_FB_O"
+    - "ns=3;s=2#UF_CSPRESS_O"
+    - "ns=3;s=2#UF_JSFLOW_O"
+    - "ns=3;s=2#UF_JSPRESS_O"
+    - "ns=3;s=2#UF_V_FB_O"
+    - "ns=3;s=2#UFBWB_CUR_FB_O"
+    - "ns=3;s=2#UFBWB_FRE_FB_O"
+    - "ns=3;s=RO_JSDD_O"
+    - "ns=3;s=RO_JSORP_O"
+    - "ns=3;s=RO_JSPH_O"
+    - "ns=3;s=RO_WSDD_O"
+    - "ns=3;s=UFGSB_FRE_FB_O"
+    - "ns=3;s=ZJS_PRESS_O"
+    - "ns=3;s=ZJS_TEMP_O"
+    - "ns=3;s=ZJS_ZD_O"
+    - "water_in"
+    - "water_out"
+    - "ROHSL"
+    - "ns=3;s=UF1_SSD_KMYC"
+    - "ns=3;s=UF2_SSD_KMYC"
+    - "ns=3;s=RO1_1D_YC"
+    - "ns=3;s=RO1_2D_YC"
+    - "ns=3;s=RO2_1D_YC"
+    - "ns=3;s=RO2_2D_YC"
+  
+  # 最终预测目标列
+  target_columns:
+    - "ns=3;s=UF1_SSD_KMYC"
+    - "ns=3;s=UF2_SSD_KMYC"
+    - "ns=3;s=RO1_1D_YC"
+    - "ns=3;s=RO1_2D_YC"
+    - "ns=3;s=RO2_1D_YC"
+    - "ns=3;s=RO2_2D_YC"

+ 0 - 0
models/prediction_models/longting/edge_index.pt → models/prediction_models/20min/lankao/edge_index.pt


BIN
models/prediction_models/20min/lankao/id_list.xlsx


+ 64 - 0
models/prediction_models/20min/lankao/input_format.txt

@@ -0,0 +1,64 @@
+index
+ns=3;s=1#RO_CSDD_O
+ns=3;s=1#RO_CSPRESS_O
+ns=3;s=1#RO_EDCSFLOW_O
+ns=3;s=1#RO_EDJSPRESS_O
+ns=3;s=1#RO_EDNSPRESS_O
+ns=3;s=1#RO_JSFLOW_O
+ns=3;s=1#RO_JSPRESS_O
+ns=3;s=1#RO_NSFLOW_O
+ns=3;s=1#RO_SDCSFLOW_O
+ns=3;s=1#RO_SDJSPRESS_O
+ns=3;s=1#RO_SDNSPRESS_O
+ns=3;s=1#RODJB_CUR_FB_O
+ns=3;s=1#RODJB_CZ_O
+ns=3;s=1#RODJB_FRE_FB_O
+ns=3;s=1#ROGYB_CUR_FB_O
+ns=3;s=1#ROGYB_CZ_O
+ns=3;s=1#ROGYB_FRE_FB_O
+ns=3;s=1#UF_CSPRESS_O
+ns=3;s=1#UF_JSFLOW_O
+ns=3;s=1#UF_JSPRESS_O
+ns=3;s=1#UF_V_FB_O
+ns=3;s=1#UFBWB_CUR_FB_O
+ns=3;s=1#UFBWB_FRE_FB_O
+ns=3;s=2#RO_CSDD_O
+ns=3;s=2#RO_CSPRESS_O
+ns=3;s=2#RO_EDCSFLOW_O
+ns=3;s=2#RO_EDJSPRESS_O
+ns=3;s=2#RO_EDNSPRESS_O
+ns=3;s=2#RO_JSFLOW_O
+ns=3;s=2#RO_JSPRESS_O
+ns=3;s=2#RO_NSFLOW_O
+ns=3;s=2#RO_SDCSFLOW_O
+ns=3;s=2#RO_SDJSPRESS_O
+ns=3;s=2#RO_SDNSPRESS_O
+ns=3;s=2#RODJB_CUR_FB_O
+ns=3;s=2#RODJB_CZ_O
+ns=3;s=2#RODJB_FRE_FB_O
+ns=3;s=2#ROGYB_CUR_FB_O
+ns=3;s=2#ROGYB_CZ_O
+ns=3;s=2#ROGYB_FRE_FB_O
+ns=3;s=2#UF_CSPRESS_O
+ns=3;s=2#UF_JSFLOW_O
+ns=3;s=2#UF_JSPRESS_O
+ns=3;s=2#UF_V_FB_O
+ns=3;s=2#UFBWB_CUR_FB_O
+ns=3;s=2#UFBWB_FRE_FB_O
+ns=3;s=RO_JSDD_O
+ns=3;s=RO_JSORP_O
+ns=3;s=RO_JSPH_O
+ns=3;s=RO_WSDD_O
+ns=3;s=UFGSB_FRE_FB_O
+ns=3;s=ZJS_PRESS_O
+ns=3;s=ZJS_TEMP_O
+ns=3;s=ZJS_ZD_O
+water_in
+water_out
+ROHSL
+ns=3;s=UF1_SSD_KMYC
+ns=3;s=UF2_SSD_KMYC
+ns=3;s=RO1_1D_YC
+ns=3;s=RO1_2D_YC
+ns=3;s=RO2_1D_YC
+ns=3;s=RO2_2D_YC

BIN
models/prediction_models/20min/lankao/model.pth


+ 2 - 0
models/prediction_models/20min/lankao/output_format.txt

@@ -0,0 +1,2 @@
+预测结果 (5x6 数组):
+[[0.30925674192810065, 0.287224452747345, 0.4037019495282173, 0.14748495053315164, 0.04417573315054178, 0.044882690453529356], [0.12443798766875269, 0.032358692350387575, 0.34641349865961074, 0.016159397334367036, 0.03854386935353279, 0.06759355280518531], [0.10832754593110086, 0.06640213284015656, 0.16280591935014724, 0.10970147202157975, 0.046568266941905016, 0.0836045376586914], [0.04305888491213322, 0.12639080519628526, 0.019993437642991545, 0.10626869505882264, 0.02149141131788492, 0.053764741209745406], [0.03132459068843723, 0.027491360438704492, 0.038493209559708835, 0.0743121422829628, 0.05017118183791637, 0.054895462408065795]]

BIN
models/prediction_models/20min/lankao/scaler.pkl


+ 131 - 0
models/prediction_models/20min/longting/config.yaml

@@ -0,0 +1,131 @@
+# longting/config.yaml
+project:
+  plant_name: "longting"
+
+files:
+  dataset_dir: "datasets"
+  file_pattern: "data_process_{}.csv"
+  model_filename: "model.pth"
+  scaler_filename: "scaler.pkl"
+  output_csv_filename: "predictions.csv"
+  edge_index_filename: "edge_index.pt"
+
+data_split:
+  start_files: 1
+  end_files: 10
+  train_start_date: "2025-11-28"
+  train_end_date: "2026-02-20"
+  val_start_date: "2025-11-28"
+  val_end_date: "2026-02-20"
+  test_start_date: "2025-11-28"
+  test_end_date: "2026-02-20"
+
+model_params:
+  seq_len: 10
+  output_size: 5
+  step_size: 5
+  resolution: 60
+  hidden_size: 64
+  num_layers: 1
+  dropout: 0.0
+
+training_params:
+  epochs: 200
+  lr: 0.01
+  batch_size: 512
+  scheduler_step_size: 100
+  scheduler_gamma: 0.9
+  patience: 200
+  min_delta: 1.0e-10
+  device: 1          # 对应 args.device
+  random_seed: 1314
+
+sensors:
+  # 输入传感器列
+  required_columns:
+    - "index"
+    - "water_in"
+    - "water_out"
+    - "RO1_TYL"
+    - "RO2_TYL"
+    - "UF1Per"
+    - "UF2Per"
+    - "2#RODJB_Eff"
+    - "1#RODJB_Eff"
+    - "2#ROGYB_Eff"
+    - "1#ROGYB_Eff"
+    - "ROHSL"
+    - "ns=3;s=1#RO_CSDD_O"
+    - "ns=3;s=1#RO_CSPRESS_O"
+    - "ns=3;s=1#RO_EDCSFLOW_O"
+    - "ns=3;s=1#RO_EDJSPRESS_O"
+    - "ns=3;s=1#RO_EDNSPRESS_O"
+    - "ns=3;s=1#RO_JSFLOW_O"
+    - "ns=3;s=1#RO_JSPRESS_O"
+    - "ns=3;s=1#RO_NSFLOW_O"
+    - "ns=3;s=1#RO_SDCSFLOW_O"
+    - "ns=3;s=1#RO_SDJSPRESS_O"
+    - "ns=3;s=1#RO_SDNSPRESS_O"
+    - "ns=3;s=1#RODJB_CUR_FB_O"
+    - "ns=3;s=1#RODJB_CZ_O"
+    - "ns=3;s=1#RODJB_FRE_FB_O"
+    - "ns=3;s=1#ROGYB_CUR_FB_O"
+    - "ns=3;s=1#ROGYB_CZ_O"
+    - "ns=3;s=1#ROGYB_FRE_FB_O"
+    - "ns=3;s=1#UF_CSPRESS_O"
+    - "ns=3;s=1#UF_JSFLOW_O"
+    - "ns=3;s=1#UF_JSPRESS_O"
+    - "ns=3;s=1#UF_V_FB_O"
+    - "ns=3;s=1#UFBWB_CUR_FB_O"
+    - "ns=3;s=1#UFBWB_FRE_FB_O"
+    - "ns=3;s=2#RO_CSDD_O"
+    - "ns=3;s=2#RO_CSPRESS_O"
+    - "ns=3;s=2#RO_EDCSFLOW_O"
+    - "ns=3;s=2#RO_EDJSPRESS_O"
+    - "ns=3;s=2#RO_EDNSPRESS_O"
+    - "ns=3;s=2#RO_JSFLOW_O"
+    - "ns=3;s=2#RO_JSPRESS_O"
+    - "ns=3;s=2#RO_NSFLOW_O"
+    - "ns=3;s=2#RO_SDCSFLOW_O"
+    - "ns=3;s=2#RO_SDJSPRESS_O"
+    - "ns=3;s=2#RO_SDNSPRESS_O"
+    - "ns=3;s=2#RODJB_CUR_FB_O"
+    - "ns=3;s=2#RODJB_CZ_O"
+    - "ns=3;s=2#RODJB_FRE_FB_O"
+    - "ns=3;s=2#ROGYB_CUR_FB_O"
+    - "ns=3;s=2#ROGYB_CZ_O"
+    - "ns=3;s=2#ROGYB_FRE_FB_O"
+    - "ns=3;s=2#UF_CSPRESS_O"
+    - "ns=3;s=2#UF_JSFLOW_O"
+    - "ns=3;s=2#UF_JSPRESS_O"
+    - "ns=3;s=2#UF_V_FB_O"
+    - "ns=3;s=2#UFBWB_CUR_FB_O"
+    - "ns=3;s=2#UFBWB_FRE_FB_O"
+    - "ns=3;s=RO_JSDD_O"
+    - "ns=3;s=RO_JSORP_O"
+    - "ns=3;s=RO_JSPH_O"
+    - "ns=3;s=RO1_1DUAN_CS_FLOW"
+    - "ns=3;s=ZJS_PRESS_O"
+    - "ns=3;s=ZJS_TEMP_O"
+    - "ns=3;s=ZJS_ZD_O"
+    - "ns=3;s=PUBLIC_RO1_MTL"
+    - "ns=3;s=PUBLIC_RO2_MTL"
+    - "ns=3;s=UF1_SSD_KMYC"
+    - "ns=3;s=UF2_SSD_KMYC"
+    - "ns=3;s=RO1_1D_YC"
+    - "ns=3;s=RO1_2D_YC"
+    - "ns=3;s=RO2_1D_YC"
+    - "ns=3;s=RO2_2D_YC"
+    - "ns=3;s=PUBLIC_BY_REAL_1"
+    - "ns=3;s=PUBLIC_BY_REAL_2"
+  
+  # 最终预测目标列
+  target_columns:
+    - "ns=3;s=UF1_SSD_KMYC"
+    - "ns=3;s=UF2_SSD_KMYC"
+    - "ns=3;s=RO1_1D_YC"
+    - "ns=3;s=RO1_2D_YC"
+    - "ns=3;s=RO2_1D_YC"
+    - "ns=3;s=RO2_2D_YC"
+    - "ns=3;s=PUBLIC_BY_REAL_1"
+    - "ns=3;s=PUBLIC_BY_REAL_2"

BIN
models/prediction_models/20min/longting/edge_index.pt


+ 0 - 0
models/prediction_models/longting/传感器信息表.xlsx → models/prediction_models/20min/longting/id_list.xlsx


+ 0 - 0
models/prediction_models/longting/input_format.txt → models/prediction_models/20min/longting/input_format.txt


+ 0 - 0
models/prediction_models/longting/model.pth → models/prediction_models/20min/longting/model.pth


+ 0 - 0
models/prediction_models/longting/output_format.txt → models/prediction_models/20min/longting/output_format.txt


+ 0 - 0
models/prediction_models/longting/scaler.pkl → models/prediction_models/20min/longting/scaler.pkl


+ 57 - 0
models/prediction_models/20min/main.py

@@ -0,0 +1,57 @@
+# main.py
+import os
+import torch
+import numpy as np
+import random
+import argparse
+from torch.nn import MSELoss
+
+from config import config
+
+def set_seed(seed):
+    random.seed(seed)
+    os.environ['PYTHONHASHSEED'] = str(seed)
+    np.random.seed(seed)
+    torch.manual_seed(seed)
+    torch.cuda.manual_seed(seed)
+    torch.backends.cudnn.deterministic = True
+    torch.backends.cudnn.benchmark = False
+
+def main():
+    parser = argparse.ArgumentParser(description="水厂预测模型训练")
+    parser.add_argument('-p', '--plant', type=str, required=True, help="水厂名称,例如: lankao")
+    args = parser.parse_args()
+    
+    # 加载对应水厂的配置
+    config.load(args.plant)
+    
+    # 延迟导入,确保 config 已加载
+    from gat_lstm import GAT_LSTM
+    from data_trainer import Trainer
+    from data_preprocessor import DataPreprocessor
+
+    set_seed(config.RANDOM_SEED)
+    device = torch.device(f"cuda:{config.DEVICE_ID}" if torch.cuda.is_available() else "cpu")
+    print(f"[*] 工作空间: {args.plant} | 序列={config.SEQ_LEN}, 特征={config.FEATURE_NUM}, 目标={config.LABELS_NUM}")
+
+    data = DataPreprocessor.read_and_combine_csv_files()
+    train_loader, val_loader, test_loader, _ = DataPreprocessor.load_and_process_data(data)
+    
+    model = GAT_LSTM().to(device)
+    if os.path.exists(config.EDGE_INDEX_PATH):
+        model.set_edge_index(torch.load(config.EDGE_INDEX_PATH, map_location=device, weights_only=True))
+        print("已加载 edge_index.pt")
+
+    trainer = Trainer(model, data)
+    optimizer = torch.optim.Adam(model.parameters(), lr=config.LR)
+    scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=config.SCHEDULER_STEP_SIZE, gamma=config.SCHEDULER_GAMMA)
+
+    print("=== 开始训练 ===")
+    trainer.train_full_model(train_loader, val_loader, optimizer, MSELoss(), scheduler)
+    trainer.save_model()
+    
+    print("=== 开始评估 ===")
+    trainer.evaluate_model(test_loader)
+
+if __name__ == "__main__":
+    main()

+ 84 - 0
models/prediction_models/20min/predict.py

@@ -0,0 +1,84 @@
+# predict.py
+import os
+import torch
+import joblib
+import argparse
+import pandas as pd
+import numpy as np
+from datetime import datetime, timedelta
+from config import config
+
+class RealTimePredictor:
+    def __init__(self):
+        self.device = torch.device(f"cuda:{config.DEVICE_ID}" if torch.cuda.is_available() else "cpu")
+        
+        if not os.path.exists(config.SCALER_PATH):
+             raise FileNotFoundError(f"未找到归一化文件: {config.SCALER_PATH}")
+        self.scaler = joblib.load(config.SCALER_PATH)
+        
+        from gat_lstm import GAT_LSTM
+        self.model = GAT_LSTM().to(self.device)
+        self.model.load_state_dict(torch.load(config.MODEL_PATH, map_location=self.device, weights_only=True))
+        self.model.eval()
+
+    def _preprocess(self, df):
+        data = df.copy()
+        if 'datetime' in data.columns: data = data.rename(columns={'datetime': 'index'})
+        if 'index' not in data.columns:
+             data['index'] = pd.date_range(end=datetime.now(), periods=len(data), freq='min')
+        data['index'] = pd.to_datetime(data['index'])
+        
+        if len(data) < config.SEQ_LEN:
+            pad_len = config.SEQ_LEN - len(data)
+            pads = pd.concat([data.iloc[0:1]] * pad_len, ignore_index=True)
+            for i in range(pad_len):
+                pads.at[i, 'index'] = data['index'].iloc[0] - timedelta(minutes=(pad_len-i))
+            data = pd.concat([pads, data], ignore_index=True)
+
+        business_cols = config.REQUIRED_COLUMNS[1:]
+        data_business = data[business_cols]
+
+        date_col = data['index']
+        minute_of_day = date_col.dt.hour * 60 + date_col.dt.minute
+        day_of_year = date_col.dt.dayofyear
+        
+        time_features = pd.DataFrame({
+            'minute_sin': np.sin(2 * np.pi * minute_of_day / 1440),
+            'minute_cos': np.cos(2 * np.pi * minute_of_day / 1440),
+            'day_year_sin': np.sin(2 * np.pi * day_of_year / 366),
+            'day_year_cos': np.cos(2 * np.pi * day_of_year / 366)
+        })
+        
+        data_to_scale = pd.concat([time_features.reset_index(drop=True), data_business.reset_index(drop=True)], axis=1)
+        return self.scaler.transform(data_to_scale)
+
+    def predict(self, df):
+        processed_data = self._preprocess(df)
+        input_seq = processed_data[-config.SEQ_LEN:] 
+        input_tensor = torch.tensor(input_seq, dtype=torch.float32).unsqueeze(0).to(self.device)
+        
+        with torch.no_grad():
+            output = self.model(input_tensor)
+        
+        preds = output.cpu().numpy().reshape(config.OUTPUT_SIZE, config.LABELS_NUM)
+        
+        target_min = self.scaler.min_[-config.LABELS_NUM:]
+        target_scale = self.scaler.scale_[-config.LABELS_NUM:]
+        
+        real_preds = np.abs((preds - target_min) / target_scale)
+        return real_preds.tolist()
+
+if __name__ == "__main__":
+    parser = argparse.ArgumentParser()
+    parser.add_argument('-p', '--plant', required=True)
+    args = parser.parse_args()
+    
+    config.load(args.plant)
+    predictor = RealTimePredictor()
+    
+    mock_data = pd.DataFrame()
+    mock_data['index'] = pd.date_range(end=datetime.now(), periods=15, freq='min')
+    for col in config.REQUIRED_COLUMNS[1:]:
+        mock_data[col] = np.random.rand(15) * 10
+        
+    print(predictor.predict(mock_data))

+ 119 - 0
models/prediction_models/20min/yancheng/config.yaml

@@ -0,0 +1,119 @@
+# yancheng/config.yaml
+project:
+  plant_name: "yancheng"
+
+files:
+  dataset_dir: "datasets"
+  file_pattern: "data_process_{}.csv"
+  model_filename: "model.pth"
+  scaler_filename: "scaler.pkl"
+  output_csv_filename: "predictions.csv"
+  edge_index_filename: "edge_index.pt"
+
+data_split:
+  start_files: 1
+  end_files: 4
+  train_start_date: "2026-01-23"
+  train_end_date: "2026-02-20"
+  val_start_date: "2026-01-23"
+  val_end_date: "2026-02-20"
+  test_start_date: "2026-01-23"
+  test_end_date: "2026-02-20"
+
+model_params:
+  seq_len: 10
+  output_size: 5
+  step_size: 5
+  resolution: 60
+  hidden_size: 64
+  num_layers: 1
+  dropout: 0.0
+
+training_params:
+  epochs: 200
+  lr: 0.01
+  batch_size: 512
+  scheduler_step_size: 100
+  scheduler_gamma: 0.9
+  patience: 200
+  min_delta: 1.0e-10
+  device: 1          # 对应 args.device
+  random_seed: 1314
+
+sensors:
+  # 输入传感器列
+  required_columns:
+    - "index"
+    - "ns=3;s=1#RO_CSDD_O"
+    - "ns=3;s=1#RO_CSPRESS_O"
+    - "ns=3;s=1#RO_EDCSFLOW_O"
+    - "ns=3;s=1#RO_EDJSPRESS_O"
+    - "ns=3;s=1#RO_EDNSPRESS_O"
+    - "ns=3;s=1#RO_JSFLOW_O"
+    - "ns=3;s=1#RO_JSPRESS_O"
+    - "ns=3;s=1#RO_NSFLOW_O"
+    - "ns=3;s=1#RO_SDCSFLOW_O"
+    - "ns=3;s=1#RO_SDJSPRESS_O"
+    - "ns=3;s=1#RO_SDNSPRESS_O"
+    - "ns=3;s=1#RODJB_CUR_FB_O"
+    - "ns=3;s=1#RODJB_CZ_O"
+    - "ns=3;s=1#RODJB_FRE_FB_O"
+    - "ns=3;s=1#ROGYB_CUR_FB_O"
+    - "ns=3;s=1#ROGYB_CZ_O"
+    - "ns=3;s=1#ROGYB_FRE_FB_O"
+    - "ns=3;s=1#UF_CSPRESS_O"
+    - "ns=3;s=1#UF_JSFLOW_O"
+    - "ns=3;s=1#UF_JSPRESS_O"
+    - "ns=3;s=1#UF_V_FB_O"
+    - "ns=3;s=1#UFBWB_CUR_FB_O"
+    - "ns=3;s=1#UFBWB_FRE_FB_O"
+    - "ns=3;s=2#RO_CSDD_O"
+    - "ns=3;s=2#RO_CSPRESS_O"
+    - "ns=3;s=2#RO_EDCSFLOW_O"
+    - "ns=3;s=2#RO_EDJSPRESS_O"
+    - "ns=3;s=2#RO_EDNSPRESS_O"
+    - "ns=3;s=2#RO_JSFLOW_O"
+    - "ns=3;s=2#RO_JSPRESS_O"
+    - "ns=3;s=2#RO_NSFLOW_O"
+    - "ns=3;s=2#RO_SDCSFLOW_O"
+    - "ns=3;s=2#RO_SDJSPRESS_O"
+    - "ns=3;s=2#RO_SDNSPRESS_O"
+    - "ns=3;s=2#RODJB_CUR_FB_O"
+    - "ns=3;s=2#RODJB_CZ_O"
+    - "ns=3;s=2#RODJB_FRE_FB_O"
+    - "ns=3;s=2#ROGYB_CUR_FB_O"
+    - "ns=3;s=2#ROGYB_CZ_O"
+    - "ns=3;s=2#ROGYB_FRE_FB_O"
+    - "ns=3;s=2#UF_CSPRESS_O"
+    - "ns=3;s=2#UF_JSFLOW_O"
+    - "ns=3;s=2#UF_JSPRESS_O"
+    - "ns=3;s=2#UF_V_FB_O"
+    - "ns=3;s=2#UFBWB_CUR_FB_O"
+    - "ns=3;s=2#UFBWB_FRE_FB_O"
+    - "ns=3;s=RO_JSDD_O"
+    - "ns=3;s=RO_JSORP_O"
+    - "ns=3;s=RO_JSPH_O"
+    - "ns=3;s=RO_WSDD_O"
+    - "ns=3;s=ZJS_ZD_O"
+    - "water_out"
+    - "water_in"
+    - "ns=3;s=V_UF1_TJV_KD_FB"
+    - "ns=3;s=V_UF2_TJV_KD_FB"
+    - "ns=3;s=ZJS_PRESS_O"
+    - "ns=3;s=ZJS_TEMP_O"
+    - "ns=3;s=UF_CS_ZD_O"
+    - "ns=3;s=UF1_SSD_KMYC"
+    - "ns=3;s=UF2_SSD_KMYC"
+    - "ns=3;s=RO1_1D_YC"
+    - "ns=3;s=RO1_2D_YC"
+    - "ns=3;s=RO2_1D_YC"
+    - "ns=3;s=RO2_2D_YC"
+  
+  # 最终预测目标列
+  target_columns:
+    - "ns=3;s=UF1_SSD_KMYC"
+    - "ns=3;s=UF2_SSD_KMYC"
+    - "ns=3;s=RO1_1D_YC"
+    - "ns=3;s=RO1_2D_YC"
+    - "ns=3;s=RO2_1D_YC"
+    - "ns=3;s=RO2_2D_YC"

BIN
models/prediction_models/20min/yancheng/edge_index.pt


BIN
models/prediction_models/20min/yancheng/id_list.xlsx


+ 65 - 0
models/prediction_models/20min/yancheng/input_format.txt

@@ -0,0 +1,65 @@
+index
+ns=3;s=1#RO_CSDD_O
+ns=3;s=1#RO_CSPRESS_O
+ns=3;s=1#RO_EDCSFLOW_O
+ns=3;s=1#RO_EDJSPRESS_O
+ns=3;s=1#RO_EDNSPRESS_O
+ns=3;s=1#RO_JSFLOW_O
+ns=3;s=1#RO_JSPRESS_O
+ns=3;s=1#RO_NSFLOW_O
+ns=3;s=1#RO_SDCSFLOW_O
+ns=3;s=1#RO_SDJSPRESS_O
+ns=3;s=1#RO_SDNSPRESS_O
+ns=3;s=1#RODJB_CUR_FB_O
+ns=3;s=1#RODJB_CZ_O
+ns=3;s=1#RODJB_FRE_FB_O
+ns=3;s=1#ROGYB_CUR_FB_O
+ns=3;s=1#ROGYB_CZ_O
+ns=3;s=1#ROGYB_FRE_FB_O
+ns=3;s=1#UF_CSPRESS_O
+ns=3;s=1#UF_JSFLOW_O
+ns=3;s=1#UF_JSPRESS_O
+ns=3;s=1#UF_V_FB_O
+ns=3;s=1#UFBWB_CUR_FB_O
+ns=3;s=1#UFBWB_FRE_FB_O
+ns=3;s=2#RO_CSDD_O
+ns=3;s=2#RO_CSPRESS_O
+ns=3;s=2#RO_EDCSFLOW_O
+ns=3;s=2#RO_EDJSPRESS_O
+ns=3;s=2#RO_EDNSPRESS_O
+ns=3;s=2#RO_JSFLOW_O
+ns=3;s=2#RO_JSPRESS_O
+ns=3;s=2#RO_NSFLOW_O
+ns=3;s=2#RO_SDCSFLOW_O
+ns=3;s=2#RO_SDJSPRESS_O
+ns=3;s=2#RO_SDNSPRESS_O
+ns=3;s=2#RODJB_CUR_FB_O
+ns=3;s=2#RODJB_CZ_O
+ns=3;s=2#RODJB_FRE_FB_O
+ns=3;s=2#ROGYB_CUR_FB_O
+ns=3;s=2#ROGYB_CZ_O
+ns=3;s=2#ROGYB_FRE_FB_O
+ns=3;s=2#UF_CSPRESS_O
+ns=3;s=2#UF_JSFLOW_O
+ns=3;s=2#UF_JSPRESS_O
+ns=3;s=2#UF_V_FB_O
+ns=3;s=2#UFBWB_CUR_FB_O
+ns=3;s=2#UFBWB_FRE_FB_O
+ns=3;s=RO_JSDD_O
+ns=3;s=RO_JSORP_O
+ns=3;s=RO_JSPH_O
+ns=3;s=RO_WSDD_O
+ns=3;s=ZJS_ZD_O
+water_out
+water_in
+ns=3;s=V_UF1_TJV_KD_FB
+ns=3;s=V_UF2_TJV_KD_FB
+ns=3;s=ZJS_PRESS_O
+ns=3;s=ZJS_TEMP_O
+ns=3;s=UF_CS_ZD_O
+ns=3;s=UF1_SSD_KMYC
+ns=3;s=UF2_SSD_KMYC
+ns=3;s=RO1_1D_YC
+ns=3;s=RO1_2D_YC
+ns=3;s=RO2_1D_YC
+ns=3;s=RO2_2D_YC

BIN
models/prediction_models/20min/yancheng/model.pth


+ 2 - 0
models/prediction_models/20min/yancheng/output_format.txt

@@ -0,0 +1,2 @@
+预测结果 (5x6 数组):
+[[0.16467712603358925, 0.1420893242437765, 0.005848054533362389, 0.03449316009426117, 0.012201102976739408, 0.020282663398236037], [0.23638365379285814, 0.004008417605882278, 0.01604065936946869, 0.030827085905849934, 0.016900173844426873, 0.13388433202683928], [0.07216809350723774, 0.01757756684007123, 0.002563064258337021, 0.05860868276381492, 0.014152163420856, 0.051853824430108074], [0.04638918270112574, 0.05213489476729184, 0.08477990115451813, 0.041321578139781955, 0.03729044299280644, 0.03419597425207496], [0.11022086086758973, 0.015112509772586637, 0.029727122640609744, 0.003631668274689466, 0.032843230359852316, 0.04623711044788361]]

BIN
models/prediction_models/20min/yancheng/scaler.pkl


+ 0 - 51
models/prediction_models/anzhen/args.py

@@ -1,51 +0,0 @@
-# args.py
-import argparse
-
-def lstm_args_parser():
-    parser = argparse.ArgumentParser(description="LSTM模型训练参数")
-    
-    # 核心数据集参数
-    parser.add_argument('--train_start_date', type=str, default='2024-10-09', help='训练集开始日期')
-    parser.add_argument('--train_end_date', type=str, default='2025-03-24', help='训练集结束日期')
-    parser.add_argument('--val_start_date', type=str, default='2024-10-09', help='验证集开始日期')
-    parser.add_argument('--val_end_date', type=str, default='2025-03-24', help='验证集结束日期')
-    parser.add_argument('--test_start_date', type=str, default='2024-10-09', help='测试集开始日期')
-    parser.add_argument('--test_end_date', type=str, default='2025-03-24', help='测试集结束日期')
-
-    # 模型架构参数
-    parser.add_argument('--seq_len', type=int, default=10, help='输入序列长度')
-    parser.add_argument('--output_size', type=int, default=5, help='预测步长')
-    parser.add_argument('--step_size', type=int, default=5, help='采样步长')
-    parser.add_argument('--resolution', type=int, default=60, help='数据分辨率(分钟)')
-    parser.add_argument('--feature_num', type=int, default=42, help='输入特征维度')
-    parser.add_argument('--labels_num', type=int, default=4, help='预测标签数量(子模型数量)')
-    
-    # 训练超参数
-    parser.add_argument('--epochs', type=int, default=200, help='训练轮数')
-    parser.add_argument('--hidden_size', type=int, default=64, help='隐藏层大小')
-    parser.add_argument('--num_layers', type=int, default=1, help='LSTM层数')
-    parser.add_argument('--dropout', type=float, default=0, help='dropout概率')
-    parser.add_argument('--lr', type=float, default=0.01, help='学习率')
-    parser.add_argument('--batch_size', type=int, default=512, help='批次大小')
-    
-    parser.add_argument('--scheduler_step_size', type=int, default=100, help='学习率调整步长')
-    parser.add_argument('--scheduler_gamma', type=float, default=0.9, help='学习率衰减率')
-    
-    parser.add_argument('--patience', type=int, default=200, help='早停耐心值')
-    parser.add_argument('--min_delta', type=float, default=1e-10, help='最小改善阈值')
-    parser.add_argument('--device', type=int, default=1, help='GPU设备ID')
-
-    # 文件路径配置
-    parser.add_argument('--start_files', type=int, default=1, help='开始文件索引')
-    parser.add_argument('--end_files', type=int, default=17, help='结束文件索引')
-    parser.add_argument('--data_dir', type=str, default='datasets_anzhen', help='数据文件夹路径')
-    parser.add_argument('--file_pattern', type=str, default='data_process_{}.csv', help='数据文件命名模式')
-    
-    parser.add_argument('--model_path', type=str, default='model.pth', help='模型保存路径')
-    parser.add_argument('--scaler_path', type=str, default='scaler.pkl', help='归一化器路径')
-    parser.add_argument('--output_csv_path', type=str, default='predictions.csv', help='预测评估结果路径')
-    
-    parser.add_argument('--random_seed', type=int, default=1314, help='随机种子')
-
-    args = parser.parse_args()
-    return args

+ 0 - 221
models/prediction_models/anzhen/data_preprocessor.py

@@ -1,221 +0,0 @@
-# data_preprocessor.py
-import os
-import torch
-import joblib
-import numpy as np
-import pandas as pd
-from tqdm import tqdm
-from sklearn.preprocessing import MinMaxScaler
-from torch.utils.data import DataLoader, TensorDataset
-from concurrent.futures import ThreadPoolExecutor
-
-class DataPreprocessor:
-    """数据预处理类"""
-    
-    # 定义必须保留的列
-    COLUMNS_TO_KEEP = [
-            'index', 
-            "AR.1#UF_JSFLOW_O",         # 1#UF进水流量
-            "AR.2#UF_JSFLOW_O",         # 2#UF进水流量
-            "AR.1#RO_JSFLOW_O",         # 1#RO进水流量
-            "AR.2#RO_JSFLOW_O",         # 2#RO进水流量
-            "AR.1#UF_JSPRESS_O",        # 1#UF进水压力
-            "AR.2#UF_JSPRESS_O",        # 2#UF进水压力
-            "AR.1#RO_JSPRESS_O",        # 1#RO进水压力
-            "AR.2#RO_JSPRESS_O",        # 2#RO进水压力
-            "AR.1#RO_EDJSPRESS_O",      # 1#RO二段进水压力
-            "AR.1#RO_SDJSPRESS_O",      # 1#RO三段进水压力
-            "AR.2#RO_EDJSPRESS_O",      # 2#RO二段进水压力
-            "AR.2#RO_SDJSPRESS_O",      # 2#RO三段进水压力
-            "AR.ZJS_TEMP_O",            # 进水温度
-            "AR.ZJS_ZD_O",              # UF进水浊度
-            "AR.RO_JSDD_O",             # RO进水电导
-            "AR.RO_JSORP_O",            # RO进水ORP
-            "AR.RO_JSPH_O",             # RO进水PH
-            "AR.1#UF_V_FB_O",           # 1#UF调节阀开度反馈
-            "AR.2#UF_V_FB_O",           # 2#UF调节阀开度反馈
-            "AR.1#UFBWB_FRE_FB_O",      # 1#UF反洗泵频率反馈
-            "AR.2#UFBWB_FRE_FB_O",      # 2#UF反洗泵频率反馈
-            "AR.1#RODJB_FRE_FB_O",      # 1#RO段间泵频率反馈
-            "AR.1#ROGYB_FRE_FB_O",      # 1#RO高压泵频率反馈
-            "AR.1#RODJB_CZ_O",          # 1#RO段间泵测振反馈
-            "AR.1#ROGYB_CZ_O",          # 1#RO高压泵测振反馈
-            "AR.2#RODJB_CZ_O",          # 2#RO段间泵测振反馈
-            "AR.2#ROGYB_CZ_O",          # 2#RO高压泵测振反馈
-            "AR.ROGSB_FRE_FB_O",        # RO供水泵频率反馈
-            "AR.UFGSB_FRE_FB_O",        # UF供水泵频率反馈
-            "AR.V_UF1_TJV_KD_FB",       # UF1调节阀开度反馈
-            "AR.V_UF2_TJV_KD_FB",       # UF2调节阀开度反馈
-            "AR.CS_LEVEL_O",            # RO产水箱液位
-            "AR.UF_CSLEVEL_O",          # UF产水箱液位
-            "AR.UF1_SSD_KMYC",          # UF1跨膜压差
-            "AR.UF2_SSD_KMYC",          # UF2跨膜压差
-            "AR.RO1_2D_YC",             # RO1二段压差
-            "AR.PUBLIC_BY_REAL_1",      # RO1三段压差
-            "1#RO_CSFLOW",              # 1#RO产水流量
-    ]
-
-    @staticmethod
-    def load_and_process_data(args, data):
-        """加载并处理数据,划分训练/验证/测试集"""
-        # 处理日期
-        data['date'] = pd.to_datetime(data['date'])
-        time_interval = pd.Timedelta(minutes=(4 * args.resolution / 60))
-        window_time_span = time_interval * (args.seq_len + 1)
-
-        val_start_date = pd.to_datetime(args.val_start_date)
-        test_start_date = pd.to_datetime(args.test_start_date)
-        
-        # 调整时间窗口
-        adjusted_val_start = val_start_date - window_time_span
-        adjusted_test_start = test_start_date - window_time_span
-        
-        train_mask = (data['date'] >= pd.to_datetime(args.train_start_date)) & \
-                     (data['date'] <= pd.to_datetime(args.train_end_date))
-        val_mask = (data['date'] >= adjusted_val_start) & \
-                   (data['date'] <= pd.to_datetime(args.val_end_date))
-        test_mask = (data['date'] >= adjusted_test_start) & \
-                    (data['date'] <= pd.to_datetime(args.test_end_date))
-
-        train_data = data[train_mask].reset_index(drop=True)
-        val_data = data[val_mask].reset_index(drop=True)
-        test_data = data[test_mask].reset_index(drop=True)
-        
-        train_data = train_data.drop(columns=['date'])
-        val_data = val_data.drop(columns=['date'])
-        test_data = test_data.drop(columns=['date'])
-    
-        # 创建数据集
-        train_supervised = DataPreprocessor.create_supervised_dataset(args, train_data, 1)
-        val_supervised = DataPreprocessor.create_supervised_dataset(args, val_data, 1)
-        test_supervised = DataPreprocessor.create_supervised_dataset(args, test_data, args.step_size)
-        
-        # 转换为DataLoader
-        train_loader = DataPreprocessor.load_data(args, train_supervised, shuffle=True)
-        val_loader = DataPreprocessor.load_data(args, val_supervised, shuffle=False)
-        test_loader = DataPreprocessor.load_data(args, test_supervised, shuffle=False)
-        
-        return train_loader, val_loader, test_loader, data
-    
-    @staticmethod
-    def read_and_combine_csv_files(args):
-        """读取文件并进行特征筛选和预处理"""
-        current_dir = os.path.dirname(__file__)
-        parent_dir = os.path.dirname(current_dir)
-        args.data_dir = os.path.join(parent_dir, args.data_dir)
-        
-        def read_file(file_count):
-            file_name = args.file_pattern.format(file_count)
-            file_path = os.path.join(args.data_dir, file_name)
-            try:
-                df = pd.read_csv(file_path)
-                # 确保只读取需要的列,若列不存在则会报错提示
-                return df[DataPreprocessor.COLUMNS_TO_KEEP]
-            except KeyError as e:
-                print(f"文件 {file_name} 中缺少列: {e}")
-                raise
-        
-        file_indices = list(range(args.start_files, args.end_files + 1))
-        max_workers = os.cpu_count()
-        
-        with ThreadPoolExecutor(max_workers=max_workers) as executor:
-            results = list(tqdm(executor.map(read_file, file_indices),
-                                total=len(file_indices),
-                                desc="正在读取文件"))
-        
-        all_data = pd.concat(results, ignore_index=True)
-        
-        # 确保列顺序一致
-        all_data = all_data[DataPreprocessor.COLUMNS_TO_KEEP]
-        
-        # 下采样
-        chunk = all_data.iloc[::args.resolution, :].reset_index(drop=True)
-        
-        # 处理特征
-        chunk = DataPreprocessor.process_date(chunk, args)
-        chunk = DataPreprocessor.scaler_data(chunk, args)
-        
-        return chunk
-    
-    @staticmethod
-    def process_date(data, args):
-        data = data.rename(columns={'index': 'date'})
-        data['date'] = pd.to_datetime(data['date'])
-    
-        time_features = []
-        # 固定生成分钟级和日级特征,保持与Predictor一致
-        data['minute_of_day'] = data['date'].dt.hour * 60 + data['date'].dt.minute
-        data['minute_sin'] = np.sin(2 * np.pi * data['minute_of_day'] / 1440)
-        data['minute_cos'] = np.cos(2 * np.pi * data['minute_of_day'] / 1440)
-        
-        data['day_of_year'] = data['date'].dt.dayofyear
-        data['day_year_sin'] = np.sin(2 * np.pi * data['day_of_year'] / 366)
-        data['day_year_cos'] = np.cos(2 * np.pi * data['day_of_year'] / 366)
-        
-        time_features.extend(['minute_sin', 'minute_cos', 'day_year_sin', 'day_year_cos'])
-        data.drop(columns=['minute_of_day', 'day_of_year'], inplace=True)
-    
-        other_columns = [col for col in data.columns if col not in ['date'] and col not in time_features]
-        data = data[['date'] + time_features + other_columns]
-        return data
-    
-    @staticmethod
-    def scaler_data(data, args):
-        date_col = data[['date']]
-        data_to_scale = data.drop(columns=['date'])
-
-        scaler = MinMaxScaler(feature_range=(0, 1))
-        scaled_data = scaler.fit_transform(data_to_scale)
-        joblib.dump(scaler, args.scaler_path)
-
-        scaled_data = pd.DataFrame(scaled_data, columns=data_to_scale.columns)
-        scaled_data = pd.concat([date_col.reset_index(drop=True), scaled_data], axis=1)
-        return scaled_data
-    
-    @staticmethod
-    def create_supervised_dataset(args, data, step_size):
-        data = pd.DataFrame(data)
-        cols = []
-        col_names = []
-        feature_columns = data.columns.tolist()
-
-        # 输入序列
-        for col in feature_columns:
-            for i in range(args.seq_len - 1, -1, -1):
-                cols.append(data[[col]].shift(i))
-                col_names.append(f"{col}(t-{i})")
-        
-        # 目标序列 (取最后labels_num列)
-        target_columns = feature_columns[-args.labels_num:]
-        for i in range(1, args.output_size + 1):
-            for col in target_columns:
-                cols.append(data[[col]].shift(-i))
-                col_names.append(f"{col}(t+{i})")
-
-        dataset = pd.concat(cols, axis=1)
-        dataset.columns = col_names
-        dataset = dataset.iloc[::step_size, :]
-        dataset.dropna(inplace=True)
-        return dataset
-
-    @staticmethod
-    def load_data(args, dataset, shuffle):
-        input_length = args.seq_len
-        n_features = args.feature_num
-        labels_num = args.labels_num
-    
-        n_features_total = n_features * input_length
-        n_labels_total = args.output_size * labels_num
-
-        X = dataset.values[:, :n_features_total]
-        y = dataset.values[:, n_features_total:n_features_total + n_labels_total]
-    
-        X = X.reshape(X.shape[0], input_length, n_features)
-        X = torch.tensor(X, dtype=torch.float32).to(args.device)
-        y = torch.tensor(y, dtype=torch.float32).to(args.device)
-
-        dataset_tensor = TensorDataset(X, y)
-        generator = torch.Generator()
-        generator.manual_seed(args.random_seed)
-        
-        return DataLoader(dataset_tensor, batch_size=args.batch_size, shuffle=shuffle, generator=generator)

+ 0 - 165
models/prediction_models/anzhen/data_trainer.py

@@ -1,165 +0,0 @@
-# data_trainer.py
-import torch
-import joblib
-import numpy as np
-import pandas as pd
-from sklearn.metrics import r2_score
-from datetime import datetime, timedelta
-from sklearn.preprocessing import MinMaxScaler
-
-class Trainer:
-    def __init__(self, model, args, data):
-        self.args = args
-        self.model = model
-        self.data = data
-        self.patience = args.patience
-        self.min_delta = args.min_delta
-        self.counter = 0
-        self.early_stop = False
-        self.best_val_loss = float('inf')
-        self.best_model_state = None
-        self.best_epoch = 0
-
-    def train_full_model(self, train_loader, val_loader, optimizer, criterion, scheduler):
-        self.counter = 0
-        self.best_val_loss = float('inf')
-        self.early_stop = False
-        self.best_model_state = None
-        self.best_epoch = 0
-        max_epochs = self.args.epochs
-
-        for epoch in range(max_epochs):
-            self.model.train()
-            running_loss = 0.0
-            
-            for inputs, targets in train_loader:
-                inputs = inputs.to(self.args.device)
-                targets = targets.to(self.args.device)
-                optimizer.zero_grad()
-                outputs = self.model(inputs)
-                loss = criterion(outputs, targets)
-                loss.backward()
-                optimizer.step()
-                running_loss += loss.item()
-            
-            train_loss = running_loss / len(train_loader)
-            val_loss = self.validate_full(val_loader, criterion) if val_loader else 0.0
-
-            print(f'Epoch {epoch+1}/{max_epochs}, Train Loss: {train_loss:.6f}, Val Loss: {val_loss:.6f}')
-
-            if val_loader:
-                if val_loss < (self.best_val_loss - self.min_delta):
-                    self.best_val_loss = val_loss
-                    self.counter = 0
-                    self.best_model_state = self.model.state_dict()
-                    self.best_epoch = epoch
-                else:
-                    self.counter += 1
-                    if self.counter >= self.patience:
-                        self.early_stop = True
-                        print(f"早停触发")
-                        
-            scheduler.step()
-            torch.cuda.empty_cache()
-            if self.early_stop:
-                break
-
-        if self.best_model_state is not None:
-            self.model.load_state_dict(self.best_model_state)
-        print(f"最佳迭代: {self.best_epoch+1}, 最佳验证损失: {self.best_val_loss:.6f}")
-        return self.model
-
-    def validate_full(self, val_loader, criterion):
-        self.model.eval()
-        total_loss = 0.0
-        with torch.no_grad():
-            for inputs, targets in val_loader:
-                inputs = inputs.to(self.args.device)
-                targets = targets.to(self.args.device)
-                outputs = self.model(inputs)
-                loss = criterion(outputs, targets)
-                total_loss += loss.item()
-        return total_loss / len(val_loader)
-
-    def save_model(self):
-        torch.save(self.model.state_dict(), self.args.model_path)
-        print(f"模型已保存到:{self.args.model_path}")
-            
-    def evaluate_model(self, test_loader, criterion):
-        self.model.eval()
-        scaler = joblib.load(self.args.scaler_path)
-        predictions = []
-        true_values = []
-        
-        with torch.no_grad():
-            for inputs, targets in test_loader:
-                inputs = inputs.to(self.args.device)
-                targets = targets.to(self.args.device)
-                outputs = self.model(inputs)
-                predictions.append(outputs.cpu().numpy())
-                true_values.append(targets.cpu().numpy())
-    
-        predictions = np.concatenate(predictions, axis=0)
-        true_values = np.concatenate(true_values, axis=0)
-    
-        # 重塑
-        reshaped_predictions = predictions.reshape(predictions.shape[0], self.args.output_size, self.args.labels_num)
-        predictions = reshaped_predictions.reshape(-1, self.args.labels_num)
-        
-        reshaped_true_values = true_values.reshape(true_values.shape[0], self.args.output_size, self.args.labels_num)
-        true_values = reshaped_true_values.reshape(-1, self.args.labels_num)
-    
-        # 反归一化 (仅标签列)
-        column_scaler = MinMaxScaler(feature_range=(0, 1))
-        column_scaler.min_ = scaler.min_[-self.args.labels_num:] 
-        column_scaler.scale_ = scaler.scale_[-self.args.labels_num:] 
-        
-        true_values = column_scaler.inverse_transform(true_values)
-        predictions = column_scaler.inverse_transform(predictions)
-    
-        # 定义4个核心变量
-        column_names = [
-            "AR.UF1_SSD_KMYC",          # UF1跨膜压差
-            "AR.RO1_2D_YC",             # RO1二段压差
-            "AR.PUBLIC_BY_REAL_1",      # RO1三段压差
-            "1#RO_CSFLOW",              # 1#RO产水流量
-        ]
-    
-        # 生成时间
-        start_datetime = datetime.strptime(self.args.test_start_date, "%Y-%m-%d")
-        time_interval = timedelta(minutes=(4 * self.args.resolution / 60))
-        total_points = len(predictions)
-        date_times = [start_datetime + i * time_interval for i in range(total_points)]
-        
-        results = pd.DataFrame({'date': date_times})
-        metrics_details = []
-        
-        for i, col_name in enumerate(column_names):
-            if i >= self.args.labels_num: break # 防止越界
-            
-            results[f'{col_name}_True'] = true_values[:, i]
-            results[f'{col_name}_Predicted'] = predictions[:, i]
-            
-            var_true = true_values[:, i]
-            var_pred = predictions[:, i]
-            
-            # 指标计算
-            non_zero_mask = var_true != 0
-            var_true_nonzero = var_true[non_zero_mask]
-            var_pred_nonzero = var_pred[non_zero_mask]
-            
-            if len(var_true_nonzero) > 0:
-                r2 = r2_score(var_true_nonzero, var_pred_nonzero)
-                rmse = np.sqrt(np.mean((var_true_nonzero - var_pred_nonzero) ** 2))
-                mape = np.mean(np.abs((var_true_nonzero - var_pred_nonzero) / np.abs(var_true_nonzero))) * 100
-                metrics_details.append(f"{col_name}: R2={r2:.4f}, RMSE={rmse:.4f}, MAPE={mape:.4f}%")
-            else:
-                metrics_details.append(f"{col_name}: 无效数据")
-
-        results.to_csv(self.args.output_csv_path, index=False)
-        
-        txt_path = self.args.output_csv_path.replace('.csv', '_metrics.txt')
-        with open(txt_path, 'w') as f:
-            f.write('\n'.join(metrics_details))
-            
-        return metrics_details

+ 0 - 54
models/prediction_models/anzhen/gat_lstm.py

@@ -1,54 +0,0 @@
-# gat_lstm.py
-import torch
-import torch.nn as nn
-
-class SingleGATLSTM(nn.Module):
-    """单个子模型:预测1个目标指标"""
-    def __init__(self, args):
-        super(SingleGATLSTM, self).__init__()
-        self.args = args
-        
-        self.lstm = nn.LSTM(
-            input_size=args.feature_num,
-            hidden_size=args.hidden_size,
-            num_layers=args.num_layers,
-            batch_first=True
-        )
-        
-        self.final_linear = nn.Sequential(
-            nn.Linear(args.hidden_size, args.hidden_size),
-            nn.LeakyReLU(0.01),
-            nn.Dropout(args.dropout * 0.4),
-            nn.Linear(args.hidden_size, args.output_size)
-        )
-        self._init_weights()
-        
-    def _init_weights(self):
-        for m in self.modules():
-            if isinstance(m, nn.Linear):
-                nn.init.xavier_uniform_(m.weight)
-                if m.bias is not None: nn.init.zeros_(m.bias)
-
-    def forward(self, x):
-        batch_size, seq_len, feature_num = x.size()
-        lstm_out, _ = self.lstm(x)
-        last_out = lstm_out[:, -1, :]
-        output = self.final_linear(last_out)
-        return output
-
-class GAT_LSTM(nn.Module):
-    """总模型:包含多个SingleGATLSTM子模型"""
-    def __init__(self, args):
-        super(GAT_LSTM, self).__init__()
-        self.args = args
-        # 创建4个独立模型(对应labels_num=4)
-        self.models = nn.ModuleList([SingleGATLSTM(args) for _ in range(args.labels_num)])
-    
-    def set_edge_index(self, edge_index):
-        self.edge_index = edge_index
-        
-    def forward(self, x):
-        outputs = []
-        for model in self.models:
-            outputs.append(model(x))
-        return torch.cat(outputs, dim=1)

+ 0 - 59
models/prediction_models/anzhen/main.py

@@ -1,59 +0,0 @@
-# main.py
-import os
-import torch
-import numpy as np
-import random
-from gat_lstm import GAT_LSTM
-from data_trainer import Trainer
-from args import lstm_args_parser
-from torch.nn import MSELoss
-from data_preprocessor import DataPreprocessor
-
-def set_seed(seed):
-    random.seed(seed)
-    os.environ['PYTHONHASHSEED'] = str(seed)
-    np.random.seed(seed)
-    torch.manual_seed(seed)
-    torch.cuda.manual_seed(seed)
-    torch.backends.cudnn.deterministic = True
-    torch.backends.cudnn.benchmark = False
-
-def main():
-    args = lstm_args_parser()
-    set_seed(args.random_seed)
-    
-    device = torch.device(f"cuda:{args.device}" if torch.cuda.is_available() else "cpu")
-    args.device = device
-
-    print(f"当前配置: 序列长度={args.seq_len}, 特征数={args.feature_num}, 目标数={args.labels_num}")
-
-    # 数据预处理
-    data = DataPreprocessor.read_and_combine_csv_files(args)
-    train_loader, val_loader, test_loader, _ = DataPreprocessor.load_and_process_data(args, data)
-    
-    # 初始化模型
-    model = GAT_LSTM(args).to(device)
-    
-    # 加载 edge_index.pt 
-    if os.path.exists('edge_index.pt'):
-        edge_index = torch.load('edge_index.pt', map_location=device, weights_only=True)
-        model.set_edge_index(edge_index)
-        print("已加载 edge_index.pt")
-    else:
-        print("未找到 edge_index.pt")
-
-    # 训练器
-    trainer = Trainer(model, args, data)
-    criterion = MSELoss()
-    optimizer = torch.optim.Adam(model.parameters(), lr=args.lr)
-    scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=args.scheduler_step_size, gamma=args.scheduler_gamma)
-
-    print("=== 开始训练 ===")
-    trainer.train_full_model(train_loader, val_loader, optimizer, criterion, scheduler)
-    trainer.save_model()
-    
-    print("=== 开始评估 ===")
-    trainer.evaluate_model(test_loader, MSELoss())
-
-if __name__ == "__main__":
-    main()

+ 0 - 271
models/prediction_models/anzhen/predict.py

@@ -1,271 +0,0 @@
-# predict.py
-import os
-import torch
-import joblib
-import pandas as pd
-import numpy as np
-from datetime import datetime, timedelta
-from gat_lstm import GAT_LSTM
-
-class RealTimePredictor:
-    def __init__(self, model_path='model.pth', scaler_path='scaler.pkl', device=None):
-        """
-        初始化预测器
-        """
-        # 1. 参数配置 (与训练 args.py 保持一致)
-        self.seq_len = 10         # 输入序列长度
-        self.feature_num = 42     # 输入特征数 (4时间编码 + 38业务特征)
-        self.labels_num = 4       # 输出标签数
-        self.hidden_size = 64
-        self.num_layers = 1
-        self.output_size = 5      # 预测未来 5 步
-        self.dropout = 0
-        
-        # 2. 设备与资源加载
-        self.device = device if device else torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
-        self.model_path = model_path
-        self.scaler_path = scaler_path
-        
-        # 加载归一化器
-        if not os.path.exists(self.scaler_path):
-             raise FileNotFoundError(f"未找到归一化文件: {self.scaler_path},请确保已完成训练。")
-        self.scaler = joblib.load(self.scaler_path)
-
-        # 加载模型
-        self._load_model()
-
-        # 定义必须存在的列名 (39个,包含index,顺序必须固定)
-        self.required_columns = [
-            'index', 
-            "AR.1#UF_JSFLOW_O",         # 1#UF进水流量
-            "AR.2#UF_JSFLOW_O",         # 2#UF进水流量
-            "AR.1#RO_JSFLOW_O",         # 1#RO进水流量
-            "AR.2#RO_JSFLOW_O",         # 2#RO进水流量
-            "AR.1#UF_JSPRESS_O",        # 1#UF进水压力
-            "AR.2#UF_JSPRESS_O",        # 2#UF进水压力
-            "AR.1#RO_JSPRESS_O",        # 1#RO进水压力
-            "AR.2#RO_JSPRESS_O",        # 2#RO进水压力
-            "AR.1#RO_EDJSPRESS_O",      # 1#RO二段进水压力
-            "AR.1#RO_SDJSPRESS_O",      # 1#RO三段进水压力
-            "AR.2#RO_EDJSPRESS_O",      # 2#RO二段进水压力
-            "AR.2#RO_SDJSPRESS_O",      # 2#RO三段进水压力
-            "AR.ZJS_TEMP_O",            # 进水温度
-            "AR.ZJS_ZD_O",              # UF进水浊度
-            "AR.RO_JSDD_O",             # RO进水电导
-            "AR.RO_JSORP_O",            # RO进水ORP
-            "AR.RO_JSPH_O",             # RO进水PH
-            "AR.1#UF_V_FB_O",           # 1#UF调节阀开度反馈
-            "AR.2#UF_V_FB_O",           # 2#UF调节阀开度反馈
-            "AR.1#UFBWB_FRE_FB_O",      # 1#UF反洗泵频率反馈
-            "AR.2#UFBWB_FRE_FB_O",      # 2#UF反洗泵频率反馈
-            "AR.1#RODJB_FRE_FB_O",      # 1#RO段间泵频率反馈
-            "AR.1#ROGYB_FRE_FB_O",      # 1#RO高压泵频率反馈
-            "AR.1#RODJB_CZ_O",          # 1#RO段间泵测振反馈
-            "AR.1#ROGYB_CZ_O",          # 1#RO高压泵测振反馈
-            "AR.2#RODJB_CZ_O",          # 2#RO段间泵测振反馈
-            "AR.2#ROGYB_CZ_O",          # 2#RO高压泵测振反馈
-            "AR.ROGSB_FRE_FB_O",        # RO供水泵频率反馈
-            "AR.UFGSB_FRE_FB_O",        # UF供水泵频率反馈
-            "AR.V_UF1_TJV_KD_FB",       # UF1调节阀开度反馈
-            "AR.V_UF2_TJV_KD_FB",       # UF2调节阀开度反馈
-            "AR.CS_LEVEL_O",            # RO产水箱液位
-            "AR.UF_CSLEVEL_O",          # UF产水箱液位
-            "AR.UF1_SSD_KMYC",          # UF1跨膜压差
-            "AR.UF2_SSD_KMYC",          # UF2跨膜压差
-            "AR.RO1_2D_YC",             # RO1二段压差
-            "AR.PUBLIC_BY_REAL_1",      # RO1三段压差
-            "1#RO_CSFLOW",              # 1#RO产水流量
-        ]
-        
-        # 用于防空值兜底机制的变量
-        self.raw_input_data = None
-        self.target_columns = self.required_columns[-self.labels_num:]
-
-    def _load_model(self):
-        """内部方法:加载模型权重"""
-        class ModelArgs: pass
-        args = ModelArgs()
-        args.feature_num = self.feature_num
-        args.hidden_size = self.hidden_size
-        args.num_layers = self.num_layers
-        args.output_size = self.output_size
-        args.labels_num = self.labels_num
-        args.dropout = self.dropout
-
-        self.model = GAT_LSTM(args).to(self.device)
-        
-        # 加载 edge_index.pt 
-        if os.path.exists('edge_index.pt'):
-            edge_index = torch.load('edge_index.pt', map_location=self.device, weights_only=True)
-            self.model.set_edge_index(edge_index)
-        
-        if not os.path.exists(self.model_path):
-            raise FileNotFoundError(f"未找到模型权重文件: {self.model_path}")
-            
-        state_dict = torch.load(self.model_path, map_location=self.device, weights_only=True)
-        self.model.load_state_dict(state_dict)
-        self.model.eval()
-
-    def _preprocess(self, df):
-        """数据预处理:补全、排序、生成时间特征、整体归一化"""
-        data = df.copy()
-        
-        # 1. 统一时间列名
-        if 'datetime' in data.columns:
-            data = data.rename(columns={'datetime': 'index'})
-        if 'index' not in data.columns:
-             data['index'] = pd.date_range(end=datetime.now(), periods=len(data), freq='min')
-        data['index'] = pd.to_datetime(data['index'])
-        
-        # 2. 补全长度 (Padding)
-        if len(data) < self.seq_len:
-            pad_len = self.seq_len - len(data)
-            first_row = data.iloc[0:1]
-            pads = pd.concat([first_row] * pad_len, ignore_index=True)
-            start_time = data['index'].iloc[0]
-            for i in range(pad_len):
-                pads.at[i, 'index'] = start_time - timedelta(minutes=(pad_len-i))
-            data = pd.concat([pads, data], ignore_index=True)
-
-        # 3. 列筛选排序 (提取业务数据,不含index)
-        try:
-            # required_columns[0] 是 'index',我们取后面的业务列
-            business_cols = self.required_columns[1:]
-            data_business = data[business_cols].copy()
-            
-            # 策略: 前向填充 -> 后向填充 -> 填充为0
-            data_business = data_business.ffill().bfill().fillna(0.0)
-            # ==========================================
-            
-        except KeyError:
-            missing = list(set(self.required_columns) - set(data.columns))
-            raise ValueError(f"缺少列: {missing}")
-
-        # 4. 生成时间特征
-        date_col = data['index']
-        minute_of_day = date_col.dt.hour * 60 + date_col.dt.minute
-        day_of_year = date_col.dt.dayofyear
-        
-        time_features = pd.DataFrame({
-            'minute_sin': np.sin(2 * np.pi * minute_of_day / 1440),
-            'minute_cos': np.cos(2 * np.pi * minute_of_day / 1440),
-            'day_year_sin': np.sin(2 * np.pi * day_of_year / 366),
-            'day_year_cos': np.cos(2 * np.pi * day_of_year / 366)
-        })
-        
-        # 5. 拼接:[时间特征 + 业务特征]
-        # 注意:训练时的顺序是 time_features + other_columns
-        # 必须重置索引以避免拼接错位
-        data_to_scale = pd.concat([
-            time_features.reset_index(drop=True), 
-            data_business.reset_index(drop=True)
-        ], axis=1)
-        
-        # 6. 整体归一化
-        # 此时 columns 应该包含: minute_sin, minute_cos..., AR.1#UF_JSFLOW_O...
-        # 顺序和名字必须与 fit 时一致
-        scaled_array = self.scaler.transform(data_to_scale)
-        
-        return scaled_array
-
-    # --- 备用防空值兜底函数 ---
-    def get_recent_values_as_fallback(self):
-        """从原始输入数据中获取最近的output_size条记录作为备用输出,避免输出空值"""
-        if self.raw_input_data is None or self.raw_input_data.empty:
-            return np.zeros((self.output_size, self.labels_num))
-
-        df_copy = self.raw_input_data.copy()
-        
-        # 统一时间列格式,防止报错
-        if 'datetime' in df_copy.columns:
-            df_copy = df_copy.rename(columns={'datetime': 'index'})
-        if 'index' not in df_copy.columns:
-            df_copy['index'] = pd.date_range(end=datetime.now(), periods=len(df_copy), freq='min')
-        df_copy['index'] = pd.to_datetime(df_copy['index'])
-
-        # 按时间排序并取最近的output_size条
-        recent_data = df_copy.sort_values('index').tail(self.output_size)
-        
-        # 若数据不足,用最后一条补充
-        if len(recent_data) < self.output_size:
-            last_row = recent_data.iloc[-1:] if not recent_data.empty else pd.DataFrame(
-                {col: [0.0] for col in self.target_columns}, index=[0])
-            while len(recent_data) < self.output_size:
-                recent_data = pd.concat([recent_data, last_row], ignore_index=True)
-        
-        # 确保提取的兜底数据中没有空值 (NaN)
-        recent_data[self.target_columns] = recent_data[self.target_columns].ffill().bfill().fillna(0.0)
-
-        # 提取目标列值并返回
-        try:
-            fallback_values = recent_data[self.target_columns].values
-        except KeyError:
-            # 极度异常情况兜底(输入中缺少目标列)
-            fallback_values = np.zeros((self.output_size, self.labels_num))
-            
-        return fallback_values
-
-    def predict(self, df):
-        """
-        返回: List[List[float]]
-        格式: [[t+1时刻的4个值], [t+2时刻的4个值], ..., [t+5时刻的4个值]]
-        """
-        # --- 保存原始输入数据用于可能的降级策略 ---
-        self.raw_input_data = df.copy()
-        
-        # 1. 预处理 (返回的是归一化后的 numpy 数组)
-        processed_data = self._preprocess(df)
-        
-        # 2. 取最后 seq_len 个时间步构建 Tensor
-        input_seq = processed_data[-self.seq_len:] 
-        input_tensor = torch.tensor(input_seq, dtype=torch.float32).unsqueeze(0).to(self.device)
-        
-        # 3. 推理
-        with torch.no_grad():
-            output = self.model(input_tensor)
-        
-        # 4. 反归一化
-        # 输出形状调整为 (5, 4) -> 5个步长, 4个变量
-        preds = output.cpu().numpy().reshape(self.output_size, self.labels_num)
-        
-        # 获取最后4列的归一化参数 (目标变量)
-        target_min = self.scaler.min_[-self.labels_num:]
-        target_scale = self.scaler.scale_[-self.labels_num:]
-        
-        real_preds = (preds - target_min) / target_scale
-        real_preds = np.abs(real_preds)
-        
-        # --- 空值/NaN 检测与兜底机制 ---
-        # 如果模型因极端情况输出 NaN 或者 inf 无穷大,触发历史数据兜底
-        if np.isnan(real_preds).any() or np.isinf(real_preds).any():
-            real_preds = self.get_recent_values_as_fallback()
-        
-        # 5. 返回纯数值列表
-        return real_preds.tolist()
-
-if __name__ == "__main__":
-    # 测试代码
-    try:
-        # 初始化
-        predictor = RealTimePredictor()
-        
-        # 生成模拟数据
-        mock_data = pd.DataFrame()
-        mock_data['index'] = pd.date_range(end=datetime.now(), periods=15, freq='min')
-        for col in predictor.required_columns[1:]:
-            mock_data[col] = np.random.rand(15) * 10
-            
-        # 人为制造空值测试鲁棒性
-        mock_data.loc[3:6, "AR.1#UF_JSFLOW_O"] = np.nan
-        mock_data.loc[12, predictor.target_columns[0]] = np.nan
-            
-        # 预测
-        result = predictor.predict(mock_data)
-        
-        print("预测结果 (5x4 数组):")
-        print(result)
-        
-    except Exception as e:
-        print(f"Error: {e}")
-        import traceback
-        traceback.print_exc()

+ 0 - 51
models/prediction_models/jianding/args.py

@@ -1,51 +0,0 @@
-# args.py
-import argparse
-
-def lstm_args_parser():
-    parser = argparse.ArgumentParser(description="LSTM模型训练参数")
-    
-    # 核心数据集参数
-    parser.add_argument('--train_start_date', type=str, default='2024-10-08', help='训练集开始日期')
-    parser.add_argument('--train_end_date', type=str, default='2026-02-13', help='训练集结束日期')
-    parser.add_argument('--val_start_date', type=str, default='2024-10-08', help='验证集开始日期')
-    parser.add_argument('--val_end_date', type=str, default='2026-02-13', help='验证集结束日期')
-    parser.add_argument('--test_start_date', type=str, default='2024-10-08', help='测试集开始日期')
-    parser.add_argument('--test_end_date', type=str, default='2026-02-13', help='测试集结束日期')
-
-    # 模型架构参数
-    parser.add_argument('--seq_len', type=int, default=10, help='输入序列长度')
-    parser.add_argument('--output_size', type=int, default=5, help='预测步长')
-    parser.add_argument('--step_size', type=int, default=5, help='采样步长')
-    parser.add_argument('--resolution', type=int, default=60, help='数据分辨率(分钟)')
-    parser.add_argument('--feature_num', type=int, default=32, help='输入特征维度')
-    parser.add_argument('--labels_num', type=int, default=4, help='预测标签数量(子模型数量)')
-    
-    # 训练超参数
-    parser.add_argument('--epochs', type=int, default=200, help='训练轮数')
-    parser.add_argument('--hidden_size', type=int, default=64, help='隐藏层大小')
-    parser.add_argument('--num_layers', type=int, default=1, help='LSTM层数')
-    parser.add_argument('--dropout', type=float, default=0, help='dropout概率')
-    parser.add_argument('--lr', type=float, default=0.01, help='学习率')
-    parser.add_argument('--batch_size', type=int, default=512, help='批次大小')
-    
-    parser.add_argument('--scheduler_step_size', type=int, default=100, help='学习率调整步长')
-    parser.add_argument('--scheduler_gamma', type=float, default=0.9, help='学习率衰减率')
-    
-    parser.add_argument('--patience', type=int, default=200, help='早停耐心值')
-    parser.add_argument('--min_delta', type=float, default=1e-10, help='最小改善阈值')
-    parser.add_argument('--device', type=int, default=1, help='GPU设备ID')
-
-    # 文件路径配置
-    parser.add_argument('--start_files', type=int, default=1, help='开始文件索引')
-    parser.add_argument('--end_files', type=int, default=24, help='结束文件索引')
-    parser.add_argument('--data_dir', type=str, default='datasets_jianding', help='数据文件夹路径')
-    parser.add_argument('--file_pattern', type=str, default='data_process_{}.csv', help='数据文件命名模式')
-    
-    parser.add_argument('--model_path', type=str, default='model.pth', help='模型保存路径')
-    parser.add_argument('--scaler_path', type=str, default='scaler.pkl', help='归一化器路径')
-    parser.add_argument('--output_csv_path', type=str, default='predictions.csv', help='预测评估结果路径')
-    
-    parser.add_argument('--random_seed', type=int, default=1314, help='随机种子')
-
-    args = parser.parse_args()
-    return args

+ 0 - 211
models/prediction_models/jianding/data_preprocessor.py

@@ -1,211 +0,0 @@
-# data_preprocessor.py
-import os
-import torch
-import joblib
-import numpy as np
-import pandas as pd
-from tqdm import tqdm
-from sklearn.preprocessing import MinMaxScaler
-from torch.utils.data import DataLoader, TensorDataset
-from concurrent.futures import ThreadPoolExecutor
-
-class DataPreprocessor:
-    """数据预处理类"""
-    
-    # 定义必须保留的列
-    COLUMNS_TO_KEEP = [
-            "index",
-            "water_out",                # 外供水流量
-            "ns=3;s=AI_ROJSLL_OUT",     # 进水流量反馈
-            "ns=3;s=AI_UFCSLL_OUT",     # UF产水流量反馈
-            "ns=3;s=RO_1DJSLL_SSD",     # SSD_Flow_1djs
-            "ns=3;s=RO_2DJSLL_SSD",     # SSD_Flow_2djs
-            "ns=3;s=RO_NS_SSD",         # SSD_Flow_ns
-            "ns=3;s=AI_JYCSLL1_OUT",    # 产水流量计1反馈
-            "ns=3;s=AI_RODJYL_OUT",     # 段间压力反馈
-            "ns=3;s=AI_ROJSYL_OUT",     # 进水压力反馈
-            "ns=3;s=AI_UFCSYL_OUT",     # UF产水压力反馈
-            "ns=3;s=AI_JYCIPPH_OUT",    # CIPph反馈
-            "ns=3;s=AI_JYCSDD_OUT",     # 外供水电导反馈
-            "ns=3;s=AI_UFCSZD_OUT",     # UF产水浊度反馈
-            "ns=3;s=AI_ROCSDD_OUT",     # 产水电导反馈
-            "ns=3;s=AI_UFJSORP_OUT",    # UF进水ORP反馈
-            "ns=3;s=AI_UFJSPH_OUT",     # UF进水ph反馈
-            "ns=3;s=AI_UFJSYW_OUT",     # UF进水温度反馈
-            "ns=3;s=AI_JYROCSYW_OUT",   # 反渗透产水液位计反馈
-            "ns=3;s=AI_JYSYW_OUT",      # 酸液位反馈
-            "ns=3;s=AI_RODJB_FR_OUT",       # RO段间泵频率反馈
-            "ns=3;s=AI_ROGSB_FR_OUT",       # RO供水泵频率反馈
-            "ns=3;s=AI_ROGYB_FR_OUT",       # RO高压泵频率反馈
-            "ns=3;s=AI_UFFXB_FR_OUT",       # UF反洗泵频率反馈
-            "ns=3;s=AI_UFCSB_FR_OUT",       # UF产水泵频率反馈
-            "ns=3;s=UF_TMP",                # SSD跨膜压差
-            "ns=3;s=RO_CHA1YL_SSD",         # SSD_PressCha1
-            "ns=3;s=RO_CHA2YL_SSD",         # SSD_PressCha2
-            "ns=3;s=RO_ZCS_SSD",            # SSD_Flow_zcs
-    ]
-
-    @staticmethod
-    def load_and_process_data(args, data):
-        """加载并处理数据,划分训练/验证/测试集"""
-        # 处理日期
-        data['date'] = pd.to_datetime(data['date'])
-        time_interval = pd.Timedelta(minutes=(4 * args.resolution / 60))
-        window_time_span = time_interval * (args.seq_len + 1)
-
-        val_start_date = pd.to_datetime(args.val_start_date)
-        test_start_date = pd.to_datetime(args.test_start_date)
-        
-        # 调整时间窗口
-        adjusted_val_start = val_start_date - window_time_span
-        adjusted_test_start = test_start_date - window_time_span
-        
-        train_mask = (data['date'] >= pd.to_datetime(args.train_start_date)) & \
-                     (data['date'] <= pd.to_datetime(args.train_end_date))
-        val_mask = (data['date'] >= adjusted_val_start) & \
-                   (data['date'] <= pd.to_datetime(args.val_end_date))
-        test_mask = (data['date'] >= adjusted_test_start) & \
-                    (data['date'] <= pd.to_datetime(args.test_end_date))
-
-        train_data = data[train_mask].reset_index(drop=True)
-        val_data = data[val_mask].reset_index(drop=True)
-        test_data = data[test_mask].reset_index(drop=True)
-        
-        train_data = train_data.drop(columns=['date'])
-        val_data = val_data.drop(columns=['date'])
-        test_data = test_data.drop(columns=['date'])
-    
-        # 创建数据集
-        train_supervised = DataPreprocessor.create_supervised_dataset(args, train_data, 1)
-        val_supervised = DataPreprocessor.create_supervised_dataset(args, val_data, 1)
-        test_supervised = DataPreprocessor.create_supervised_dataset(args, test_data, args.step_size)
-        
-        # 转换为DataLoader
-        train_loader = DataPreprocessor.load_data(args, train_supervised, shuffle=True)
-        val_loader = DataPreprocessor.load_data(args, val_supervised, shuffle=False)
-        test_loader = DataPreprocessor.load_data(args, test_supervised, shuffle=False)
-        
-        return train_loader, val_loader, test_loader, data
-    
-    @staticmethod
-    def read_and_combine_csv_files(args):
-        """读取文件并进行特征筛选和预处理"""
-        current_dir = os.path.dirname(__file__)
-        parent_dir = os.path.dirname(current_dir)
-        args.data_dir = os.path.join(parent_dir, args.data_dir)
-        
-        def read_file(file_count):
-            file_name = args.file_pattern.format(file_count)
-            file_path = os.path.join(args.data_dir, file_name)
-            try:
-                df = pd.read_csv(file_path)
-                # 确保只读取需要的列,若列不存在则会报错提示
-                return df[DataPreprocessor.COLUMNS_TO_KEEP]
-            except KeyError as e:
-                print(f"文件 {file_name} 中缺少列: {e}")
-                raise
-        
-        file_indices = list(range(args.start_files, args.end_files + 1))
-        max_workers = os.cpu_count()
-        
-        with ThreadPoolExecutor(max_workers=max_workers) as executor:
-            results = list(tqdm(executor.map(read_file, file_indices),
-                                total=len(file_indices),
-                                desc="正在读取文件"))
-        
-        all_data = pd.concat(results, ignore_index=True)
-        
-        # 确保列顺序一致
-        all_data = all_data[DataPreprocessor.COLUMNS_TO_KEEP]
-        
-        # 下采样
-        chunk = all_data.iloc[::args.resolution, :].reset_index(drop=True)
-        
-        # 处理特征
-        chunk = DataPreprocessor.process_date(chunk, args)
-        chunk = DataPreprocessor.scaler_data(chunk, args)
-        
-        return chunk
-    
-    @staticmethod
-    def process_date(data, args):
-        data = data.rename(columns={'index': 'date'})
-        data['date'] = pd.to_datetime(data['date'])
-    
-        time_features = []
-        # 固定生成分钟级和日级特征,保持与Predictor一致
-        data['minute_of_day'] = data['date'].dt.hour * 60 + data['date'].dt.minute
-        data['minute_sin'] = np.sin(2 * np.pi * data['minute_of_day'] / 1440)
-        data['minute_cos'] = np.cos(2 * np.pi * data['minute_of_day'] / 1440)
-        
-        data['day_of_year'] = data['date'].dt.dayofyear
-        data['day_year_sin'] = np.sin(2 * np.pi * data['day_of_year'] / 366)
-        data['day_year_cos'] = np.cos(2 * np.pi * data['day_of_year'] / 366)
-        
-        time_features.extend(['minute_sin', 'minute_cos', 'day_year_sin', 'day_year_cos'])
-        data.drop(columns=['minute_of_day', 'day_of_year'], inplace=True)
-    
-        other_columns = [col for col in data.columns if col not in ['date'] and col not in time_features]
-        data = data[['date'] + time_features + other_columns]
-        return data
-    
-    @staticmethod
-    def scaler_data(data, args):
-        date_col = data[['date']]
-        data_to_scale = data.drop(columns=['date'])
-
-        scaler = MinMaxScaler(feature_range=(0, 1))
-        scaled_data = scaler.fit_transform(data_to_scale)
-        joblib.dump(scaler, args.scaler_path)
-
-        scaled_data = pd.DataFrame(scaled_data, columns=data_to_scale.columns)
-        scaled_data = pd.concat([date_col.reset_index(drop=True), scaled_data], axis=1)
-        return scaled_data
-    
-    @staticmethod
-    def create_supervised_dataset(args, data, step_size):
-        data = pd.DataFrame(data)
-        cols = []
-        col_names = []
-        feature_columns = data.columns.tolist()
-
-        # 输入序列
-        for col in feature_columns:
-            for i in range(args.seq_len - 1, -1, -1):
-                cols.append(data[[col]].shift(i))
-                col_names.append(f"{col}(t-{i})")
-        
-        # 目标序列 (取最后labels_num列)
-        target_columns = feature_columns[-args.labels_num:]
-        for i in range(1, args.output_size + 1):
-            for col in target_columns:
-                cols.append(data[[col]].shift(-i))
-                col_names.append(f"{col}(t+{i})")
-
-        dataset = pd.concat(cols, axis=1)
-        dataset.columns = col_names
-        dataset = dataset.iloc[::step_size, :]
-        dataset.dropna(inplace=True)
-        return dataset
-
-    @staticmethod
-    def load_data(args, dataset, shuffle):
-        input_length = args.seq_len
-        n_features = args.feature_num
-        labels_num = args.labels_num
-    
-        n_features_total = n_features * input_length
-        n_labels_total = args.output_size * labels_num
-
-        X = dataset.values[:, :n_features_total]
-        y = dataset.values[:, n_features_total:n_features_total + n_labels_total]
-    
-        X = X.reshape(X.shape[0], input_length, n_features)
-        X = torch.tensor(X, dtype=torch.float32).to(args.device)
-        y = torch.tensor(y, dtype=torch.float32).to(args.device)
-
-        dataset_tensor = TensorDataset(X, y)
-        generator = torch.Generator()
-        generator.manual_seed(args.random_seed)
-        
-        return DataLoader(dataset_tensor, batch_size=args.batch_size, shuffle=shuffle, generator=generator)

+ 0 - 165
models/prediction_models/jianding/data_trainer.py

@@ -1,165 +0,0 @@
-# data_trainer.py
-import torch
-import joblib
-import numpy as np
-import pandas as pd
-from sklearn.metrics import r2_score
-from datetime import datetime, timedelta
-from sklearn.preprocessing import MinMaxScaler
-
-class Trainer:
-    def __init__(self, model, args, data):
-        self.args = args
-        self.model = model
-        self.data = data
-        self.patience = args.patience
-        self.min_delta = args.min_delta
-        self.counter = 0
-        self.early_stop = False
-        self.best_val_loss = float('inf')
-        self.best_model_state = None
-        self.best_epoch = 0
-
-    def train_full_model(self, train_loader, val_loader, optimizer, criterion, scheduler):
-        self.counter = 0
-        self.best_val_loss = float('inf')
-        self.early_stop = False
-        self.best_model_state = None
-        self.best_epoch = 0
-        max_epochs = self.args.epochs
-
-        for epoch in range(max_epochs):
-            self.model.train()
-            running_loss = 0.0
-            
-            for inputs, targets in train_loader:
-                inputs = inputs.to(self.args.device)
-                targets = targets.to(self.args.device)
-                optimizer.zero_grad()
-                outputs = self.model(inputs)
-                loss = criterion(outputs, targets)
-                loss.backward()
-                optimizer.step()
-                running_loss += loss.item()
-            
-            train_loss = running_loss / len(train_loader)
-            val_loss = self.validate_full(val_loader, criterion) if val_loader else 0.0
-
-            print(f'Epoch {epoch+1}/{max_epochs}, Train Loss: {train_loss:.6f}, Val Loss: {val_loss:.6f}')
-
-            if val_loader:
-                if val_loss < (self.best_val_loss - self.min_delta):
-                    self.best_val_loss = val_loss
-                    self.counter = 0
-                    self.best_model_state = self.model.state_dict()
-                    self.best_epoch = epoch
-                else:
-                    self.counter += 1
-                    if self.counter >= self.patience:
-                        self.early_stop = True
-                        print(f"早停触发")
-                        
-            scheduler.step()
-            torch.cuda.empty_cache()
-            if self.early_stop:
-                break
-
-        if self.best_model_state is not None:
-            self.model.load_state_dict(self.best_model_state)
-        print(f"最佳迭代: {self.best_epoch+1}, 最佳验证损失: {self.best_val_loss:.6f}")
-        return self.model
-
-    def validate_full(self, val_loader, criterion):
-        self.model.eval()
-        total_loss = 0.0
-        with torch.no_grad():
-            for inputs, targets in val_loader:
-                inputs = inputs.to(self.args.device)
-                targets = targets.to(self.args.device)
-                outputs = self.model(inputs)
-                loss = criterion(outputs, targets)
-                total_loss += loss.item()
-        return total_loss / len(val_loader)
-
-    def save_model(self):
-        torch.save(self.model.state_dict(), self.args.model_path)
-        print(f"模型已保存到:{self.args.model_path}")
-            
-    def evaluate_model(self, test_loader, criterion):
-        self.model.eval()
-        scaler = joblib.load(self.args.scaler_path)
-        predictions = []
-        true_values = []
-        
-        with torch.no_grad():
-            for inputs, targets in test_loader:
-                inputs = inputs.to(self.args.device)
-                targets = targets.to(self.args.device)
-                outputs = self.model(inputs)
-                predictions.append(outputs.cpu().numpy())
-                true_values.append(targets.cpu().numpy())
-    
-        predictions = np.concatenate(predictions, axis=0)
-        true_values = np.concatenate(true_values, axis=0)
-    
-        # 重塑
-        reshaped_predictions = predictions.reshape(predictions.shape[0], self.args.output_size, self.args.labels_num)
-        predictions = reshaped_predictions.reshape(-1, self.args.labels_num)
-        
-        reshaped_true_values = true_values.reshape(true_values.shape[0], self.args.output_size, self.args.labels_num)
-        true_values = reshaped_true_values.reshape(-1, self.args.labels_num)
-    
-        # 反归一化 (仅标签列)
-        column_scaler = MinMaxScaler(feature_range=(0, 1))
-        column_scaler.min_ = scaler.min_[-self.args.labels_num:] 
-        column_scaler.scale_ = scaler.scale_[-self.args.labels_num:] 
-        
-        true_values = column_scaler.inverse_transform(true_values)
-        predictions = column_scaler.inverse_transform(predictions)
-    
-        # 定义4个核心变量
-        column_names = [
-            "ns=3;s=UF_TMP",                # SSD跨膜压差
-            "ns=3;s=RO_CHA1YL_SSD",         # SSD_PressCha1
-            "ns=3;s=RO_CHA2YL_SSD",         # SSD_PressCha2
-            "ns=3;s=RO_ZCS_SSD",            # SSD_Flow_zcs
-        ]
-    
-        # 生成时间
-        start_datetime = datetime.strptime(self.args.test_start_date, "%Y-%m-%d")
-        time_interval = timedelta(minutes=(4 * self.args.resolution / 60))
-        total_points = len(predictions)
-        date_times = [start_datetime + i * time_interval for i in range(total_points)]
-        
-        results = pd.DataFrame({'date': date_times})
-        metrics_details = []
-        
-        for i, col_name in enumerate(column_names):
-            if i >= self.args.labels_num: break # 防止越界
-            
-            results[f'{col_name}_True'] = true_values[:, i]
-            results[f'{col_name}_Predicted'] = predictions[:, i]
-            
-            var_true = true_values[:, i]
-            var_pred = predictions[:, i]
-            
-            # 指标计算
-            non_zero_mask = var_true != 0
-            var_true_nonzero = var_true[non_zero_mask]
-            var_pred_nonzero = var_pred[non_zero_mask]
-            
-            if len(var_true_nonzero) > 0:
-                r2 = r2_score(var_true_nonzero, var_pred_nonzero)
-                rmse = np.sqrt(np.mean((var_true_nonzero - var_pred_nonzero) ** 2))
-                mape = np.mean(np.abs((var_true_nonzero - var_pred_nonzero) / np.abs(var_true_nonzero))) * 100
-                metrics_details.append(f"{col_name}: R2={r2:.4f}, RMSE={rmse:.4f}, MAPE={mape:.4f}%")
-            else:
-                metrics_details.append(f"{col_name}: 无效数据")
-
-        results.to_csv(self.args.output_csv_path, index=False)
-        
-        txt_path = self.args.output_csv_path.replace('.csv', '_metrics.txt')
-        with open(txt_path, 'w') as f:
-            f.write('\n'.join(metrics_details))
-            
-        return metrics_details

+ 0 - 54
models/prediction_models/jianding/gat_lstm.py

@@ -1,54 +0,0 @@
-# gat_lstm.py
-import torch
-import torch.nn as nn
-
-class SingleGATLSTM(nn.Module):
-    """单个子模型:预测1个目标指标"""
-    def __init__(self, args):
-        super(SingleGATLSTM, self).__init__()
-        self.args = args
-        
-        self.lstm = nn.LSTM(
-            input_size=args.feature_num,
-            hidden_size=args.hidden_size,
-            num_layers=args.num_layers,
-            batch_first=True
-        )
-        
-        self.final_linear = nn.Sequential(
-            nn.Linear(args.hidden_size, args.hidden_size),
-            nn.LeakyReLU(0.01),
-            nn.Dropout(args.dropout * 0.4),
-            nn.Linear(args.hidden_size, args.output_size)
-        )
-        self._init_weights()
-        
-    def _init_weights(self):
-        for m in self.modules():
-            if isinstance(m, nn.Linear):
-                nn.init.xavier_uniform_(m.weight)
-                if m.bias is not None: nn.init.zeros_(m.bias)
-
-    def forward(self, x):
-        batch_size, seq_len, feature_num = x.size()
-        lstm_out, _ = self.lstm(x)
-        last_out = lstm_out[:, -1, :]
-        output = self.final_linear(last_out)
-        return output
-
-class GAT_LSTM(nn.Module):
-    """总模型:包含多个SingleGATLSTM子模型"""
-    def __init__(self, args):
-        super(GAT_LSTM, self).__init__()
-        self.args = args
-        # 创建4个独立模型(对应labels_num=4)
-        self.models = nn.ModuleList([SingleGATLSTM(args) for _ in range(args.labels_num)])
-    
-    def set_edge_index(self, edge_index):
-        self.edge_index = edge_index
-        
-    def forward(self, x):
-        outputs = []
-        for model in self.models:
-            outputs.append(model(x))
-        return torch.cat(outputs, dim=1)

+ 0 - 59
models/prediction_models/jianding/main.py

@@ -1,59 +0,0 @@
-# main.py
-import os
-import torch
-import numpy as np
-import random
-from gat_lstm import GAT_LSTM
-from data_trainer import Trainer
-from args import lstm_args_parser
-from torch.nn import MSELoss
-from data_preprocessor import DataPreprocessor
-
-def set_seed(seed):
-    random.seed(seed)
-    os.environ['PYTHONHASHSEED'] = str(seed)
-    np.random.seed(seed)
-    torch.manual_seed(seed)
-    torch.cuda.manual_seed(seed)
-    torch.backends.cudnn.deterministic = True
-    torch.backends.cudnn.benchmark = False
-
-def main():
-    args = lstm_args_parser()
-    set_seed(args.random_seed)
-    
-    device = torch.device(f"cuda:{args.device}" if torch.cuda.is_available() else "cpu")
-    args.device = device
-
-    print(f"当前配置: 序列长度={args.seq_len}, 特征数={args.feature_num}, 目标数={args.labels_num}")
-
-    # 数据预处理
-    data = DataPreprocessor.read_and_combine_csv_files(args)
-    train_loader, val_loader, test_loader, _ = DataPreprocessor.load_and_process_data(args, data)
-    
-    # 初始化模型
-    model = GAT_LSTM(args).to(device)
-    
-    # 加载 edge_index.pt 
-    if os.path.exists('edge_index.pt'):
-        edge_index = torch.load('edge_index.pt', map_location=device, weights_only=True)
-        model.set_edge_index(edge_index)
-        print("已加载 edge_index.pt")
-    else:
-        print("未找到 edge_index.pt")
-
-    # 训练器
-    trainer = Trainer(model, args, data)
-    criterion = MSELoss()
-    optimizer = torch.optim.Adam(model.parameters(), lr=args.lr)
-    scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=args.scheduler_step_size, gamma=args.scheduler_gamma)
-
-    print("=== 开始训练 ===")
-    trainer.train_full_model(train_loader, val_loader, optimizer, criterion, scheduler)
-    trainer.save_model()
-    
-    print("=== 开始评估 ===")
-    trainer.evaluate_model(test_loader, MSELoss())
-
-if __name__ == "__main__":
-    main()

+ 0 - 258
models/prediction_models/jianding/predict.py

@@ -1,258 +0,0 @@
-# predict.py
-import os
-import torch
-import joblib
-import pandas as pd
-import numpy as np
-from datetime import datetime, timedelta
-from gat_lstm import GAT_LSTM
-
-class RealTimePredictor:
-    def __init__(self, model_path='model.pth', scaler_path='scaler.pkl', device=None):
-        """
-        初始化预测器
-        """
-        # 1. 参数配置 (与训练 args.py 保持一致)
-        self.seq_len = 10         # 输入序列长度
-        self.feature_num = 32     # 输入特征数 (4时间编码 + 28业务特征)
-        self.labels_num = 4       # 输出标签数
-        self.hidden_size = 64
-        self.num_layers = 1
-        self.output_size = 5      # 预测未来 5 步
-        self.dropout = 0
-        
-        # 2. 设备与资源加载
-        self.device = device if device else torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
-        self.model_path = model_path
-        self.scaler_path = scaler_path
-        
-        # 加载归一化器
-        if not os.path.exists(self.scaler_path):
-             raise FileNotFoundError(f"未找到归一化文件: {self.scaler_path},请确保已完成训练。")
-        self.scaler = joblib.load(self.scaler_path)
-
-        # 加载模型
-        self._load_model()
-
-        # 定义必须存在的列名 (29个,包含index,顺序必须固定)
-        self.required_columns = [
-            "index",
-            "water_out",                # 外供水流量
-            "ns=3;s=AI_ROJSLL_OUT",     # 进水流量反馈
-            "ns=3;s=AI_UFCSLL_OUT",     # UF产水流量反馈
-            "ns=3;s=RO_1DJSLL_SSD",     # SSD_Flow_1djs
-            "ns=3;s=RO_2DJSLL_SSD",     # SSD_Flow_2djs
-            "ns=3;s=RO_NS_SSD",         # SSD_Flow_ns
-            "ns=3;s=AI_JYCSLL1_OUT",    # 产水流量计1反馈
-            "ns=3;s=AI_RODJYL_OUT",     # 段间压力反馈
-            "ns=3;s=AI_ROJSYL_OUT",     # 进水压力反馈
-            "ns=3;s=AI_UFCSYL_OUT",     # UF产水压力反馈
-            "ns=3;s=AI_JYCIPPH_OUT",    # CIPph反馈
-            "ns=3;s=AI_JYCSDD_OUT",     # 外供水电导反馈
-            "ns=3;s=AI_UFCSZD_OUT",     # UF产水浊度反馈
-            "ns=3;s=AI_ROCSDD_OUT",     # 产水电导反馈
-            "ns=3;s=AI_UFJSORP_OUT",    # UF进水ORP反馈
-            "ns=3;s=AI_UFJSPH_OUT",     # UF进水ph反馈
-            "ns=3;s=AI_UFJSYW_OUT",     # UF进水温度反馈
-            "ns=3;s=AI_JYROCSYW_OUT",   # 反渗透产水液位计反馈
-            "ns=3;s=AI_JYSYW_OUT",      # 酸液位反馈
-            "ns=3;s=AI_RODJB_FR_OUT",       # RO段间泵频率反馈
-            "ns=3;s=AI_ROGSB_FR_OUT",       # RO供水泵频率反馈
-            "ns=3;s=AI_ROGYB_FR_OUT",       # RO高压泵频率反馈
-            "ns=3;s=AI_UFFXB_FR_OUT",       # UF反洗泵频率反馈
-            "ns=3;s=AI_UFCSB_FR_OUT",       # UF产水泵频率反馈
-            "ns=3;s=UF_TMP",                # SSD跨膜压差
-            "ns=3;s=RO_CHA1YL_SSD",         # SSD_PressCha1
-            "ns=3;s=RO_CHA2YL_SSD",         # SSD_PressCha2
-            "ns=3;s=RO_ZCS_SSD",            # SSD_Flow_zcs
-        ]
-
-        # 用于防空值兜底机制的变量
-        self.raw_input_data = None
-        self.target_columns = self.required_columns[-self.labels_num:]
-
-    def _load_model(self):
-        """内部方法:加载模型权重"""
-        class ModelArgs: pass
-        args = ModelArgs()
-        args.feature_num = self.feature_num
-        args.hidden_size = self.hidden_size
-        args.num_layers = self.num_layers
-        args.output_size = self.output_size
-        args.labels_num = self.labels_num
-        args.dropout = self.dropout
-
-        self.model = GAT_LSTM(args).to(self.device)
-        
-        # 加载edge_index.pt 
-        if os.path.exists('edge_index.pt'):
-            edge_index = torch.load('edge_index.pt', map_location=self.device, weights_only=True)
-            self.model.set_edge_index(edge_index)
-        
-        if not os.path.exists(self.model_path):
-            raise FileNotFoundError(f"未找到模型权重文件: {self.model_path}")
-            
-        state_dict = torch.load(self.model_path, map_location=self.device, weights_only=True)
-        self.model.load_state_dict(state_dict)
-        self.model.eval()
-
-    def _preprocess(self, df):
-        """数据预处理:补全、排序、生成时间特征、整体归一化"""
-        data = df.copy()
-        
-        # 1. 统一时间列名
-        if 'datetime' in data.columns:
-            data = data.rename(columns={'datetime': 'index'})
-        if 'index' not in data.columns:
-             data['index'] = pd.date_range(end=datetime.now(), periods=len(data), freq='min')
-        data['index'] = pd.to_datetime(data['index'])
-        
-        # 2. 补全长度 (Padding)
-        if len(data) < self.seq_len:
-            pad_len = self.seq_len - len(data)
-            first_row = data.iloc[0:1]
-            pads = pd.concat([first_row] * pad_len, ignore_index=True)
-            start_time = data['index'].iloc[0]
-            for i in range(pad_len):
-                pads.at[i, 'index'] = start_time - timedelta(minutes=(pad_len-i))
-            data = pd.concat([pads, data], ignore_index=True)
-
-        # 3. 列筛选排序 (提取业务数据,不含index)
-        try:
-            # required_columns[0] 是 'index',我们取后面的业务列
-            business_cols = self.required_columns[1:]
-            data_business = data[business_cols].copy()
-            
-            # 策略: 前向填充 -> 后向填充 -> 填充为0
-            data_business = data_business.ffill().bfill().fillna(0.0)
-            
-        except KeyError:
-            missing = list(set(self.required_columns) - set(data.columns))
-            raise ValueError(f"缺少列: {missing}")
-
-        # 4. 生成时间特征
-        date_col = data['index']
-        minute_of_day = date_col.dt.hour * 60 + date_col.dt.minute
-        day_of_year = date_col.dt.dayofyear
-        
-        time_features = pd.DataFrame({
-            'minute_sin': np.sin(2 * np.pi * minute_of_day / 1440),
-            'minute_cos': np.cos(2 * np.pi * minute_of_day / 1440),
-            'day_year_sin': np.sin(2 * np.pi * day_of_year / 366),
-            'day_year_cos': np.cos(2 * np.pi * day_of_year / 366)
-        })
-        
-        # 5. 拼接:[时间特征 + 业务特征]
-        # 注意:训练时的顺序是 time_features + other_columns
-        # 必须重置索引以避免拼接错位
-        data_to_scale = pd.concat([
-            time_features.reset_index(drop=True), 
-            data_business.reset_index(drop=True)
-        ], axis=1)
-        
-        # 6. 整体归一化
-        scaled_array = self.scaler.transform(data_to_scale)
-        
-        return scaled_array
-
-    # --- 备用防空值兜底函数 ---
-    def get_recent_values_as_fallback(self):
-        """从原始输入数据中获取最近的output_size条记录作为备用输出,避免输出空值"""
-        if self.raw_input_data is None or self.raw_input_data.empty:
-            return np.zeros((self.output_size, self.labels_num))
-
-        df_copy = self.raw_input_data.copy()
-        
-        # 统一时间列格式,防止报错
-        if 'datetime' in df_copy.columns:
-            df_copy = df_copy.rename(columns={'datetime': 'index'})
-        if 'index' not in df_copy.columns:
-            df_copy['index'] = pd.date_range(end=datetime.now(), periods=len(df_copy), freq='min')
-        df_copy['index'] = pd.to_datetime(df_copy['index'])
-
-        # 按时间排序并取最近的output_size条
-        recent_data = df_copy.sort_values('index').tail(self.output_size)
-        
-        # 若数据不足,用最后一条补充
-        if len(recent_data) < self.output_size:
-            last_row = recent_data.iloc[-1:] if not recent_data.empty else pd.DataFrame(
-                {col: [0.0] for col in self.target_columns}, index=[0])
-            while len(recent_data) < self.output_size:
-                recent_data = pd.concat([recent_data, last_row], ignore_index=True)
-        
-        # 确保提取的兜底数据中没有空值 (NaN)
-        recent_data[self.target_columns] = recent_data[self.target_columns].ffill().bfill().fillna(0.0)
-
-        # 提取目标列值并返回
-        try:
-            fallback_values = recent_data[self.target_columns].values
-        except KeyError:
-            # 极度异常情况兜底(输入中缺少目标列)
-            fallback_values = np.zeros((self.output_size, self.labels_num))
-            
-        return fallback_values
-
-    def predict(self, df):
-        """
-        返回: List[List[float]]
-        格式: [[t+1时刻的4个值], [t+2时刻的4个值], ..., [t+5时刻的4个值]]
-        """
-        # --- 保存原始输入数据用于可能的降级策略 ---
-        self.raw_input_data = df.copy()
-        
-        # 1. 预处理 (返回的是归一化后的 numpy 数组)
-        processed_data = self._preprocess(df)
-        
-        # 2. 取最后 seq_len 个时间步构建 Tensor
-        input_seq = processed_data[-self.seq_len:] 
-        input_tensor = torch.tensor(input_seq, dtype=torch.float32).unsqueeze(0).to(self.device)
-        
-        # 3. 推理
-        with torch.no_grad():
-            output = self.model(input_tensor)
-        
-        # 4. 反归一化
-        # 输出形状调整为 (5, 4) -> 5个步长, 4个变量
-        preds = output.cpu().numpy().reshape(self.output_size, self.labels_num)
-        
-        # 获取最后4列的归一化参数 (目标变量)
-        target_min = self.scaler.min_[-self.labels_num:]
-        target_scale = self.scaler.scale_[-self.labels_num:]
-        
-        real_preds = (preds - target_min) / target_scale
-        real_preds = np.abs(real_preds)
-        
-        # --- 空值/NaN 检测与兜底机制 ---
-        # 如果模型因极端情况输出 NaN 或者 inf 无穷大,触发历史数据兜底
-        if np.isnan(real_preds).any() or np.isinf(real_preds).any():
-            real_preds = self.get_recent_values_as_fallback()
-        
-        # 5. 返回纯数值列表
-        return real_preds.tolist()
-
-if __name__ == "__main__":
-    # 测试代码
-    try:
-        # 初始化
-        predictor = RealTimePredictor()
-        
-        # 生成模拟数据
-        mock_data = pd.DataFrame()
-        mock_data['index'] = pd.date_range(end=datetime.now(), periods=15, freq='min')
-        for col in predictor.required_columns[1:]:
-            mock_data[col] = np.random.rand(15) * 10
-            
-        # 人为制造空值测试鲁棒性
-        mock_data.loc[3:6, "water_out"] = np.nan
-        mock_data.loc[12, predictor.target_columns[0]] = np.nan
-            
-        # 预测
-        result = predictor.predict(mock_data)
-        
-        print("预测结果 (5x4 数组):")
-        print(result)
-        
-    except Exception as e:
-        print(f"Error: {e}")
-        import traceback
-        traceback.print_exc()

+ 0 - 51
models/prediction_models/longting/args.py

@@ -1,51 +0,0 @@
-# args.py
-import argparse
-
-def lstm_args_parser():
-    parser = argparse.ArgumentParser(description="LSTM模型训练参数")
-    
-    # 核心数据集参数
-    parser.add_argument('--train_start_date', type=str, default='2025-11-28', help='训练集开始日期')
-    parser.add_argument('--train_end_date', type=str, default='2026-02-20', help='训练集结束日期')
-    parser.add_argument('--val_start_date', type=str, default='2025-11-28', help='验证集开始日期')
-    parser.add_argument('--val_end_date', type=str, default='2026-02-20', help='验证集结束日期')
-    parser.add_argument('--test_start_date', type=str, default='2025-11-28', help='测试集开始日期')
-    parser.add_argument('--test_end_date', type=str, default='2026-02-20', help='测试集结束日期')
-
-    # 模型架构参数
-    parser.add_argument('--seq_len', type=int, default=10, help='输入序列长度')
-    parser.add_argument('--output_size', type=int, default=5, help='预测步长')
-    parser.add_argument('--step_size', type=int, default=5, help='采样步长')
-    parser.add_argument('--resolution', type=int, default=60, help='数据分辨率(分钟)')
-    parser.add_argument('--feature_num', type=int, default=78, help='输入特征维度')
-    parser.add_argument('--labels_num', type=int, default=8, help='预测标签数量(子模型数量)')
-    
-    # 训练超参数
-    parser.add_argument('--epochs', type=int, default=200, help='训练轮数')
-    parser.add_argument('--hidden_size', type=int, default=64, help='隐藏层大小')
-    parser.add_argument('--num_layers', type=int, default=1, help='LSTM层数')
-    parser.add_argument('--dropout', type=float, default=0, help='dropout概率')
-    parser.add_argument('--lr', type=float, default=0.01, help='学习率')
-    parser.add_argument('--batch_size', type=int, default=512, help='批次大小')
-    
-    parser.add_argument('--scheduler_step_size', type=int, default=100, help='学习率调整步长')
-    parser.add_argument('--scheduler_gamma', type=float, default=0.9, help='学习率衰减率')
-    
-    parser.add_argument('--patience', type=int, default=200, help='早停耐心值')
-    parser.add_argument('--min_delta', type=float, default=1e-10, help='最小改善阈值')
-    parser.add_argument('--device', type=int, default=1, help='GPU设备ID')
-
-    # 文件路径配置
-    parser.add_argument('--start_files', type=int, default=1, help='开始文件索引')
-    parser.add_argument('--end_files', type=int, default=10, help='结束文件索引')
-    parser.add_argument('--data_dir', type=str, default='datasets_longting', help='数据文件夹路径')
-    parser.add_argument('--file_pattern', type=str, default='data_process_{}.csv', help='数据文件命名模式')
-    
-    parser.add_argument('--model_path', type=str, default='model.pth', help='模型保存路径')
-    parser.add_argument('--scaler_path', type=str, default='scaler.pkl', help='归一化器路径')
-    parser.add_argument('--output_csv_path', type=str, default='predictions.csv', help='预测评估结果路径')
-    
-    parser.add_argument('--random_seed', type=int, default=1314, help='随机种子')
-
-    args = parser.parse_args()
-    return args

+ 0 - 257
models/prediction_models/longting/data_preprocessor.py

@@ -1,257 +0,0 @@
-# data_preprocessor.py
-import os
-import torch
-import joblib
-import numpy as np
-import pandas as pd
-from tqdm import tqdm
-from sklearn.preprocessing import MinMaxScaler
-from torch.utils.data import DataLoader, TensorDataset
-from concurrent.futures import ThreadPoolExecutor
-
-class DataPreprocessor:
-    """数据预处理类"""
-    
-    # 定义必须保留的列
-    COLUMNS_TO_KEEP = [
-        'index',
-        "water_in",              # 进水量
-        "water_out",             # 外供水流量
-        "RO1_TYL",               # RO1脱盐率
-        "RO2_TYL",               # RO2脱盐率
-        "UF1Per",                # UF1渗透率
-        "UF2Per",                # UF2渗透率
-        "2#RODJB_Eff",           # 2#RO段间泵效率
-        "1#RODJB_Eff",           # 1#RO段间泵效率
-        "2#ROGYB_Eff",           # 2#RO高压泵效率
-        "1#ROGYB_Eff",           # 1#RO高压泵效率
-        "ROHSL",                 # 反渗透回收率
-        "ns=3;s=1#RO_CSDD_O",    # 1#RO产水电导
-        "ns=3;s=1#RO_CSPRESS_O",       # 1#RO产水压力
-        "ns=3;s=1#RO_EDCSFLOW_O",      # 1#RO二段产水流量
-        "ns=3;s=1#RO_EDJSPRESS_O",     # 1#RO二段进水压力
-        "ns=3;s=1#RO_EDNSPRESS_O",     # 1#RO二段浓水压力
-        "ns=3;s=1#RO_JSFLOW_O",        # 1#RO进水流量
-        "ns=3;s=1#RO_JSPRESS_O",       # 1#RO进水压力
-        "ns=3;s=1#RO_NSFLOW_O",        # 1#RO浓水流量
-        "ns=3;s=1#RO_SDCSFLOW_O",      # 1#RO三段产水流量
-        "ns=3;s=1#RO_SDJSPRESS_O",     # 1#RO三段进水压力
-        "ns=3;s=1#RO_SDNSPRESS_O",     # 1#RO三段浓水压力
-        "ns=3;s=1#RODJB_CUR_FB_O",     # 1#RO段间泵电流反馈
-        "ns=3;s=1#RODJB_CZ_O",         # 1#RO段间泵测振反馈
-        "ns=3;s=1#RODJB_FRE_FB_O",       # 1#RO段间泵频率反馈
-        "ns=3;s=1#ROGYB_CUR_FB_O",       # 1#RO高压泵电流反馈
-        "ns=3;s=1#ROGYB_CZ_O",           # 1#RO高压泵测振反馈
-        "ns=3;s=1#ROGYB_FRE_FB_O",       # 1#RO高压泵频率反馈
-        "ns=3;s=1#UF_CSPRESS_O",         # 1#UF产水压力
-        "ns=3;s=1#UF_JSFLOW_O",          # 1#UF进水流量
-        "ns=3;s=1#UF_JSPRESS_O",         # 1#UF进水压力
-        "ns=3;s=1#UF_V_FB_O",            # 1#UF调节阀开度反馈
-        "ns=3;s=1#UFBWB_CUR_FB_O",       # 1#UF反洗泵电流反馈
-        "ns=3;s=1#UFBWB_FRE_FB_O",       # 1#UF反洗泵频率反馈
-        "ns=3;s=2#RO_CSDD_O",            # 2#RO产水电导
-        "ns=3;s=2#RO_CSPRESS_O",         # 2#RO产水压力
-        "ns=3;s=2#RO_EDCSFLOW_O",        # 2#RO二段产水流量
-        "ns=3;s=2#RO_EDJSPRESS_O",       # 2#RO二段进水压力
-        "ns=3;s=2#RO_EDNSPRESS_O",       # 2#RO二段浓水压力
-        "ns=3;s=2#RO_JSFLOW_O",          # 2#RO进水流量
-        "ns=3;s=2#RO_JSPRESS_O",         # 2#RO进水压力
-        "ns=3;s=2#RO_NSFLOW_O",          # 2#RO浓水流量
-        "ns=3;s=2#RO_SDCSFLOW_O",        # 2#RO三段产水流量
-        "ns=3;s=2#RO_SDJSPRESS_O",       # 2#RO三段进水压力
-        "ns=3;s=2#RO_SDNSPRESS_O",       # 2#RO三段浓水压力
-        "ns=3;s=2#RODJB_CUR_FB_O",       # 2#RO段间泵电流反馈
-        "ns=3;s=2#RODJB_CZ_O",           # 2#RO段间泵测振反馈
-        "ns=3;s=2#RODJB_FRE_FB_O",       # 2#RO段间泵频率反馈
-        "ns=3;s=2#ROGYB_CUR_FB_O",    # 2#RO高压泵电流反馈	
-        "ns=3;s=2#ROGYB_CZ_O",        # 2#RO高压泵测振反馈	
-        "ns=3;s=2#ROGYB_FRE_FB_O",    # 2#RO高压泵频率反馈
-        "ns=3;s=2#UF_CSPRESS_O",      #	2#UF产水压力
-        "ns=3;s=2#UF_JSFLOW_O",       #	2#UF进水流量
-        "ns=3;s=2#UF_JSPRESS_O",      #	2#UF进水压力
-        "ns=3;s=2#UF_V_FB_O",         #	2#UF调节阀开度反馈
-        "ns=3;s=2#UFBWB_CUR_FB_O",    #	2#UF反洗泵电流反馈
-        "ns=3;s=2#UFBWB_FRE_FB_O",    #	2#UF反洗泵频率反馈
-        "ns=3;s=RO_JSDD_O",           # RO进水电导
-        "ns=3;s=RO_JSORP_O",          # RO进水ORP
-        "ns=3;s=RO_JSPH_O",           # RO进水PH
-        "ns=3;s=RO1_1DUAN_CS_FLOW",   # RO1一段产水流量
-        "ns=3;s=ZJS_PRESS_O",         # 进水压力
-        "ns=3;s=ZJS_TEMP_O",          # 进水温度
-        "ns=3;s=ZJS_ZD_O",            # UF进水浊度
-        "ns=3;s=PUBLIC_RO1_MTL",      # RO1膜通量
-        "ns=3;s=PUBLIC_RO2_MTL",      # RO2膜通量
-        "ns=3;s=UF1_SSD_KMYC",        # UF1跨膜压差
-        "ns=3;s=UF2_SSD_KMYC",        # UF2跨膜压差
-        "ns=3;s=RO1_1D_YC",           # RO1一段压差
-        "ns=3;s=RO1_2D_YC",           # RO1二段压差
-        "ns=3;s=RO2_1D_YC",           # RO2一段压差
-        "ns=3;s=RO2_2D_YC",           # RO2二段压差
-        "ns=3;s=PUBLIC_BY_REAL_1",    # RO1三段压差
-        "ns=3;s=PUBLIC_BY_REAL_2",    # RO2三段压差 
-    ]
-
-    @staticmethod
-    def load_and_process_data(args, data):
-        """加载并处理数据,划分训练/验证/测试集"""
-        # 处理日期
-        data['date'] = pd.to_datetime(data['date'])
-        time_interval = pd.Timedelta(minutes=(4 * args.resolution / 60))
-        window_time_span = time_interval * (args.seq_len + 1)
-
-        val_start_date = pd.to_datetime(args.val_start_date)
-        test_start_date = pd.to_datetime(args.test_start_date)
-        
-        # 调整时间窗口
-        adjusted_val_start = val_start_date - window_time_span
-        adjusted_test_start = test_start_date - window_time_span
-        
-        train_mask = (data['date'] >= pd.to_datetime(args.train_start_date)) & \
-                     (data['date'] <= pd.to_datetime(args.train_end_date))
-        val_mask = (data['date'] >= adjusted_val_start) & \
-                   (data['date'] <= pd.to_datetime(args.val_end_date))
-        test_mask = (data['date'] >= adjusted_test_start) & \
-                    (data['date'] <= pd.to_datetime(args.test_end_date))
-
-        train_data = data[train_mask].reset_index(drop=True)
-        val_data = data[val_mask].reset_index(drop=True)
-        test_data = data[test_mask].reset_index(drop=True)
-        
-        train_data = train_data.drop(columns=['date'])
-        val_data = val_data.drop(columns=['date'])
-        test_data = test_data.drop(columns=['date'])
-    
-        # 创建数据集
-        train_supervised = DataPreprocessor.create_supervised_dataset(args, train_data, 1)
-        val_supervised = DataPreprocessor.create_supervised_dataset(args, val_data, 1)
-        test_supervised = DataPreprocessor.create_supervised_dataset(args, test_data, args.step_size)
-        
-        # 转换为DataLoader
-        train_loader = DataPreprocessor.load_data(args, train_supervised, shuffle=True)
-        val_loader = DataPreprocessor.load_data(args, val_supervised, shuffle=False)
-        test_loader = DataPreprocessor.load_data(args, test_supervised, shuffle=False)
-        
-        return train_loader, val_loader, test_loader, data
-    
-    @staticmethod
-    def read_and_combine_csv_files(args):
-        """读取文件并进行特征筛选和预处理"""
-        current_dir = os.path.dirname(__file__)
-        parent_dir = os.path.dirname(current_dir)
-        args.data_dir = os.path.join(parent_dir, args.data_dir)
-        
-        def read_file(file_count):
-            file_name = args.file_pattern.format(file_count)
-            file_path = os.path.join(args.data_dir, file_name)
-            try:
-                df = pd.read_csv(file_path)
-                # 确保只读取需要的列,若列不存在则会报错提示
-                return df[DataPreprocessor.COLUMNS_TO_KEEP]
-            except KeyError as e:
-                print(f"文件 {file_name} 中缺少列: {e}")
-                raise
-        
-        file_indices = list(range(args.start_files, args.end_files + 1))
-        max_workers = os.cpu_count()
-        
-        with ThreadPoolExecutor(max_workers=max_workers) as executor:
-            results = list(tqdm(executor.map(read_file, file_indices),
-                                total=len(file_indices),
-                                desc="正在读取文件"))
-        
-        all_data = pd.concat(results, ignore_index=True)
-        
-        # 确保列顺序一致
-        all_data = all_data[DataPreprocessor.COLUMNS_TO_KEEP]
-        
-        # 下采样
-        chunk = all_data.iloc[::args.resolution, :].reset_index(drop=True)
-        
-        # 处理特征
-        chunk = DataPreprocessor.process_date(chunk, args)
-        chunk = DataPreprocessor.scaler_data(chunk, args)
-        
-        return chunk
-    
-    @staticmethod
-    def process_date(data, args):
-        data = data.rename(columns={'index': 'date'})
-        data['date'] = pd.to_datetime(data['date'])
-    
-        time_features = []
-        # 固定生成分钟级和日级特征,保持与Predictor一致
-        data['minute_of_day'] = data['date'].dt.hour * 60 + data['date'].dt.minute
-        data['minute_sin'] = np.sin(2 * np.pi * data['minute_of_day'] / 1440)
-        data['minute_cos'] = np.cos(2 * np.pi * data['minute_of_day'] / 1440)
-        
-        data['day_of_year'] = data['date'].dt.dayofyear
-        data['day_year_sin'] = np.sin(2 * np.pi * data['day_of_year'] / 366)
-        data['day_year_cos'] = np.cos(2 * np.pi * data['day_of_year'] / 366)
-        
-        time_features.extend(['minute_sin', 'minute_cos', 'day_year_sin', 'day_year_cos'])
-        data.drop(columns=['minute_of_day', 'day_of_year'], inplace=True)
-    
-        other_columns = [col for col in data.columns if col not in ['date'] and col not in time_features]
-        data = data[['date'] + time_features + other_columns]
-        return data
-    
-    @staticmethod
-    def scaler_data(data, args):
-        date_col = data[['date']]
-        data_to_scale = data.drop(columns=['date'])
-
-        scaler = MinMaxScaler(feature_range=(0, 1))
-        scaled_data = scaler.fit_transform(data_to_scale)
-        joblib.dump(scaler, args.scaler_path)
-
-        scaled_data = pd.DataFrame(scaled_data, columns=data_to_scale.columns)
-        scaled_data = pd.concat([date_col.reset_index(drop=True), scaled_data], axis=1)
-        return scaled_data
-    
-    @staticmethod
-    def create_supervised_dataset(args, data, step_size):
-        data = pd.DataFrame(data)
-        cols = []
-        col_names = []
-        feature_columns = data.columns.tolist()
-
-        # 输入序列
-        for col in feature_columns:
-            for i in range(args.seq_len - 1, -1, -1):
-                cols.append(data[[col]].shift(i))
-                col_names.append(f"{col}(t-{i})")
-        
-        # 目标序列 (取最后labels_num列)
-        target_columns = feature_columns[-args.labels_num:]
-        for i in range(1, args.output_size + 1):
-            for col in target_columns:
-                cols.append(data[[col]].shift(-i))
-                col_names.append(f"{col}(t+{i})")
-
-        dataset = pd.concat(cols, axis=1)
-        dataset.columns = col_names
-        dataset = dataset.iloc[::step_size, :]
-        dataset.dropna(inplace=True)
-        return dataset
-
-    @staticmethod
-    def load_data(args, dataset, shuffle):
-        input_length = args.seq_len
-        n_features = args.feature_num
-        labels_num = args.labels_num
-    
-        n_features_total = n_features * input_length
-        n_labels_total = args.output_size * labels_num
-
-        X = dataset.values[:, :n_features_total]
-        y = dataset.values[:, n_features_total:n_features_total + n_labels_total]
-    
-        X = X.reshape(X.shape[0], input_length, n_features)
-        X = torch.tensor(X, dtype=torch.float32).to(args.device)
-        y = torch.tensor(y, dtype=torch.float32).to(args.device)
-
-        dataset_tensor = TensorDataset(X, y)
-        generator = torch.Generator()
-        generator.manual_seed(args.random_seed)
-        
-        return DataLoader(dataset_tensor, batch_size=args.batch_size, shuffle=shuffle, generator=generator)

+ 0 - 169
models/prediction_models/longting/data_trainer.py

@@ -1,169 +0,0 @@
-# data_trainer.py
-import torch
-import joblib
-import numpy as np
-import pandas as pd
-from sklearn.metrics import r2_score
-from datetime import datetime, timedelta
-from sklearn.preprocessing import MinMaxScaler
-
-class Trainer:
-    def __init__(self, model, args, data):
-        self.args = args
-        self.model = model
-        self.data = data
-        self.patience = args.patience
-        self.min_delta = args.min_delta
-        self.counter = 0
-        self.early_stop = False
-        self.best_val_loss = float('inf')
-        self.best_model_state = None
-        self.best_epoch = 0
-
-    def train_full_model(self, train_loader, val_loader, optimizer, criterion, scheduler):
-        self.counter = 0
-        self.best_val_loss = float('inf')
-        self.early_stop = False
-        self.best_model_state = None
-        self.best_epoch = 0
-        max_epochs = self.args.epochs
-
-        for epoch in range(max_epochs):
-            self.model.train()
-            running_loss = 0.0
-            
-            for inputs, targets in train_loader:
-                inputs = inputs.to(self.args.device)
-                targets = targets.to(self.args.device)
-                optimizer.zero_grad()
-                outputs = self.model(inputs)
-                loss = criterion(outputs, targets)
-                loss.backward()
-                optimizer.step()
-                running_loss += loss.item()
-            
-            train_loss = running_loss / len(train_loader)
-            val_loss = self.validate_full(val_loader, criterion) if val_loader else 0.0
-
-            print(f'Epoch {epoch+1}/{max_epochs}, Train Loss: {train_loss:.6f}, Val Loss: {val_loss:.6f}')
-
-            if val_loader:
-                if val_loss < (self.best_val_loss - self.min_delta):
-                    self.best_val_loss = val_loss
-                    self.counter = 0
-                    self.best_model_state = self.model.state_dict()
-                    self.best_epoch = epoch
-                else:
-                    self.counter += 1
-                    if self.counter >= self.patience:
-                        self.early_stop = True
-                        print(f"早停触发")
-                        
-            scheduler.step()
-            torch.cuda.empty_cache()
-            if self.early_stop:
-                break
-
-        if self.best_model_state is not None:
-            self.model.load_state_dict(self.best_model_state)
-        print(f"最佳迭代: {self.best_epoch+1}, 最佳验证损失: {self.best_val_loss:.6f}")
-        return self.model
-
-    def validate_full(self, val_loader, criterion):
-        self.model.eval()
-        total_loss = 0.0
-        with torch.no_grad():
-            for inputs, targets in val_loader:
-                inputs = inputs.to(self.args.device)
-                targets = targets.to(self.args.device)
-                outputs = self.model(inputs)
-                loss = criterion(outputs, targets)
-                total_loss += loss.item()
-        return total_loss / len(val_loader)
-
-    def save_model(self):
-        torch.save(self.model.state_dict(), self.args.model_path)
-        print(f"模型已保存到:{self.args.model_path}")
-            
-    def evaluate_model(self, test_loader, criterion):
-        self.model.eval()
-        scaler = joblib.load(self.args.scaler_path)
-        predictions = []
-        true_values = []
-        
-        with torch.no_grad():
-            for inputs, targets in test_loader:
-                inputs = inputs.to(self.args.device)
-                targets = targets.to(self.args.device)
-                outputs = self.model(inputs)
-                predictions.append(outputs.cpu().numpy())
-                true_values.append(targets.cpu().numpy())
-    
-        predictions = np.concatenate(predictions, axis=0)
-        true_values = np.concatenate(true_values, axis=0)
-    
-        # 重塑
-        reshaped_predictions = predictions.reshape(predictions.shape[0], self.args.output_size, self.args.labels_num)
-        predictions = reshaped_predictions.reshape(-1, self.args.labels_num)
-        
-        reshaped_true_values = true_values.reshape(true_values.shape[0], self.args.output_size, self.args.labels_num)
-        true_values = reshaped_true_values.reshape(-1, self.args.labels_num)
-    
-        # 反归一化 (仅标签列)
-        column_scaler = MinMaxScaler(feature_range=(0, 1))
-        column_scaler.min_ = scaler.min_[-self.args.labels_num:] 
-        column_scaler.scale_ = scaler.scale_[-self.args.labels_num:] 
-        
-        true_values = column_scaler.inverse_transform(true_values)
-        predictions = column_scaler.inverse_transform(predictions)
-    
-        # 定义8个核心变量
-        column_names = [
-            "ns=3;s=UF1_SSD_KMYC",        # UF1跨膜压差
-            "ns=3;s=UF2_SSD_KMYC",        # UF2跨膜压差
-            "ns=3;s=RO1_1D_YC",           # RO1一段压差
-            "ns=3;s=RO1_2D_YC",           # RO1二段压差
-            "ns=3;s=RO2_1D_YC",           # RO2一段压差
-            "ns=3;s=RO2_2D_YC",           # RO2二段压差
-            "ns=3;s=PUBLIC_BY_REAL_1",    # RO1三段压差
-            "ns=3;s=PUBLIC_BY_REAL_2",    # RO2三段压差 
-        ]
-    
-        # 生成时间
-        start_datetime = datetime.strptime(self.args.test_start_date, "%Y-%m-%d")
-        time_interval = timedelta(minutes=(4 * self.args.resolution / 60))
-        total_points = len(predictions)
-        date_times = [start_datetime + i * time_interval for i in range(total_points)]
-        
-        results = pd.DataFrame({'date': date_times})
-        metrics_details = []
-        
-        for i, col_name in enumerate(column_names):
-            if i >= self.args.labels_num: break # 防止越界
-            
-            results[f'{col_name}_True'] = true_values[:, i]
-            results[f'{col_name}_Predicted'] = predictions[:, i]
-            
-            var_true = true_values[:, i]
-            var_pred = predictions[:, i]
-            
-            # 指标计算
-            non_zero_mask = var_true != 0
-            var_true_nonzero = var_true[non_zero_mask]
-            var_pred_nonzero = var_pred[non_zero_mask]
-            
-            if len(var_true_nonzero) > 0:
-                r2 = r2_score(var_true_nonzero, var_pred_nonzero)
-                rmse = np.sqrt(np.mean((var_true_nonzero - var_pred_nonzero) ** 2))
-                mape = np.mean(np.abs((var_true_nonzero - var_pred_nonzero) / np.abs(var_true_nonzero))) * 100
-                metrics_details.append(f"{col_name}: R2={r2:.4f}, RMSE={rmse:.4f}, MAPE={mape:.4f}%")
-            else:
-                metrics_details.append(f"{col_name}: 无效数据")
-
-        results.to_csv(self.args.output_csv_path, index=False)
-        
-        txt_path = self.args.output_csv_path.replace('.csv', '_metrics.txt')
-        with open(txt_path, 'w') as f:
-            f.write('\n'.join(metrics_details))
-            
-        return metrics_details

+ 0 - 54
models/prediction_models/longting/gat_lstm.py

@@ -1,54 +0,0 @@
-# gat_lstm.py
-import torch
-import torch.nn as nn
-
-class SingleGATLSTM(nn.Module):
-    """单个子模型:预测1个目标指标"""
-    def __init__(self, args):
-        super(SingleGATLSTM, self).__init__()
-        self.args = args
-        
-        self.lstm = nn.LSTM(
-            input_size=args.feature_num,
-            hidden_size=args.hidden_size,
-            num_layers=args.num_layers,
-            batch_first=True
-        )
-        
-        self.final_linear = nn.Sequential(
-            nn.Linear(args.hidden_size, args.hidden_size),
-            nn.LeakyReLU(0.01),
-            nn.Dropout(args.dropout * 0.4),
-            nn.Linear(args.hidden_size, args.output_size)
-        )
-        self._init_weights()
-        
-    def _init_weights(self):
-        for m in self.modules():
-            if isinstance(m, nn.Linear):
-                nn.init.xavier_uniform_(m.weight)
-                if m.bias is not None: nn.init.zeros_(m.bias)
-
-    def forward(self, x):
-        batch_size, seq_len, feature_num = x.size()
-        lstm_out, _ = self.lstm(x)
-        last_out = lstm_out[:, -1, :]
-        output = self.final_linear(last_out)
-        return output
-
-class GAT_LSTM(nn.Module):
-    """总模型:包含多个SingleGATLSTM子模型"""
-    def __init__(self, args):
-        super(GAT_LSTM, self).__init__()
-        self.args = args
-        # 创建4个独立模型(对应labels_num=4)
-        self.models = nn.ModuleList([SingleGATLSTM(args) for _ in range(args.labels_num)])
-    
-    def set_edge_index(self, edge_index):
-        self.edge_index = edge_index
-        
-    def forward(self, x):
-        outputs = []
-        for model in self.models:
-            outputs.append(model(x))
-        return torch.cat(outputs, dim=1)

+ 0 - 59
models/prediction_models/longting/main.py

@@ -1,59 +0,0 @@
-# main.py
-import os
-import torch
-import numpy as np
-import random
-from gat_lstm import GAT_LSTM
-from data_trainer import Trainer
-from args import lstm_args_parser
-from torch.nn import MSELoss
-from data_preprocessor import DataPreprocessor
-
-def set_seed(seed):
-    random.seed(seed)
-    os.environ['PYTHONHASHSEED'] = str(seed)
-    np.random.seed(seed)
-    torch.manual_seed(seed)
-    torch.cuda.manual_seed(seed)
-    torch.backends.cudnn.deterministic = True
-    torch.backends.cudnn.benchmark = False
-
-def main():
-    args = lstm_args_parser()
-    set_seed(args.random_seed)
-    
-    device = torch.device(f"cuda:{args.device}" if torch.cuda.is_available() else "cpu")
-    args.device = device
-
-    print(f"当前配置: 序列长度={args.seq_len}, 特征数={args.feature_num}, 目标数={args.labels_num}")
-
-    # 数据预处理
-    data = DataPreprocessor.read_and_combine_csv_files(args)
-    train_loader, val_loader, test_loader, _ = DataPreprocessor.load_and_process_data(args, data)
-    
-    # 初始化模型
-    model = GAT_LSTM(args).to(device)
-    
-    # 加载edge_index.pt
-    if os.path.exists('edge_index.pt'):
-        edge_index = torch.load('edge_index.pt', map_location=device, weights_only=True)
-        model.set_edge_index(edge_index)
-        print("已加载 edge_index.pt")
-    else:
-        print("未找到 edge_index.pt")
-
-    # 训练器
-    trainer = Trainer(model, args, data)
-    criterion = MSELoss()
-    optimizer = torch.optim.Adam(model.parameters(), lr=args.lr)
-    scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=args.scheduler_step_size, gamma=args.scheduler_gamma)
-
-    print("=== 开始训练 ===")
-    trainer.train_full_model(train_loader, val_loader, optimizer, criterion, scheduler)
-    trainer.save_model()
-    
-    print("=== 开始评估 ===")
-    trainer.evaluate_model(test_loader, MSELoss())
-
-if __name__ == "__main__":
-    main()

+ 0 - 305
models/prediction_models/longting/predict.py

@@ -1,305 +0,0 @@
-# predict.py
-import os
-import torch
-import joblib
-import pandas as pd
-import numpy as np
-from datetime import datetime, timedelta
-from gat_lstm import GAT_LSTM
-
-class RealTimePredictor:
-    def __init__(self, model_path='model.pth', scaler_path='scaler.pkl', device=None):
-        """
-        初始化预测器
-        """
-        # 1. 参数配置 (与训练 args.py 保持一致)
-        self.seq_len = 10         # 输入序列长度
-        self.feature_num = 78     # 输入特征数 (4时间编码 + 38业务特征)
-        self.labels_num = 8       # 输出标签数
-        self.hidden_size = 64
-        self.num_layers = 1
-        self.output_size = 5      # 预测未来 5 步
-        self.dropout = 0
-        
-        # 2. 设备与资源加载
-        self.device = device if device else torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
-        self.model_path = model_path
-        self.scaler_path = scaler_path
-        
-        # 加载归一化器
-        if not os.path.exists(self.scaler_path):
-             raise FileNotFoundError(f"未找到归一化文件: {self.scaler_path},请确保已完成训练。")
-        self.scaler = joblib.load(self.scaler_path)
-
-        # 加载模型
-        self._load_model()
-
-        # 定义必须存在的列名 (75个)
-        self.required_columns = [
-            'index',
-            "water_in",              # 进水量
-            "water_out",             # 外供水流量
-            "RO1_TYL",               # RO1脱盐率
-            "RO2_TYL",               # RO2脱盐率
-            "UF1Per",                # UF1渗透率
-            "UF2Per",                # UF2渗透率
-            "2#RODJB_Eff",           # 2#RO段间泵效率
-            "1#RODJB_Eff",           # 1#RO段间泵效率
-            "2#ROGYB_Eff",           # 2#RO高压泵效率
-            "1#ROGYB_Eff",           # 1#RO高压泵效率
-            "ROHSL",                 # 反渗透回收率
-            "ns=3;s=1#RO_CSDD_O",    # 1#RO产水电导
-            "ns=3;s=1#RO_CSPRESS_O",       # 1#RO产水压力
-            "ns=3;s=1#RO_EDCSFLOW_O",      # 1#RO二段产水流量
-            "ns=3;s=1#RO_EDJSPRESS_O",     # 1#RO二段进水压力
-            "ns=3;s=1#RO_EDNSPRESS_O",     # 1#RO二段浓水压力
-            "ns=3;s=1#RO_JSFLOW_O",        # 1#RO进水流量
-            "ns=3;s=1#RO_JSPRESS_O",       # 1#RO进水压力
-            "ns=3;s=1#RO_NSFLOW_O",        # 1#RO浓水流量
-            "ns=3;s=1#RO_SDCSFLOW_O",      # 1#RO三段产水流量
-            "ns=3;s=1#RO_SDJSPRESS_O",     # 1#RO三段进水压力
-            "ns=3;s=1#RO_SDNSPRESS_O",     # 1#RO三段浓水压力
-            "ns=3;s=1#RODJB_CUR_FB_O",     # 1#RO段间泵电流反馈
-            "ns=3;s=1#RODJB_CZ_O",         # 1#RO段间泵测振反馈
-            "ns=3;s=1#RODJB_FRE_FB_O",       # 1#RO段间泵频率反馈
-            "ns=3;s=1#ROGYB_CUR_FB_O",       # 1#RO高压泵电流反馈
-            "ns=3;s=1#ROGYB_CZ_O",           # 1#RO高压泵测振反馈
-            "ns=3;s=1#ROGYB_FRE_FB_O",       # 1#RO高压泵频率反馈
-            "ns=3;s=1#UF_CSPRESS_O",         # 1#UF产水压力
-            "ns=3;s=1#UF_JSFLOW_O",          # 1#UF进水流量
-            "ns=3;s=1#UF_JSPRESS_O",         # 1#UF进水压力
-            "ns=3;s=1#UF_V_FB_O",            # 1#UF调节阀开度反馈
-            "ns=3;s=1#UFBWB_CUR_FB_O",       # 1#UF反洗泵电流反馈
-            "ns=3;s=1#UFBWB_FRE_FB_O",       # 1#UF反洗泵频率反馈
-            "ns=3;s=2#RO_CSDD_O",            # 2#RO产水电导
-            "ns=3;s=2#RO_CSPRESS_O",         # 2#RO产水压力
-            "ns=3;s=2#RO_EDCSFLOW_O",        # 2#RO二段产水流量
-            "ns=3;s=2#RO_EDJSPRESS_O",       # 2#RO二段进水压力
-            "ns=3;s=2#RO_EDNSPRESS_O",       # 2#RO二段浓水压力
-            "ns=3;s=2#RO_JSFLOW_O",          # 2#RO进水流量
-            "ns=3;s=2#RO_JSPRESS_O",         # 2#RO进水压力
-            "ns=3;s=2#RO_NSFLOW_O",          # 2#RO浓水流量
-            "ns=3;s=2#RO_SDCSFLOW_O",        # 2#RO三段产水流量
-            "ns=3;s=2#RO_SDJSPRESS_O",       # 2#RO三段进水压力
-            "ns=3;s=2#RO_SDNSPRESS_O",       # 2#RO三段浓水压力
-            "ns=3;s=2#RODJB_CUR_FB_O",       # 2#RO段间泵电流反馈
-            "ns=3;s=2#RODJB_CZ_O",           # 2#RO段间泵测振反馈
-            "ns=3;s=2#RODJB_FRE_FB_O",       # 2#RO段间泵频率反馈
-            "ns=3;s=2#ROGYB_CUR_FB_O",    # 2#RO高压泵电流反馈	
-            "ns=3;s=2#ROGYB_CZ_O",        # 2#RO高压泵测振反馈	
-            "ns=3;s=2#ROGYB_FRE_FB_O",    # 2#RO高压泵频率反馈
-            "ns=3;s=2#UF_CSPRESS_O",      #	2#UF产水压力
-            "ns=3;s=2#UF_JSFLOW_O",       #	2#UF进水流量
-            "ns=3;s=2#UF_JSPRESS_O",      #	2#UF进水压力
-            "ns=3;s=2#UF_V_FB_O",         #	2#UF调节阀开度反馈
-            "ns=3;s=2#UFBWB_CUR_FB_O",    #	2#UF反洗泵电流反馈
-            "ns=3;s=2#UFBWB_FRE_FB_O",    #	2#UF反洗泵频率反馈
-            "ns=3;s=RO_JSDD_O",           # RO进水电导
-            "ns=3;s=RO_JSORP_O",          # RO进水ORP
-            "ns=3;s=RO_JSPH_O",           # RO进水PH
-            "ns=3;s=RO1_1DUAN_CS_FLOW",   # RO1一段产水流量
-            "ns=3;s=ZJS_PRESS_O",         # 进水压力
-            "ns=3;s=ZJS_TEMP_O",          # 进水温度
-            "ns=3;s=ZJS_ZD_O",            # UF进水浊度
-            "ns=3;s=PUBLIC_RO1_MTL",      # RO1膜通量
-            "ns=3;s=PUBLIC_RO2_MTL",      # RO2膜通量
-            "ns=3;s=UF1_SSD_KMYC",        # UF1跨膜压差
-            "ns=3;s=UF2_SSD_KMYC",        # UF2跨膜压差
-            "ns=3;s=RO1_1D_YC",           # RO1一段压差
-            "ns=3;s=RO1_2D_YC",           # RO1二段压差
-            "ns=3;s=RO2_1D_YC",           # RO2一段压差
-            "ns=3;s=RO2_2D_YC",           # RO2二段压差
-            "ns=3;s=PUBLIC_BY_REAL_1",    # RO1三段压差
-            "ns=3;s=PUBLIC_BY_REAL_2",    # RO2三段压差 
-        ]
-        
-        # --- 用于防空值兜底机制的变量 ---
-        self.raw_input_data = None
-        # 目标列名自动推导(最后 labels_num 个列)
-        self.target_columns = self.required_columns[-self.labels_num:]
-
-    def _load_model(self):
-        """内部方法:加载模型权重"""
-        class ModelArgs: pass
-        args = ModelArgs()
-        args.feature_num = self.feature_num
-        args.hidden_size = self.hidden_size
-        args.num_layers = self.num_layers
-        args.output_size = self.output_size
-        args.labels_num = self.labels_num
-        args.dropout = self.dropout
-
-        self.model = GAT_LSTM(args).to(self.device)
-        
-        # 加载 edge_index.pt
-        if os.path.exists('edge_index.pt'):
-            edge_index = torch.load('edge_index.pt', map_location=self.device, weights_only=True)
-            self.model.set_edge_index(edge_index)
-        
-        if not os.path.exists(self.model_path):
-            raise FileNotFoundError(f"未找到模型权重文件: {self.model_path}")
-            
-        state_dict = torch.load(self.model_path, map_location=self.device, weights_only=True)
-        self.model.load_state_dict(state_dict)
-        self.model.eval()
-
-    def _preprocess(self, df):
-        """数据预处理:补全、排序、生成时间特征、整体归一化"""
-        data = df.copy()
-        
-        # 1. 统一时间列名
-        if 'datetime' in data.columns:
-            data = data.rename(columns={'datetime': 'index'})
-        if 'index' not in data.columns:
-             data['index'] = pd.date_range(end=datetime.now(), periods=len(data), freq='min')
-        data['index'] = pd.to_datetime(data['index'])
-        
-        # 2. 补全长度 (Padding)
-        if len(data) < self.seq_len:
-            pad_len = self.seq_len - len(data)
-            first_row = data.iloc[0:1]
-            pads = pd.concat([first_row] * pad_len, ignore_index=True)
-            start_time = data['index'].iloc[0]
-            for i in range(pad_len):
-                pads.at[i, 'index'] = start_time - timedelta(minutes=(pad_len-i))
-            data = pd.concat([pads, data], ignore_index=True)
-
-        # 3. 列筛选排序 (提取业务数据,不含index)
-        try:
-            # required_columns[0] 是 'index',我们取后面的业务列
-            business_cols = self.required_columns[1:]
-            data_business = data[business_cols]
-            # 策略: 前向填充 -> 后向填充 -> 填充为0
-            data_business = data_business.ffill().bfill().fillna(0.0)
-        except KeyError:
-            missing = list(set(self.required_columns) - set(data.columns))
-            raise ValueError(f"缺少列: {missing}")
-
-        # 4. 生成时间特征
-        date_col = data['index']
-        minute_of_day = date_col.dt.hour * 60 + date_col.dt.minute
-        day_of_year = date_col.dt.dayofyear
-        
-        time_features = pd.DataFrame({
-            'minute_sin': np.sin(2 * np.pi * minute_of_day / 1440),
-            'minute_cos': np.cos(2 * np.pi * minute_of_day / 1440),
-            'day_year_sin': np.sin(2 * np.pi * day_of_year / 366),
-            'day_year_cos': np.cos(2 * np.pi * day_of_year / 366)
-        })
-        
-        # 5. 拼接:[时间特征 + 业务特征]
-        # 注意:训练时的顺序是 time_features + other_columns
-        # 必须重置索引以避免拼接错位
-        data_to_scale = pd.concat([
-            time_features.reset_index(drop=True), 
-            data_business.reset_index(drop=True)
-        ], axis=1)
-        
-        # 6. 整体归一化
-        # 此时 columns 应该包含: minute_sin, minute_cos..., AR.1#UF_JSFLOW_O...
-        # 顺序和名字必须与 fit 时一致
-        scaled_array = self.scaler.transform(data_to_scale)
-        
-        return scaled_array
-    
-    # --- 备用防空值兜底函数 ---
-    def get_recent_values_as_fallback(self):
-        """从原始输入数据中获取最近的output_size条记录作为备用输出,避免输出空值"""
-        if self.raw_input_data is None or self.raw_input_data.empty:
-            return np.zeros((self.output_size, self.labels_num))
-
-        df_copy = self.raw_input_data.copy()
-        
-        # 统一时间列格式,防止报错
-        if 'datetime' in df_copy.columns:
-            df_copy = df_copy.rename(columns={'datetime': 'index'})
-        if 'index' not in df_copy.columns:
-            df_copy['index'] = pd.date_range(end=datetime.now(), periods=len(df_copy), freq='min')
-        df_copy['index'] = pd.to_datetime(df_copy['index'])
-
-        # 按时间排序并取最近的output_size条
-        recent_data = df_copy.sort_values('index').tail(self.output_size)
-        
-        # 若数据不足,用最后一条补充
-        if len(recent_data) < self.output_size:
-            last_row = recent_data.iloc[-1:] if not recent_data.empty else pd.DataFrame(
-                {col: [0.0] for col in self.target_columns}, index=[0])
-            while len(recent_data) < self.output_size:
-                recent_data = pd.concat([recent_data, last_row], ignore_index=True)
-        
-        # 确保提取的兜底数据中没有空值 (NaN)
-        recent_data[self.target_columns] = recent_data[self.target_columns].ffill().bfill().fillna(0.0)
-        
-        # 提取目标列值并返回
-        try:
-            fallback_values = recent_data[self.target_columns].values
-        except KeyError:
-            # 极度异常情况兜底(输入中缺少目标列)
-            fallback_values = np.zeros((self.output_size, self.labels_num))
-            
-        return fallback_values
-
-    def predict(self, df):
-        """
-        返回: List[List[float]]
-        格式: [[t+1时刻的4个值], [t+2时刻的4个值], ..., [t+5时刻的4个值]]
-        """
-        # --- 保存原始输入数据用于可能的降级策略 ---
-        self.raw_input_data = df.copy()
-        
-        # 1. 预处理 (返回的是归一化后的 numpy 数组)
-        processed_data = self._preprocess(df)
-        
-        # 2. 取最后 seq_len 个时间步构建 Tensor
-        input_seq = processed_data[-self.seq_len:] 
-        input_tensor = torch.tensor(input_seq, dtype=torch.float32).unsqueeze(0).to(self.device)
-        
-        # 3. 推理
-        with torch.no_grad():
-            output = self.model(input_tensor)
-        
-        # 4. 反归一化
-        # 输出形状调整为 (5, 4) -> 5个步长, 4个变量
-        preds = output.cpu().numpy().reshape(self.output_size, self.labels_num)
-        
-        # 获取最后4列的归一化参数 (目标变量)
-        target_min = self.scaler.min_[-self.labels_num:]
-        target_scale = self.scaler.scale_[-self.labels_num:]
-        
-        real_preds = (preds - target_min) / target_scale
-        real_preds = np.abs(real_preds)
-        
-        # --- 空值/NaN 检测与兜底机制 ---
-        # 如果模型因极端情况输出 NaN 或者 inf 无穷大,触发历史数据兜底
-        if np.isnan(real_preds).any() or np.isinf(real_preds).any():
-            real_preds = self.get_recent_values_as_fallback()
-        
-        # 5. 返回纯数值列表
-        return real_preds.tolist()
-
-if __name__ == "__main__":
-    # 测试代码
-    try:
-        # 初始化
-        predictor = RealTimePredictor()
-        
-        # 生成模拟数据
-        mock_data = pd.DataFrame()
-        mock_data['index'] = pd.date_range(end=datetime.now(), periods=15, freq='min')
-        for col in predictor.required_columns[1:]:
-            mock_data[col] = np.random.rand(15) * 10
-            
-        # 人为制造一些空值进行测试
-        # mock_data.loc[5:7, 'water_in'] = np.nan
-        # mock_data.loc[12, predictor.target_columns[0]] = np.nan
-        
-        # 预测
-        result = predictor.predict(mock_data)
-        
-        print("预测结果 (5x8 数组):")
-        print(result)
-        
-    except Exception as e:
-        print(f"Error: {e}")
-        import traceback
-        traceback.print_exc()

+ 0 - 384
models/pressure-predictor/20分钟TMP预测模型源码/20min_predict.py

@@ -1,384 +0,0 @@
-import os
-import torch
-import pandas as pd
-import numpy as np
-import joblib
-import pywt
-from datetime import datetime, timedelta
-from torch.utils.data import DataLoader, TensorDataset
-from gat_lstm import GAT_LSTM    # 导入自定义的GAT-LSTM模型
-from tqdm import tqdm
-
-def set_seed(seed):
-    """设置随机种子,保证实验可重复性"""
-    import random
-    random.seed(seed)
-    os.environ['PYTHONHASHSEED'] = str(seed)
-    np.random.seed(seed)
-    torch.manual_seed(seed)
-    torch.cuda.manual_seed(seed)
-    torch.cuda.manual_seed_all(seed)
-    torch.backends.cudnn.deterministic = True
-    torch.backends.cudnn.benchmark = False
-
-class Predictor:
-    """预测器类,用于加载数据、模型并执行预测流程"""
-    def __init__(self):
-        self.seq_len = 10    # 输入序列长度(历史时间步)
-        self.output_size = 5    # 预测步长(未来预测的时间步数)
-        self.labels_num = 16    # 预测目标数量(16个待预测的指标)
-        self.feature_num = 79   # 输入特征总维度
-        self.step_size = 5      # 数据采样步长(每隔step_size取一个样本)
-        self.dropout = 0        # dropout概率(防止过拟合)
-        self.lr = 0.01          # 学习率(训练时使用,预测时仅作参数记录)
-        self.num_heads = 8      # 注意力头数(模型结构参数)
-        self.hidden_size = 64   # 隐藏层维度
-        self.batch_size = 512   # 批处理大小
-        self.num_layers = 1     # LSTM层数
-        self.resolution = 60    # 数据分辨率(原始数据每隔60条取一条,下采样)
-        self.test_start_date = '2025-07-01'  # 测试集起始日期(初始值,会动态更新)
-        self.wavelet = 'db4'    # 小波变换类型(预留,未实际使用)
-        self.level = 3          # 小波分解层数(预留)
-        self.level_after = 4    # 后续小波处理层数(预留)
-        self.mode = 'soft'      # 小波阈值模式(预留)
-        self.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")  # 计算设备(GPU优先)
-        self.model_path = '20min_model.pth'   # 模型权重保存路径
-        self.output_csv_path = '20min_predictions.csv'   # 预测结果保存路径
-        self.random_seed = 1314    # 随机种子
-        self.min_rows = 600         # 定义最小数据行数要求(600行)
-        self.uf_threshold = 0.001   # UF指标阈值(预留)
-        self.ro_threshold = 0.01    # RO指标阈值(预留)
-        self.flow_threshold = 1.0   # 流量阈值(预留)
-
-        set_seed(self.random_seed)    # 初始化随机种子
-        self.scaler = joblib.load('20min_scaler.pkl')    # 加载数据归一化器(训练时保存)
-        self.model = None         # 模型实例(后续加载)
-        self.edge_index = None    # 图结构边索引(图模型用)
-        self.test_loader = None   # 测试数据加载器(后续创建)
-        
-    def ensure_min_rows(self, df):
-        """
-        确保数据至少有600行,不足则进行前后补充
-        向前补充:使用最早的数据向前扩展
-        向后补充:使用最新的数据向后扩展
-        """
-        current_rows = len(df)
-        if current_rows >= self.min_rows:
-            return df
-        
-        # 计算需要补充的行数
-        need_rows = self.min_rows - current_rows
-        print(f"数据行数不足{self.min_rows}行(当前{current_rows}行),需要补充{need_rows}行")
-        
-        # 计算时间间隔(假设数据是均匀采样的)
-        time_col = 'index'
-        df[time_col] = pd.to_datetime(df[time_col])
-        time_diff = (df[time_col].iloc[1] - df[time_col].iloc[0]).total_seconds()
-        
-        # 向前补充(使用最早的数据)
-        forward_rows = need_rows // 2
-        if forward_rows > 0:
-            earliest_data = df.iloc[0:1].copy()
-            forward_data = []
-            for i in range(1, forward_rows + 1):
-                new_row = earliest_data.copy()
-                new_row[time_col] = earliest_data[time_col] - timedelta(seconds=time_diff * i)
-                forward_data.append(new_row)
-            forward_df = pd.concat(forward_data, ignore_index=True)
-            df = pd.concat([forward_df, df], ignore_index=True)
-        
-        # 检查是否还需要向后补充
-        current_rows = len(df)
-        if current_rows < self.min_rows:
-            backward_rows = self.min_rows - current_rows
-            latest_data = df.iloc[-1:].copy()
-            backward_data = []
-            for i in range(1, backward_rows + 1):
-                new_row = latest_data.copy()
-                new_row[time_col] = latest_data[time_col] + timedelta(seconds=time_diff * i)
-                backward_data.append(new_row)
-            backward_df = pd.concat(backward_data, ignore_index=True)
-            df = pd.concat([df, backward_df], ignore_index=True)
-        
-        print(f"数据补充完成,当前行数:{len(df)}行")
-        return df
-
-    def reorder_columns(self, df):
-        """
-        调整数据列顺序,确保与训练时的特征顺序一致
-        避免因列顺序不一致导致模型输入特征错位
-        """
-        desired_order = [
-            'index',
-            'C.M.FT_ZGJJY1@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.UF1_FT_JS@out','C.M.UF2_FT_JS@out','C.M.UF3_FT_JS@out',
-            'C.M.UF4_FT_JS@out','C.M.UF_FT_ZCS@out','C.M.FT_ZGJJY2@out','C.M.FT_ZGJJY3@out',
-            'C.M.FT_ZGJJY4@out','C.M.RO1_PT_JS@out','C.M.RO2_PT_JS@out','C.M.RO3_PT_JS@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',
-            'C.M.LT_JSC@out','C.M.RO1_PT_CS@out','C.M.RO1_PT_DJ2@out','C.M.RO2_PT_CS@out',
-            'C.M.RO2_PT_DJ2@out','C.M.RO3_PT_CS@out','C.M.RO3_PT_DJ2@out','C.M.RO4_PT_CS@out',
-            'C.M.RO4_PT_DJ2@out','C.M.RO4_PT_JS@out','C.M.LT_HCl@out','C.M.LT_NaClO@out',
-            'C.M.LT_PAC@out','C.M.LT_QSC@out','C.M.RO_Cond_ZCS@out','C.M.RO_TT_ZJS@out',
-            'C.M.UF1_JSF_kd@out','C.M.UF2_JSF_kd@out','C.M.UF_GSB4_fre@out','C.M.UF_ORP_ZCS@out',
-            'C.M.JYB2_ZGJ1_fre@out','C.M.JYB2_ZGJ2_fre@out','C.M.JYB2_ZGJ3_fre@out','C.M.JYB2_ZGJ4_fre@out',
-            '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.UF3_JSF_kd@out','C.M.UF4_JSF_kd@out','C.M.UF_FXB2_fre@out','C.M.RO1_DJB_fre@out',
-            'C.M.RO1_GYBF_kd@out','C.M.RO2_DJB_fre@out','C.M.RO2_GYBF_kd@out','C.M.RO3_DJB_fre@out',
-            'C.M.RO3_GYBF_kd@out','C.M.RO4_DJB_fre@out','C.M.RO4_GYBF_kd@out',
-            'C.M.UF1_DB@press_PV','C.M.UF2_DB@press_PV','C.M.UF3_DB@press_PV','C.M.UF4_DB@press_PV',
-            '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',
-        ]
-        return df.loc[:, desired_order]
-
-    def process_date(self, data):
-        """
-        处理日期列,生成周期性时间特征(捕捉时间周期性模式)
-        包括:分钟级正弦/余弦特征(每日周期)、年中日正弦/余弦特征(年度周期)
-        """
-        if 'index' in data.columns:
-            data = data.rename(columns={'index': 'date'})
-        data['date'] = pd.to_datetime(data['date'])
-        data['minute_of_day'] = data['date'].dt.hour * 60 + data['date'].dt.minute
-        data['day_of_year'] = data['date'].dt.dayofyear
-        
-        # 周期性编码(将时间转换为正弦/余弦值,确保周期性连续)
-        data['minute_sin'] = np.sin(2 * np.pi * data['minute_of_day'] / 1440)  # 分钟正弦特征
-        data['minute_cos'] = np.cos(2 * np.pi * data['minute_of_day'] / 1440)  # 分钟余弦特征
-        data['day_year_sin'] = np.sin(2 * np.pi * data['day_of_year'] / 366)   # 年中日正弦特征
-        data['day_year_cos'] = np.cos(2 * np.pi * data['day_of_year'] / 366)   # 年中日余弦特征
-        # 移除原始时间列(仅保留编码后的特征)
-        data.drop(columns=['minute_of_day', 'day_of_year'], inplace=True)
-        
-        # 调整列顺序:日期 + 时间特征 + 其他特征
-        time_features = ['minute_sin', 'minute_cos', 'day_year_sin', 'day_year_cos']
-        other_columns = [col for col in data.columns if col not in ['date'] + time_features]
-        return data[['date'] + time_features + other_columns]
-
-    def scaler_data(self, data):
-        """
-        对数据进行归一化(使用训练时保存的scaler)
-        保持与训练数据的归一化方式一致(0-1缩放)
-        """
-        date_col = data[['date']]
-        data_to_scale = data.drop(columns=['date'])
-        scaled = self.scaler.transform(data_to_scale)
-        scaled_df = pd.DataFrame(scaled, columns=data_to_scale.columns)
-        # 拼接日期列和归一化后的特征列
-        return pd.concat([date_col.reset_index(drop=True), scaled_df], axis=1)
-    
-    def remove_outliers(self, predictions):
-        """
-        用四分位法处理预测结果中的异常值
-        异常值定义:小于Q1-1.5*IQR或大于Q3+1.5*IQR的值
-        异常值替换为正常值的平均值(避免极端值影响)
-        """
-        cleaned = predictions.copy()
-        # 遍历每个特征列(16个标签)
-        for col in range(cleaned.shape[1]):
-            values = cleaned[:, col]
-            # 计算四分位数
-            q1 = np.percentile(values, 25)
-            q3 = np.percentile(values, 75)
-            iqr = q3 - q1
-            # 异常值边界
-            lower_bound = q1 - 1.5 * iqr
-            upper_bound = q3 + 1.5 * iqr
-            # 筛选正常值
-            normal_values = values[(values >= lower_bound) & (values <= upper_bound)]
-            # 用正常值的平均值替换异常值
-            if len(normal_values) > 0:
-                mean_normal = np.mean(normal_values)
-                cleaned[(values < lower_bound) | (values > upper_bound), col] = mean_normal
-        return cleaned
-    
-    def smooth_predictions(self, predictions):
-        """
-        对预测结果进行加权平滑处理,减少预测波动
-        采用滑动窗口加权平均:中间值权重为2,前后邻居权重为1(边缘值特殊处理)
-        """
-        smoothed = predictions.copy()
-        n_timesteps = predictions.shape[0]
-        if n_timesteps <= 1:
-            return smoothed
-        
-        # 遍历每个特征列
-        for col in range(predictions.shape[1]):
-            values = predictions[:, col]
-            # 第一个值:加权前两个值(避免边缘过度平滑)
-            smoothed[0, col] = (2 * values[0] + values[1]) / 3
-            # 中间值:加权前后邻居(核心平滑)
-            for i in range(1, n_timesteps - 1):
-                smoothed[i, col] = (values[i-1] + 2 * values[i] + values[i+1]) / 4
-            # 最后一个值:加权最后两个值(避免边缘过度平滑)
-            smoothed[-1, col] = (values[-2] + 2 * values[-1]) / 3
-        return smoothed
-
-    def create_test_loader(self, df):
-        """
-        构建测试数据加载器(将原始数据转换为模型输入格式)
-        输入:预处理后的DataFrame
-        输出:PyTorch DataLoader(批量加载模型输入)
-        """
-        df['date'] = pd.to_datetime(df['date'])
-        # 计算时间间隔(根据分辨率,单位:分钟)
-        time_interval = pd.Timedelta(minutes=(4 * self.resolution / 60))
-        # 计算窗口时间跨度(确保能覆盖输入序列长度+预测步长)
-        window_time_span = time_interval * (self.seq_len + 20)
-        # 调整测试集起始时间(确保有足够的历史数据构建输入序列)
-        adjusted_test_start = pd.to_datetime(self.test_start_date) - window_time_span
-        # 筛选所需的历史数据
-        test_df = df[df['date'] >= adjusted_test_start].reset_index(drop=True)
-
-        test_df = test_df.drop(columns=['date'])
-
-        # 构建监督学习数据集(输入序列+目标序列的占位)
-        feature_columns = test_df.columns.tolist()
-        cols = []
-        
-        # 构建输入序列(历史seq_len个时间步的特征)
-        for col in feature_columns:
-            for i in range(self.seq_len - 1, -1, -1):
-                cols.append(test_df[[col]].shift(i))   # 滞后i步的特征(t-0到t-(seq_len-1))
-                
-        # 构建目标序列占位(未来output_size个时间步的标签,预测时不使用真实值)
-        for i in range(1, self.output_size + 1):
-            for col in feature_columns[-self.labels_num:]:
-                cols.append(test_df[[col]].shift(-i))    # 超前i步的标签(t+1到t+output_size)
-                
-        # 合并列并按步长采样,最后取最后一行作为预测输入(最新的历史数据)
-        dataset = pd.concat(cols, axis=1).iloc[::self.step_size]
-        dataset = dataset.iloc[[-1]]
-    
-        # 提取输入特征(前n_features_total列)
-        n_features_total = self.feature_num * self.seq_len
-        supervised_data = dataset.iloc[:, :n_features_total]
-
-        # 转换为模型输入格式:[样本数, 序列长度, 特征数]
-        X = supervised_data.values.reshape(-1, self.seq_len, self.feature_num)
-        X = torch.tensor(X, dtype=torch.float32).to(self.device)
-        tensor_dataset = TensorDataset(X)
-        loader = DataLoader(tensor_dataset, batch_size=self.batch_size, shuffle=False)
-        return loader
-
-    def load_data(self, df):
-        """
-        数据加载主流程:重排列、下采样、日期处理、归一化、创建测试加载器
-        确保输入数据格式与训练时一致
-        """
-        df = self.reorder_columns(df)
-        df = df.iloc[::self.resolution, :].reset_index(drop=True)
-        df = self.process_date(df)
-        df = self.scaler_data(df)
-        self.test_loader = self.create_test_loader(df)
-        self.edge_index = torch.load('edge_index.pt', map_location=self.device, weights_only=True)
-
-    def load_model(self):
-        """加载模型结构和预训练权重,并设置为评估模式"""
-        self.model = GAT_LSTM(self).to(self.device)
-        if self.edge_index is not None:
-            self.model.set_edge_index(self.edge_index.to(self.device))   # 设置图边索引
-        self.model.load_state_dict(torch.load(self.model_path, map_location=self.device, weights_only=True))
-        self.model.eval()
-
-    def predict(self, df):
-        """
-        执行预测主流程:更新测试起始时间、加载数据、加载模型、执行预测、反归一化
-        输入:原始数据DataFrame
-        输出:反归一化后的预测结果(numpy数组)
-        """
-        # 确保数据行数不少于600行
-        df = self.ensure_min_rows(df)
-        
-        # 更新测试起始时间为输入数据最新时间+4分钟(预测起始点)
-        self.test_start_date = (pd.to_datetime(df['index']).max() + timedelta(minutes=4)).strftime("%Y-%m-%d %H:%M:%S")
-        self.load_data(df)
-        self.load_model()
-
-        all_predictions = []
-        with torch.no_grad():
-            for batch in self.test_loader:
-                inputs = batch[0].to(self.device)
-                outputs = self.model(inputs)
-                all_predictions.append(outputs.cpu().numpy())
-        
-        # 拼接所有批次的预测结果,并重塑为[时间步, 标签数]
-        predictions = np.concatenate(all_predictions, axis=0).reshape(-1, self.labels_num)
-        
-        # 反归一化(仅对标签列,使用训练时的scaler参数)
-        from sklearn.preprocessing import MinMaxScaler
-        inverse_scaler = MinMaxScaler()
-        inverse_scaler.min_ = self.scaler.min_[-self.labels_num:]
-        inverse_scaler.scale_ = self.scaler.scale_[-self.labels_num:]
-        predictions = inverse_scaler.inverse_transform(predictions)
-        
-        # 可选:异常值处理和平滑(当前注释掉,可根据需求启用)
-        # predictions = self.remove_outliers(predictions)  # 处理异常值
-        # predictions = self.smooth_predictions(predictions)  # 平滑处理
-        return predictions
-
-    def save_predictions(self, predictions):
-        """
-        将预测结果保存为CSV文件,包含时间戳和各指标的预测值
-        输入:反归一化后的预测结果(numpy数组)
-        """
-        start_time = datetime.strptime(self.test_start_date, "%Y-%m-%d %H:%M:%S")
-        time_interval = timedelta(minutes=(4 * self.resolution / 60))
-        timestamps = [start_time + i * time_interval for i in range(len(predictions))]
-
-        # 定义16个预测目标的原始列名
-        base_columns = [
-            'C.M.UF1_DB@press_PV', 'C.M.UF2_DB@press_PV', 'C.M.UF3_DB@press_PV', 'C.M.UF4_DB@press_PV',
-            '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',
-        ]
-        pred_columns = [f'{col}_pred' for col in base_columns]
-        df_result = pd.DataFrame(predictions, columns=pred_columns)
-        df_result.insert(0, 'date', timestamps)
-        df_result.to_csv(self.output_csv_path, index=False)
-        print(f"预测结果保存至:{self.output_csv_path}")
-
-if __name__ == '__main__':
-    """主函数:初始化预测器、加载数据、执行预测并保存结果"""
-    import json  # 用于解析JSON结构
-    import os
-    import pandas as pd
-    from datetime import timedelta
-
-    predictor = Predictor()
-    
-    # 读取JSON文件作为输入数据
-    json_file_path = 'pp.json'  # pp.json文件路径,可根据实际位置修改
-    if not os.path.exists(json_file_path):
-        raise FileNotFoundError(f"未找到文件: {json_file_path}")
-    print(f"读取文件:{json_file_path}")
-    
-    # 解析JSON并提取data字段(不使用try,直接判断)
-    with open(json_file_path, 'r', encoding='utf-8') as f:
-        json_data = json.load(f)
-    
-    # 检查data字段存在性及格式
-    if 'data' not in json_data:
-        raise ValueError("JSON文件中未找到'data'字段,请检查结构")
-    data_list = json_data['data']
-    if not isinstance(data_list, list) or len(data_list) == 0:
-        raise ValueError("'data'字段必须是非空列表")
-    
-    # 转换为DataFrame
-    df = pd.DataFrame(data_list)
-    
-    # 检查并处理datetime列
-    if 'datetime' not in df.columns:
-        raise ValueError("数据中未找到'datetime'字段,请检查键名")
-    df = df.rename(columns={'datetime': 'index'})
-    
-    # 转换index列为datetime格式
-    df['index'] = pd.to_datetime(df['index'])  # 若格式错误会直接抛出异常
-    
-    # 执行预测并保存结果
-    predictions = predictor.predict(df)
-    # predictor.save_predictions(predictions)
-    
-    

+ 0 - 261
models/pressure-predictor/20分钟TMP预测模型源码/README.md

@@ -1,261 +0,0 @@
-# 20分钟TMP预测模型训练逻辑说明
-
-## 模型概述
-
-这是一个用于预测超滤(UF)和反渗透(RO)系统未来20分钟压力和流量变化的时间序列预测模型。
-
-**预测目标**:16个关键指标
-- 4个UF跨膜压差(TMP):`C.M.UF1-4_DB@press_PV`
-- 8个RO压力差:`C.M.RO1-4_DB@DPT_1` 和 `C.M.RO1-4_DB@DPT_2`
-- 4个RO浓水流量:`RO1-4_CSFlow`
-
-## 核心思路
-
-### 1. 模型架构:16个"专家"并行工作
-
-想象有16个专家,每个专家只负责预测一个指标。虽然他们看到的输入数据相同(79个传感器数据),但各自独立学习预测自己负责的那一个指标。
-
-```
-输入(79个特征) → [专家1预测UF1压力]
-                 → [专家2预测UF2压力]
-                 → ...
-                 → [专家16预测RO4流量]
-```
-
-**为什么这样设计?**
-- 每个指标的变化规律可能不同,独立建模更精准
-- 但它们共享输入特征,仍能捕捉系统的整体状态
-
-### 2. 时间窗口:用过去预测未来
-
-**输入窗口**:过去60个时间点(4小时历史数据,每4分钟一个点)
-**输出窗口**:未来5个时间点(20分钟,每4分钟预测一次)
-
-```
-历史:t-60 → t-59 → ... → t-1 → t
-                              ↓
-                        [LSTM处理]
-                              ↓
-未来:      t+1 → t+2 → t+3 → t+4 → t+5
-```
-
-### 3. 网络结构:LSTM捕捉时间依赖
-
-每个"专家"内部使用LSTM(长短期记忆网络):
-```
-LSTM层(64隐藏单元) → 取最后时刻状态 → 全连接层 → 输出5步预测
-```
-
-**LSTM的作用**:
-- 记住长期趋势(比如压力缓慢上升)
-- 捕捉短期波动(比如突然的流量变化)
-- 自动提取时间序列中的重要模式
-
-## 训练流程详解
-
-### 步骤1:数据预处理(`data_preprocessor.py`)
-
-#### 1.1 数据读取和采样
-```python
-# 多线程读取51个CSV文件,提速明显
-read_and_combine_csv_files()
-```
-- 原始数据每4秒一条,采样后每4分钟一条(`resolution=60`)
-- 这样做减少数据量,同时保留关键变化趋势
-
-#### 1.2 时间特征编码
-```python
-# 把时间转成周期性特征
-minute_sin = sin(2π × 分钟数 / 1440)  # 一天的周期
-day_sin = sin(2π × 天数 / 366)        # 一年的周期
-```
-**为什么这样做?**
-- 模型能理解"早上8点"和"第二天早上8点"是类似的时刻
-- 能捕捉季节性变化(比如夏天和冬天的水质差异)
-
-#### 1.3 归一化
-```python
-# 把所有数据缩放到0-1之间
-scaler = MinMaxScaler()
-scaled_data = scaler.fit_transform(data)
-joblib.dump(scaler, 'scaler.pkl')  # 保存归一化器供预测时使用
-```
-**作用**:让不同量级的特征(压力0.03MPa vs 流量360m³/h)在训练时权重平衡
-
-#### 1.4 构建监督学习样本
-```python
-# 滑动窗口生成样本
-输入:t-60到t的所有特征(60×79=4740维)
-输出:t+1到t+5的16个目标(5×16=80维)
-```
-
-### 步骤2:模型初始化(`gat_lstm.py`)
-
-#### 单个专家模型结构
-```python
-class SingleGATLSTM:
-    def __init__(self):
-        # LSTM层:处理时间序列
-        self.lstm = nn.LSTM(input_size=79, hidden_size=64, num_layers=1)
-        
-        # 输出层:LSTM输出 → 5步预测
-        self.final_linear = nn.Sequential(
-            nn.Linear(64, 64),       # 第一层全连接
-            nn.LeakyReLU(0.01),     # 激活函数
-            nn.Dropout(0),          # Dropout防止过拟合(这里设为0)
-            nn.Linear(64, 5)        # 输出5个时间步
-        )
-```
-
-#### 16个专家组合
-```python
-class GAT_LSTM:
-    def __init__(self):
-        # 创建16个独立的专家
-        self.models = nn.ModuleList([SingleGATLSTM() for _ in range(16)])
-    
-    def forward(self, x):
-        # 每个专家独立预测
-        outputs = [model(x) for model in self.models]
-        # 拼接结果:[batch, 5] × 16 → [batch, 80]
-        return torch.cat(outputs, dim=1)
-```
-
-### 步骤3:联合训练(`data_trainer.py`)
-
-#### 训练循环
-```python
-for epoch in range(max_epochs):
-    for inputs, targets in train_loader:
-        # 1. 前向传播:所有16个专家并行预测
-        outputs = model(inputs)  # [batch, 80]
-        
-        # 2. 计算整体损失:MSE(均方误差)
-        loss = MSELoss(outputs, targets)
-        
-        # 3. 反向传播:更新所有16个专家的参数
-        loss.backward()
-        optimizer.step()
-```
-
-**关键设计**:
-- 虽然有16个专家,但用**一个损失函数**联合优化
-- 好处:专家之间能通过共享梯度信息"互相学习"
-
-#### 早停机制
-```python
-# 如果验证集损失连续500轮不下降,提前停止
-if val_loss没有改善 > patience(500轮):
-    停止训练,加载最优模型权重
-```
-防止过拟合,保存泛化能力最强的模型
-
-#### 学习率调度
-```python
-# 每100轮学习率乘以0.9
-scheduler = StepLR(step_size=100, gamma=0.9)
-```
-训练后期降低学习率,让模型更稳定地收敛
-
-## 训练参数说明
-
-| 参数 | 值 | 说明 |
-|------|-----|------|
-| `seq_len` | 60 | 输入历史长度(4小时) |
-| `output_size` | 5 | 预测未来步数(20分钟) |
-| `feature_num` | 79 | 输入特征数 |
-| `labels_num` | 16 | 预测目标数 |
-| `hidden_size` | 64 | LSTM隐藏层大小 |
-| `batch_size` | 1024 | 每批训练样本数 |
-| `lr` | 0.01 | 初始学习率 |
-| `epochs` | 1000 | 最大训练轮数 |
-| `patience` | 500 | 早停耐心值 |
-
-## 预测流程(`predict.py`)
-
-### 实时预测步骤
-1. **加载最新数据**:读取最近4小时的传感器数据
-2. **预处理**:
-   - 按分辨率下采样(每4分钟一个点)
-   - 时间特征编码(正弦/余弦)
-   - 归一化(使用训练时保存的scaler)
-3. **模型推理**:
-   ```python
-   model.eval()  # 切换到评估模式
-   with torch.no_grad():  # 不计算梯度
-       predictions = model(inputs)
-   ```
-4. **反归一化**:将0-1范围的预测值还原到真实物理量
-5. **可选后处理**:
-   - 异常值检测(四分位法)
-   - 平滑处理(加权平均)
-
-## 模型性能评估
-
-训练完成后,在测试集上计算:
-- **R²(决定系数)**:越接近1越好,表示预测值与真实值的拟合程度
-- **RMSE(均方根误差)**:越小越好,单位与目标变量相同
-- **MAPE(平均绝对百分比误差)**:越小越好,百分比形式更直观
-
-## 文件结构说明
-
-```
-20分钟TMP预测模型源码/
-├── args.py              # 参数配置(数据集日期、模型超参数)
-├── data_preprocessor.py # 数据预处理(读取、归一化、构建样本)
-├── gat_lstm.py          # 模型定义(16个专家模型架构)
-├── data_trainer.py      # 训练器(训练循环、早停、评估)
-├── main.py              # 训练入口(整合所有流程)
-├── predict.py           # 预测接口(加载模型、实时推理)
-├── model.pth            # 训练好的模型权重
-├── scaler.pkl           # 归一化器(保证预测时数据处理一致)
-└── edge_index.pt        # 图结构索引(如果使用GAT则需要)
-```
-
-## 使用建议
-
-### 训练新模型
-```bash
-python main.py
-```
-会自动完成:数据加载 → 预处理 → 训练 → 验证 → 测试 → 保存模型
-
-### 使用模型预测
-```python
-from predict import Predictor
-
-predictor = Predictor()
-predictions = predictor.predict(df)  # df是最新的传感器数据
-predictor.save_predictions(predictions)
-```
-
-### 调参建议
-1. **数据量不足时**:
-   - 减小 `hidden_size`(比如32)
-   - 增大 `dropout`(比如0.2)
-   - 减少 `epochs`
-
-2. **预测效果不好时**:
-   - 检查数据质量(异常值、缺失值)
-   - 增加 `seq_len`(更长历史窗口)
-   - 调整 `resolution`(尝试不同采样率)
-
-3. **训练太慢时**:
-   - 减小 `batch_size`
-   - 减少数据文件范围(`start_files` - `end_files`)
-   - 使用GPU(自动检测)
-
-## 常见问题
-
-**Q:为什么用16个独立模型而不是一个大模型?**  
-A:每个指标的变化模式不同,独立建模能让每个专家专注于自己的任务,预测更准确。
-
-**Q:为什么输入79个特征,只预测16个?**  
-A:79个特征包含系统的全面信息(流量、压力、温度、化学指标等),但我们只关心16个关键指标的未来变化。
-
-**Q:如何判断模型训练好了?**  
-A:看验证集R²是否>0.9,MAPE是否<5%,以及预测曲线是否与真实值吻合。
-
-**Q:模型能预测多远?**  
-A:设计是20分钟,时间越远精度越低。如果需要更长预测(比如1小时),建议增加`output_size`并调整模型结构。
-

+ 0 - 55
models/pressure-predictor/20分钟TMP预测模型源码/args.py

@@ -1,55 +0,0 @@
-# args.py
-import argparse
-
-def lstm_args_parser():
-    parser = argparse.ArgumentParser(description="LSTM模型训练参数")
-    
-    # 数据集划分
-    parser.add_argument('--train_start_date', type=str, default='2024-02-23', help='训练集开始日期')
-    parser.add_argument('--train_end_date', type=str, default='2025-08-13', help='训练集结束日期')
-    parser.add_argument('--val_start_date', type=str, default='2025-08-01', help='验证集开始日期')
-    parser.add_argument('--val_end_date', type=str, default='2025-08-13', help='验证集结束日期')
-    parser.add_argument('--test_start_date', type=str, default='2025-08-01', help='测试集开始日期')
-    parser.add_argument('--test_end_date', type=str, default='2025-08-13', help='测试集结束日期')
-
-    # 模型相关参数
-    parser.add_argument('--seq_len', type=int, default=60, help='输入序列的长度(输入步长)')
-    parser.add_argument('--output_size', type=int, default=5, help='输出数据的维度(预测步长)')
-    parser.add_argument('--step_size', type=int, default=5, help='输入数据间隔')
-    parser.add_argument('--resolution', type=int, default=60, help='输入数据分辨率(每多少个数据取一次)')
-    parser.add_argument('--epochs', type=int, default=1000, help='训练轮数')
-    parser.add_argument('--feature_num', type=int, default=79, help='特征维度')
-    parser.add_argument('--labels_num', type=int, default=16, help='标签维度(子模型数量)')
-    parser.add_argument('--hidden_size', type=int, default=64, help='隐藏层大小')
-    parser.add_argument('--num_layers', type=int, default=1, help='LSTM层数')
-    parser.add_argument('--dropout', type=float, default=0, help='dropout的概率')
-    parser.add_argument('--lr', type=float, default=0.01, help='学习率')
-    parser.add_argument('--batch_size', type=int, default=1024, help='批次大小')
-    
-    # 学习率调度器
-    parser.add_argument('--scheduler_step_size', type=int, default=100, help='学习率调整步长')
-    parser.add_argument('--scheduler_gamma', type=float, default=0.9, help='学习率衰减率')
-    
-    # 早停
-    parser.add_argument('--patience', type=int, default=500, help='早停耐心值')
-    parser.add_argument('--min_delta', type=float, default=1e-10, help='最小改善阈值')
-    
-    # 设备选择
-    parser.add_argument('--device', type=int, default=0, help='选择使用的GPU设备')
-
-    # 数据处理相关参数
-    parser.add_argument('--start_files', type=int, default=1, help='开始文件索引')
-    parser.add_argument('--end_files', type=int, default=51, help='结束文件索引')
-    parser.add_argument('--data_dir', type=str, default='datasets_xishan', help='数据文件夹路径')
-    parser.add_argument('--file_pattern', type=str, default='data_process_{}.csv', help='数据文件命名模式')
-    
-    # 模型保存路径
-    parser.add_argument('--model_path', type=str, default='model.pth', help='模型保存路径')
-    parser.add_argument('--output_csv_path', type=str, default='predictions.csv', help='预测文件保存路径')
-    
-    # 随机种子
-    parser.add_argument('--random_seed', type=int, default=1314, help='随机种子')
-
-    args = parser.parse_args()
-    
-    return args

+ 0 - 85
models/pressure-predictor/20分钟TMP预测模型源码/data_export.py

@@ -1,85 +0,0 @@
-from sqlalchemy import create_engine
-import pandas as pd
-import os
-
-username = os.getenv('DB_USERNAME', 'whu')
-password = os.getenv('DB_PASSWORD', '09093f4e6b33ddd')
-host = os.getenv('DB_HOST', '222.130.26.206')
-database = os.getenv('DB_DATABASE', 'ws_data')
-port = int(os.getenv('DB_PORT', '4000'))
-database_url = f'mysql+pymysql://{username}:{password}@{host}:{port}/{database}?charset=utf8mb4'
-engine = create_engine(database_url)
-
-start_time = "2025-08-01 00:01:00"
-end_time = "2025-09-10 00:00:00"
-
-query = """
-SELECT * 
-FROM dc_item_history_data_92
-WHERE item_name in (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
-    AND h_time >= %s
-    AND h_time <= %s
-"""
-
-params=(
-        "C.M.RO1_FT_JS@out",     # RO1反渗透进水流量
-        "C.M.RO2_FT_JS@out",     # RO2反渗透进水流量
-        "C.M.RO3_FT_JS@out",     # RO3反渗透进水流量
-        "C.M.RO4_FT_JS@out",     # RO4反渗透进水流量
-        "C.M.RO_TT_ZJS@out",     # 反渗透总进水温度
-        'C.M.RO_Cond_ZJS@out',   # 反渗透总进水电导
-        'C.M.RO_Cond_ZCS@out',   # 反渗透总产水电导
-        'C.M.RO1_DB@DPT_1',      # RO1一段压差
-        'C.M.RO1_DB@DPT_2',      # RO1二段压差
-        'C.M.RO2_DB@DPT_1',      # RO2一段压差
-        'C.M.RO2_DB@DPT_2',      # RO2二段压差
-        'C.M.RO3_DB@DPT_1',      # RO3一段压差
-        'C.M.RO3_DB@DPT_2',      # RO3二段压差
-        'C.M.RO4_DB@DPT_1',      # RO4一段压差
-        'C.M.RO4_DB@DPT_2',      # RO4二段压差
-
-        start_time,
-        end_time)
-
-data_origin = pd.read_sql(query, engine, params=params)
-
-engine.dispose()
-data = data_origin.pivot_table(index='h_time', columns='item_name', values='val')
-data = data.reset_index()
-
-# 定义标准时间序列(每1秒一个时间戳)
-standard_times = pd.date_range(start=start_time, end=end_time, freq='s')
-
-# 对齐到标准时间序列
-data_aligned = data.set_index('h_time').reindex(standard_times).reset_index()
-
-# 缺失值向前填充
-data_filled = data_aligned.ffill()
-
-# 每60个数据取一次
-data_sampled = data_filled.iloc[::60, :]
-            
-# 数据导出
-current_directory = os.getcwd()
-parent_directory = os.path.dirname(current_directory)
-folder_name = 'datasets_xishan'
-folder_path = os.path.join(parent_directory, folder_name)
-# 确保文件夹存在
-if not os.path.exists(folder_path):
-    os.makedirs(folder_path)
-
-# 多文件并存
-chunk_size = 500000 
-
-file_count = 8  # 初始化文件计数器
-for start in range(0, len(data_sampled), chunk_size):
-    end = min(start + chunk_size, len(data_sampled))
-    chunk = data_sampled.iloc[start:end]
-    
-    # 生成唯一的文件名
-    file_name = f'data_{file_count+1}.csv'
-    file_path = os.path.join(folder_path, file_name)
-    chunk.to_csv(file_path, index=False, encoding='utf_8_sig')
-    
-    file_count += 1 
-

+ 0 - 276
models/pressure-predictor/20分钟TMP预测模型源码/data_preprocessor.py

@@ -1,276 +0,0 @@
-# data_preprocessor.py
-import os
-import torch
-import joblib
-import numpy as np
-import pandas as pd
-from tqdm import tqdm    # 进度条显示
-from sklearn.preprocessing import MinMaxScaler    # 数据归一化工具
-from torch.utils.data import DataLoader, TensorDataset    # PyTorch数据加载工具
-from concurrent.futures import ThreadPoolExecutor    # 多线程读取文件
-
-class DataPreprocessor:
-    """数据预处理类,负责数据加载、划分、转换为模型可输入的格式"""
-    
-    @staticmethod
-    def load_and_process_data(args, data):
-        
-        """
-        加载并处理数据,划分训练/验证/测试集,创建数据加载器
-        参数:
-            args: 配置参数(包含数据集划分日期、序列长度等)
-            data: 预处理后的完整数据(含日期列)
-        返回:
-            train_loader: 训练集数据加载器
-            val_loader: 验证集数据加载器
-            test_loader: 测试集数据加载器
-            data: 原始数据(用于后续处理)
-        """
-        
-        # 处理日期列
-        data['date'] = pd.to_datetime(data['date'])
-        time_interval = pd.Timedelta(minutes=(4 * args.resolution / 60))
-        window_time_span = time_interval * (args.seq_len + 1)
-
-        # 划分训练/验证/测试集(调整起始日期以适应滑动窗口)
-        val_start_date = pd.to_datetime(args.val_start_date)
-        test_start_date = pd.to_datetime(args.test_start_date)
-        
-        # 调整验证集/测试集起始时间(向前推一个窗口,确保有足够历史数据构建输入序列)
-        adjusted_val_start = val_start_date - window_time_span
-        adjusted_test_start = test_start_date - window_time_span
-        
-        # 构建数据集掩码(按日期筛选)
-        train_mask = (data['date'] >= pd.to_datetime(args.train_start_date)) & \
-                     (data['date'] <= pd.to_datetime(args.train_end_date))
-
-        val_mask = (data['date'] >= adjusted_val_start) & \
-                   (data['date'] <= pd.to_datetime(args.val_end_date))
-
-        test_mask = (data['date'] >= adjusted_test_start) & \
-                    (data['date'] <= pd.to_datetime(args.test_end_date))
-
-        # 筛选数据并重置索引
-        train_data = data[train_mask].reset_index(drop=True)
-        val_data = data[val_mask].reset_index(drop=True)
-        test_data = data[test_mask].reset_index(drop=True)
-        
-        # 移除日期列用于建模
-        train_data = train_data.drop(columns=['date'])
-        val_data = val_data.drop(columns=['date'])
-        test_data = test_data.drop(columns=['date'])
-    
-        # 创建监督学习数据集(输入序列+目标序列)
-        train_supervised = DataPreprocessor.create_supervised_dataset(
-            args,
-            train_data,
-            1
-        )
-        
-        val_supervised = DataPreprocessor.create_supervised_dataset(
-            args,
-            val_data,
-            1
-        )
-        
-        test_supervised = DataPreprocessor.create_supervised_dataset(
-            args,
-            test_data,
-            args.step_size
-        )
-        
-        # 转换为DataLoader
-        train_loader = DataPreprocessor.load_data(
-            args, 
-            train_supervised,
-            shuffle=True
-        )
-        
-        val_loader = DataPreprocessor.load_data(
-            args, 
-            val_supervised,
-            shuffle=False
-        )
-        
-        test_loader = DataPreprocessor.load_data(
-            args, 
-            test_supervised,
-            shuffle=False
-        )
-        
-        return train_loader, val_loader, test_loader, data  # 返回原始数据用于后续处理
-    
-    @staticmethod
-    def read_and_combine_csv_files(args):
-        """
-        多线程读取并合并多个CSV文件,进行下采样、日期处理和归一化
-        参数:
-            args: 配置参数(包含数据路径、文件范围等)
-        返回:
-            chunk: 预处理后的合并数据(含日期和归一化特征)
-        """
-        current_dir = os.path.dirname(__file__)
-        parent_dir = os.path.dirname(current_dir)
-        args.data_dir = os.path.join(parent_dir, args.data_dir)
-        
-        def read_file(file_count):
-            """读取单个CSV文件的函数(供多线程调用)"""
-            file_name = args.file_pattern.format(file_count)
-            file_path = os.path.join(args.data_dir, file_name)
-            return pd.read_csv(file_path)
-        
-        # 生成待读取的文件索引列表
-        file_indices = list(range(args.start_files, args.end_files + 1))
-        
-        # 多线程读取文件(加速大文件读取)
-        max_workers = os.cpu_count()
-        with ThreadPoolExecutor(max_workers=max_workers) as executor:
-            results = list(tqdm(executor.map(read_file, file_indices),
-                                total=len(file_indices),
-                                desc="正在读取文件"))
-        
-        all_data = pd.concat(results, ignore_index=True)
-        # 按分辨率下采样
-        chunk = all_data.iloc[::args.resolution, :].reset_index(drop=True)
-        
-        # 处理日期和时间特征
-        chunk = DataPreprocessor.process_date(chunk)
-        # 归一化
-        chunk = DataPreprocessor.scaler_data(chunk)
-        
-        return chunk
-    
-    @staticmethod
-    def process_date(data):
-        """
-        处理日期列,生成周期性时间特征(与Predictor中的方法一致,保证一致性)
-        参数:
-            data: 含'index'列(原始日期)的DataFrame
-        返回:
-            data: 处理后的DataFrame(含日期列和时间特征)
-        """
-        data = data.rename(columns={'index': 'date'})
-        data['date'] = pd.to_datetime(data['date'])
-
-        # 生成周期性时间特征
-        data['minute_of_day'] = data['date'].dt.hour * 60 + data['date'].dt.minute
-        data['day_of_year'] = data['date'].dt.dayofyear
-
-        # 周期性编码(正弦/余弦转换,确保时间连续性)
-        data['minute_sin'] = np.sin(2 * np.pi * data['minute_of_day'] / 1440)
-        data['minute_cos'] = np.cos(2 * np.pi * data['minute_of_day'] / 1440)
-        data['day_year_sin'] = np.sin(2 * np.pi * data['day_of_year'] / 366)
-        data['day_year_cos'] = np.cos(2 * np.pi * data['day_of_year'] / 366)
-
-        # 移除原始时间列,保留特征列
-        data.drop(columns=['minute_of_day', 'day_of_year'], inplace=True)
-
-        # 调整列顺序(日期+时间特征+其他特征)
-        time_features = ['minute_sin', 'minute_cos', 'day_year_sin', 'day_year_cos']
-        other_columns = [col for col in data.columns if col not in ['date'] and col not in time_features]
-        data = data[['date'] + time_features + other_columns]
-
-        return data
-    
-    @staticmethod
-    def scaler_data(data):
-        """
-        对数据进行归一化(0-1缩放),并保存归一化器(供预测时反归一化)
-        参数:
-            data: 含'date'列和特征列的DataFrame
-        返回:
-            scaled_data: 归一化后的DataFrame(含日期列)
-        """
-        date_col = data[['date']]
-        data_to_scale = data.drop(columns=['date'])
-
-        scaler = MinMaxScaler(feature_range=(0, 1))
-        scaled_data = scaler.fit_transform(data_to_scale)
-        joblib.dump(scaler, 'scaler.pkl')  # 保存归一化器
-
-        # 转换为DataFrame并拼接日期列
-        scaled_data = pd.DataFrame(scaled_data, columns=data_to_scale.columns)
-        scaled_data = pd.concat([date_col.reset_index(drop=True), scaled_data], axis=1)
-        
-        return scaled_data
-    
-    @staticmethod
-    def create_supervised_dataset(args, data, step_size):
-        """
-        创建监督学习数据集(输入序列+目标序列)
-        输入序列:历史seq_len个时间步的所有特征
-        目标序列:未来output_size个时间步的标签特征(最后labels_num列)
-        参数:
-            args: 配置参数(含seq_len、output_size等)
-            data: 输入数据(不含日期列的特征数据)
-            step_size: 采样步长(每隔step_size取一个样本)
-        返回:
-            dataset: 监督学习数据集(DataFrame)
-        """
-        data = pd.DataFrame(data)
-        cols = []
-        col_names = []
-        
-        feature_columns = data.columns.tolist()
-
-        # 输入序列(t-0到t-(seq_len-1))
-        for col in feature_columns:
-            for i in range(args.seq_len - 1, -1, -1):
-                cols.append(data[[col]].shift(i))
-                col_names.append(f"{col}(t-{i})")
-        
-        # 目标序列(仅取最后labels_num列作为预测目标)
-        target_columns = feature_columns[-args.labels_num:]
-        for i in range(1, args.output_size + 1):
-            for col in target_columns:
-                cols.append(data[[col]].shift(-i))
-                col_names.append(f"{col}(t+{i})")
-
-        # 合并并清洗数据
-        dataset = pd.concat(cols, axis=1)
-        dataset.columns = col_names
-        dataset = dataset.iloc[::step_size, :]  # 按步长采样
-        dataset.dropna(inplace=True)  # 移除含缺失值的行
-        
-        return dataset
-
-    @staticmethod
-    def load_data(args, dataset, shuffle):
-        """
-        将监督学习数据集转换为PyTorch张量,并创建DataLoader
-        参数:
-            args: 配置参数(含特征数、批大小等)
-            dataset: 监督学习数据集(DataFrame)
-            shuffle: 是否打乱数据(训练集True,验证/测试集False)
-        返回:
-            data_loader: PyTorch DataLoader
-        """
-        input_length = args.seq_len
-        n_features = args.feature_num
-        labels_num = args.labels_num
-    
-        n_features_total = n_features * input_length  # 输入特征总维度
-        n_labels_total = args.output_size * labels_num  # 目标总维度
-
-        # 分割输入和目标
-        X = dataset.values[:, :n_features_total]
-        y = dataset.values[:, n_features_total:n_features_total + n_labels_total]
-    
-        # 重塑输入为[样本数, 序列长度, 特征数]
-        X = X.reshape(X.shape[0], input_length, n_features)
-        X = torch.tensor(X, dtype=torch.float32).to(args.device)
-        y = torch.tensor(y, dtype=torch.float32).to(args.device)
-
-        # 创建数据集和数据加载器
-        dataset_tensor = TensorDataset(X, y)
-        generator = torch.Generator()
-        generator.manual_seed(args.random_seed)  # 固定随机种子确保可复现
-        
-        data_loader = DataLoader(
-            dataset_tensor, 
-            batch_size=args.batch_size, 
-            shuffle=shuffle,
-            generator=generator
-        )
-    
-        return data_loader

+ 0 - 256
models/pressure-predictor/20分钟TMP预测模型源码/data_trainer.py

@@ -1,256 +0,0 @@
-# data_trainer.py
-import torch
-import joblib
-import numpy as np
-import pandas as pd
-from sklearn.metrics import r2_score
-from datetime import datetime, timedelta
-from sklearn.preprocessing import MinMaxScaler
-
-class Trainer:
-    def __init__(self, model, args, data):
-        """
-        模型训练器类,负责模型训练、验证、保存和评估
-        参数:
-            model: 待训练的模型实例
-            args: 配置参数
-            data: 原始数据(用于生成评估的时间戳)
-        """
-        self.args = args
-        self.model = model
-        self.data = data
-        
-        # 早停相关参数
-        self.patience = args.patience
-        self.min_delta = args.min_delta
-        self.counter = 0
-        self.early_stop = False
-        self.best_val_loss = float('inf')
-        self.best_model_state = None
-        self.best_epoch = 0
-
-    def train_full_model(self, train_loader, val_loader, optimizer, criterion, scheduler):
-        """
-        联合训练所有16个子模型(端到端训练)
-        参数:
-            train_loader: 训练集数据加载器
-            val_loader: 验证集数据加载器
-            optimizer: 优化器(如Adam)
-            criterion: 损失函数(如MSE)
-            scheduler: 学习率调度器
-        返回:
-            训练好的模型(加载最佳权重)
-        """
-        self.counter = 0
-        self.best_val_loss = float('inf')
-        self.early_stop = False
-        self.best_model_state = None
-        self.best_epoch = 0
-        max_epochs = self.args.epochs
-
-        for epoch in range(max_epochs):
-            self.model.train()
-            running_loss = 0.0
-            
-            for inputs, targets in train_loader:
-                inputs = inputs.to(self.args.device)
-                targets = targets.to(self.args.device)  # 整体目标值(包含所有16个因变量)
-                
-                optimizer.zero_grad()
-                outputs = self.model(inputs)  # 整体模型输出
-                
-                loss = criterion(outputs, targets)  # 计算整体损失
-                loss.backward()
-                optimizer.step()
-                running_loss += loss.item()
-            
-            train_loss = running_loss / len(train_loader)
-            val_loss = self.validate_full(val_loader, criterion) if val_loader else 0.0
-
-            print(f'Epoch {epoch+1}/{max_epochs}, '
-                  f'Train Loss: {train_loss:.6f}, '
-                  f'Val Loss: {val_loss:.6f}, '
-                  f'LR: {optimizer.param_groups[0]["lr"]:.6f}')
-
-            # 早停逻辑(基于整体验证损失)
-            if val_loader:
-                improved = val_loss < (self.best_val_loss - self.min_delta)
-                if improved:
-                    self.best_val_loss = val_loss
-                    self.counter = 0
-                    self.best_model_state = self.model.state_dict()
-                    self.best_epoch = epoch
-                else:
-                    self.counter += 1
-                    if self.counter >= self.patience:
-                        self.early_stop = True
-                        print(f"早停触发")
-                        
-            scheduler.step()
-            torch.cuda.empty_cache()
-            if self.early_stop:
-                break
-
-        # 加载最佳状态
-        if self.best_model_state is not None:
-            self.model.load_state_dict(self.best_model_state)
-        print(f"最佳迭代: {self.best_epoch+1}, 最佳验证损失: {self.best_val_loss:.6f}")
-        return self.model
-
-    def validate_full(self, val_loader, criterion):
-        """
-        验证整个模型(计算验证集损失)
-        参数:
-            val_loader: 验证集数据加载器
-            criterion: 损失函数
-        返回:
-            平均验证损失
-        """
-        self.model.eval()
-        total_loss = 0.0
-        with torch.no_grad():
-            for inputs, targets in val_loader:
-                inputs = inputs.to(self.args.device)
-                targets = targets.to(self.args.device)  # 整体目标值
-                
-                outputs = self.model(inputs)  # 整体模型输出
-                loss = criterion(outputs, targets)  # 整体损失计算
-                total_loss += loss.item()
-        return total_loss / len(val_loader)
-
-    def save_model(self):
-        """保存模型最佳权重到指定路径"""
-        torch.save(self.model.state_dict(), self.args.model_path)
-        print(f"模型已保存到:{self.args.model_path}")
-            
-    def evaluate_model(self, test_loader, criterion):
-        """
-        评估模型在测试集上的性能,计算R方、RMSE、MAPE等指标,并保存结果
-        参数:
-            test_loader: 测试集数据加载器
-            criterion: 损失函数(用于计算测试损失)
-        返回:
-            各指标的字典(R方、RMSE、MAPE)
-        """
-        self.model.eval()
-        scaler_path = 'scaler.pkl'
-        scaler = joblib.load(scaler_path)
-        predictions = []
-        true_values = []
-        device = self.args.device
-        
-        with torch.no_grad():
-            for inputs, targets in test_loader:
-                inputs = inputs.to(device)
-                targets = targets.to(device)
-                outputs = self.model(inputs)
-                predictions.append(outputs.cpu().numpy())
-                true_values.append(targets.cpu().numpy())
-    
-        predictions = np.concatenate(predictions, axis=0)
-        true_values = np.concatenate(true_values, axis=0)
-    
-        # 重塑预测值和真实值形状以匹配反归一化要求
-        reshaped_predictions = predictions.reshape(predictions.shape[0], 
-                                                   self.args.output_size, 
-                                                   self.args.labels_num)
-        predictions = reshaped_predictions.reshape(-1, self.args.labels_num)
-        
-        reshaped_true_values = true_values.reshape(true_values.shape[0], 
-                                                   self.args.output_size, 
-                                                   self.args.labels_num)
-        true_values = reshaped_true_values.reshape(-1, self.args.labels_num)
-    
-        # 反归一化(仅对标签列)
-        column_scaler = MinMaxScaler(feature_range=(0, 1))
-        column_scaler.min_ = scaler.min_[-self.args.labels_num:] 
-        column_scaler.scale_ = scaler.scale_[-self.args.labels_num:] 
-        
-        true_values = column_scaler.inverse_transform(true_values)
-        predictions = column_scaler.inverse_transform(predictions)
-    
-        # 定义列名(16个因变量)
-        column_names = [
-            '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.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'
-        ]
-    
-        # 生成时间序列
-        start_datetime = datetime.strptime(self.args.test_start_date, "%Y-%m-%d")
-        time_interval = timedelta(minutes=(4 * self.args.resolution / 60))
-        total_points = len(predictions)
-        date_times = [start_datetime + i * time_interval for i in range(total_points)]
-        
-        # 保存结果到DataFrame
-        results = pd.DataFrame({'date': date_times})
-
-        # 计算评估指标
-        r2_scores = {}
-        rmse_scores = {}
-        mape_scores = {}
-        metrics_details = []
-        
-        for i, col_name in enumerate(column_names):
-            results[f'{col_name}_True'] = true_values[:, i]
-            results[f'{col_name}_Predicted'] = predictions[:, i]
-
-            var_true = true_values[:, i]
-            var_pred = predictions[:, i]
-
-            # 过滤零值(避免除零错误)
-            non_zero_mask = var_true != 0
-            var_true_nonzero = var_true[non_zero_mask]
-            var_pred_nonzero = var_pred[non_zero_mask]
-
-            r2 = float('nan')
-            rmse = float('nan')
-            mape = float('nan')
-            
-            if len(var_true_nonzero) > 0:
-                r2 = r2_score(var_true_nonzero, var_pred_nonzero)
-                rmse = np.sqrt(np.mean((var_true_nonzero - var_pred_nonzero) ** 2))
-                mape = np.mean(np.abs((var_true_nonzero - var_pred_nonzero) / np.abs(var_true_nonzero))) * 100
-                
-                r2_scores[col_name] = r2
-                rmse_scores[col_name] = rmse
-                mape_scores[col_name] = mape
-                
-                detail = f"{col_name}:\n  R方 = {r2:.6f}\n  RMSE = {rmse:.6f}\n  MAPE = {mape:.6f}%"
-                metrics_details.append(detail)
-                print(f"{col_name} R方: {r2:.6f}")
-            else:
-                metrics_details.append(f"{col_name}: 没有有效数据用于计算指标")
-                print(f"{col_name} 没有有效数据用于计算R方")
-
-        # 计算平均指标
-        valid_r2 = [score for score in r2_scores.values() if not np.isnan(score)]
-        valid_rmse = [score for score in rmse_scores.values() if not np.isnan(score)]
-        valid_mape = [score for score in mape_scores.values() if not np.isnan(score)]
-        
-        avg_r2 = np.mean(valid_r2) if valid_r2 else float('nan')
-        avg_rmse = np.mean(valid_rmse) if valid_rmse else float('nan')
-        avg_mape = np.mean(valid_mape) if valid_mape else float('nan')
-
-        avg_detail = f"\n平均指标:\n  R方 = {avg_r2:.6f}\n  RMSE = {avg_rmse:.6f}\n  MAPE = {avg_mape:.6f}%"
-        if np.isnan(avg_r2):
-            avg_detail = "\n平均指标: 没有有效的指标可用于计算平均值"
-        
-        metrics_details.append(avg_detail)
-        print(avg_detail)
-
-        # 保存结果
-        results.to_csv(self.args.output_csv_path, index=False)
-        print(f"预测结果已保存到:{self.args.output_csv_path}")
-
-        txt_path = self.args.output_csv_path.replace('.csv', '_metrics_results.txt')
-        with open(txt_path, 'w') as f:
-            f.write("各变量预测指标结果:\n")
-            f.write("===================\n\n")
-            for detail in metrics_details:
-                f.write(detail + '\n')
-        
-        print(f"预测指标结果已保存到:{txt_path}")
-        
-        return r2_scores, rmse_scores, mape_scores

BIN
models/pressure-predictor/20分钟TMP预测模型源码/edge_index.pt


+ 0 - 99
models/pressure-predictor/20分钟TMP预测模型源码/gat_lstm.py

@@ -1,99 +0,0 @@
-# gat_lstm.py
-import torch
-import torch.nn as nn    # PyTorch神经网络模块
-
-# 单个独立模型(对应1个因变量)
-class SingleGATLSTM(nn.Module):
-    def __init__(self, args):
-        """
-        单个子模型:包含GAT-LSTM层和输出层,用于预测1个目标指标
-        参数:
-            args: 配置参数(含特征数、隐藏层大小等)
-        """
-        super(SingleGATLSTM, self).__init__()
-        self.args = args
-        
-        # 独立的LSTM层
-        self.lstm = nn.LSTM(
-            input_size=args.feature_num,
-            hidden_size=args.hidden_size,
-            num_layers=args.num_layers,
-            batch_first=True
-        )
-        
-        # 独立的输出层
-        self.final_linear = nn.Sequential(
-            nn.Linear(args.hidden_size, args.hidden_size),
-            nn.LeakyReLU(0.01),
-            nn.Dropout(args.dropout * 0.4),
-            nn.Linear(args.hidden_size, args.output_size)
-        )
-        
-        self._init_weights()
-        
-    def _init_weights(self):
-        """初始化网络权重,加速模型收敛"""
-        for m in self.modules():
-            if isinstance(m, nn.Linear):
-                nn.init.xavier_uniform_(m.weight)
-                if m.bias is not None:
-                    nn.init.zeros_(m.bias)
-            elif isinstance(m, nn.BatchNorm1d):
-                nn.init.constant_(m.weight, 1)
-                nn.init.constant_(m.bias, 0)
-
-        # 初始化LSTM权重
-        for name, param in self.lstm.named_parameters():
-            if 'weight_ih' in name:
-                nn.init.xavier_uniform_(param.data)
-            elif 'weight_hh' in name:
-                nn.init.orthogonal_(param.data)
-            elif 'bias' in name:
-                param.data.fill_(0)
-                n = param.size(0)
-                start, end = n // 4, n // 2
-                param.data[start:end].fill_(1)
-        
-    def forward(self, x):
-        """
-        前向传播:输入序列经过LSTM和输出层,得到预测结果
-        参数:
-            x: 输入序列,形状为[batch_size, seq_len, feature_num]
-        返回:
-            output: 预测结果,形状为[batch_size, output_size]
-        """
-        batch_size, seq_len, feature_num = x.size()
-        lstm_out, _ = self.lstm(x)
-        # 取最后一个时间步的输出
-        last_out = lstm_out[:, -1, :]
-        
-        # 输出层预测
-        output = self.final_linear(last_out)
-        return output  # [batch_size, output_size]
-
-
-# 16个独立模型的容器(总模型)
-class GAT_LSTM(nn.Module):
-    def __init__(self, args):
-        """
-        总模型:包含多个SingleGATLSTM子模型,分别预测不同的目标
-        参数:
-            args: 配置参数(含labels_num,即子模型数量)
-        """
-        super(GAT_LSTM, self).__init__()
-        self.args = args
-        # 创建16个独立模型(数量由labels_num指定)
-        self.models = nn.ModuleList([SingleGATLSTM(args) for _ in range(args.labels_num)])
-    
-    def forward(self, x):
-        """
-        前向传播:所有子模型并行处理输入,拼接预测结果
-        参数:
-            x: 输入序列,形状为[batch_size, seq_len, feature_num]
-        返回:
-            拼接后的预测结果,形状为[batch_size, output_size * labels_num]
-        """
-        outputs = []
-        for model in self.models:
-            outputs.append(model(x))  # 每个输出为[batch, output_size]
-        return torch.cat(outputs, dim=1)  # 拼接后[batch, output_size * labels_num]

Некоторые файлы не были показаны из-за большого количества измененных файлов