Prechádzať zdrojové kódy

1. 添加了5个水厂20min预测代码 2. 代码整理

zhanghao 3 týždňov pred
rodič
commit
9f5b15fea4
60 zmenil súbory, kde vykonal 1239 pridanie a 2514 odobranie
  1. 40 0
      models/prediction_models/20min/README.md
  2. 92 0
      models/prediction_models/20min/anzhen/config.yaml
  3. 0 0
      models/prediction_models/20min/anzhen/edge_index.pt
  4. 0 0
      models/prediction_models/20min/anzhen/id_list.xlsx
  5. 0 0
      models/prediction_models/20min/anzhen/input_format.txt
  6. 0 0
      models/prediction_models/20min/anzhen/model.pth
  7. 0 0
      models/prediction_models/20min/anzhen/output_format.txt
  8. 0 0
      models/prediction_models/20min/anzhen/scaler.pkl
  9. 74 0
      models/prediction_models/20min/config.py
  10. 145 0
      models/prediction_models/20min/data_preprocessor.py
  11. 121 0
      models/prediction_models/20min/data_trainer.py
  12. 44 0
      models/prediction_models/20min/gat_lstm.py
  13. 81 0
      models/prediction_models/20min/jianding/config.yaml
  14. 0 0
      models/prediction_models/20min/jianding/edge_index.pt
  15. 0 0
      models/prediction_models/20min/jianding/id_list.xlsx
  16. 0 0
      models/prediction_models/20min/jianding/input_format.txt
  17. 0 0
      models/prediction_models/20min/jianding/model.pth
  18. 0 0
      models/prediction_models/20min/jianding/output_format.txt
  19. 0 0
      models/prediction_models/20min/jianding/scaler.pkl
  20. 118 0
      models/prediction_models/20min/lankao/config.yaml
  21. 0 0
      models/prediction_models/20min/lankao/edge_index.pt
  22. BIN
      models/prediction_models/20min/lankao/id_list.xlsx
  23. 64 0
      models/prediction_models/20min/lankao/input_format.txt
  24. BIN
      models/prediction_models/20min/lankao/model.pth
  25. 2 0
      models/prediction_models/20min/lankao/output_format.txt
  26. BIN
      models/prediction_models/20min/lankao/scaler.pkl
  27. 131 0
      models/prediction_models/20min/longting/config.yaml
  28. BIN
      models/prediction_models/20min/longting/edge_index.pt
  29. 0 0
      models/prediction_models/20min/longting/id_list.xlsx
  30. 0 0
      models/prediction_models/20min/longting/input_format.txt
  31. 0 0
      models/prediction_models/20min/longting/model.pth
  32. 0 0
      models/prediction_models/20min/longting/output_format.txt
  33. 0 0
      models/prediction_models/20min/longting/scaler.pkl
  34. 57 0
      models/prediction_models/20min/main.py
  35. 84 0
      models/prediction_models/20min/predict.py
  36. 119 0
      models/prediction_models/20min/yancheng/config.yaml
  37. BIN
      models/prediction_models/20min/yancheng/edge_index.pt
  38. BIN
      models/prediction_models/20min/yancheng/id_list.xlsx
  39. 65 0
      models/prediction_models/20min/yancheng/input_format.txt
  40. BIN
      models/prediction_models/20min/yancheng/model.pth
  41. 2 0
      models/prediction_models/20min/yancheng/output_format.txt
  42. BIN
      models/prediction_models/20min/yancheng/scaler.pkl
  43. 0 51
      models/prediction_models/anzhen/args.py
  44. 0 221
      models/prediction_models/anzhen/data_preprocessor.py
  45. 0 165
      models/prediction_models/anzhen/data_trainer.py
  46. 0 54
      models/prediction_models/anzhen/gat_lstm.py
  47. 0 59
      models/prediction_models/anzhen/main.py
  48. 0 271
      models/prediction_models/anzhen/predict.py
  49. 0 51
      models/prediction_models/jianding/args.py
  50. 0 211
      models/prediction_models/jianding/data_preprocessor.py
  51. 0 165
      models/prediction_models/jianding/data_trainer.py
  52. 0 54
      models/prediction_models/jianding/gat_lstm.py
  53. 0 59
      models/prediction_models/jianding/main.py
  54. 0 258
      models/prediction_models/jianding/predict.py
  55. 0 51
      models/prediction_models/longting/args.py
  56. 0 257
      models/prediction_models/longting/data_preprocessor.py
  57. 0 169
      models/prediction_models/longting/data_trainer.py
  58. 0 54
      models/prediction_models/longting/gat_lstm.py
  59. 0 59
      models/prediction_models/longting/main.py
  60. 0 305
      models/prediction_models/longting/predict.py

+ 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()