Forráskód Böngészése

1:项目结构修正

wmy 5 hónapja
szülő
commit
574c973f3b
24 módosított fájl, 3754 hozzáadás és 0 törlés
  1. 261 0
      models/pressure-predictor/20分钟TMP预测模型源码/README.md
  2. 340 0
      models/pressure-predictor/90天TMP预测模型源码/README.md
  3. BIN
      models/pressure-predictor/gat-lstm_model/20min/20min_model.pth
  4. BIN
      models/pressure-predictor/gat-lstm_model/20min/20min_scaler.pkl
  5. 15 0
      models/pressure-predictor/gat-lstm_model/20min/__init__.py
  6. 714 0
      models/pressure-predictor/gat-lstm_model/20min/predict.py
  7. BIN
      models/pressure-predictor/gat-lstm_model/90day/90day_model.pth
  8. BIN
      models/pressure-predictor/gat-lstm_model/90day/90day_scaler.pkl
  9. 15 0
      models/pressure-predictor/gat-lstm_model/90day/__init__.py
  10. 325 0
      models/pressure-predictor/gat-lstm_model/90day/predict.py
  11. 239 0
      models/pressure-predictor/gat-lstm_model/README.md
  12. 273 0
      models/pressure-predictor/gat-lstm_model/api_main.py
  13. 31 0
      models/pressure-predictor/gat-lstm_model/requirements.txt
  14. 12 0
      models/pressure-predictor/gat-lstm_model/shared/__init__.py
  15. 60 0
      models/pressure-predictor/gat-lstm_model/shared/args.py
  16. 309 0
      models/pressure-predictor/gat-lstm_model/shared/data_preprocessor.py
  17. 267 0
      models/pressure-predictor/gat-lstm_model/shared/data_trainer.py
  18. BIN
      models/pressure-predictor/gat-lstm_model/shared/edge_index.pt
  19. 103 0
      models/pressure-predictor/gat-lstm_model/shared/gat_lstm.py
  20. 76 0
      models/pressure-predictor/gat-lstm_model/start.sh
  21. 87 0
      models/pressure-predictor/gat-lstm_model/status.sh
  22. 55 0
      models/pressure-predictor/gat-lstm_model/stop.sh
  23. 72 0
      models/pressure-predictor/gat-lstm_model/test_import.py
  24. 500 0
      models/uf-rl/README.md

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

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

+ 340 - 0
models/pressure-predictor/90天TMP预测模型源码/README.md

@@ -0,0 +1,340 @@
+# 90天TMP预测模型训练逻辑说明
+
+## 模型概述
+
+这是一个用于预测反渗透(RO)系统未来90天压力差变化的**长期趋势预测模型**。与20分钟模型不同,这个模型关注的是长期的膜污染趋势。
+
+**预测目标**:8个RO压力差指标
+- 4个一段压差:`C.M.RO1-4_DB@DPT_1`
+- 4个二段压差:`C.M.RO1-4_DB@DPT_2`
+
+## 与20分钟模型的区别
+
+| 特性 | 20分钟模型 | 90天模型 |
+|------|-----------|---------|
+| **预测时长** | 20分钟(5步) | 90天(2160步) |
+| **时间分辨率** | 4分钟/点 | 1小时/点 |
+| **输入历史** | 4小时(60点) | 180天(4320点) |
+| **预测目标** | 16个指标 | 8个指标 |
+| **输入特征** | 79个 | 16个 |
+| **应用场景** | 实时监控、短期调度 | 长期趋势、维护计划 |
+
+## 核心思路
+
+### 1. 为什么需要长期预测?
+
+膜污染是一个**缓慢累积**的过程:
+- 短期(分钟级):压力波动主要由流量、温度变化引起
+- 长期(天级):压力持续上升是膜表面污染物积累导致
+
+**模型目标**:预测未来90天内,每个RO膜组的压力差如何演变,从而:
+- 提前规划化学清洗(CEB)时间
+- 评估膜寿命
+- 优化运行参数
+
+### 2. 超长输入窗口:4320个时间点
+
+```
+过去180天 → [LSTM处理] → 未来90天
+(4320小时)              (2160小时)
+```
+
+**为什么这么长?**
+- 膜污染速率受季节、水质、运行策略影响
+- 需要足够长的历史才能捕捉这些缓慢变化
+- 类似"看过去半年天气,预测未来三个月天气"
+
+### 3. 精简特征集:只用16个核心指标
+
+不同于20分钟模型的79个特征,这里只用16个:
+- 4个RO进水流量:`C.M.RO1-4_FT_JS@out`
+- 2个RO系统指标:温度 `C.M.RO_TT_ZJS@out`、电导率 `C.M.RO_Cond_ZJS@out`
+- 8个当前压差:`C.M.RO1-4_DB@DPT_1/2`(输入也是输出,捕捉自相关)
+- 2个时间特征:年周期的正弦/余弦编码
+
+**为什么减少特征?**
+- 长期趋势主要由流量、温度、当前压差决定
+- 太多特征在超长序列上会导致过拟合
+- 简化计算,减少内存占用
+
+## 训练流程详解
+
+### 步骤1:数据预处理(`data_preprocessor.py`)
+
+#### 1.1 时间分辨率处理
+```python
+resolution = 60  # 每60条原始数据取1条
+chunk = all_data.iloc[::60, :]  # 从4秒一条变成4分钟一条
+```
+之后再在训练时按小时聚合
+
+#### 1.2 年周期时间编码
+```python
+# 只编码年周期,不编码日周期(长期趋势不关心一天内变化)
+day_year_sin = sin(2π × 天数 / 366)
+day_year_cos = cos(2π × 天数 / 366)
+```
+
+#### 1.3 滑动窗口策略
+```python
+# 超长窗口需要特殊处理
+window_time_span = 1小时 × (4320 + 314)  # 额外314小时buffer
+
+# 训练样本生成:step_size=1(密集采样)
+# 测试样本生成:step_size=2160(跳跃采样,节省计算)
+```
+
+**为什么测试集跳跃采样?**
+- 测试时不需要每个时间点都预测
+- 只需验证模型在不同起点的预测能力
+
+### 步骤2:模型架构(`gat_lstm.py`)
+
+#### 结构与20分钟模型完全相同
+```python
+# 8个独立专家(对应8个压差指标)
+class GAT_LSTM:
+    def __init__(self):
+        self.models = nn.ModuleList([SingleGATLSTM() for _ in range(8)])
+```
+
+#### 每个专家内部
+```python
+class SingleGATLSTM:
+    # LSTM层
+    self.lstm = nn.LSTM(
+        input_size=16,      # 输入16个特征
+        hidden_size=64,     # 隐藏层64单元
+        num_layers=1,       # 单层LSTM
+        batch_first=True
+    )
+    
+    # 输出层
+    self.final_linear = nn.Sequential(
+        nn.Linear(64, 64),
+        nn.LeakyReLU(0.01),
+        nn.Dropout(0),
+        nn.Linear(64, 2160)  # 输出2160步(90天)
+    )
+```
+
+**注意**:虽然输入序列长达4320,但LSTM的隐藏层只有64维:
+- LSTM能压缩长时间序列的信息
+- 避免梯度消失(如果层数太多会有问题)
+
+### 步骤3:训练策略(`data_trainer.py`)
+
+#### 与20分钟模型相同的训练流程
+```python
+for epoch in range(1000):
+    # 前向传播
+    outputs = model(inputs)  # [batch, 2160×8]
+    
+    # 计算MSE损失
+    loss = MSELoss(outputs, targets)
+    
+    # 反向传播
+    loss.backward()
+    optimizer.step()
+    
+    # 验证
+    val_loss = validate(val_loader)
+    
+    # 早停
+    if val_loss没有改善超过500轮:
+        break
+```
+
+#### 特别注意的参数
+```python
+batch_size = 128  # 比20分钟模型小(1024 vs 128)
+                  # 因为每个样本更长,内存占用大
+
+learning_rate = 0.01  # 学习率相同
+scheduler_step = 100   # 每100轮衰减0.9
+```
+
+### 步骤4:评估与反归一化(`data_trainer.py`)
+
+#### 多步预测的展平
+```python
+# 模型输出:[样本数, 2160×8]
+# 需要重塑为:[样本数, 2160步, 8个指标]
+predictions = predictions.reshape(n_samples, 2160, 8)
+
+# 再展平为:[样本数×2160, 8]用于反归一化
+predictions = predictions.reshape(-1, 8)
+```
+
+#### 反归一化
+```python
+# 只反归一化最后8列(压差指标)
+column_scaler = MinMaxScaler()
+column_scaler.min_ = scaler.min_[-8:]
+column_scaler.scale_ = scaler.scale_[-8:]
+predictions = column_scaler.inverse_transform(predictions)
+```
+
+## 预测流程(`predict.py`)
+
+### 实时预测接口
+
+```python
+class Predictor:
+    # 参数设置
+    seq_len = 4320        # 需要180天历史数据
+    output_size = 2160    # 预测90天
+    feature_num = 16      # 16个输入特征
+    labels_num = 8        # 8个预测目标
+```
+
+### 预测步骤
+1. **历史数据准备**:
+   - 需要连续180天的数据(4320小时)
+   - 按小时分辨率采样
+
+2. **预测起始时间**:
+   ```python
+   # 预测起点 = 最新数据时间 + 3小时
+   test_start_date = max_date + timedelta(hours=3)
+   ```
+
+3. **多重平滑处理**:
+   ```python
+   # 对预测结果进行3层平滑(减少噪声)
+   predictions = moving_average_smooth(predictions)
+   predictions = exponential_smooth(predictions)
+   predictions = savitzky_golay_smooth(predictions)  # 可选
+   ```
+
+**为什么需要平滑?**
+- 90天预测本身就是粗粒度的
+- 小的波动可能是噪声而非真实趋势
+- 平滑后的曲线更符合长期趋势的预期
+
+### 三种平滑方法
+
+#### 1. 滑动平均
+```python
+window = 30  # 30小时窗口
+smoothed = convolve(data, window)
+```
+简单有效,适合去除高频噪声
+
+#### 2. 指数移动平均
+```python
+EMA[t] = α × data[t] + (1-α) × EMA[t-1]
+```
+近期数据权重更高,适合捕捉趋势变化
+
+#### 3. Savitzky-Golay滤波
+```python
+# 用多项式拟合窗口内的数据
+savgol_filter(data, window=25, polyorder=2)
+```
+保留趋势的同时降噪,适合平滑的长期曲线
+
+## 训练参数说明
+
+| 参数 | 值 | 说明 |
+|------|-----|------|
+| `seq_len` | 4320 | 输入历史长度(180天) |
+| `output_size` | 2160 | 预测未来步数(90天) |
+| `feature_num` | 16 | 输入特征数 |
+| `labels_num` | 8 | 预测目标数 |
+| `hidden_size` | 64 | LSTM隐藏层大小 |
+| `batch_size` | 128 | 每批训练样本数 |
+| `lr` | 0.01 | 初始学习率 |
+| `epochs` | 1000 | 最大训练轮数 |
+| `patience` | 500 | 早停耐心值 |
+
+## 文件结构说明
+
+```
+90天TMP预测模型源码/
+├── args.py              # 参数配置(长期预测专用参数)
+├── data_preprocessor.py # 数据预处理(年周期编码、长序列处理)
+├── gat_lstm.py          # 模型定义(8个专家模型)
+├── data_trainer.py      # 训练器(与20分钟版本类似)
+├── main.py              # 训练入口
+├── predict.py           # 预测接口(包含多重平滑)
+├── model.pth            # 训练好的模型权重
+└── scaler.pkl           # 归一化器
+```
+
+## 使用场景
+
+### 1. 维护计划
+```python
+# 预测90天后的压差
+predictions = predictor.predict(historical_data)
+
+# 找出何时需要CEB
+threshold = 0.20  # 压差超过0.20 MPa需要清洗
+ceb_dates = find_dates_above_threshold(predictions, threshold)
+```
+
+### 2. 膜寿命评估
+```python
+# 分析压差上升速率
+rate = (predictions[-1] - predictions[0]) / 90  # 平均每天增速
+expected_lifetime = (max_TMP - current_TMP) / rate
+```
+
+### 3. 运行策略优化
+```python
+# 对比不同运行参数下的长期压差
+scenario_A = predict_with_params(flow=300)
+scenario_B = predict_with_params(flow=360)
+```
+
+## 模型局限性
+
+1. **需要大量历史数据**:至少180天连续数据,数据缺失会影响预测
+2. **对突变事件敏感**:如果中途更换膜组或大修,需要重新训练
+3. **长期预测不确定性**:越往后预测,误差累积越大
+4. **假设运行模式不变**:如果未来改变流量、温度等运行参数,预测会失准
+
+## 改进建议
+
+### 提升预测精度
+1. **增加外部因素**:
+   - 水质指标(浊度、COD等)
+   - 清洗历史(上次CEB时间、效果)
+   - 季节变量(显式编码春夏秋冬)
+
+2. **改进模型结构**:
+   - 使用Transformer替代LSTM(更适合超长序列)
+   - 添加注意力机制(关注关键时间点)
+   - 引入外部变量的条件预测
+
+3. **集成多模型**:
+   - 训练多个模型取平均(集成学习)
+   - 分段预测(前30天精确,后60天粗略)
+
+### 加速训练
+```python
+# 使用更小的batch_size
+batch_size = 64
+
+# 减少训练数据量(只用近期数据)
+start_files = 5  # 从第5个文件开始
+
+# 降低采样率
+step_size = 10  # 训练时每10步取1个样本
+```
+
+## 常见问题
+
+**Q:为什么用4320步输入,而不是更短?**  
+A:膜污染是周期性+趋势性的复合过程,需要看足够长的历史才能区分周期波动和长期趋势。
+
+**Q:预测90天会不会太长?**  
+A:对于膜污染这种缓慢过程,90天只是一个CEB周期。实际应用中可以每周重新预测,不断修正。
+
+**Q:如何判断模型是否学到了长期趋势?**  
+A:绘制预测曲线,看是否捕捉到压差的缓慢上升。如果曲线波动剧烈,可能只学到了短期噪声。
+
+**Q:能否用这个模型做短期预测?**  
+A:不建议。这个模型牺牲了短期精度换取长期趋势,用于短期预测反而不如20分钟模型。
+

BIN
models/pressure-predictor/gat-lstm_model/20min/20min_model.pth


BIN
models/pressure-predictor/gat-lstm_model/20min/20min_scaler.pkl


+ 15 - 0
models/pressure-predictor/gat-lstm_model/20min/__init__.py

@@ -0,0 +1,15 @@
+"""
+20分钟TMP预测模型模块
+
+这个模块包含20分钟短期TMP预测的所有相关组件:
+- Predictor类:处理数据、加载模型、执行预测
+- 模型权重文件:20min_model.pth
+- 数据归一化器:20min_scaler.pkl
+"""
+
+__version__ = '1.0.0'
+
+from .predict import Predictor
+
+__all__ = ['Predictor']
+

+ 714 - 0
models/pressure-predictor/gat-lstm_model/20min/predict.py

@@ -0,0 +1,714 @@
+"""
+20分钟TMP预测模型
+版本:1.0
+最后更新:2025-10-28
+"""
+
+import os
+import sys
+import torch
+import pandas as pd
+import numpy as np
+import joblib
+import pywt
+from datetime import datetime, timedelta
+from torch.utils.data import DataLoader, TensorDataset
+from tqdm import tqdm
+
+# 添加父目录到系统路径以导入shared模块
+current_dir = os.path.dirname(os.path.abspath(__file__))
+parent_dir = os.path.dirname(current_dir)
+if parent_dir not in sys.path:
+    sys.path.insert(0, parent_dir)
+
+# 从shared目录导入GAT-LSTM模型
+sys.path.insert(0, os.path.join(parent_dir, 'shared'))
+from gat_lstm import GAT_LSTM
+
+# 尝试导入common模块,如果失败则使用标准库
+try:
+    project_root = os.path.abspath(os.path.join(parent_dir, '../..'))
+    if project_root not in sys.path:
+        sys.path.insert(0, project_root)
+    from common.utils.logger import setup_logger, log_execution_time
+    from common.utils.config import Config
+except ImportError:
+    # 使用标准库作为fallback
+    import logging
+    import yaml
+    from functools import wraps
+    import time
+    
+    def setup_logger(name, level='INFO', log_file=None, format_type='colored', max_bytes=10485760, backup_count=5):
+        """简化版logger设置"""
+        logger = logging.getLogger(name)
+        logger.setLevel(getattr(logging, level))
+        
+        formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
+        
+        # 控制台处理器
+        console_handler = logging.StreamHandler()
+        console_handler.setFormatter(formatter)
+        logger.addHandler(console_handler)
+        
+        # 文件处理器
+        if log_file:
+            from logging.handlers import RotatingFileHandler
+            file_handler = RotatingFileHandler(log_file, maxBytes=max_bytes, backupCount=backup_count)
+            file_handler.setFormatter(formatter)
+            logger.addHandler(file_handler)
+        
+        return logger
+    
+    def log_execution_time(func):
+        """简化版执行时间装饰器"""
+        @wraps(func)
+        def wrapper(*args, **kwargs):
+            start_time = time.time()
+            result = func(*args, **kwargs)
+            end_time = time.time()
+            if hasattr(args[0], 'logger'):
+                args[0].logger.info(f"{func.__name__} 执行时间: {end_time - start_time:.2f}秒")
+            return result
+        return wrapper
+    
+    class Config:
+        """简化版配置类"""
+        def __init__(self, config_file):
+            with open(config_file, 'r', encoding='utf-8') as f:
+                self.config = yaml.safe_load(f)
+        
+        def get(self, key, default=None):
+            keys = key.split('.')
+            value = self.config
+            for k in keys:
+                if isinstance(value, dict):
+                    value = value.get(k)
+                else:
+                    return default
+                if value is None:
+                    return default
+            return value
+
+def set_seed(seed):
+    """
+    设置全局随机种子,保证实验可重复性
+    
+    Args:
+        seed: 随机种子值
+        
+    Note:
+        - 设置Python、NumPy、PyTorch的随机种子
+        - 确保CUDA操作的确定性
+        - 关闭CUDA的性能优化(以确保可重复性)
+    """
+    import random
+    random.seed(seed)                              # Python随机数生成器
+    os.environ['PYTHONHASHSEED'] = str(seed)       # Python哈希种子
+    np.random.seed(seed)                           # NumPy随机数生成器
+    torch.manual_seed(seed)                        # PyTorch CPU随机数生成器
+    torch.cuda.manual_seed(seed)                   # 当前GPU随机数生成器
+    torch.cuda.manual_seed_all(seed)               # 所有GPU随机数生成器
+    torch.backends.cudnn.deterministic = True      # 确保CUDA操作确定性
+    torch.backends.cudnn.benchmark = False         # 关闭CUDA性能优化
+
+class Predictor:
+    """
+    TMP预测器类
+    
+    功能:
+        - 加载并预处理输入数据
+        - 加载训练好的GAT-LSTM模型
+        - 执行预测并保存结果
+        
+    使用示例:
+        predictor = Predictor()
+        predictions = predictor.predict(df)
+        result_df = predictor.save_predictions(predictions, start_date)
+    """
+    
+    def __init__(self, config_path='../config.yaml'):
+        """
+        初始化预测器
+        
+        Args:
+            config_path: 配置文件路径,相对于gat-lstm_model根目录
+            
+        Raises:
+            FileNotFoundError: 配置文件或模型文件不存在
+            
+        Note:
+            - 从配置文件加载所有参数
+            - 自动检测并使用GPU(如果可用)
+            - 加载训练时保存的数据归一化器
+        """
+        # 加载配置文件(指向父目录的config.yaml)
+        current_dir = os.path.dirname(__file__)
+        parent_dir = os.path.dirname(current_dir)
+        config_file = os.path.join(parent_dir, 'config.yaml')
+        self.config = Config(config_file)
+        
+        # 设置日志目录(在gat-lstm_model根目录的logs下)
+        log_dir = os.path.join(parent_dir, 'logs')
+        os.makedirs(log_dir, exist_ok=True)
+        
+        log_file = os.path.join(log_dir, '20min_predict.log')
+        self.logger = setup_logger(
+            name='20min_predict',
+            level=self.config.get('logging.level', 'INFO'),
+            log_file=log_file,
+            format_type=self.config.get('logging.format', 'colored'),
+            max_bytes=self.config.get('logging.max_bytes', 10*1024*1024),
+            backup_count=self.config.get('logging.backup_count', 5)
+        )
+        
+        self.logger.info("=" * 80)
+        self.logger.info("初始化20分钟TMP预测器")
+        self.logger.info("=" * 80)
+        
+        # 模型参数(从配置文件加载)
+        self.seq_len = self.config.get('model.seq_len', 10)
+        self.output_size = self.config.get('model.output_size', 5)
+        self.labels_num = self.config.get('model.labels_num', 16)
+        self.feature_num = self.config.get('model.feature_num', 79)
+        self.step_size = self.config.get('model.step_size', 5)
+        self.dropout = self.config.get('model.dropout', 0)
+        self.lr = self.config.get('model.lr', 0.01)
+        self.num_heads = self.config.get('model.num_heads', 8)
+        self.hidden_size = self.config.get('model.hidden_size', 64)
+        self.batch_size = self.config.get('model.batch_size', 512)
+        self.num_layers = self.config.get('model.num_layers', 1)
+        self.random_seed = self.config.get('model.random_seed', 1314)
+        
+        # 数据处理参数
+        self.resolution = self.config.get('data.resolution', 60)
+        self.test_start_date = self.config.get('data.test_start_date', '2025-07-01')
+        self.wavelet = self.config.get('data.wavelet.type', 'db4')
+        self.level = self.config.get('data.wavelet.level', 3)
+        self.level_after = self.config.get('data.wavelet.level_after', 4)
+        self.mode = self.config.get('data.wavelet.mode', 'soft')
+        
+        # 阈值参数
+        self.uf_threshold = self.config.get('data.threshold.uf', 0.001)
+        self.ro_threshold = self.config.get('data.threshold.ro', 0.01)
+        self.flow_threshold = self.config.get('data.threshold.flow', 1.0)
+        
+        # 文件路径(相对于20min目录)
+        self.model_path = os.path.join(current_dir, '20min_model.pth')
+        self.scaler_path = os.path.join(current_dir, '20min_scaler.pkl')
+        self.edge_index_path = os.path.join(parent_dir, 'shared', 'edge_index.pt')
+        self.output_csv_path = os.path.join(current_dir, '20min_predictions.csv')
+        
+        # 后处理参数
+        self.remove_outliers_flag = self.config.get('postprocess.remove_outliers', False)
+        self.smooth_flag = self.config.get('postprocess.smooth', False)
+        
+        # 设备配置
+        use_cuda = self.config.get('device.use_cuda', True)
+        cuda_device = self.config.get('device.cuda_device', 0)
+        
+        if use_cuda and torch.cuda.is_available():
+            self.device = torch.device(f"cuda:{cuda_device}")
+            self.logger.info(f"使用GPU设备: {self.device} ({torch.cuda.get_device_name(cuda_device)})")
+        else:
+            self.device = torch.device("cpu")
+            self.logger.info("使用CPU设备")
+        
+        # 设置随机种子
+        self.logger.info(f"设置随机种子: {self.random_seed}")
+        set_seed(self.random_seed)
+        
+        # 加载数据归一化器
+        if not os.path.exists(self.scaler_path):
+            self.logger.error(f"归一化器文件不存在: {self.scaler_path}")
+            raise FileNotFoundError(f"归一化器文件不存在: {self.scaler_path}")
+        
+        self.logger.info(f"加载数据归一化器: {self.scaler_path}")
+        self.scaler = joblib.load(self.scaler_path)
+        
+        # 初始化模型和数据加载器(后续加载)
+        self.model = None
+        self.edge_index = None
+        self.test_loader = None
+        
+        self.logger.info("预测器初始化完成")
+        self.logger.info(f"模型参数: seq_len={self.seq_len}, output_size={self.output_size}, "
+                        f"labels_num={self.labels_num}, feature_num={self.feature_num}")
+        self.logger.info(f"数据参数: resolution={self.resolution}, batch_size={self.batch_size}")
+        
+    def reorder_columns(self, df):
+        """
+        调整数据列顺序,确保与训练时的特征顺序一致
+        
+        Args:
+            df: 输入的DataFrame
+            
+        Returns:
+            DataFrame: 列顺序调整后的DataFrame
+            
+        Note:
+            - 避免因列顺序不一致导致模型输入特征错位
+            - 必须包含所有必需的特征列
+        """
+        self.logger.debug("开始重排数据列顺序")
+        desired_order = [
+            'index',
+            'C.M.FT_ZGJJY1@out','C.M.RO1_FT_JS@out','C.M.RO2_FT_JS@out','C.M.RO3_FT_JS@out',
+            'C.M.RO4_FT_JS@out','C.M.UF1_FT_JS@out','C.M.UF2_FT_JS@out','C.M.UF3_FT_JS@out',
+            'C.M.UF4_FT_JS@out','C.M.UF_FT_ZCS@out','C.M.FT_ZGJJY2@out','C.M.FT_ZGJJY3@out',
+            'C.M.FT_ZGJJY4@out','C.M.RO1_PT_JS@out','C.M.RO2_PT_JS@out','C.M.RO3_PT_JS@out',
+            'C.M.UF1_PT_JS@out','C.M.UF2_PT_JS@out','C.M.UF3_PT_JS@out','C.M.UF4_PT_JS@out',
+            'C.M.LT_JSC@out','C.M.RO1_PT_CS@out','C.M.RO1_PT_DJ2@out','C.M.RO2_PT_CS@out',
+            'C.M.RO2_PT_DJ2@out','C.M.RO3_PT_CS@out','C.M.RO3_PT_DJ2@out','C.M.RO4_PT_CS@out',
+            'C.M.RO4_PT_DJ2@out','C.M.RO4_PT_JS@out','C.M.LT_HCl@out','C.M.LT_NaClO@out',
+            'C.M.LT_PAC@out','C.M.LT_QSC@out','C.M.RO_Cond_ZCS@out','C.M.RO_TT_ZJS@out',
+            'C.M.UF1_JSF_kd@out','C.M.UF2_JSF_kd@out','C.M.UF_GSB4_fre@out','C.M.UF_ORP_ZCS@out',
+            'C.M.JYB2_ZGJ1_fre@out','C.M.JYB2_ZGJ2_fre@out','C.M.JYB2_ZGJ3_fre@out','C.M.JYB2_ZGJ4_fre@out',
+            'C.M.RO1_GYB_fre@out','C.M.RO2_GYB_fre@out','C.M.RO3_GYB_fre@out','C.M.RO4_GYB_fre@out',
+            'C.M.UF3_JSF_kd@out','C.M.UF4_JSF_kd@out','C.M.UF_FXB2_fre@out','C.M.RO1_DJB_fre@out',
+            'C.M.RO1_GYBF_kd@out','C.M.RO2_DJB_fre@out','C.M.RO2_GYBF_kd@out','C.M.RO3_DJB_fre@out',
+            'C.M.RO3_GYBF_kd@out','C.M.RO4_DJB_fre@out','C.M.RO4_GYBF_kd@out',
+            'C.M.UF1_DB@press_PV','C.M.UF2_DB@press_PV','C.M.UF3_DB@press_PV','C.M.UF4_DB@press_PV',
+            'UF1Per','UF2Per','UF3Per','UF4Per',
+            'C.M.RO1_DB@DPT_1','C.M.RO2_DB@DPT_1','C.M.RO3_DB@DPT_1','C.M.RO4_DB@DPT_1',
+            'C.M.RO1_DB@DPT_2','C.M.RO2_DB@DPT_2','C.M.RO3_DB@DPT_2','C.M.RO4_DB@DPT_2',
+        ]
+        self.logger.debug(f"原始列: {list(df.columns)}")
+        self.logger.debug(f"目标列顺序: {desired_order}")
+        return df.loc[:, desired_order]
+
+    def process_date(self, data):
+        """
+        处理日期列,生成周期性时间特征
+        
+        Args:
+            data: 输入DataFrame,必须包含'index'或'date'列
+            
+        Returns:
+            DataFrame: 包含时间特征的DataFrame
+            
+        Note:
+            - 生成分钟级正弦/余弦特征(捕捉每日周期性模式)
+            - 生成年中日正弦/余弦特征(捕捉年度周期性模式)
+            - 使用三角函数编码确保时间连续性(避免边界突变)
+        """
+        self.logger.debug("开始处理日期特征")
+        if 'index' in data.columns:
+            data = data.rename(columns={'index': 'date'})
+        data['date'] = pd.to_datetime(data['date'])
+        data['minute_of_day'] = data['date'].dt.hour * 60 + data['date'].dt.minute
+        data['day_of_year'] = data['date'].dt.dayofyear
+        
+        # 周期性编码(将时间转换为正弦/余弦值,确保周期性连续)
+        data['minute_sin'] = np.sin(2 * np.pi * data['minute_of_day'] / 1440)  # 分钟正弦特征
+        data['minute_cos'] = np.cos(2 * np.pi * data['minute_of_day'] / 1440)  # 分钟余弦特征
+        data['day_year_sin'] = np.sin(2 * np.pi * data['day_of_year'] / 366)   # 年中日正弦特征
+        data['day_year_cos'] = np.cos(2 * np.pi * data['day_of_year'] / 366)   # 年中日余弦特征
+        # 移除原始时间列(仅保留编码后的特征)
+        data.drop(columns=['minute_of_day', 'day_of_year'], inplace=True)
+        
+        # 调整列顺序:日期 + 时间特征 + 其他特征
+        time_features = ['minute_sin', 'minute_cos', 'day_year_sin', 'day_year_cos']
+        other_columns = [col for col in data.columns if col not in ['date'] + time_features]
+        self.logger.debug(f"生成时间特征: {time_features}")
+        return data[['date'] + time_features + other_columns]
+
+    def scaler_data(self, data):
+        """
+        对数据进行归一化处理
+        
+        Args:
+            data: 输入DataFrame
+            
+        Returns:
+            DataFrame: 归一化后的DataFrame
+            
+        Note:
+            - 使用训练时保存的scaler进行归一化
+            - 保持与训练数据的归一化方式一致(MinMax 0-1缩放)
+            - 日期列不参与归一化
+        """
+        self.logger.debug("开始数据归一化")
+        date_col = data[['date']]
+        data_to_scale = data.drop(columns=['date'])
+        scaled = self.scaler.transform(data_to_scale)
+        scaled_df = pd.DataFrame(scaled, columns=data_to_scale.columns)
+        # 拼接日期列和归一化后的特征列
+        result = pd.concat([date_col.reset_index(drop=True), scaled_df], axis=1)
+        self.logger.debug(f"归一化完成,数据形状: {result.shape}")
+        return result
+    
+    def remove_outliers(self, predictions):
+        """
+        使用四分位法处理预测结果中的异常值
+        
+        Args:
+            predictions: numpy数组,形状为[时间步, 标签数]
+            
+        Returns:
+            numpy数组: 处理异常值后的预测结果
+            
+        Note:
+            - 异常值定义:小于Q1-1.5*IQR或大于Q3+1.5*IQR的值
+            - 异常值替换为正常值的平均值(避免极端值影响结果)
+            - 按列(每个指标)独立处理
+        """
+        self.logger.info("开始移除异常值(四分位法)")
+        cleaned = predictions.copy()
+        # 遍历每个特征列(16个标签)
+        for col in range(cleaned.shape[1]):
+            values = cleaned[:, col]
+            # 计算四分位数
+            q1 = np.percentile(values, 25)
+            q3 = np.percentile(values, 75)
+            iqr = q3 - q1
+            # 异常值边界
+            lower_bound = q1 - 1.5 * iqr
+            upper_bound = q3 + 1.5 * iqr
+            # 筛选正常值
+            normal_values = values[(values >= lower_bound) & (values <= upper_bound)]
+            # 用正常值的平均值替换异常值
+            if len(normal_values) > 0:
+                mean_normal = np.mean(normal_values)
+                outlier_count = np.sum((values < lower_bound) | (values > upper_bound))
+                if outlier_count > 0:
+                    self.logger.debug(f"列{col}: 检测到{outlier_count}个异常值,替换为均值{mean_normal:.4f}")
+                cleaned[(values < lower_bound) | (values > upper_bound), col] = mean_normal
+        self.logger.info("异常值处理完成")
+        return cleaned
+    
+    def smooth_predictions(self, predictions):
+        """
+        对预测结果进行加权平滑处理
+        
+        Args:
+            predictions: numpy数组,形状为[时间步, 标签数]
+            
+        Returns:
+            numpy数组: 平滑后的预测结果
+            
+        Note:
+            - 采用滑动窗口加权平均减少预测波动
+            - 中间值权重为2,前后邻居权重为1
+            - 边缘值特殊处理(避免过度平滑)
+        """
+        self.logger.info("开始平滑预测结果")
+        smoothed = predictions.copy()
+        n_timesteps = predictions.shape[0]
+        if n_timesteps <= 1:
+            return smoothed
+        
+        # 遍历每个特征列
+        for col in range(predictions.shape[1]):
+            values = predictions[:, col]
+            # 第一个值:加权前两个值(避免边缘过度平滑)
+            smoothed[0, col] = (2 * values[0] + values[1]) / 3
+            # 中间值:加权前后邻居(核心平滑)
+            for i in range(1, n_timesteps - 1):
+                smoothed[i, col] = (values[i-1] + 2 * values[i] + values[i+1]) / 4
+            # 最后一个值:加权最后两个值(避免边缘过度平滑)
+            smoothed[-1, col] = (values[-2] + 2 * values[-1]) / 3
+        self.logger.info("预测结果平滑完成")
+        return smoothed
+
+    def create_test_loader(self, df):
+        """
+        构建测试数据加载器
+        
+        Args:
+            df: 预处理后的DataFrame
+            
+        Returns:
+            DataLoader: PyTorch数据加载器
+            
+        Note:
+            - 将原始时间序列数据转换为模型输入格式
+            - 构建滑动窗口序列:[样本数, 序列长度, 特征数]
+            - 确保有足够的历史数据构建输入序列
+        """
+        self.logger.info("创建测试数据加载器")
+        df['date'] = pd.to_datetime(df['date'])
+        # 计算时间间隔(根据分辨率,单位:分钟)
+        time_interval = pd.Timedelta(minutes=(4 * self.resolution / 60))
+        # 计算窗口时间跨度(确保能覆盖输入序列长度+预测步长)
+        window_time_span = time_interval * (self.seq_len + 20)
+        # 调整测试集起始时间(确保有足够的历史数据构建输入序列)
+        adjusted_test_start = pd.to_datetime(self.test_start_date) - window_time_span
+        # 筛选所需的历史数据
+        test_df = df[df['date'] >= adjusted_test_start].reset_index(drop=True)
+
+        test_df = test_df.drop(columns=['date'])
+
+        # 构建监督学习数据集(输入序列+目标序列的占位)
+        feature_columns = test_df.columns.tolist()
+        cols = []
+        
+        # 构建输入序列(历史seq_len个时间步的特征)
+        for col in feature_columns:
+            for i in range(self.seq_len - 1, -1, -1):
+                cols.append(test_df[[col]].shift(i))   # 滞后i步的特征(t-0到t-(seq_len-1))
+                
+        # 构建目标序列占位(未来output_size个时间步的标签,预测时不使用真实值)
+        for i in range(1, self.output_size + 1):
+            for col in feature_columns[-self.labels_num:]:
+                cols.append(test_df[[col]].shift(-i))    # 超前i步的标签(t+1到t+output_size)
+                
+        # 合并列并按步长采样,最后取最后一行作为预测输入(最新的历史数据)
+        dataset = pd.concat(cols, axis=1).iloc[::self.step_size]
+        dataset = dataset.iloc[[-1]]
+    
+        # 提取输入特征(前n_features_total列)
+        n_features_total = self.feature_num * self.seq_len
+        supervised_data = dataset.iloc[:, :n_features_total]
+
+        # 转换为模型输入格式:[样本数, 序列长度, 特征数]
+        X = supervised_data.values.reshape(-1, self.seq_len, self.feature_num)
+        X = torch.tensor(X, dtype=torch.float32).to(self.device)
+        tensor_dataset = TensorDataset(X)
+        loader = DataLoader(tensor_dataset, batch_size=self.batch_size, shuffle=False)
+        
+        self.logger.info(f"测试数据加载器创建完成,输入形状: {X.shape}")
+        return loader
+
+    @log_execution_time
+    def load_data(self, df):
+        """
+        数据加载和预处理主流程
+        
+        Args:
+            df: 原始输入DataFrame
+            
+        Note:
+            - 重排列特征列顺序
+            - 下采样(根据resolution参数)
+            - 日期特征工程
+            - 数据归一化
+            - 创建测试数据加载器
+            - 加载图结构边索引
+        """
+        self.logger.info("开始加载和预处理数据")
+        self.logger.info(f"原始数据形状: {df.shape}")
+        
+        df = self.reorder_columns(df)
+        self.logger.info(f"下采样率: {self.resolution}")
+        df = df.iloc[::self.resolution, :].reset_index(drop=True)
+        self.logger.info(f"下采样后数据形状: {df.shape}")
+        
+        df = self.process_date(df)
+        df = self.scaler_data(df)
+        self.test_loader = self.create_test_loader(df)
+        
+        if not os.path.exists(self.edge_index_path):
+            self.logger.error(f"图边索引文件不存在: {self.edge_index_path}")
+            raise FileNotFoundError(f"图边索引文件不存在: {self.edge_index_path}")
+        
+        self.logger.info(f"加载图边索引: {self.edge_index_path}")
+        self.edge_index = torch.load(self.edge_index_path, map_location=self.device, weights_only=True)
+        self.logger.info("数据加载和预处理完成")
+
+    @log_execution_time
+    def load_model(self):
+        """
+        加载模型结构和预训练权重
+        
+        Raises:
+            FileNotFoundError: 模型文件不存在
+            
+        Note:
+            - 实例化GAT-LSTM模型
+            - 加载预训练权重
+            - 设置为评估模式(关闭dropout和batch normalization)
+            - 设置图结构边索引
+        """
+        if not os.path.exists(self.model_path):
+            self.logger.error(f"模型文件不存在: {self.model_path}")
+            raise FileNotFoundError(f"模型文件不存在: {self.model_path}")
+        
+        self.logger.info("开始加载模型")
+        self.logger.info(f"模型路径: {self.model_path}")
+        
+        self.model = GAT_LSTM(self).to(self.device)
+        
+        if self.edge_index is not None:
+            self.logger.debug(f"设置图边索引,形状: {self.edge_index.shape}")
+            self.model.set_edge_index(self.edge_index.to(self.device))
+        
+        self.model.load_state_dict(torch.load(self.model_path, map_location=self.device, weights_only=True))
+        self.model.eval()
+        
+        # 统计模型参数量
+        total_params = sum(p.numel() for p in self.model.parameters())
+        trainable_params = sum(p.numel() for p in self.model.parameters() if p.requires_grad)
+        self.logger.info(f"模型加载完成 - 总参数量: {total_params:,}, 可训练参数量: {trainable_params:,}")
+
+    @log_execution_time
+    def predict(self, df):
+        """
+        执行预测主流程
+        
+        Args:
+            df: 原始输入DataFrame,必须包含'index'列(时间戳)
+            
+        Returns:
+            numpy数组: 反归一化后的预测结果,形状为[output_size, labels_num]
+            
+        Note:
+            - 自动更新测试起始时间为输入数据最新时间+4分钟
+            - 执行数据预处理
+            - 加载模型
+            - 执行批量预测
+            - 反归一化预测结果
+            - 可选的异常值处理和平滑
+        """
+        self.logger.info("=" * 80)
+        self.logger.info("开始预测流程")
+        self.logger.info("=" * 80)
+        
+        # 更新测试起始时间为输入数据最新时间+4分钟(预测起始点)
+        latest_time = pd.to_datetime(df['index']).max()
+        self.test_start_date = (latest_time + timedelta(minutes=4)).strftime("%Y-%m-%d %H:%M:%S")
+        self.logger.info(f"输入数据最新时间: {latest_time}")
+        self.logger.info(f"预测起始时间: {self.test_start_date}")
+        
+        # 加载和预处理数据
+        self.load_data(df)
+        
+        # 加载模型
+        self.load_model()
+
+        # 执行预测
+        self.logger.info("开始模型推理")
+        all_predictions = []
+        with torch.no_grad():
+            for batch_idx, batch in enumerate(self.test_loader):
+                inputs = batch[0].to(self.device)
+                outputs = self.model(inputs)
+                all_predictions.append(outputs.cpu().numpy())
+                self.logger.debug(f"批次 {batch_idx + 1} 推理完成,输入形状: {inputs.shape}, 输出形状: {outputs.shape}")
+        
+        # 拼接所有批次的预测结果,并重塑为[时间步, 标签数]
+        predictions = np.concatenate(all_predictions, axis=0).reshape(-1, self.labels_num)
+        self.logger.info(f"模型推理完成,预测结果形状: {predictions.shape}")
+        
+        # 反归一化(仅对标签列,使用训练时的scaler参数)
+        self.logger.info("开始反归一化预测结果")
+        from sklearn.preprocessing import MinMaxScaler
+        inverse_scaler = MinMaxScaler()
+        inverse_scaler.min_ = self.scaler.min_[-self.labels_num:]
+        inverse_scaler.scale_ = self.scaler.scale_[-self.labels_num:]
+        predictions = inverse_scaler.inverse_transform(predictions)
+        self.logger.info("反归一化完成")
+        
+        # 可选:异常值处理和平滑(根据配置文件决定是否启用)
+        if self.remove_outliers_flag:
+            predictions = self.remove_outliers(predictions)
+        
+        if self.smooth_flag:
+            predictions = self.smooth_predictions(predictions)
+        
+        self.logger.info(f"预测流程完成,最终预测结果形状: {predictions.shape}")
+        self.logger.info(f"预测值范围: min={predictions.min():.4f}, max={predictions.max():.4f}, mean={predictions.mean():.4f}")
+        
+        return predictions
+
+    def save_predictions(self, predictions, start_date=None, output_path=None):
+        """
+        保存预测结果为CSV文件并返回DataFrame
+        
+        Args:
+            predictions: 反归一化后的预测结果(numpy数组)
+            start_date: 预测起始时间字符串,格式:'YYYY-MM-DD HH:MM:SS',如果为None则使用test_start_date
+            output_path: 输出CSV路径,如果为None则使用默认路径
+            
+        Returns:
+            DataFrame: 包含日期和预测结果的DataFrame
+            
+        Note:
+            - 生成时间戳序列
+            - 添加列名
+            - 保存为CSV格式
+        """
+        self.logger.info("开始保存预测结果")
+        
+        if start_date is None:
+            start_date = self.test_start_date
+            
+        start_time = datetime.strptime(start_date, "%Y-%m-%d %H:%M:%S")
+        time_interval = timedelta(minutes=(4 * self.resolution / 60))
+        timestamps = [start_time + i * time_interval for i in range(len(predictions))]
+
+        # 定义16个预测目标的原始列名
+        base_columns = [
+            'C.M.UF1_DB@press_PV', 'C.M.UF2_DB@press_PV', 'C.M.UF3_DB@press_PV', 'C.M.UF4_DB@press_PV',
+            'UF1Per','UF2Per','UF3Per','UF4Per',
+            'C.M.RO1_DB@DPT_1', 'C.M.RO2_DB@DPT_1', 'C.M.RO3_DB@DPT_1', 'C.M.RO4_DB@DPT_1',
+            'C.M.RO1_DB@DPT_2', 'C.M.RO2_DB@DPT_2', 'C.M.RO3_DB@DPT_2', 'C.M.RO4_DB@DPT_2',
+        ]
+
+        pred_columns = [f'{col}_Predicted' for col in base_columns]
+        df_result = pd.DataFrame(predictions, columns=pred_columns)
+        df_result.insert(0, 'index', timestamps)
+        
+        # 如果指定了输出路径则使用,否则使用默认路径
+        save_path = output_path if output_path else self.output_csv_path
+        df_result.to_csv(save_path, index=False)
+        
+        self.logger.info(f"预测结果已保存至: {save_path}")
+        self.logger.info(f"预测时间范围: {timestamps[0]} 至 {timestamps[-1]}")
+        self.logger.info(f"预测记录数: {len(predictions)}")
+        
+        return df_result
+
+
+if __name__ == '__main__':
+    """
+    主函数:执行20分钟TMP预测
+    
+    使用方法:
+        1. 准备输入数据(JSON格式)
+        2. 运行此脚本
+        3. 查看预测结果(保存在20min_predictions.csv)
+        
+    输入数据格式:
+        - JSON文件,包含历史时间序列数据
+        - 必须包含'index'列(时间戳)和所有必需的特征列
+    """
+    import json
+    import os
+    import pandas as pd
+    from datetime import timedelta
+    
+    try:
+        # 初始化预测器(自动加载配置文件)
+        predictor = Predictor()
+        
+        # 读取JSON文件作为输入数据
+        json_file_path = '/Users/wmy/Downloads/pp.json'  # pp.json文件路径,可根据实际位置修改
+        
+        if not os.path.exists(json_file_path):
+            predictor.logger.error(f"输入文件不存在: {json_file_path}")
+            raise FileNotFoundError(f"未找到文件: {json_file_path}")
+        
+        predictor.logger.info(f"读取输入文件: {json_file_path}")
+        
+        # 解析JSON并转换为DataFrame
+        with open(json_file_path, 'r', encoding='utf-8') as f:
+            json_data = json.load(f)
+            df = pd.DataFrame(json_data)
+            predictor.logger.info(f"成功读取输入数据,数据形状: {df.shape}")
+
+        # 执行预测并保存结果
+        predictions = predictor.predict(df)
+        predictor.save_predictions(predictions)
+        
+        predictor.logger.info("=" * 80)
+        predictor.logger.info("预测任务全部完成!")
+        predictor.logger.info("=" * 80)
+        
+    except Exception as e:
+        if 'predictor' in locals():
+            predictor.logger.error(f"预测过程发生错误: {str(e)}", exc_info=True)
+        else:
+            print(f"初始化预测器时发生错误: {str(e)}")
+        raise
+

BIN
models/pressure-predictor/gat-lstm_model/90day/90day_model.pth


BIN
models/pressure-predictor/gat-lstm_model/90day/90day_scaler.pkl


+ 15 - 0
models/pressure-predictor/gat-lstm_model/90day/__init__.py

@@ -0,0 +1,15 @@
+"""
+90天TMP预测模型模块
+
+这个模块包含90天长期TMP预测的所有相关组件:
+- Predictor类:处理数据、加载模型、执行预测
+- 模型权重文件:90day_model.pth
+- 数据归一化器:90day_scaler.pkl
+"""
+
+__version__ = '1.0.0'
+
+from .predict import Predictor
+
+__all__ = ['Predictor']
+

+ 325 - 0
models/pressure-predictor/gat-lstm_model/90day/predict.py

@@ -0,0 +1,325 @@
+"""
+90天TMP预测模型
+版本:1.0
+最后更新:2025-10-28
+"""
+
+import os
+import sys
+import torch
+import pandas as pd
+import numpy as np
+import joblib
+from datetime import datetime, timedelta
+from torch.utils.data import DataLoader, TensorDataset
+from scipy.signal import savgol_filter    # Savitzky-Golay滤波工具
+from sklearn.preprocessing import MinMaxScaler    # 数据标准化工具
+
+# 添加父目录到系统路径以导入shared模块
+current_dir = os.path.dirname(os.path.abspath(__file__))
+parent_dir = os.path.dirname(current_dir)
+if parent_dir not in sys.path:
+    sys.path.insert(0, parent_dir)
+
+# 从shared目录导入GAT-LSTM模型
+sys.path.insert(0, os.path.join(parent_dir, 'shared'))
+from gat_lstm import GAT_LSTM
+
+def set_seed(seed):
+    """设置随机种子,保证实验可复现性"""
+    import random
+    random.seed(seed)
+    os.environ['PYTHONHASHSEED'] = str(seed)
+    np.random.seed(seed)
+    torch.manual_seed(seed)
+    torch.cuda.manual_seed(seed)
+    torch.cuda.manual_seed_all(seed)
+    torch.backends.cudnn.deterministic = True 
+    torch.backends.cudnn.benchmark = False
+
+class Predictor:
+    """预测器类,封装了数据处理、模型加载、预测和结果保存的完整流程"""
+    def __init__(self):
+        # 模型和数据相关参数
+        self.seq_len = 360  # 输入序列长度
+        self.output_size = 180  # 预测输出长度
+        self.labels_num = 8  # 预测目标特征数量
+        self.feature_num = 16  # 输入特征总数量
+        self.step_size = 180  # 滑动窗口步长
+        self.dropout = 0  # 模型dropout参数
+        self.lr = 0.01  # 学习率
+        self.hidden_size = 64  # LSTM隐藏层大小
+        self.batch_size = 128  # 批处理大小
+        self.num_layers = 1  # LSTM层数
+        self.resolution = 5400  # 数据时间分辨率(单位:秒)
+        self.test_start_date = '2025-09-24'  # 预测起始日期(动态更新)
+        self.device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
+        
+        # 文件路径(相对于90day目录)
+        current_dir = os.path.dirname(__file__)
+        self.model_path = os.path.join(current_dir, '90day_model.pth')
+        self.scaler_path = os.path.join(current_dir, '90day_scaler.pkl')
+        self.output_csv_path = os.path.join(current_dir, '90day_predictions.csv')
+        self.random_seed = 1314  # 随机种子
+
+        # 预测结果平滑参数
+        self.smooth_window = 30    # 滑动平均窗口大小
+        self.ema_alpha = 0.1    # 指数移动平均系数(权重)
+        self.use_savitzky = True    # 是否使用Savitzky-Golay滤波
+        self.sg_window = 25    # Savitzky-Golay窗口大小
+        self.sg_polyorder = 2    # Savitzky-Golay多项式阶数
+
+        # 初始化设置
+        set_seed(self.random_seed)    # 设置随机种子
+        self.scaler = joblib.load(self.scaler_path)  # 加载标准化器
+        self.model = None
+        self.edge_index = None
+        self.test_loader = None
+        
+    def reorder_columns(self, df):
+        """
+        调整DataFrame列顺序,确保与模型训练时的特征顺序一致
+        (特征顺序对模型输入至关重要,必须与训练时保持一致)
+        """
+        desired_order = [
+            'index',  # 时间索引列
+            'C.M.RO1_FT_JS@out','C.M.RO2_FT_JS@out','C.M.RO3_FT_JS@out','C.M.RO4_FT_JS@out',
+            'C.M.RO_TT_ZJS@out','C.M.RO_Cond_ZJS@out',
+            'C.M.RO1_DB@DPT_1','C.M.RO1_DB@DPT_2',
+            'C.M.RO2_DB@DPT_1','C.M.RO2_DB@DPT_2',
+            'C.M.RO3_DB@DPT_1','C.M.RO3_DB@DPT_2',
+            'C.M.RO4_DB@DPT_1','C.M.RO4_DB@DPT_2',
+        ]
+        return df.loc[:, desired_order]
+
+    def process_date(self, data):
+        """
+        处理日期特征,生成周期性时间编码(年周期)
+        将时间特征转换为正弦/余弦编码,捕捉周期性规律(如季节变化)
+        """
+        if 'index' in data.columns:
+            data = data.rename(columns={'index': 'date'})
+        data['date'] = pd.to_datetime(data['date'])
+        data['day_of_year'] = data['date'].dt.dayofyear
+        # 生成正弦/余弦编码(周期为366天,适应闰年)
+        data['day_year_sin'] = np.sin(2 * np.pi * data['day_of_year'] / 366)
+        data['day_year_cos'] = np.cos(2 * np.pi * data['day_of_year'] / 366)
+        data.drop(columns=['day_of_year'], inplace=True)
+        
+        # 调整列顺序:日期 + 时间特征 + 其他特征
+        time_features = ['day_year_sin', 'day_year_cos']
+        other_columns = [col for col in data.columns if col not in ['date'] + time_features]
+        return data[['date'] + time_features + other_columns]
+
+    def scaler_data(self, data):
+        """
+        使用预训练的标准化器对数据进行标准化(保留date列不处理)
+        标准化是为了让不同量级的特征在模型中权重均衡
+        """
+        date_col = data[['date']]    # 提取日期列(不参与标准化)
+        data_to_scale = data.drop(columns=['date'])
+        scaled = self.scaler.transform(data_to_scale)
+        scaled_df = pd.DataFrame(scaled, columns=data_to_scale.columns)
+        return pd.concat([date_col.reset_index(drop=True), scaled_df], axis=1)   # 拼接日期列和标准化后的数据
+
+    def create_test_loader(self, df):
+        """
+        将预处理后的DataFrame转换为模型输入的测试数据加载器
+        生成符合模型要求的张量格式([样本数, 序列长度, 特征数])
+        """
+        if 'date' in df.columns:
+            test_data = df.drop(columns=['date']).values
+        else:
+            test_data = df.values
+
+        # 重塑为LSTM输入格式:[样本数, 序列长度, 特征数]
+        X = test_data.reshape(-1, self.seq_len, self.feature_num)
+        X = torch.tensor(X, dtype=torch.float32).to(self.device)
+        tensor_dataset = TensorDataset(X)  # 创建数据集(仅输入,无标签)
+        
+        # 创建数据加载器(不打乱顺序,按批次加载)
+        return DataLoader(tensor_dataset, batch_size=self.batch_size, shuffle=False)
+    
+    def load_data(self, df):
+        """数据加载与预处理统一接口,依次执行列重排、日期处理、标准化和生成数据加载器"""
+        df = self.reorder_columns(df)    # 调整列顺序
+        df = self.process_date(df)    # 处理日期特征
+        df = self.scaler_data(df)    # 标准化数据
+        self.test_loader = self.create_test_loader(df)
+
+    def load_model(self):
+        """加载预训练模型并设置为评估模式(关闭dropout等训练特有层)"""
+        self.model = GAT_LSTM(self).to(self.device)
+        # 加载模型权重(map_location确保在指定设备加载,weights_only=True提高安全性)
+        self.model.load_state_dict(torch.load(self.model_path, map_location=self.device, weights_only=True))
+        self.model.eval()
+
+    def moving_average_smooth(self, data):
+        """
+        滑动平均平滑处理:对每个特征单独做滑动平均,减少高频噪声
+        采用边缘填充避免边界效应
+        """
+        smoothed = []
+        for i in range(data.shape[1]):
+            feature = data[:, i]
+            
+            # 边缘填充:用边缘值填充窗口外的部分,避免边界数据失真
+            padded = np.pad(feature, (self.smooth_window//2, self.smooth_window//2), mode='edge')
+            window = np.ones(self.smooth_window) / self.smooth_window    # 平均窗口权重
+            smoothed_feature = np.convolve(padded, window, mode='valid')    # 卷积计算滑动平均
+            smoothed.append(smoothed_feature.reshape(-1, 1))    # 保留维度并收集结果
+        return np.concatenate(smoothed, axis=1)    # 拼接所有特征
+
+    def exponential_smooth(self, data):
+        """
+        指数移动平均平滑:对每个特征做指数加权平均,近期数据权重更高
+        相比简单滑动平均更关注近期趋势
+        """
+        smoothed = []
+        for i in range(data.shape[1]):   # 遍历每个特征
+            feature = data[:, i]
+            smoothed_feature = np.zeros_like(feature)
+            smoothed_feature[0] = feature[0]
+            for t in range(1, len(feature)):
+                smoothed_feature[t] = self.ema_alpha * feature[t] + (1 - self.ema_alpha) * smoothed_feature[t-1]
+            smoothed.append(smoothed_feature.reshape(-1, 1))
+        return np.concatenate(smoothed, axis=1)
+
+    def savitzky_golay_smooth(self, data):
+        """
+        Savitzky-Golay滤波:基于多项式拟合的滑动窗口滤波,保留趋势的同时降噪
+        窗口大小需为奇数,若数据长度不足则调整窗口
+        """
+        smoothed = []
+        for i in range(data.shape[1]):
+            feature = data[:, i]
+            # 确保窗口为奇数且不超过数据长度
+            window = min(self.sg_window, len(feature) if len(feature) % 2 == 1 else len(feature)-1)
+            if window < 3:    # 窗口过小则不滤波(至少需要3个点拟合2阶多项式)
+                smoothed.append(feature.reshape(-1, 1))
+                continue
+            # 应用Savitzky-Golay滤波
+            smoothed_feature = savgol_filter(feature, window_length=window, polyorder=self.sg_polyorder)
+            smoothed.append(smoothed_feature.reshape(-1, 1))
+        return np.concatenate(smoothed, axis=1)
+
+    def smooth_predictions(self, predictions):
+        """
+        组合多步平滑策略处理预测结果:先滑动平均,再指数平滑,最后可选Savitzky-Golay滤波
+        多层平滑进一步降低噪声,使预测曲线更平滑
+        """
+        smoothed = self.moving_average_smooth(predictions)
+        smoothed = self.exponential_smooth(smoothed)
+        if self.use_savitzky and len(predictions) >= self.sg_window:
+            smoothed = self.savitzky_golay_smooth(smoothed)
+        return smoothed
+
+    def predict(self, df):
+        """
+        核心预测接口:输入原始数据,返回处理后的预测结果
+        流程:更新起始时间 -> 数据预处理 -> 加载模型 -> 批量预测 -> 反标准化 -> 平滑处理
+        """
+        # 预测起始时间为输入数据的最大时间+3小时(根据业务需求设定)
+        self.test_start_date = (pd.to_datetime(df['index']).max() + timedelta(hours=3)).strftime("%Y-%m-%d %H:%M:%S")
+        self.load_data(df)
+        self.load_model()
+
+        all_predictions = []
+        with torch.no_grad():
+            for batch in self.test_loader:
+                inputs = batch[0].to(self.device)
+                outputs = self.model(inputs)
+                all_predictions.append(outputs.cpu().numpy())   # 结果移回CPU并转为numpy
+        
+        # 拼接所有批次结果并重塑为[样本数, 目标特征数]
+        predictions = np.concatenate(all_predictions, axis=0).reshape(-1, self.labels_num)
+        
+        # 反标准化处理
+        inverse_scaler = MinMaxScaler()
+        
+        # 复用训练时的标准化参数(仅使用目标特征对应的参数)
+        inverse_scaler.min_ = self.scaler.min_[-self.labels_num:]
+        inverse_scaler.scale_ = self.scaler.scale_[-self.labels_num:]
+        predictions = inverse_scaler.inverse_transform(predictions)
+        predictions = np.clip(predictions, 0, None)
+        
+        # 平滑处理
+        predictions = self.smooth_predictions(predictions)
+        
+        return predictions
+
+    def save_predictions(self, predictions, start_date=None, output_path=None):
+        """
+        保存预测结果到CSV文件并返回DataFrame(适配API使用)
+        
+        Args:
+            predictions: 预测结果数组
+            start_date: 预测起始时间字符串,格式:'YYYY-MM-DD HH:MM:SS',如果为None则使用test_start_date
+            output_path: 输出CSV路径,如果为None则使用默认路径
+            
+        Returns:
+            DataFrame: 包含日期和预测结果的DataFrame
+        """
+        if start_date is None:
+            start_date = self.test_start_date
+            
+        # 解析预测起始时间
+        start_time = datetime.strptime(start_date, "%Y-%m-%d %H:%M:%S")
+        # 计算时间间隔(根据分辨率转换为小时)
+        time_interval = pd.Timedelta(hours=(self.resolution / 60))
+        # 生成所有预测时间戳
+        timestamps = [start_time + i * time_interval for i in range(len(predictions))]
+        
+        # 定义目标特征列名(与训练时一致)
+        base_columns = [
+            'C.M.RO1_DB@DPT_1', 'C.M.RO2_DB@DPT_1', 'C.M.RO3_DB@DPT_1', 'C.M.RO4_DB@DPT_1',
+            'C.M.RO1_DB@DPT_2', 'C.M.RO2_DB@DPT_2', 'C.M.RO3_DB@DPT_2', 'C.M.RO4_DB@DPT_2',
+        ]
+        pred_columns = [f'{col}_Predicted' for col in base_columns]
+        df_result = pd.DataFrame(predictions, columns=pred_columns)
+        df_result.insert(0, 'index', timestamps)
+        
+        # 如果指定了输出路径则使用,否则使用默认路径
+        save_path = output_path if output_path else self.output_csv_path
+        df_result.to_csv(save_path, index=False)
+        print(f"预测结果保存至:{save_path}")
+        
+        return df_result
+
+
+if __name__ == '__main__':
+    """
+    主函数:执行90天TMP预测
+    
+    使用方法:
+        1. 准备输入数据(CSV或JSON格式)
+        2. 运行此脚本
+        3. 查看预测结果(保存在90day_predictions.csv)
+    """
+    import json
+    
+    try:
+        # 初始化预测器
+        predictor = Predictor()
+        
+        # 读取测试数据(根据实际情况修改路径和格式)
+        # 示例:从JSON文件读取
+        # with open('test_data.json', 'r', encoding='utf-8') as f:
+        #     json_data = json.load(f)
+        #     df = pd.DataFrame(json_data)
+        
+        # 示例:从CSV文件读取
+        # df = pd.read_csv('test_data.csv')
+        
+        print("请准备输入数据并取消注释相应的加载代码")
+        
+        # 执行预测并保存结果
+        # predictions = predictor.predict(df)
+        # predictor.save_predictions(predictions)
+        
+        # print("预测任务完成!")
+        
+    except Exception as e:
+        print(f"预测过程发生错误: {str(e)}")
+        raise
+

+ 239 - 0
models/pressure-predictor/gat-lstm_model/README.md

@@ -0,0 +1,239 @@
+# GAT-LSTM TMP预测模型 API 服务
+
+基于 GAT-LSTM 神经网络的跨膜压力(TMP)预测模型,提供 RESTful API 接口。
+
+## 目录结构
+
+```
+gat-lstm_model/
+├── 20min/                  # 20分钟短期预测模块
+│   ├── __init__.py
+│   ├── predict.py          # 预测器实现
+│   ├── 20min_model.pth     # 训练好的模型权重
+│   └── 20min_scaler.pkl    # 数据归一化器
+│
+├── 90day/                  # 90天长期预测模块
+│   ├── __init__.py
+│   ├── predict.py          # 预测器实现
+│   ├── 90day_model.pth     # 训练好的模型权重
+│   └── 90day_scaler.pkl    # 数据归一化器
+│
+├── shared/                 # 共享模块
+│   ├── __init__.py
+│   ├── gat_lstm.py         # GAT-LSTM模型结构
+│   ├── args.py             # 命令行参数解析
+│   ├── data_preprocessor.py  # 数据预处理
+│   ├── data_trainer.py     # 模型训练工具
+│   └── edge_index.pt       # 图结构边索引
+│
+├── test_files/             # 测试数据文件
+│   └── pp.json
+│
+├── logs/                   # 日志目录
+│   ├── api.log            # API服务日志
+│   └── 20min_predict.log  # 预测日志
+│
+├── api_main.py            # FastAPI主程序
+├── config.yaml            # 配置文件
+├── start.sh               # 启动脚本
+├── stop.sh                # 停止脚本
+├── status.sh              # 状态查询脚本
+├── cleanup_old_files.sh   # 旧文件清理脚本
+├── test_import.py         # 导入测试脚本
+├── requirements.txt       # Python依赖
+└── README.md             # 本文档
+```
+
+## 快速开始
+
+### 安装依赖
+
+```bash
+pip install -r requirements.txt
+```
+
+### 启动服务
+
+```bash
+# 生产模式
+bash start.sh
+
+# 开发模式(详细日志)
+bash start.sh dev
+```
+
+### 查看状态
+
+```bash
+bash status.sh
+```
+
+### 停止服务
+
+```bash
+bash stop.sh
+```
+
+## API 接口
+
+### 1. 双膜预测接口
+
+**POST** `/api/v1/process_model/double_membrance`
+
+**请求示例:**
+```json
+{
+  "data": [
+    {
+      "datetime": "2025-10-29 10:00:00",
+      "C.M.FT_ZGJJY1@out": 150.5,
+      ...
+    }
+  ]
+}
+```
+
+**响应示例:**
+```json
+{
+  "success": true,
+  "predict_result": [
+    {
+      "datetime": "2025-10-29 10:04:00",
+      "C.M.UF1_DB@press_PV_Predicted": 25.3,
+      ...
+    }
+  ]
+}
+```
+
+### 2. 测试接口
+
+**GET** `/api/v1/process_model/test_double_membrance_from_file`
+
+从本地JSON文件加载测试数据进行预测。
+
+### API文档
+
+服务启动后访问:
+- Swagger UI: http://localhost:7980/docs
+- ReDoc: http://localhost:7980/redoc
+
+## 配置说明
+
+编辑 `config.yaml` 文件调整参数:
+
+```yaml
+model:
+  seq_len: 10              # 输入序列长度
+  output_size: 5           # 预测步长
+  labels_num: 16           # 预测目标数量
+  feature_num: 79          # 输入特征维度
+  batch_size: 512          # 批处理大小
+
+logging:
+  level: 'INFO'            # 日志级别
+  log_file: 'logs/20min_predict.log'
+
+device:
+  use_cuda: true           # 是否使用GPU
+  cuda_device: 0           # GPU设备编号
+```
+
+## 直接调用预测器
+
+### Python调用示例
+
+```python
+import sys
+import os
+import importlib.util
+import pandas as pd
+
+# 加载20分钟预测模块
+model_dir = '/path/to/gat-lstm_model'
+spec = importlib.util.spec_from_file_location(
+    "predict_20min",
+    os.path.join(model_dir, "20min", "predict.py")
+)
+module = importlib.util.module_from_spec(spec)
+spec.loader.exec_module(module)
+
+# 使用预测器
+predictor = module.Predictor()
+df = pd.read_csv('input_data.csv')
+predictions = predictor.predict(df)
+result_df = predictor.save_predictions(predictions, start_date)
+```
+
+### API调用示例
+
+```python
+import requests
+
+url = "http://localhost:7980/api/v1/process_model/double_membrance"
+response = requests.post(url, json={"data": [...]})
+result = response.json()
+```
+
+## 测试验证
+
+```bash
+# 测试导入
+python test_import.py
+
+# 测试API
+curl http://localhost:7980/api/v1/process_model/test_double_membrance_from_file
+```
+
+## 日志管理
+
+```bash
+# 实时查看日志
+tail -f logs/api.log
+
+# 查看最近100行
+tail -n 100 logs/api.log
+
+# 搜索错误
+grep ERROR logs/api.log
+```
+
+## 环境变量
+
+```bash
+# 设置日志级别
+export LOG_LEVEL=INFO
+
+# 开启详细日志
+export DETAILED_LOGS=true
+```
+
+## 注意事项
+
+1. 确保输入数据格式与训练数据一致
+2. 生产环境建议使用 GPU 加速
+3. 定期检查日志文件大小
+4. 模型文件路径保持相对位置不变
+
+## 故障排查
+
+### 服务无法启动
+
+```bash
+# 检查端口占用
+lsof -i:7980
+
+# 查看日志
+tail -f logs/api.log
+```
+
+### GPU相关问题
+
+切换到CPU模式,编辑 `config.yaml`:
+```yaml
+device:
+  use_cuda: false
+```
+
+---

+ 273 - 0
models/pressure-predictor/gat-lstm_model/api_main.py

@@ -0,0 +1,273 @@
+"""
+GAT-LSTM TMP预测模型 - FastAPI服务
+版本:1.1.0
+最后更新:2025-10-29
+
+提供20分钟短期TMP预测的API服务
+"""
+
+import os
+import sys
+import logging
+from logging.handlers import RotatingFileHandler
+import datetime
+import json
+import pandas as pd
+import uvicorn
+from fastapi import FastAPI, HTTPException
+from fastapi.middleware.cors import CORSMiddleware
+from pydantic import BaseModel
+from typing import List, Dict, Any
+
+# --- 日志配置 ---
+# 日志保存在logs目录下
+log_dir = os.path.join(os.path.dirname(__file__), 'logs')
+os.makedirs(log_dir, exist_ok=True)
+
+log_handler = RotatingFileHandler(
+    os.path.join(log_dir, "api.log"),
+    maxBytes=2 * 1024 * 1024,  # 2 MB
+    backupCount=5,
+    encoding='utf-8'
+)
+
+# 支持环境变量控制日志详细程度
+LOG_LEVEL = os.getenv('LOG_LEVEL', 'INFO').upper()
+DETAILED_LOGS = os.getenv('DETAILED_LOGS', 'false').lower() == 'true'
+
+# 避免重复配置日志处理器
+if not logging.getLogger().handlers:
+    logging.basicConfig(
+        level=getattr(logging, LOG_LEVEL),
+        format='%(asctime)s - %(levelname)s - %(message)s',
+        handlers=[
+            log_handler,
+            logging.StreamHandler()
+        ]
+    )
+logger = logging.getLogger(__name__)
+
+# --- 添加当前目录到Python路径 ---
+current_dir = os.path.dirname(os.path.abspath(__file__))
+if current_dir not in sys.path:
+    sys.path.insert(0, current_dir)
+
+# --- 模型导入与模拟 ---
+# 优先尝试导入真实模型,如果失败则使用模拟类(Mock Class)替代
+try:
+    # 使用importlib动态导入(因为模块名以数字开头)
+    import importlib.util
+    predict_module_path = os.path.join(current_dir, '20min', 'predict.py')
+    spec = importlib.util.spec_from_file_location("predict_20min", predict_module_path)
+    predict_module = importlib.util.module_from_spec(spec)
+    spec.loader.exec_module(predict_module)
+    Predictor = predict_module.Predictor
+    logger.info("成功加载20分钟TMP预测模型模块。")
+except Exception as e:
+    logger.warning(f"未能找到20分钟模型模块: {e},将使用模拟类进行替代。")
+    logger.warning("请确保模型模块路径正确。")
+    
+    class Predictor:
+        """模拟预测器"""
+        def predict(self, df: pd.DataFrame):
+            logger.info("正在使用模拟的 Predictor.predict 方法...")
+            import numpy as np
+            # 模拟返回5个时间步,16个特征的预测结果
+            return np.random.rand(5, 16) * 100
+        
+        def save_predictions(self, res, start_date: str, output_path: str = None) -> pd.DataFrame:
+            logger.info("正在使用模拟的 Predictor.save_predictions 方法...")
+            start_dt = datetime.datetime.strptime(start_date, "%Y-%m-%d %H:%M:%S")
+            time_index = [start_dt + datetime.timedelta(minutes=4 * i) for i in range(len(res))]
+            # 创建包含16个预测列的DataFrame
+            columns = [
+                'C.M.UF1_DB@press_PV_Predicted', 'C.M.UF2_DB@press_PV_Predicted',
+                'C.M.UF3_DB@press_PV_Predicted', 'C.M.UF4_DB@press_PV_Predicted',
+                'UF1Per_Predicted', 'UF2Per_Predicted', 'UF3Per_Predicted', 'UF4Per_Predicted',
+                'C.M.RO1_DB@DPT_1_Predicted', 'C.M.RO2_DB@DPT_1_Predicted',
+                'C.M.RO3_DB@DPT_1_Predicted', 'C.M.RO4_DB@DPT_1_Predicted',
+                'C.M.RO1_DB@DPT_2_Predicted', 'C.M.RO2_DB@DPT_2_Predicted',
+                'C.M.RO3_DB@DPT_2_Predicted', 'C.M.RO4_DB@DPT_2_Predicted'
+            ]
+            result_df = pd.DataFrame(res, columns=columns)
+            result_df.insert(0, 'index', time_index)
+            return result_df
+
+# --- FastAPI 应用初始化 ---
+app = FastAPI(
+    title="智能决策与预测 API",
+    description="一个集成了GAT-LSTM TMP预测模型的 FastAPI 服务。",
+    version="1.1.0"
+)
+
+# 配置CORS中间件
+app.add_middleware(
+    CORSMiddleware,
+    allow_origins=["*"],
+    allow_credentials=True,
+    allow_methods=["*"],
+    allow_headers=["*"],
+)
+
+# --- 全局模型实例 ---
+xishan_predict = Predictor()
+logger.info("预测模型实例初始化完成")
+
+
+# --- Pydantic 数据校验模型 ---
+class TimeSeriesDataPoint(BaseModel):
+    """定义单个时间序列数据点的结构,允许包含除datetime外的其他任意字段"""
+    datetime: str
+    
+    class Config:
+        extra = "allow"
+
+
+class DoubleMembranceRequest(BaseModel):
+    """双膜模型预测的请求体结构"""
+    data: List[TimeSeriesDataPoint]
+
+
+class SuccessResponse(BaseModel):
+    """定义标准的成功响应结构"""
+    success: bool = True
+    predict_result: List[Dict[str, Any]]
+
+
+# --- API 端点定义 ---
+@app.post(
+    "/api/v1/process_model/double_membrance",
+    response_model=SuccessResponse,
+    summary="双膜环境体模型预测",
+    tags=["模型处理"]
+)
+def get_double_membrance_model(request: DoubleMembranceRequest):
+    """接收历史时序数据,使用GAT-LSTM模型进行未来趋势预测。"""
+    try:
+        # 精简的请求开始日志
+        logger.info(f"开始双膜环境体模型预测 - 数据点: {len(request.data)}")
+        
+        if not request.data:
+            raise HTTPException(status_code=400, detail="输入数据 'data' 不能为空")
+        
+        # 详细日志仅在调试模式下记录
+        if DETAILED_LOGS:
+            logger.info("输入数据结构分析:")
+            logger.info(f"  - 数据点数量: {len(request.data)}")
+            logger.info(f"  - 时间范围: {request.data[0].datetime} 到 {request.data[-1].datetime}")
+            
+            sample_data = request.data[0].dict()
+            feature_count = len([k for k in sample_data.keys() if k != 'datetime'])
+            logger.info(f"  - 特征数量: {feature_count}")
+        
+        # 将输入数据转换为DataFrame并进行预处理
+        df = pd.DataFrame([item.dict() for item in request.data])
+        df["datetime"] = pd.to_datetime(df["datetime"])
+        df = df.sort_values(by="datetime").rename(columns={"datetime": "index"})
+        
+        if DETAILED_LOGS:
+            logger.info(f"数据预处理完成 - 形状: {df.shape}")
+        
+        # 调用模型进行预测
+        logger.info("开始模型预测...")
+        res = xishan_predict.predict(df)
+        logger.info(f"模型预测完成 - 结果形状: {res.shape}")
+        
+        predict_start_time = (df['index'].max() + datetime.timedelta(minutes=4)).strftime("%Y-%m-%d %H:%M:%S")
+        predict_result_df = xishan_predict.save_predictions(res, start_date=predict_start_time)
+        
+        # 格式化预测结果以符合API输出
+        predict_result_df["index"] = predict_result_df["index"].apply(lambda x: x.strftime("%Y-%m-%d %H:%M:%S"))
+        predict_result_df = predict_result_df.rename(columns={"index": "datetime"})
+        predict_result = predict_result_df.to_dict(orient="records")
+        
+        # 精简的输出日志
+        logger.info(
+            f"预测完成 - 预测点: {len(predict_result)}, 时间范围: {predict_result[0]['datetime']} 到 {predict_result[-1]['datetime']}")
+        
+        return {"success": True, "predict_result": predict_result}
+    except Exception as e:
+        logger.error("处理 'double_membrance' 请求时发生错误:", exc_info=True)
+        raise HTTPException(status_code=500, detail=str(e))
+
+
+@app.get(
+    "/api/v1/process_model/test_double_membrance_from_file",
+    response_model=SuccessResponse,
+    summary="从本地文件测试双膜环境体模型预测",
+    tags=["模型处理-测试"]
+)
+def test_double_membrance_from_file():
+    """
+    从本地JSON文件加载模拟数据,用于测试环境体模型预测,无需调用接口传递数据。
+    """
+    try:
+        base_dir = os.path.dirname(os.path.abspath(__file__))
+        file_path = os.path.join(base_dir, "test_files", "pp.json")
+        
+        logger.info(f"开始本地文件测试 - 文件: {file_path}")
+        
+        with open(file_path, 'r', encoding='utf-8') as f:
+            request_data = json.load(f)
+        
+        if "data" not in request_data:
+            raise HTTPException(status_code=400, detail=f"JSON文件 {file_path} 中缺少 'data' 键")
+        
+        input_data = request_data["data"]
+        if not input_data:
+            raise HTTPException(status_code=400, detail="JSON文件中的 'data' 列表不能为空")
+        
+        # 详细日志仅在调试模式下记录
+        if DETAILED_LOGS:
+            logger.info("测试数据结构分析:")
+            logger.info(f"  - 数据点数量: {len(input_data)}")
+            logger.info(f"  - 时间范围: {input_data[0]['datetime']} 到 {input_data[-1]['datetime']}")
+            
+            sample_data = input_data[0]
+            feature_count = len([k for k in sample_data.keys() if k != 'datetime'])
+            logger.info(f"  - 特征数量: {feature_count}")
+        
+        # 后续逻辑与 get_double_membrance_model 相同
+        df = pd.DataFrame(input_data)
+        df["datetime"] = pd.to_datetime(df["datetime"])
+        df = df.sort_values(by="datetime").rename(columns={"datetime": "index"})
+        
+        if DETAILED_LOGS:
+            logger.info(f"测试数据预处理完成 - 形状: {df.shape}")
+        
+        logger.info("开始测试模型预测...")
+        res = xishan_predict.predict(df)
+        logger.info(f"测试预测完成 - 结果形状: {res.shape}")
+        
+        predict_start_time = (df['index'].max() + datetime.timedelta(minutes=4)).strftime("%Y-%m-%d %H:%M:%S")
+        predict_result_df = xishan_predict.save_predictions(res, start_date=predict_start_time)
+        
+        predict_result_df["index"] = predict_result_df["index"].apply(lambda x: x.strftime("%Y-%m-%d %H:%M:%S"))
+        predict_result_df = predict_result_df.rename(columns={"index": "datetime"})
+        predict_result = predict_result_df.to_dict(orient="records")
+        
+        logger.info(
+            f"测试完成 - 预测点: {len(predict_result)}, 时间范围: {predict_result[0]['datetime']} 到 {predict_result[-1]['datetime']}")
+        return {"success": True, "predict_result": predict_result}
+    
+    except FileNotFoundError:
+        logger.error(f"测试文件未找到: {file_path}")
+        raise HTTPException(status_code=404, detail=f"测试文件未找到: {file_path}")
+    except json.JSONDecodeError:
+        logger.error(f"无法解析JSON文件: {file_path}")
+        raise HTTPException(status_code=400, detail=f"无法解析JSON文件,请检查格式: {file_path}")
+    except Exception as e:
+        logger.error("处理本地文件测试请求时发生未知错误:", exc_info=True)
+        raise HTTPException(status_code=500, detail=str(e))
+
+
+@app.get("/", include_in_schema=False)
+def root():
+    """根路径,提供API文档链接。"""
+    return {"message": "欢迎使用GAT-LSTM TMP预测 API. 请访问 /docs 查看 API 文档."}
+
+
+# --- 服务启动入口 ---
+if __name__ == "__main__":
+    uvicorn.run("api_main:app", host="0.0.0.0", port=7980, reload=False)
+

+ 31 - 0
models/pressure-predictor/gat-lstm_model/requirements.txt

@@ -0,0 +1,31 @@
+# GAT-LSTM TMP预测模型依赖包
+# Python 3.8+
+
+# 深度学习框架
+torch>=1.10.0
+torch-geometric>=2.0.0
+
+# 数据处理
+pandas>=1.3.0
+numpy>=1.21.0
+scikit-learn>=1.0.0
+
+# Web框架
+fastapi>=0.95.0
+uvicorn[standard]>=0.20.0
+pydantic>=1.10.0
+
+# 信号处理
+scipy>=1.7.0
+PyWavelets>=1.1.1
+
+# 工具库
+joblib>=1.1.0
+tqdm>=4.62.0
+colorlog>=6.6.0
+pyyaml>=6.0
+
+# 可选:CUDA支持(根据需要安装)
+# torch-cu117  # CUDA 11.7
+# torch-cu118  # CUDA 11.8
+

+ 12 - 0
models/pressure-predictor/gat-lstm_model/shared/__init__.py

@@ -0,0 +1,12 @@
+"""
+共享模块 - 包含GAT-LSTM模型通用组件
+
+模块:
+    - gat_lstm: GAT-LSTM模型结构定义
+    - args: 命令行参数解析
+    - data_preprocessor: 数据预处理工具
+    - data_trainer: 模型训练工具
+"""
+
+__version__ = '1.0.0'
+

+ 60 - 0
models/pressure-predictor/gat-lstm_model/shared/args.py

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

+ 309 - 0
models/pressure-predictor/gat-lstm_model/shared/data_preprocessor.py

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

+ 267 - 0
models/pressure-predictor/gat-lstm_model/shared/data_trainer.py

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

BIN
models/pressure-predictor/gat-lstm_model/shared/edge_index.pt


+ 103 - 0
models/pressure-predictor/gat-lstm_model/shared/gat_lstm.py

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

+ 76 - 0
models/pressure-predictor/gat-lstm_model/start.sh

@@ -0,0 +1,76 @@
+#!/bin/bash
+
+# GAT-LSTM TMP预测模型 API 服务启动脚本
+# 使用方法: bash start.sh [dev|prod]
+
+set -e
+
+# 默认配置
+MODE=${1:-prod}
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+LOG_DIR="$SCRIPT_DIR/logs"
+LOG_FILE="$LOG_DIR/api.log"
+PID_FILE="$SCRIPT_DIR/api.pid"
+
+echo "[启动] 启动GAT-LSTM TMP预测模型 API 服务..."
+
+# 创建日志目录
+mkdir -p "$LOG_DIR"
+
+# 检查是否已经在运行
+if [ -f "$PID_FILE" ]; then
+    PID=$(cat "$PID_FILE")
+    if ps -p $PID > /dev/null 2>&1; then
+        echo "[警告] 服务已经在运行 (PID: $PID)"
+        echo "       如需重启,请先运行: bash stop.sh"
+        exit 1
+    else
+        echo "[清理] 清理旧的PID文件"
+        rm -f "$PID_FILE"
+    fi
+fi
+
+# 根据模式设置环境变量
+if [ "$MODE" = "dev" ]; then
+    echo "[开发模式] 启动开发模式 (详细日志)"
+    export LOG_LEVEL=INFO
+    export DETAILED_LOGS=true
+    LOG_FILE="$LOG_DIR/api_debug.log"
+else
+    echo "[生产模式] 启动生产模式 (精简日志)"
+    export LOG_LEVEL=INFO
+    export DETAILED_LOGS=false
+fi
+
+# 启动服务
+echo "[启动服务] 启动服务..."
+cd "$SCRIPT_DIR"
+nohup python api_main.py > "$LOG_FILE" 2>&1 &
+PID=$!
+
+# 保存PID
+echo $PID > "$PID_FILE"
+
+# 等待服务启动
+echo "[等待] 等待服务启动..."
+sleep 3
+
+# 检查服务是否启动成功
+if ps -p $PID > /dev/null 2>&1; then
+    echo "[成功] 服务启动成功!"
+    echo "       PID: $PID"
+    echo "       工作目录: $SCRIPT_DIR"
+    echo "       日志文件: $LOG_FILE"
+    echo "       API文档: http://localhost:7980/docs"
+    echo "       测试接口: http://localhost:7980/api/v1/process_model/test_double_membrance_from_file"
+    echo ""
+    echo "[管理命令]"
+    echo "       查看日志: tail -f $LOG_FILE"
+    echo "       停止服务: bash stop.sh"
+    echo "       查看状态: bash status.sh"
+else
+    echo "[失败] 服务启动失败!"
+    echo "       请检查日志文件: $LOG_FILE"
+    rm -f "$PID_FILE"
+    exit 1
+fi

+ 87 - 0
models/pressure-predictor/gat-lstm_model/status.sh

@@ -0,0 +1,87 @@
+#!/bin/bash
+
+# GAT-LSTM TMP预测模型 API 服务状态查询脚本
+# 使用方法: bash status.sh
+
+set -e
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+PID_FILE="$SCRIPT_DIR/api.pid"
+LOG_DIR="$SCRIPT_DIR/logs"
+LOG_FILE="$LOG_DIR/api.log"
+
+echo "[服务状态] GAT-LSTM TMP预测模型 API 服务状态"
+echo "=========================================="
+
+# 检查PID文件是否存在
+if [ ! -f "$PID_FILE" ]; then
+    echo "状态: [未运行]"
+    echo "PID文件不存在"
+    exit 0
+fi
+
+# 读取PID
+PID=$(cat "$PID_FILE")
+
+# 检查进程是否存在
+if ! ps -p $PID > /dev/null 2>&1; then
+    echo "状态: [未运行]"
+    echo "PID文件存在但进程不存在 (可能异常退出)"
+    echo "建议: 运行 bash stop.sh 清理,然后 bash start.sh 重新启动"
+    exit 0
+fi
+
+# 进程存在,显示详细信息
+echo "状态: [运行中]"
+echo "PID: $PID"
+echo "工作目录: $SCRIPT_DIR"
+echo ""
+
+# 显示进程信息
+echo "[进程信息]"
+ps -p $PID -o pid,ppid,%cpu,%mem,etime,command
+
+echo ""
+echo "[文件状态]"
+echo "  配置文件: $SCRIPT_DIR/config.yaml"
+if [ -f "$SCRIPT_DIR/config.yaml" ]; then
+    echo "            [存在]"
+else
+    echo "            [不存在]"
+fi
+
+echo "  日志文件: $LOG_FILE"
+if [ -f "$LOG_FILE" ]; then
+    LOG_SIZE=$(du -h "$LOG_FILE" | cut -f1)
+    LOG_LINES=$(wc -l < "$LOG_FILE")
+    echo "            [存在] (大小: $LOG_SIZE, 行数: $LOG_LINES)"
+else
+    echo "            [不存在]"
+fi
+
+echo ""
+echo "[服务端点]"
+echo "  API文档: http://localhost:7980/docs"
+echo "  Swagger UI: http://localhost:7980/docs"
+echo "  ReDoc: http://localhost:7980/redoc"
+echo "  健康检查: http://localhost:7980/"
+echo "  测试接口: http://localhost:7980/api/v1/process_model/test_double_membrance_from_file"
+
+echo ""
+echo "[管理命令]"
+echo "  查看日志: tail -f $LOG_FILE"
+echo "  最近10条日志: tail -n 10 $LOG_FILE"
+echo "  停止服务: bash stop.sh"
+echo "  重启服务: bash stop.sh && bash start.sh"
+
+# 如果日志文件存在,显示最后几行
+if [ -f "$LOG_FILE" ]; then
+    echo ""
+    echo "[最近5条日志]"
+    echo "----------------------------------------"
+    tail -n 5 "$LOG_FILE"
+    echo "----------------------------------------"
+fi
+
+echo ""
+echo "=========================================="

+ 55 - 0
models/pressure-predictor/gat-lstm_model/stop.sh

@@ -0,0 +1,55 @@
+#!/bin/bash
+
+# GAT-LSTM TMP预测模型 API 服务停止脚本
+# 使用方法: bash stop.sh
+
+set -e
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+PID_FILE="$SCRIPT_DIR/api.pid"
+
+echo "[停止] 停止GAT-LSTM TMP预测模型 API 服务..."
+
+# 检查PID文件是否存在
+if [ ! -f "$PID_FILE" ]; then
+    echo "[警告] 未找到PID文件,服务可能未运行"
+    exit 0
+fi
+
+# 读取PID
+PID=$(cat "$PID_FILE")
+
+# 检查进程是否存在
+if ! ps -p $PID > /dev/null 2>&1; then
+    echo "[警告] 进程 (PID: $PID) 不存在,可能已经停止"
+    rm -f "$PID_FILE"
+    exit 0
+fi
+
+# 尝试优雅停止(SIGTERM)
+echo "[停止信号] 发送停止信号 (SIGTERM) 到进程 $PID..."
+kill -TERM $PID
+
+# 等待进程停止
+echo "[等待] 等待进程停止..."
+TIMEOUT=10
+COUNTER=0
+while ps -p $PID > /dev/null 2>&1; do
+    sleep 1
+    COUNTER=$((COUNTER + 1))
+    if [ $COUNTER -ge $TIMEOUT ]; then
+        echo "[警告] 进程未在 ${TIMEOUT}秒内停止,尝试强制停止..."
+        kill -KILL $PID
+        sleep 1
+        break
+    fi
+done
+
+# 再次检查进程是否停止
+if ps -p $PID > /dev/null 2>&1; then
+    echo "[失败] 进程停止失败 (PID: $PID)"
+    exit 1
+else
+    echo "[成功] 服务已成功停止 (PID: $PID)"
+    rm -f "$PID_FILE"
+fi

+ 72 - 0
models/pressure-predictor/gat-lstm_model/test_import.py

@@ -0,0 +1,72 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""
+测试导入是否正常
+"""
+
+import sys
+import os
+
+# 添加当前目录到路径
+current_dir = os.path.dirname(os.path.abspath(__file__))
+sys.path.insert(0, current_dir)
+
+print("测试1: 导入20分钟预测模块...")
+try:
+    # 使用importlib动态加载
+    import importlib.util
+    spec = importlib.util.spec_from_file_location(
+        "predict_20min",
+        os.path.join(current_dir, "20min", "predict.py")
+    )
+    predict_20min = importlib.util.module_from_spec(spec)
+    spec.loader.exec_module(predict_20min)
+    print("  [成功] 20分钟预测模块导入成功")
+    print(f"  Predictor类: {predict_20min.Predictor}")
+except Exception as e:
+    print(f"  [失败] {e}")
+    import traceback
+    traceback.print_exc()
+
+print("\n测试2: 导入90天预测模块...")
+try:
+    spec = importlib.util.spec_from_file_location(
+        "predict_90day",
+        os.path.join(current_dir, "90day", "predict.py")
+    )
+    predict_90day = importlib.util.module_from_spec(spec)
+    spec.loader.exec_module(predict_90day)
+    print("  [成功] 90天预测模块导入成功")
+    print(f"  Predictor类: {predict_90day.Predictor}")
+except Exception as e:
+    print(f"  [失败] {e}")
+    import traceback
+    traceback.print_exc()
+
+print("\n测试3: 导入共享模块...")
+try:
+    sys.path.insert(0, os.path.join(current_dir, "shared"))
+    from shared.gat_lstm import GAT_LSTM
+    print("  [成功] GAT_LSTM模型导入成功")
+    print(f"  GAT_LSTM类: {GAT_LSTM}")
+except Exception as e:
+    print(f"  [失败] {e}")
+    import traceback
+    traceback.print_exc()
+
+print("\n测试4: 测试API主程序...")
+try:
+    spec = importlib.util.spec_from_file_location(
+        "api_main",
+        os.path.join(current_dir, "api_main.py")
+    )
+    api_main = importlib.util.module_from_spec(spec)
+    # 不执行module,只检查能否加载
+    print("  [成功] API主程序可以加载")
+except Exception as e:
+    print(f"  [失败] {e}")
+    import traceback
+    traceback.print_exc()
+
+print("\n全部测试完成!")
+

+ 500 - 0
models/uf-rl/README.md

@@ -0,0 +1,500 @@
+# UF超滤系统强化学习决策模型训练逻辑说明
+
+## 模型概述
+
+这是一个基于**深度强化学习(DQN)**的超滤系统运行参数优化模型。不同于前两个"预测模型",这个模型的目标是**决策**:在给定当前跨膜压差(TMP)的情况下,自动决定最优的产水时长和反洗时长。
+
+**核心问题**:如何平衡产水量、回收率、能耗和膜寿命?
+
+## 问题背景
+
+### 超滤运行周期
+
+超滤系统运行遵循"小周期"模式:
+```
+[产水L秒] → [反洗t_bw秒] → [产水L秒] → [反洗t_bw秒] → ... → [化学清洗CEB]
+```
+
+- **产水阶段**:过滤原水,TMP逐渐升高(膜污染)
+- **反洗阶段**:反向冲洗,TMP部分恢复
+- **化学清洗(CEB)**:每48小时一次,TMP完全恢复
+
+### 决策难题
+
+**调节杠杆**:
+- `L_s`:单次产水时长(3600-6000秒)
+- `t_bw_s`:单次反洗时长(40-60秒)
+
+**矛盾目标**:
+1. **产水量↑**:希望L_s长、t_bw_s短(多产水、少反洗)
+2. **回收率↑**:希望t_bw_s短(减少反洗水耗)
+3. **膜保护↓**:希望L_s短、t_bw_s长(频繁反洗、TMP不升太高)
+4. **能耗↓**:产水时间越长,单位吨水的泵能耗越低
+
+**传统方法**:人工经验+固定参数,难以在复杂约束下找到最优解  
+**强化学习方法**:让AI自己探索,学习在不同TMP下的最佳决策
+
+## 核心思路:强化学习框架
+
+### 1. 强化学习是什么?
+
+把决策问题想象成玩游戏:
+```
+游戏状态(TMP)→ AI选择动作(L_s, t_bw_s)→ 执行动作 → 获得奖励(回收率、净供水率)→ 新状态(TMP更新)
+```
+
+AI通过**反复试错**,学习哪些动作能获得高奖励。
+
+### 2. Markov决策过程(MDP)建模
+
+#### 状态(State)
+```python
+state = [
+    TMP0_normalized,           # 当前初始TMP(归一化到0-1)
+    last_L_s_normalized,       # 上一次产水时长(归一化)
+    last_t_bw_s_normalized,    # 上一次反洗时长(归一化)
+    max_TMP_normalized         # 本周期最高TMP(归一化)
+]
+```
+**4维状态向量**描述当前系统状态
+
+#### 动作(Action)
+```python
+# 离散动作空间:L_s × t_bw_s的网格
+L_s范围:3800-6000秒,步长60秒 → 37个选项
+t_bw_s范围:40-60秒,步长5秒 → 5个选项
+
+总动作数 = 37 × 5 = 185个
+```
+
+每个动作对应一个`(L_s, t_bw_s)`组合
+
+#### 奖励(Reward)
+```python
+# 多目标加权奖励
+reward = 0.8 × recovery           # 回收率(主要目标)
+       + 0.2 × rate_normalized    # 净供水率
+       - 0.2 × headroom_penalty   # TMP贴边惩罚
+```
+
+**奖励设计原则**:
+- 高回收率 → 高奖励
+- 高净供水率 → 高奖励
+- TMP接近上限 → 负奖励(膜风险)
+- 违反约束 → 大负奖励(-20)
+
+#### 状态转移
+```python
+# 模拟器:根据物理模型计算下一个状态
+def simulate_one_supercycle(TMP0, L_s, t_bw_s):
+    # 1. 计算产水阶段TMP上升
+    delta_TMP = model_fp(L_s)  # 调用TMP增长模型
+    TMP_peak = TMP0 + delta_TMP
+    
+    # 2. 计算反洗恢复
+    phi = model_bw(L_s, t_bw_s)  # 调用反洗恢复模型
+    TMP_after_bw = TMP_peak - phi × (TMP_peak - TMP0)
+    
+    # 3. 多次小周期后CEB
+    TMP_new = TMP0  # 化学清洗后完全恢复
+    
+    # 4. 计算指标
+    recovery = (产水 - 反洗水耗 - CEB水耗) / 产水
+    net_rate = 净产水 / 总时间
+    
+    return TMP_new, recovery, net_rate, ...
+```
+
+## DQN算法详解
+
+### 什么是DQN?
+
+**Deep Q-Network(深度Q网络)**:
+- 用神经网络估计**Q值函数**:`Q(state, action) = 预期累积奖励`
+- 最优策略:在每个状态选择Q值最大的动作
+
+```
+状态 → [神经网络] → 每个动作的Q值 → 选择最大Q值的动作
+```
+
+### 神经网络结构
+
+```python
+# Stable-Baselines3的MlpPolicy默认结构
+输入层:4维状态
+隐藏层1:64神经元 + ReLU
+隐藏层2:64神经元 + ReLU
+输出层:185个动作的Q值
+```
+
+### 训练流程(`DQN_train.py`)
+
+#### 1. 经验回放(Experience Replay)
+```python
+buffer_size = 10000  # 存储10000条经验
+
+# 交互过程
+for step in range(total_timesteps):
+    action = model.select_action(state)        # ε-贪心选择动作
+    next_state, reward = env.step(action)      # 执行动作
+    buffer.store(state, action, reward, next_state)  # 存入缓冲区
+    
+    # 从缓冲区随机采样训练
+    if step > learning_starts:
+        batch = buffer.sample(batch_size=32)
+        model.train_on_batch(batch)
+```
+
+**为什么需要经验回放?**
+- 打破数据相关性(连续状态往往相似)
+- 提高样本利用效率(同一条经验可多次使用)
+
+#### 2. ε-贪心探索
+```python
+# 随机探索 vs 利用已学知识
+if random() < epsilon:
+    action = random_action()   # 探索:随机选
+else:
+    action = argmax(Q(state))  # 利用:选Q值最大的
+
+# epsilon从1.0衰减到0.02
+epsilon = 1.0 → 0.8 → ... → 0.02
+```
+
+**探索-利用权衡**:
+- 初期多探索(发现好动作)
+- 后期多利用(稳定在最优策略)
+
+#### 3. 目标网络(Target Network)
+```python
+# 两个网络:当前网络 + 目标网络
+Q_current(state, action)  # 每步更新
+Q_target(next_state, a')   # 每2000步同步一次
+
+# TD误差
+loss = MSE(Q_current(s,a), reward + γ × max(Q_target(s', a')))
+```
+
+**为什么需要目标网络?**
+- 稳定训练(避免"追逐移动目标"问题)
+- 减少Q值估计的震荡
+
+#### 4. 训练超参数
+
+```python
+class DQNParams:
+    learning_rate = 1e-4          # 学习率
+    buffer_size = 10000           # 经验池大小
+    learning_starts = 200         # 200步后开始学习
+    batch_size = 32               # 每次训练32个样本
+    gamma = 0.95                  # 折扣因子(重视长期奖励)
+    train_freq = 4                # 每4步训练一次
+    target_update_interval = 2000 # 每2000步更新目标网络
+    exploration_fraction = 0.3    # 前30%训练时间用于探索
+    exploration_final_eps = 0.02  # 最终保留2%探索
+```
+
+## 模拟环境(`DQN_env.py`)
+
+### UFSuperCycleEnv类
+
+```python
+class UFSuperCycleEnv(gym.Env):
+    def reset(self):
+        # 重置环境:随机初始TMP
+        self.TMP0 = random.uniform(0.01, 0.03)
+        return self._get_obs()
+    
+    def step(self, action):
+        # 执行动作
+        L_s, t_bw_s = self._decode_action(action)
+        
+        # 调用模拟器
+        feasible, info = simulate_one_supercycle(self.TMP0, L_s, t_bw_s)
+        
+        if feasible:
+            reward = _score(info)  # 计算奖励
+            self.TMP0 = info["TMP_after_ceb"]  # 更新TMP
+            done = False
+        else:
+            reward = -20  # 违反约束,大负奖励
+            done = True   # episode终止
+        
+        return next_state, reward, done, info
+```
+
+### 约束检查
+
+```python
+# 硬约束1:TMP峰值不得超过0.06 MPa
+if TMP_peak > 0.06:
+    return False
+
+# 硬约束2:单次残余增量不得超过0.001 MPa
+if (TMP_after_bw - TMP0) > 0.001:
+    return False
+
+# 硬约束3:TMP不得超过上限的98%
+if TMP_peak / TMP_max > 0.98:
+    return False
+```
+
+### 物理模型集成
+
+```python
+# TMP增长模型(uf_fp.pth)
+def _delta_tmp(L_h):
+    return model_fp(params, L_h)  # 产水时长 → TMP增量
+
+# 反洗恢复模型(uf_bw.pth)
+def phi_bw_of(L_s, t_bw_s):
+    return model_bw(params, L_s, t_bw_s)  # (产水时长, 反洗时长) → 恢复比例
+```
+
+这两个模型是基于数据拟合或物理建模得到的。
+
+## 决策使用(`DQN_decide.py`)
+
+### 单步决策接口
+
+```python
+def run_uf_DQN_decide(uf_params, TMP0_value):
+    # 1. 创建环境
+    env = UFSuperCycleEnv(uf_params)
+    env.current_params.TMP0 = TMP0_value  # 设置当前TMP
+    
+    # 2. 加载训练好的模型
+    model = DQN.load("dqn_model.zip")
+    
+    # 3. 预测动作(确定性,不探索)
+    action, _ = model.predict(state, deterministic=True)
+    
+    # 4. 解码动作
+    L_s, t_bw_s = decode_action(action)
+    
+    return {
+        "action": action,
+        "L_s": L_s,
+        "t_bw_s": t_bw_s,
+        "expected_recovery": info["recovery"],
+        ...
+    }
+```
+
+### PLC指令生成
+
+为了避免频繁大幅调整(工艺稳定性),使用**渐进式调整**:
+
+```python
+def generate_plc_instructions(current, model_prev, model_current):
+    # 计算差异
+    diff = model_current - effective_current
+    
+    # 渐进调整:每次只调整一个步长
+    if abs(diff) >= threshold:
+        adjustment = +step_size if diff > 0 else -step_size
+    else:
+        adjustment = 0
+    
+    next_value = effective_current + adjustment
+    return next_value
+```
+
+**示例**:
+```
+当前L_s = 4000秒
+模型建议 = 4300秒
+步长 = 60秒
+
+第1轮下发:4060秒(+60)
+第2轮下发:4120秒(+60)
+...
+第5轮下发:4300秒(到达目标)
+```
+
+## 性能指标计算(`DQN_decide.py`)
+
+```python
+def calc_uf_cycle_metrics(TMP0, L_s, t_bw_s):
+    # 模拟一个超级周期
+    feasible, info = simulate_one_supercycle(params, L_s, t_bw_s)
+    
+    return {
+        "k_bw_per_ceb": 小周期次数,
+        "recovery": 回收率,
+        "net_delivery_rate_m3ph": 净供水率(m³/h),
+        "daily_prod_time_h": 日均产水时间(h/天),
+        "ton_water_energy_kWh_per_m3": 吨水电耗(kWh/m³),
+        "max_permeability": 最高渗透率(lmh/bar)
+    }
+```
+
+## 文件结构说明
+
+```
+uf-rl/
+├── DQN_train.py         # 强化学习训练脚本(DQN算法)
+├── DQN_env.py           # 模拟环境(MDP定义、物理模拟)
+├── DQN_decide.py        # 决策接口(加载模型、生成指令)
+├── UF_decide.py         # 传统优化方法(网格搜索,用于对比)
+├── UF_models.py         # 物理模型定义(TMP增长、反洗恢复)
+├── uf_fp.pth            # TMP增长模型权重
+├── uf_bw.pth            # 反洗恢复模型权重
+└── dqn_model.zip        # 训练好的DQN模型
+```
+
+## 训练流程总结
+
+```mermaid
+graph LR
+    A[初始化环境] --> B[随机初始TMP]
+    B --> C{ε-贪心选择动作}
+    C -->|探索| D[随机动作]
+    C -->|利用| E[Q值最大动作]
+    D --> F[模拟执行]
+    E --> F
+    F --> G{约束检查}
+    G -->|可行| H[计算奖励]
+    G -->|不可行| I[负奖励-20]
+    H --> J[存入经验池]
+    I --> J
+    J --> K{达到学习步数?}
+    K -->|是| L[采样训练]
+    K -->|否| M[继续交互]
+    L --> N{episode结束?}
+    M --> N
+    N -->|否| C
+    N -->|是| B
+```
+
+## 与传统方法对比
+
+### 传统网格搜索(`UF_decide.py`)
+
+```python
+# 穷举所有(L_s, t_bw_s)组合
+for L_s in [3600, 3660, ..., 4200]:
+    for t_bw_s in [90, 92, ..., 100]:
+        feasible, metrics = simulate(L_s, t_bw_s)
+        if feasible and score > best_score:
+            best = (L_s, t_bw_s)
+```
+
+**优点**:简单、可解释、保证找到网格上的最优解  
+**缺点**:
+- 计算量大(数百次模拟)
+- 参数空间离散化(可能错过真正最优点)
+- 无法泛化(每个TMP都要重新搜索)
+
+### 强化学习(DQN)
+
+**优点**:
+- 训练后推理快(一次前向传播)
+- 能泛化到不同TMP(学到状态-动作映射)
+- 可处理更复杂的状态(如历史趋势)
+
+**缺点**:
+- 训练耗时(需要大量交互)
+- 黑盒性(难以解释为何选择某动作)
+- 性能受模拟器精度影响
+
+## 训练建议
+
+### 提升策略性能
+
+1. **改进奖励设计**:
+   ```python
+   # 添加渗透率奖励
+   reward += 0.1 × permeability
+   
+   # 添加稳定性奖励(动作变化小)
+   reward -= 0.05 × |action - last_action|
+   ```
+
+2. **增加状态信息**:
+   ```python
+   state = [
+       TMP0, last_L, last_t_bw, max_TMP,
+       water_quality,  # 水质指标
+       days_since_ceb, # 距上次CEB天数
+       ...
+   ]
+   ```
+
+3. **课程学习(Curriculum Learning)**:
+   ```python
+   # 阶段1:简单场景(TMP变化小)
+   env.TMP_range = [0.025, 0.035]
+   train(10000 steps)
+   
+   # 阶段2:中等场景
+   env.TMP_range = [0.01, 0.04]
+   train(20000 steps)
+   
+   # 阶段3:困难场景(全范围)
+   env.TMP_range = [0.01, 0.05]
+   train(20000 steps)
+   ```
+
+### 加速训练
+
+```python
+# 1. 减少训练步数
+total_timesteps = 10000  # 从50000降到10000
+
+# 2. 增大batch_size(如果内存足够)
+batch_size = 64
+
+# 3. 调高learning_rate(小心不稳定)
+learning_rate = 5e-4
+
+# 4. 预训练:从传统方法生成初始数据
+buffer.load_from_grid_search()
+```
+
+## 常见问题
+
+**Q:为什么用强化学习而不是监督学习?**  
+A:监督学习需要"正确答案"标签,但这里没有标准答案(最优策略本身就是要学习的)。强化学习通过奖励信号自己探索最优策略。
+
+**Q:模拟器不准确怎么办?**  
+A:这是强化学习最大风险。解决方法:
+- 用真实数据校准模拟器
+- Sim-to-Real迁移(在真实系统上微调)
+- 保守策略(加大安全裕度)
+
+**Q:能否用于在线学习?**  
+A:可以,但需谨慎:
+- 设置安全约束(避免危险动作)
+- 分阶段部署(先离线验证)
+- 人工监督(关键决策需人工确认)
+
+**Q:为什么动作空间是离散的?**  
+A:DQN擅长离散动作(每个动作一个Q值)。如果需要连续动作,可用DDPG、SAC等算法。
+
+**Q:如何评估策略好坏?**  
+A:
+- 离线:在验证集上计算平均回收率、净供水率
+- 在线:实际运行后对比历史数据
+- 对比基线:与传统固定参数、网格搜索比较
+
+## 未来优化方向
+
+1. **多智能体协同**:多个UF模组联合优化
+2. **分层强化学习**:高层决策策略,低层决策参数
+3. **模型预测控制(MPC)集成**:结合物理模型和学习策略
+4. **安全强化学习**:硬约束保证(Safety RL)
+5. **离线强化学习**:仅用历史数据训练(Offline RL)
+
+## 总结
+
+UF-RL模型是一个**决策优化系统**,通过深度强化学习学习在不同跨膜压差下的最优运行策略。相比传统方法:
+- **更智能**:能适应不同状态,无需人工调参
+- **更高效**:训练后推理快速
+- **更全面**:平衡多个矛盾目标
+
+但同时也需要:
+- **准确的模拟器**:保证学到的策略有效
+- **充分的训练**:探索足够多的状态-动作组合
+- **谨慎的部署**:实际应用前充分验证
+