Browse Source

封装轻量超滤模型为模块级单例

jiyuhang 3 months ago
parent
commit
eca7cc9e1c
100 changed files with 13633 additions and 1 deletions
  1. 3 1
      .gitignore
  2. 0 0
      algorithm/__init__.py
  3. BIN
      algorithm/__pycache__/__init__.cpython-314.pyc
  4. BIN
      algorithm/base/__pycache__/algorithm_base.cpython-314.pyc
  5. 24 0
      algorithm/base/algorithm_base.py
  6. 32 0
      algorithm/uf_rl/UF_RL项目架构说明.md
  7. 12 0
      algorithm/uf_rl/__init__.py
  8. BIN
      algorithm/uf_rl/__pycache__/__init__.cpython-314.pyc
  9. BIN
      algorithm/uf_rl/__pycache__/dqn_decide.cpython-314.pyc
  10. BIN
      algorithm/uf_rl/config_and_model/anzhen/48h_dqn_model.zip
  11. 221 0
      algorithm/uf_rl/config_and_model/anzhen/data_to_rl_config.yaml
  12. 26 0
      algorithm/uf_rl/config_and_model/anzhen/dqn_config.yaml
  13. 142 0
      algorithm/uf_rl/config_and_model/anzhen/env_config.yaml
  14. 49 0
      algorithm/uf_rl/config_and_model/anzhen/uf_analyze_config.yaml
  15. BIN
      algorithm/uf_rl/config_and_model/lankao/48h_dqn_model.zip
  16. 221 0
      algorithm/uf_rl/config_and_model/lankao/data_to_rl_config.yaml
  17. 26 0
      algorithm/uf_rl/config_and_model/lankao/dqn_config.yaml
  18. 142 0
      algorithm/uf_rl/config_and_model/lankao/env_config.yaml
  19. 49 0
      algorithm/uf_rl/config_and_model/lankao/uf_analyze_config.yaml
  20. BIN
      algorithm/uf_rl/config_and_model/longting/48h_dqn_model.zip
  21. 221 0
      algorithm/uf_rl/config_and_model/longting/data_to_rl_config.yaml
  22. 26 0
      algorithm/uf_rl/config_and_model/longting/dqn_config.yaml
  23. 145 0
      algorithm/uf_rl/config_and_model/longting/env_config.yaml
  24. 55 0
      algorithm/uf_rl/config_and_model/longting/uf_analyze_config.yaml
  25. BIN
      algorithm/uf_rl/config_and_model/xishan/48h_dqn_model.zip
  26. BIN
      algorithm/uf_rl/config_and_model/xishan/48times_dqn_model.zip
  27. 221 0
      algorithm/uf_rl/config_and_model/xishan/data_to_rl_config.yaml
  28. 26 0
      algorithm/uf_rl/config_and_model/xishan/dqn_config.yaml
  29. 134 0
      algorithm/uf_rl/config_and_model/xishan/env_config.yaml
  30. 48 0
      algorithm/uf_rl/config_and_model/xishan/uf_analyze_config.yaml
  31. 55 0
      algorithm/uf_rl/config_and_model/yancheng/uf_analyze_config.yaml
  32. 0 0
      algorithm/uf_rl/data_to_rl/__init__.py
  33. 83 0
      algorithm/uf_rl/data_to_rl/data_splitter.py
  34. 34 0
      algorithm/uf_rl/data_to_rl/get_reset_pool.py
  35. 33 0
      algorithm/uf_rl/data_to_rl/loader.py
  36. 78 0
      algorithm/uf_rl/data_to_rl/run_data_to_rl_pipeline.py
  37. 28 0
      algorithm/uf_rl/data_to_rl/state_construction.py
  38. 47 0
      algorithm/uf_rl/data_to_rl/state_space_bounds.py
  39. 113 0
      algorithm/uf_rl/dqn_decide.py
  40. 0 0
      algorithm/uf_rl/dqn_decide_totalstate.py
  41. 0 0
      algorithm/uf_rl/env/__init__.py
  42. BIN
      algorithm/uf_rl/env/__pycache__/__init__.cpython-314.pyc
  43. BIN
      algorithm/uf_rl/env/__pycache__/env_config_loader.cpython-314.pyc
  44. BIN
      algorithm/uf_rl/env/__pycache__/env_params.cpython-314.pyc
  45. BIN
      algorithm/uf_rl/env/__pycache__/env_reset.cpython-314.pyc
  46. BIN
      algorithm/uf_rl/env/__pycache__/uf_env.cpython-314.pyc
  47. BIN
      algorithm/uf_rl/env/__pycache__/uf_physics.cpython-314.pyc
  48. BIN
      algorithm/uf_rl/env/__pycache__/uf_resistance_models_define.cpython-314.pyc
  49. BIN
      algorithm/uf_rl/env/__pycache__/uf_resistance_models_load.cpython-314.pyc
  50. 256 0
      algorithm/uf_rl/env/env_config_loader.py
  51. 367 0
      algorithm/uf_rl/env/env_params.py
  52. 190 0
      algorithm/uf_rl/env/env_reset.py
  53. 110 0
      algorithm/uf_rl/env/env_visual.py
  54. BIN
      algorithm/uf_rl/env/resistance_model_bw.pth
  55. BIN
      algorithm/uf_rl/env/resistance_model_fp.pth
  56. 487 0
      algorithm/uf_rl/env/uf_env.py
  57. 588 0
      algorithm/uf_rl/env/uf_physics.py
  58. 169 0
      algorithm/uf_rl/env/uf_resistance_models_define.py
  59. 58 0
      algorithm/uf_rl/env/uf_resistance_models_load.py
  60. 500 0
      algorithm/uf_rl/rl_model/DQN/README.md
  61. 0 0
      algorithm/uf_rl/rl_model/DQN/__init__.py
  62. BIN
      algorithm/uf_rl/rl_model/DQN/__pycache__/__init__.cpython-314.pyc
  63. 0 0
      algorithm/uf_rl/rl_model/DQN/dqn_model/__init__.py
  64. 181 0
      algorithm/uf_rl/rl_model/DQN/dqn_model/dqn_config_loader.py
  65. 94 0
      algorithm/uf_rl/rl_model/DQN/dqn_model/dqn_params.py
  66. 332 0
      algorithm/uf_rl/rl_model/DQN/dqn_model/dqn_statebuilder.py
  67. 0 0
      algorithm/uf_rl/rl_model/DQN/uf_decide/__init__.py
  68. BIN
      algorithm/uf_rl/rl_model/DQN/uf_decide/__pycache__/__init__.cpython-314.pyc
  69. BIN
      algorithm/uf_rl/rl_model/DQN/uf_decide/__pycache__/dqn_decider.cpython-314.pyc
  70. BIN
      algorithm/uf_rl/rl_model/DQN/uf_decide/__pycache__/run_dqn_decide.cpython-314.pyc
  71. 186 0
      algorithm/uf_rl/rl_model/DQN/uf_decide/dqn_decider.py
  72. 334 0
      algorithm/uf_rl/rl_model/DQN/uf_decide/run_dqn_decide.py
  73. 377 0
      algorithm/uf_rl/rl_model/DQN/uf_decide/run_dqn_decide_totalstate.py
  74. 11 0
      algorithm/uf_rl/rl_model/DQN/uf_decide/test_online_datasets/UF_init_cycle.csv
  75. 22 0
      algorithm/uf_rl/rl_model/DQN/uf_decide/test_online_datasets/UF_predict_cycle.csv
  76. 2511 0
      algorithm/uf_rl/rl_model/DQN/uf_decide/test_online_datasets/UF_prev_cycle.csv
  77. 0 0
      algorithm/uf_rl/rl_model/DQN/uf_train/__init__.py
  78. 170 0
      algorithm/uf_rl/rl_model/DQN/uf_train/dqn_trainer.py
  79. 303 0
      algorithm/uf_rl/rl_model/DQN/uf_train/run_dqn_train.py
  80. 0 0
      algorithm/uf_rl/rl_model/__init__.py
  81. BIN
      algorithm/uf_rl/rl_model/__pycache__/__init__.cpython-314.pyc
  82. 249 0
      algorithm/uf_rl/uf_data_process/calculate.py
  83. 427 0
      algorithm/uf_rl/uf_data_process/data_export.py
  84. 160 0
      algorithm/uf_rl/uf_data_process/filter.py
  85. 427 0
      algorithm/uf_rl/uf_data_process/fit.py
  86. 89 0
      algorithm/uf_rl/uf_data_process/label.py
  87. 41 0
      algorithm/uf_rl/uf_data_process/load.py
  88. 404 0
      algorithm/uf_rl/uf_data_process/pipeline.py
  89. 93 0
      algorithm/uf_rl/uf_data_process/plot.py
  90. 34 0
      algorithm/uf_rl/uf_data_process/run_ufdata_pipeline.py
  91. 168 0
      algorithm/uf_rl/uf_data_process/水厂变量名称记录.md
  92. 161 0
      algorithm/uf_rl/进水动作版超滤训练源码/UF_resistance_models.py
  93. 233 0
      algorithm/uf_rl/进水动作版超滤训练源码/fixed_DQN_decide.py
  94. 1052 0
      algorithm/uf_rl/进水动作版超滤训练源码/fixed_DQN_env.py
  95. 536 0
      algorithm/uf_rl/进水动作版超滤训练源码/fixed_DQN_train.py
  96. 0 0
      algorithm/uf_rl/进水动作版超滤训练源码/model/dqn_model.zip
  97. 0 0
      algorithm/uf_rl/进水动作版超滤训练源码/resistance_model_bw.pth
  98. 0 0
      algorithm/uf_rl/进水动作版超滤训练源码/resistance_model_fp.pth
  99. 10 0
      env.example
  100. 9 0
      main.py

+ 3 - 1
.gitignore

@@ -5,4 +5,6 @@
 /models/uf_rl/plot
 /models/uf_rl/rl_model/PPO
 /models/ro_mechanism_predict/datasets
-/models/ro_mechanism_predict/RO_inf
+/models/ro_mechanism_predict/RO_inf
+
+/.env

+ 0 - 0
algorithm/__init__.py


BIN
algorithm/__pycache__/__init__.cpython-314.pyc


BIN
algorithm/base/__pycache__/algorithm_base.cpython-314.pyc


+ 24 - 0
algorithm/base/algorithm_base.py

@@ -0,0 +1,24 @@
+from abc import ABC, abstractmethod
+
+from typing import Dict
+
+class AlgorithmBase(ABC):
+    """算法接入标准"""
+
+    def __init__(self, params=None):
+        """算法初始化"""
+        pass
+
+    def __call__(self, x:Dict, *args, **kwargs)->Dict:
+        """调用主逻辑,子类必须实现"""
+        pass
+
+    @abstractmethod
+    def get(self):
+        """获取算法实时运行参数,子类必须实现"""
+        pass
+
+    @abstractmethod
+    def set(self):
+        """实现参数热更新,子类必须实现"""
+        pass

+ 32 - 0
algorithm/uf_rl/UF_RL项目架构说明.md

@@ -0,0 +1,32 @@
+# UF-RL 项目架构说明
+
+## 项目整体结构
+* uf-rl/
+* ├── uf_data_process/             # 数据预处理
+* ├── data_to_rl/                  # 强化学习数据处理
+* ├── env/                         # 环境定义
+* ├── longting/                    # 龙亭配置
+* ├── longting/                    # 锡山配置
+* ├── rl_model/                    # 强化学习模型核心
+* │   ├── DQN/                     # DQN算法实现
+* │   │   ├── dqn_model         # dqn模型定义
+* │   │   ├── uf_train          # 训练执行
+* │   │   └── uf_decide         # 调用执行
+* │   │   │   └── test_online_datasets    # 在线调用测试数据集
+
+
+## 核心设计说明
+
+`rl_model` 是整个项目的强化学习核心目录,存放各类强化学习算法的 agent 实现。
+
+### 算法隔离设计
+当前使用 **DQN 算法**,因此将训练和决策代码统一放在 `DQN/` 目录下:
+
+- `dqn_model` - DQN 网络结构定义
+- `uf_train`   - DQN 训练流程实现  
+- `uf_decide`  - DQN 在线决策调用
+
+### 扩展性考虑
+后续论文需要对比 **PPO、SAC、A2C** 等其他强化学习算法,目录结构设计考虑扩展性: 
+
+各算法的 `uf_train.py` 和 `uf_decide.py` 保持相同的对外接口,不同算法的训练和决策逻辑差异较大,分开存放便于维护

+ 12 - 0
algorithm/uf_rl/__init__.py

@@ -0,0 +1,12 @@
+from algorithm.uf_rl.dqn_decide import DQNDecide
+
+# 模块级变量类型声明, 变量名称不要与模块下的脚本名称同名
+dqn_decide_model: DQNDecide
+
+def __getattr__(name):
+    global dqn_decide_model
+    # 全局单例 + 惰性加载
+    if name == "dqn_decide_model":
+        dqn_decide_model = DQNDecide()
+        return dqn_decide_model
+    raise AttributeError(f"module '{__name__!r}' has no attribute '{name!r}'")

BIN
algorithm/uf_rl/__pycache__/__init__.cpython-314.pyc


BIN
algorithm/uf_rl/__pycache__/dqn_decide.cpython-314.pyc


BIN
algorithm/uf_rl/config_and_model/anzhen/48h_dqn_model.zip


+ 221 - 0
algorithm/uf_rl/config_and_model/anzhen/data_to_rl_config.yaml

@@ -0,0 +1,221 @@
+# ============================================================
+# 项目级路径配置
+# ============================================================
+
+Paths:
+  project_root: "E:/Greentech"
+  __comment__: >
+    项目根目录,所有路径均相对于该目录展开。
+    不同工程师只需修改这一项即可迁移环境。
+
+  raw_data:
+    filtered_cycles_dir: "models/uf_rl/datasets/UF_longting_data/processed/filter_segments"
+    cycle_file_pattern: "UF{unit}_filtered_cycles.csv"
+
+    enabled_units: [1, 2]
+    disabled_units: None
+
+    __comment__: >
+      已完成清洗与周期切分的真实工厂数据。
+      每个 CSV 对应一个机组,文件内每一行是一个【物理周期】。
+      data_to_rl 阶段会:
+        - 读取 enabled_units 中所有机组
+        - 合并为统一数据集
+        - 机组编号仅用于筛选,不进入状态空间
+
+  data_to_rl:
+    output_dir: "models/uf_rl/datasets/UF_longting_data/rl_ready/output"
+    cache_dir: "models/uf_rl/datasets/UF_longting_data/rl_ready/cache"
+
+    __comment__: >
+      data_to_rl 模块输出内容:
+        - 全机组合并后的状态空间范围
+        - reset 初始状态分布
+        - reward 统计量
+
+
+# ============================================================
+# 数据层级定义(非常重要)
+# ============================================================
+
+DataHierarchy:
+
+  physical_cycle:
+    id_column: "seg_id"
+    time_columns:
+      start: "start_time"
+      end: "end_time"
+    __comment__: >
+      物理周期是最细粒度的数据层级:
+      - 用于物理模型验证
+      - 用于环境内部子步模拟
+      - 不直接暴露给强化学习算法
+
+  chemical_cycle:
+    id_column: "chem_cycle_id"
+    validity_column: "chem_cycle_valid"
+    step_definition: "one_rl_step_per_chemical_cycle"
+    __comment__: >
+      化学周期是强化学习的【唯一 step 单位】。
+      强化学习中:
+        - 一个 step = 一个化学周期
+        - 一个状态 = 一个化学周期的整体状态
+
+# ============================================================
+# 强化学习状态定义(工程师重点关注)
+# ============================================================
+
+RLState:
+  level: chemical_cycle
+  dimension: 8
+  __comment__: >
+    强化学习 observation 的定义。
+    所有变量在一个 step(化学周期)内保持不变。
+
+  variables:
+
+    q_UF:
+      source: "flow_mean"
+      aggregation: "mean_within_cycle"
+      unit: "m3_per_h"
+      description: "化学周期代表性进水流量"
+      __comment__: >
+        原始数据在物理周期层级变化,
+        这里使用化学周期内的平均值作为状态。
+
+    temp:
+      source: "temp_mean"
+      aggregation: "mean_within_cycle"
+      unit: "celsius"
+      description: "化学周期代表性水温"
+
+    TMP:
+      source: "tmp_start"
+      selection: "first_physical_cycle"
+      unit: "MPa"
+      description: "化学周期起始跨膜压差"
+      __comment__: >
+        虽然 TMP 在物理周期内会变化,
+        但强化学习只关心化学周期开始时的状态。
+
+    R:
+      source: "R_scaled_start"
+      selection: "first_physical_cycle"
+      unit: "scaled_resistance"
+      description: "化学周期起始膜阻力"
+
+    nuK:
+      source: "cycle_nuK"
+      unit: "scaled"
+      description: "短期污染增长系数"
+      __comment__: >
+        在同一化学周期内视为常数,
+        后续 step (下一化学周期)中根据动作进行经验性更新。
+
+    slope:
+      source: "cycle_long_a"
+      unit: "scaled"
+      description: "长期不可逆污染幂律系数 a"
+
+    power:
+      source: "cycle_long_b"
+      unit: "dimensionless"
+      description: "长期不可逆污染幂律指数 b"
+
+    ceb_removal:
+      source: "cycle_R_removed"
+      unit: "scaled"
+      description: "CEB 可去除的膜阻力"
+
+# ============================================================
+# 状态空间范围提取(reset 使用)
+# ============================================================
+
+StateSpaceExtraction:
+  enabled: true
+  level: chemical_cycle
+  include_only_valid_cycles: true
+
+  __comment__: >
+    用真实工厂数据统计【合理状态空间范围】,
+    用于:
+      - reset 初始状态采样
+      - 状态边界 sanity check
+
+  statistics:
+    method: "empirical"
+    percentiles: [1, 5, 50, 95, 99]
+    __comment__: >
+      使用经验分布统计,避免假设正态分布。
+
+  output:
+    save_csv: true
+    filename: "state_space_bounds.csv"
+
+# ============================================================
+# 物理周期层级的用途声明
+# ============================================================
+
+PhysicalCycleUsage:
+  enabled: true
+
+  purposes:
+    - internal_simulation
+    - parameter_sanity_check
+    - post_training_validation
+
+  variables_available:
+    - R_scaled_start
+    - R_scaled_end
+    - tmp_start
+    - tmp_end
+    - flow_mean
+    - flow_std
+    - temp_mean
+    - temp_std
+
+  __comment__: >
+    物理周期数据:
+      ✔ 可用于环境内部计算
+      ✔ 可用于训练后验证
+      ✘ 不可作为 RL observation
+
+# ============================================================
+# 真实数据在强化学习中的角色(工程边界声明)
+# ============================================================
+
+RealDataUsagePolicy:
+
+  reset_initialization:
+    enabled: true
+    source: "chemical_cycle_state_distribution"
+    __comment__: >
+      reset 时从真实化学周期状态分布中采样,
+      确保训练从“现实可达状态”开始。
+
+  training:
+    use_real_transitions: false
+    __comment__: >
+      真实数据不用于:
+        - 动力学拟合
+        - 监督策略
+      强化学习仍在模拟环境中进行。
+
+  validation:
+    enabled: true
+    compare_with_real_cycles: true
+    __comment__: >
+      训练完成后:
+        - 将策略运行结果
+        - 与真实化学周期统计特性进行对比
+      用于评估策略的现实可行性与安全性。
+
+# ============================================================
+# 方法论与工程风险声明
+# ============================================================
+
+MethodologyNotes:
+  - "强化学习的 step 定义严格等同于一个化学周期"
+  - "所有 RL 状态变量在一个 step 内视为常数"
+  - "物理周期层级仅用于环境内部模拟"
+  - "真实数据用于分布约束与验证,而非动力学建模"

+ 26 - 0
algorithm/uf_rl/config_and_model/anzhen/dqn_config.yaml

@@ -0,0 +1,26 @@
+# ============================================================
+# DQN 超参数配置
+# ============================================================
+# 神经网络参数
+learning_rate: 0.0001           # 学习率 1e-4
+
+# 经验回放参数
+buffer_size: 100000              # 经验回放缓冲区大小
+learning_starts: 10000           # 开始训练前收集的步数
+batch_size: 32                   # 训练批次大小
+
+# 强化学习参数
+gamma: 0.95                      # 折扣因子
+train_freq: 4                    # 训练频率(步数)
+
+# 目标网络参数
+target_update_interval: 1        # 目标网络更新间隔
+tau: 0.005                       # 软更新系数
+
+# 探索策略参数
+exploration_initial_eps: 1.0     # 初始探索率
+exploration_fraction: 0.3        # 探索率衰减比例
+exploration_final_eps: 0.02      # 最终探索率
+
+# 日志参数
+remark: "uf_dqn_real_reset"      # 实验备注

+ 142 - 0
algorithm/uf_rl/config_and_model/anzhen/env_config.yaml

@@ -0,0 +1,142 @@
+UFState:
+  # ===== 膜动态运行参数 =====
+  q_UF: 150.0
+  TMP: 0.02
+  temp: 20.0
+  R: 200.0
+
+  # ===== 膜阻力模型参数 =====
+  nuK: 170.0
+  slope: 2.0
+  power: 1.1
+  ceb_removal: 200.0
+
+
+UFPhysicsParams:
+  # ===== TMP 全局约束 =====
+  global_TMP_hard_limit: 0.08 # 跨膜压差硬上限
+  global_TMP_soft_limit: 0.06 # 跨膜压差软上限
+
+  # ===== 物理反洗参数 =====
+  tau_bw_s: 20.0
+  gamma_t: 1.0
+  q_bw_m3ph: 700.0
+
+  # ===== CEB 化学反洗参数 =====
+  T_ceb_interval_h: 48.0
+  v_ceb_m3: 20.0
+  t_ceb_s: 1800.0
+
+  # ===== 膜组件参数 =====
+  A: 3200.0
+
+  # ===== 吨水电耗查找表 =====
+  energy_lookup:
+    2700: 0.1088
+    2760: 0.1083
+    2820: 0.1078
+    2880: 0.1074
+    2940: 0.1070
+    3000: 0.1066
+    3060: 0.1062
+    3120: 0.1059
+    3180: 0.1055
+    3240: 0.1052
+    3300: 0.1049
+    3360: 0.1045
+    3420: 0.1042
+    3480: 0.1039
+    3540: 0.1036
+    3600: 0.1034
+    3660: 0.1031
+    3720: 0.1029
+    3780: 0.1026
+    3840: 0.1023
+    3900: 0.1021
+    3960: 0.1019
+    4020: 0.1017
+    4080: 0.1015
+    4140: 0.1012
+    4200: 0.1011
+    4260: 0.1008
+    4320: 0.1007
+    4380: 0.1005
+    4440: 0.1003
+    4500: 0.1001
+    4560: 0.0999
+    4620: 0.0998
+    4680: 0.0996
+    4740: 0.0995
+    4800: 0.0993
+
+  p_feed_kw_min: 5.0
+  p_feed_kw_max: 15.0
+  p_bw_kw: 30.0
+
+  dose_min: 0.05
+  dose_max: 0.15
+  dose_area: 0.56
+
+
+UFActionSpec:
+  # ===== 动作空间范围 =====
+  L_min_s: 3600.0
+  L_max_s: 4800.0
+  t_bw_min_s: 50.0
+  t_bw_max_s: 70.0
+
+  # ===== 动作离散化步长 =====
+  L_step_s: 60.0
+  t_bw_step_s: 5.0
+
+
+UFRewardParams:
+  # ===== TMP 安全与惩罚 =====
+  global_TMP_hard_limit: 0.08
+  global_TMP_soft_limit: 0.06
+  w_tmp_hard: 5.0
+  w_tmp: 1.5
+  p: 3.0
+  w_trend: 1.0
+
+  # ===== 经济成本 =====
+  k_cost: 3.0
+  chemical_price: 13.0
+  energy_price: 0.667
+  cost_low: 0.08
+  cost_high:  0.20
+  w_cost: 1.0
+
+  # ===== 残余污染 =====
+  k_res: 3.0
+  residual_ref_ratio: null
+  w_res: 1.0
+
+
+UFStateBounds:
+  # ===== 流量初始化约束 =====
+  q_UF_min: 150.0
+  q_UF_max: 200.0
+
+  # ===== 温度初始化约束 =====
+  temp_min: 9.0
+  temp_max: 25.0
+
+  # ===== TMP 初始化约束 =====
+  TMP0_min: 0.01
+  TMP0_max: 0.04
+  global_TMP_hard_limit: 0.08
+
+  # ===== 短期污染参数 =====
+  nuK_min: 100.0
+  nuK_max: 250.0
+
+  # ===== 长期污染参数 =====
+  slope_min: 1.28
+  slope_max: 150
+  power_min: 0.25
+  power_max: 2.5
+
+  # ===== CEB 去除能力 =====
+  ceb_removal_min: 150.0
+  ceb_removal_max: 350.0

+ 49 - 0
algorithm/uf_rl/config_and_model/anzhen/uf_analyze_config.yaml

@@ -0,0 +1,49 @@
+UF:
+  units: [ "1", "2" ]
+  area_m2: 128 * 40
+
+  inlet_codes: [215.0, 301.0]
+  stable_codes: [220.0, 260.0]
+
+  physical_bw_code: [301.0, 340.0]
+  chemical_bw_code: [400.0, 660.0]
+
+  # 列名
+  column_formats:
+    flow_col: "AR.{unit}#UF_JSFLOW_O"
+    tmp_col: "AR.UF{unit}_SSD_KMYC"
+    press_col: "AR.{unit}#UF_JSPRESS_O"
+    ctrl_col: "AR.UF{unit}_STEP"
+    temp_col: "AR.ZJS_TEMP_O"
+    orp_col: "AR.RO_JSORP_O"
+    ph_col: "AR.RO_JSPH_O"
+
+
+Params:
+  # 稳定段提取
+  min_stable_points: 30
+  initial_points: 10
+
+  # 阻力趋势计算
+  segment_head_n: 5
+  segment_tail_n: 5
+
+  scale_factor: 1e10
+
+
+Plot:
+  figsize: [12, 6]
+  dpi: 120
+  color_inlet: "#1f77b4"
+  color_bw_phys: "#ff7f0e"
+  color_bw_chem: "#d62728"
+
+Paths:
+  project_root: "E:/Greentech" # 请根据项目根目录修改相应路径
+
+  raw_data_path: "models/uf_rl/datasets/UF_anzhen_data/raw"
+  output_path: "models/uf_rl/datasets/UF_anzhen_data/processed/segments"
+  filter_output_path: "models/uf_rl/datasets/UF_anzhen_data/processed/filter_segments"
+
+  output_format: "csv"
+

BIN
algorithm/uf_rl/config_and_model/lankao/48h_dqn_model.zip


+ 221 - 0
algorithm/uf_rl/config_and_model/lankao/data_to_rl_config.yaml

@@ -0,0 +1,221 @@
+# ============================================================
+# 项目级路径配置
+# ============================================================
+
+Paths:
+  project_root: "E:/Greentech"
+  __comment__: >
+    项目根目录,所有路径均相对于该目录展开。
+    不同工程师只需修改这一项即可迁移环境。
+
+  raw_data:
+    filtered_cycles_dir: "models/uf_rl/datasets/UF_lankao_data/processed/filter_segments"
+    cycle_file_pattern: "UF{unit}_filtered_cycles.csv"
+
+    enabled_units: [1, 2]
+    disabled_units: None
+
+    __comment__: >
+      已完成清洗与周期切分的真实工厂数据。
+      每个 CSV 对应一个机组,文件内每一行是一个【物理周期】。
+      data_to_rl 阶段会:
+        - 读取 enabled_units 中所有机组
+        - 合并为统一数据集
+        - 机组编号仅用于筛选,不进入状态空间
+
+  data_to_rl:
+    output_dir: "models/uf_rl/datasets/UF_lankao_data/rl_ready/output"
+    cache_dir: "models/uf_rl/datasets/UF_lankao_data/rl_ready/cache"
+
+    __comment__: >
+      data_to_rl 模块输出内容:
+        - 全机组合并后的状态空间范围
+        - reset 初始状态分布
+        - reward 统计量
+
+
+# ============================================================
+# 数据层级定义(非常重要)
+# ============================================================
+
+DataHierarchy:
+
+  physical_cycle:
+    id_column: "seg_id"
+    time_columns:
+      start: "start_time"
+      end: "end_time"
+    __comment__: >
+      物理周期是最细粒度的数据层级:
+      - 用于物理模型验证
+      - 用于环境内部子步模拟
+      - 不直接暴露给强化学习算法
+
+  chemical_cycle:
+    id_column: "chem_cycle_id"
+    validity_column: "chem_cycle_valid"
+    step_definition: "one_rl_step_per_chemical_cycle"
+    __comment__: >
+      化学周期是强化学习的【唯一 step 单位】。
+      强化学习中:
+        - 一个 step = 一个化学周期
+        - 一个状态 = 一个化学周期的整体状态
+
+# ============================================================
+# 强化学习状态定义(工程师重点关注)
+# ============================================================
+
+RLState:
+  level: chemical_cycle
+  dimension: 8
+  __comment__: >
+    强化学习 observation 的定义。
+    所有变量在一个 step(化学周期)内保持不变。
+
+  variables:
+
+    q_UF:
+      source: "flow_mean"
+      aggregation: "mean_within_cycle"
+      unit: "m3_per_h"
+      description: "化学周期代表性进水流量"
+      __comment__: >
+        原始数据在物理周期层级变化,
+        这里使用化学周期内的平均值作为状态。
+
+    temp:
+      source: "temp_mean"
+      aggregation: "mean_within_cycle"
+      unit: "celsius"
+      description: "化学周期代表性水温"
+
+    TMP:
+      source: "tmp_start"
+      selection: "first_physical_cycle"
+      unit: "MPa"
+      description: "化学周期起始跨膜压差"
+      __comment__: >
+        虽然 TMP 在物理周期内会变化,
+        但强化学习只关心化学周期开始时的状态。
+
+    R:
+      source: "R_scaled_start"
+      selection: "first_physical_cycle"
+      unit: "scaled_resistance"
+      description: "化学周期起始膜阻力"
+
+    nuK:
+      source: "cycle_nuK"
+      unit: "scaled"
+      description: "短期污染增长系数"
+      __comment__: >
+        在同一化学周期内视为常数,
+        后续 step (下一化学周期)中根据动作进行经验性更新。
+
+    slope:
+      source: "cycle_long_a"
+      unit: "scaled"
+      description: "长期不可逆污染幂律系数 a"
+
+    power:
+      source: "cycle_long_b"
+      unit: "dimensionless"
+      description: "长期不可逆污染幂律指数 b"
+
+    ceb_removal:
+      source: "cycle_R_removed"
+      unit: "scaled"
+      description: "CEB 可去除的膜阻力"
+
+# ============================================================
+# 状态空间范围提取(reset 使用)
+# ============================================================
+
+StateSpaceExtraction:
+  enabled: true
+  level: chemical_cycle
+  include_only_valid_cycles: true
+
+  __comment__: >
+    用真实工厂数据统计【合理状态空间范围】,
+    用于:
+      - reset 初始状态采样
+      - 状态边界 sanity check
+
+  statistics:
+    method: "empirical"
+    percentiles: [1, 5, 50, 95, 99]
+    __comment__: >
+      使用经验分布统计,避免假设正态分布。
+
+  output:
+    save_csv: true
+    filename: "state_space_bounds.csv"
+
+# ============================================================
+# 物理周期层级的用途声明
+# ============================================================
+
+PhysicalCycleUsage:
+  enabled: true
+
+  purposes:
+    - internal_simulation
+    - parameter_sanity_check
+    - post_training_validation
+
+  variables_available:
+    - R_scaled_start
+    - R_scaled_end
+    - tmp_start
+    - tmp_end
+    - flow_mean
+    - flow_std
+    - temp_mean
+    - temp_std
+
+  __comment__: >
+    物理周期数据:
+      ✔ 可用于环境内部计算
+      ✔ 可用于训练后验证
+      ✘ 不可作为 RL observation
+
+# ============================================================
+# 真实数据在强化学习中的角色(工程边界声明)
+# ============================================================
+
+RealDataUsagePolicy:
+
+  reset_initialization:
+    enabled: true
+    source: "chemical_cycle_state_distribution"
+    __comment__: >
+      reset 时从真实化学周期状态分布中采样,
+      确保训练从“现实可达状态”开始。
+
+  training:
+    use_real_transitions: false
+    __comment__: >
+      真实数据不用于:
+        - 动力学拟合
+        - 监督策略
+      强化学习仍在模拟环境中进行。
+
+  validation:
+    enabled: true
+    compare_with_real_cycles: true
+    __comment__: >
+      训练完成后:
+        - 将策略运行结果
+        - 与真实化学周期统计特性进行对比
+      用于评估策略的现实可行性与安全性。
+
+# ============================================================
+# 方法论与工程风险声明
+# ============================================================
+
+MethodologyNotes:
+  - "强化学习的 step 定义严格等同于一个化学周期"
+  - "所有 RL 状态变量在一个 step 内视为常数"
+  - "物理周期层级仅用于环境内部模拟"
+  - "真实数据用于分布约束与验证,而非动力学建模"

+ 26 - 0
algorithm/uf_rl/config_and_model/lankao/dqn_config.yaml

@@ -0,0 +1,26 @@
+# ============================================================
+# DQN 超参数配置
+# ============================================================
+# 神经网络参数
+learning_rate: 0.0001           # 学习率 1e-4
+
+# 经验回放参数
+buffer_size: 100000              # 经验回放缓冲区大小
+learning_starts: 10000           # 开始训练前收集的步数
+batch_size: 32                   # 训练批次大小
+
+# 强化学习参数
+gamma: 0.95                      # 折扣因子
+train_freq: 4                    # 训练频率(步数)
+
+# 目标网络参数
+target_update_interval: 1        # 目标网络更新间隔
+tau: 0.005                       # 软更新系数
+
+# 探索策略参数
+exploration_initial_eps: 1.0     # 初始探索率
+exploration_fraction: 0.3        # 探索率衰减比例
+exploration_final_eps: 0.02      # 最终探索率
+
+# 日志参数
+remark: "uf_dqn_real_reset"      # 实验备注

+ 142 - 0
algorithm/uf_rl/config_and_model/lankao/env_config.yaml

@@ -0,0 +1,142 @@
+UFState:
+  # ===== 膜动态运行参数 =====
+  q_UF: 150.0
+  TMP: 0.02
+  temp: 20.0
+  R: 200.0
+
+  # ===== 膜阻力模型参数 =====
+  nuK: 170.0
+  slope: 2.0
+  power: 1.1
+  ceb_removal: 200.0
+
+
+UFPhysicsParams:
+  # ===== TMP 全局约束 =====
+  global_TMP_hard_limit: 0.08 # 跨膜压差硬上限
+  global_TMP_soft_limit: 0.06 # 跨膜压差软上限
+
+  # ===== 物理反洗参数 =====
+  tau_bw_s: 20.0
+  gamma_t: 1.0
+  q_bw_m3ph: 500.0
+
+  # ===== CEB 化学反洗参数 =====
+  T_ceb_interval_h: 48.0
+  v_ceb_m3: 20.0
+  t_ceb_s: 1800.0
+
+  # ===== 膜组件参数 =====
+  A: 3200.0
+
+  # ===== 吨水电耗查找表 =====
+  energy_lookup:
+    2700: 0.1088
+    2760: 0.1083
+    2820: 0.1078
+    2880: 0.1074
+    2940: 0.1070
+    3000: 0.1066
+    3060: 0.1062
+    3120: 0.1059
+    3180: 0.1055
+    3240: 0.1052
+    3300: 0.1049
+    3360: 0.1045
+    3420: 0.1042
+    3480: 0.1039
+    3540: 0.1036
+    3600: 0.1034
+    3660: 0.1031
+    3720: 0.1029
+    3780: 0.1026
+    3840: 0.1023
+    3900: 0.1021
+    3960: 0.1019
+    4020: 0.1017
+    4080: 0.1015
+    4140: 0.1012
+    4200: 0.1011
+    4260: 0.1008
+    4320: 0.1007
+    4380: 0.1005
+    4440: 0.1003
+    4500: 0.1001
+    4560: 0.0999
+    4620: 0.0998
+    4680: 0.0996
+    4740: 0.0995
+    4800: 0.0993
+
+  p_feed_kw_min: 15.0
+  p_feed_kw_max: 20.0
+  p_bw_kw: 25.0
+
+  dose_min: 0.10
+  dose_max: 0.20
+  dose_area: 0.56
+
+
+UFActionSpec:
+  # ===== 动作空间范围 =====
+  L_min_s: 2400.0
+  L_max_s: 3800.0
+  t_bw_min_s: 40.0
+  t_bw_max_s: 60.0
+
+  # ===== 动作离散化步长 =====
+  L_step_s: 60.0
+  t_bw_step_s: 5.0
+
+
+UFRewardParams:
+  # ===== TMP 安全与惩罚 =====
+  global_TMP_hard_limit: 0.08
+  global_TMP_soft_limit: 0.06
+  w_tmp_hard: 5.0
+  w_tmp: 1.5
+  p: 3.0
+  w_trend: 1.0
+
+  # ===== 经济成本 =====
+  k_cost: 3.0
+  chemical_price: 13.0
+  energy_price: 0.667
+  cost_low: 0.08
+  cost_high:  0.20
+  w_cost: 1.0
+
+  # ===== 残余污染 =====
+  k_res: 3.0
+  residual_ref_ratio: null
+  w_res: 1.0
+
+
+UFStateBounds:
+  # ===== 流量初始化约束 =====
+  q_UF_min: 140.0
+  q_UF_max: 210.0
+
+  # ===== 温度初始化约束 =====
+  temp_min: 15.0
+  temp_max: 25.0
+
+  # ===== TMP 初始化约束 =====
+  TMP0_min: 0.01
+  TMP0_max: 0.04
+  global_TMP_hard_limit: 0.08
+
+  # ===== 短期污染参数 =====
+  nuK_min: 100.0
+  nuK_max: 250.0
+
+  # ===== 长期污染参数 =====
+  slope_min: 1.28
+  slope_max: 150
+  power_min: 0.25
+  power_max: 2.5
+
+  # ===== CEB 去除能力 =====
+  ceb_removal_min: 150.0
+  ceb_removal_max: 350.0

+ 49 - 0
algorithm/uf_rl/config_and_model/lankao/uf_analyze_config.yaml

@@ -0,0 +1,49 @@
+UF:
+  units: [ "1", "2" ]
+  area_m2: 3200
+
+  inlet_codes: [220.0, 301.0]
+  stable_codes: [220.0, 260.0]
+
+  physical_bw_code: [301.0, 340.0]
+  chemical_bw_code: [400.0, 660.0]
+
+  # 列名
+  column_formats:
+    flow_col: "ns=3;s={unit}#UF_JSFLOW_O"
+    tmp_col: "ns=3;s=UF{unit}_SSD_KMYC"
+    press_col: "ns=3;s={unit}#UF_JSPRESS_O"
+    ctrl_col: "ns=3;s=UF{unit}_STEP"
+    temp_col: "ns=3;s=ZJS_TEMP_O"
+    orp_col: "ns=3;s=RO_JSORP_O"
+    ph_col: "ns=3;s=RO_JSPH_O"
+
+
+Params:
+  # 稳定段提取
+  min_stable_points: 30
+  initial_points: 10
+
+  # 阻力趋势计算
+  segment_head_n: 5
+  segment_tail_n: 5
+
+  scale_factor: 1e10
+
+
+Plot:
+  figsize: [12, 6]
+  dpi: 120
+  color_inlet: "#1f77b4"
+  color_bw_phys: "#ff7f0e"
+  color_bw_chem: "#d62728"
+
+Paths:
+  project_root: "E:/Greentech" # 请根据项目根目录修改相应路径
+
+  raw_data_path: "models/uf_rl/datasets/UF_lankao_data/raw"
+  output_path: "models/uf_rl/datasets/UF_lankao_data/processed/segments"
+  filter_output_path: "models/uf_rl/datasets/UF_lankao_data/processed/filter_segments"
+
+  output_format: "csv"
+

BIN
algorithm/uf_rl/config_and_model/longting/48h_dqn_model.zip


+ 221 - 0
algorithm/uf_rl/config_and_model/longting/data_to_rl_config.yaml

@@ -0,0 +1,221 @@
+# ============================================================
+# 项目级路径配置
+# ============================================================
+
+Paths:
+  project_root: "E:/Greentech"
+  __comment__: >
+    项目根目录,所有路径均相对于该目录展开。
+    不同工程师只需修改这一项即可迁移环境。
+
+  raw_data:
+    filtered_cycles_dir: "models/uf_rl/datasets/UF_longting_data/processed/filter_segments"
+    cycle_file_pattern: "UF{unit}_filtered_cycles.csv"
+
+    enabled_units: [1, 2]
+    disabled_units: None
+
+    __comment__: >
+      已完成清洗与周期切分的真实工厂数据。
+      每个 CSV 对应一个机组,文件内每一行是一个【物理周期】。
+      data_to_rl 阶段会:
+        - 读取 enabled_units 中所有机组
+        - 合并为统一数据集
+        - 机组编号仅用于筛选,不进入状态空间
+
+  data_to_rl:
+    output_dir: "models/uf_rl/datasets/UF_longting_data/rl_ready/output"
+    cache_dir: "models/uf_rl/datasets/UF_longting_data/rl_ready/cache"
+
+    __comment__: >
+      data_to_rl 模块输出内容:
+        - 全机组合并后的状态空间范围
+        - reset 初始状态分布
+        - reward 统计量
+
+
+# ============================================================
+# 数据层级定义(非常重要)
+# ============================================================
+
+DataHierarchy:
+
+  physical_cycle:
+    id_column: "seg_id"
+    time_columns:
+      start: "start_time"
+      end: "end_time"
+    __comment__: >
+      物理周期是最细粒度的数据层级:
+      - 用于物理模型验证
+      - 用于环境内部子步模拟
+      - 不直接暴露给强化学习算法
+
+  chemical_cycle:
+    id_column: "chem_cycle_id"
+    validity_column: "chem_cycle_valid"
+    step_definition: "one_rl_step_per_chemical_cycle"
+    __comment__: >
+      化学周期是强化学习的【唯一 step 单位】。
+      强化学习中:
+        - 一个 step = 一个化学周期
+        - 一个状态 = 一个化学周期的整体状态
+
+# ============================================================
+# 强化学习状态定义(工程师重点关注)
+# ============================================================
+
+RLState:
+  level: chemical_cycle
+  dimension: 8
+  __comment__: >
+    强化学习 observation 的定义。
+    所有变量在一个 step(化学周期)内保持不变。
+
+  variables:
+
+    q_UF:
+      source: "flow_mean"
+      aggregation: "mean_within_cycle"
+      unit: "m3_per_h"
+      description: "化学周期代表性进水流量"
+      __comment__: >
+        原始数据在物理周期层级变化,
+        这里使用化学周期内的平均值作为状态。
+
+    temp:
+      source: "temp_mean"
+      aggregation: "mean_within_cycle"
+      unit: "celsius"
+      description: "化学周期代表性水温"
+
+    TMP:
+      source: "tmp_start"
+      selection: "first_physical_cycle"
+      unit: "MPa"
+      description: "化学周期起始跨膜压差"
+      __comment__: >
+        虽然 TMP 在物理周期内会变化,
+        但强化学习只关心化学周期开始时的状态。
+
+    R:
+      source: "R_scaled_start"
+      selection: "first_physical_cycle"
+      unit: "scaled_resistance"
+      description: "化学周期起始膜阻力"
+
+    nuK:
+      source: "cycle_nuK"
+      unit: "scaled"
+      description: "短期污染增长系数"
+      __comment__: >
+        在同一化学周期内视为常数,
+        后续 step (下一化学周期)中根据动作进行经验性更新。
+
+    slope:
+      source: "cycle_long_a"
+      unit: "scaled"
+      description: "长期不可逆污染幂律系数 a"
+
+    power:
+      source: "cycle_long_b"
+      unit: "dimensionless"
+      description: "长期不可逆污染幂律指数 b"
+
+    ceb_removal:
+      source: "cycle_R_removed"
+      unit: "scaled"
+      description: "CEB 可去除的膜阻力"
+
+# ============================================================
+# 状态空间范围提取(reset 使用)
+# ============================================================
+
+StateSpaceExtraction:
+  enabled: true
+  level: chemical_cycle
+  include_only_valid_cycles: true
+
+  __comment__: >
+    用真实工厂数据统计【合理状态空间范围】,
+    用于:
+      - reset 初始状态采样
+      - 状态边界 sanity check
+
+  statistics:
+    method: "empirical"
+    percentiles: [1, 5, 50, 95, 99]
+    __comment__: >
+      使用经验分布统计,避免假设正态分布。
+
+  output:
+    save_csv: true
+    filename: "state_space_bounds.csv"
+
+# ============================================================
+# 物理周期层级的用途声明
+# ============================================================
+
+PhysicalCycleUsage:
+  enabled: true
+
+  purposes:
+    - internal_simulation
+    - parameter_sanity_check
+    - post_training_validation
+
+  variables_available:
+    - R_scaled_start
+    - R_scaled_end
+    - tmp_start
+    - tmp_end
+    - flow_mean
+    - flow_std
+    - temp_mean
+    - temp_std
+
+  __comment__: >
+    物理周期数据:
+      ✔ 可用于环境内部计算
+      ✔ 可用于训练后验证
+      ✘ 不可作为 RL observation
+
+# ============================================================
+# 真实数据在强化学习中的角色(工程边界声明)
+# ============================================================
+
+RealDataUsagePolicy:
+
+  reset_initialization:
+    enabled: true
+    source: "chemical_cycle_state_distribution"
+    __comment__: >
+      reset 时从真实化学周期状态分布中采样,
+      确保训练从“现实可达状态”开始。
+
+  training:
+    use_real_transitions: false
+    __comment__: >
+      真实数据不用于:
+        - 动力学拟合
+        - 监督策略
+      强化学习仍在模拟环境中进行。
+
+  validation:
+    enabled: true
+    compare_with_real_cycles: true
+    __comment__: >
+      训练完成后:
+        - 将策略运行结果
+        - 与真实化学周期统计特性进行对比
+      用于评估策略的现实可行性与安全性。
+
+# ============================================================
+# 方法论与工程风险声明
+# ============================================================
+
+MethodologyNotes:
+  - "强化学习的 step 定义严格等同于一个化学周期"
+  - "所有 RL 状态变量在一个 step 内视为常数"
+  - "物理周期层级仅用于环境内部模拟"
+  - "真实数据用于分布约束与验证,而非动力学建模"

+ 26 - 0
algorithm/uf_rl/config_and_model/longting/dqn_config.yaml

@@ -0,0 +1,26 @@
+# ============================================================
+# DQN 超参数配置
+# ============================================================
+# 神经网络参数
+learning_rate: 0.0001           # 学习率 1e-4
+
+# 经验回放参数
+buffer_size: 100000              # 经验回放缓冲区大小
+learning_starts: 10000           # 开始训练前收集的步数
+batch_size: 32                   # 训练批次大小
+
+# 强化学习参数
+gamma: 0.95                      # 折扣因子
+train_freq: 4                    # 训练频率(步数)
+
+# 目标网络参数
+target_update_interval: 1        # 目标网络更新间隔
+tau: 0.005                       # 软更新系数
+
+# 探索策略参数
+exploration_initial_eps: 1.0     # 初始探索率
+exploration_fraction: 0.3        # 探索率衰减比例
+exploration_final_eps: 0.02      # 最终探索率
+
+# 日志参数
+remark: "uf_dqn_real_reset"      # 实验备注

+ 145 - 0
algorithm/uf_rl/config_and_model/longting/env_config.yaml

@@ -0,0 +1,145 @@
+UFState:
+  # ===== 膜动态运行参数 =====
+  q_UF: 150.0 # 流量
+  TMP: 0.03 # CEB周期初始跨膜压差
+  temp: 20.0 # 温度
+  R: 200.0 # 膜阻力(占位,实际上应用时根据前三个变量计算)
+
+  # ===== 膜阻力模型参数 =====
+  nuK: 500.0 # 短期膜阻力上升速率
+  slope: 2.0 # 长期膜阻力上升幂律模型斜率
+  power: 1.5 # 长期膜阻力上升幂律模型次数
+  ceb_removal: 300.0 # 本次CEB去除膜阻力值
+
+
+UFPhysicsParams:
+  # ===== TMP 全局约束 =====
+  global_TMP_hard_limit: 0.08 # 跨膜压差硬上限
+  global_TMP_soft_limit: 0.06 # 跨膜压差软上限
+
+  # ===== 物理反洗参数 =====
+  tau_bw_s: 20.0
+  gamma_t: 1.0
+  q_bw_m3ph: 500.0
+
+  # ===== CEB 化学反洗参数 =====
+  T_ceb_interval_h: 48.0
+  T_ceb_interval_times: 48
+  v_ceb_m3: 20.0
+  t_ceb_s: 2400.0   # 40 * 60
+
+  # ===== 膜组件参数 =====
+  A: 5120.0   # 128 * 40
+
+  # ===== 吨水电耗查找表 =====
+  energy_lookup:
+    2700: 0.1088
+    2760: 0.1083
+    2820: 0.1078
+    2880: 0.1074
+    2940: 0.1070
+    3000: 0.1066
+    3060: 0.1062
+    3120: 0.1059
+    3180: 0.1055
+    3240: 0.1052
+    3300: 0.1049
+    3360: 0.1045
+    3420: 0.1042
+    3480: 0.1039
+    3540: 0.1036
+    3600: 0.1034
+    3660: 0.1031
+    3720: 0.1029
+    3780: 0.1026
+    3840: 0.1023
+    3900: 0.1021
+    3960: 0.1019
+    4020: 0.1017
+    4080: 0.1015
+    4140: 0.1012
+    4200: 0.1011
+    4260: 0.1008
+    4320: 0.1007
+    4380: 0.1005
+    4440: 0.1003
+    4500: 0.1001
+    4560: 0.0999
+    4620: 0.0998
+    4680: 0.0996
+    4740: 0.0995
+    4800: 0.0993
+
+  p_feed_kw_min: 16.0
+  p_feed_kw_max: 19.0
+  p_bw_kw: 20.0
+
+  dose_min:  0.10
+  dose_max:  0.20
+  dose_area: 0.56
+
+
+
+UFActionSpec:
+  # ===== 动作空间范围 =====
+  L_min_s: 3000.0
+  L_max_s: 3800.0
+  t_bw_min_s: 50.0
+  t_bw_max_s: 70.0
+
+  # ===== 动作离散化步长 =====
+  L_step_s: 60.0
+  t_bw_step_s: 5.0
+
+
+UFRewardParams:
+  # ===== TMP 安全与惩罚 =====
+  global_TMP_hard_limit: 0.08
+  global_TMP_soft_limit: 0.06
+  w_tmp_hard: 5.0
+  w_tmp: 1.5
+  p: 3.0
+  w_trend: 1.0
+
+  # ===== 经济成本 =====
+  k_cost: 1.0
+  chemical_price: 13.0
+  energy_price: 0.667
+  cost_low: 0.11
+  cost_high:  0.14
+  w_cost: 1.0
+
+  # ===== 残余污染 =====
+  k_res: 3.0
+  residual_ref_ratio: null
+  w_res: 1.0
+
+
+
+UFStateBounds:
+  # ===== 流量初始化约束 =====
+  q_UF_min: 130.0
+  q_UF_max: 170.0
+
+  # ===== 温度初始化约束 =====
+  temp_min: 14.0
+  temp_max: 32.0
+
+  # ===== TMP 初始化约束 =====
+  TMP0_min: 0.01
+  TMP0_max: 0.06
+  global_TMP_hard_limit: 0.08
+
+  # ===== 短期污染参数 =====
+  nuK_min: 300.0
+  nuK_max: 700.0
+
+  # ===== 长期污染参数 =====
+  slope_min: 1.28
+  slope_max: 150
+  power_min: 0.25
+  power_max: 2.5
+
+  # ===== CEB 去除能力 =====
+  ceb_removal_min: 200.0
+  ceb_removal_max: 500.0

+ 55 - 0
algorithm/uf_rl/config_and_model/longting/uf_analyze_config.yaml

@@ -0,0 +1,55 @@
+UF:
+  units: [ "1", "2" ]
+  area_m2: 128 * 40
+
+  inlet_codes: [221.0, 300.0]
+  stable_codes: [221.0, 260.0]
+
+  physical_bw_code: [301.0, 340.0]
+  chemical_bw_code: [400.0, 660.0]
+
+  # 列名
+  column_formats:
+    flow_col: "ns=3;s={unit}#UF_JSFLOW_O"
+    tmp_col: "ns=3;s=UF{unit}_SSD_KMYC"
+    press_col: "ns=3;s={unit}#UF_JSPRESS_O"
+    ctrl_col: "ns=3;s=UF{unit}_STEP"
+    temp_col: "ns=3;s=ZJS_TEMP_O"
+    orp_col: "ns=3;s=RO_JSORP_O"
+    ph_col: "ns=3;s=RO_JSPH_O"
+    event_col: "event_type"
+    BWB_POWER_col: "ns=3;s=ZZ_{unit}#UFBWB_POWER"
+    GSB_POWER_col: "ns=3;s=ZZ_UFGSB_POWER"
+    NaClO_col: "ns=3;s=CN_LEVEL_O"
+    HCL_col: "ns=3;s=S_LEVEL_O"
+    NaOH_col: "ns=3;s=J_LEVEL_O"
+
+
+Params:
+  # 稳定段提取
+  min_stable_points: 30
+  initial_points: 10
+
+  # 阻力趋势计算
+  segment_head_n: 5
+  segment_tail_n: 5
+
+  scale_factor: 1e10
+
+
+Plot:
+  figsize: [12, 6]
+  dpi: 120
+  color_inlet: "#1f77b4"
+  color_bw_phys: "#ff7f0e"
+  color_bw_chem: "#d62728"
+
+Paths:
+  project_root: "E:/Greentech" # 请根据项目根目录修改相应路径
+
+  raw_data_path: "models/uf_rl/datasets/UF_longting_data/raw"
+  output_path: "models/uf_rl/datasets/UF_longting_data/processed/segments"
+  filter_output_path: "models/uf_rl/datasets/UF_longting_data/processed/filter_segments"
+
+  output_format: "csv"
+

BIN
algorithm/uf_rl/config_and_model/xishan/48h_dqn_model.zip


BIN
algorithm/uf_rl/config_and_model/xishan/48times_dqn_model.zip


+ 221 - 0
algorithm/uf_rl/config_and_model/xishan/data_to_rl_config.yaml

@@ -0,0 +1,221 @@
+# ============================================================
+# 项目级路径配置
+# ============================================================
+
+Paths:
+  project_root: "E:/Greentech"
+  __comment__: >
+    项目根目录,所有路径均相对于该目录展开。
+    不同工程师只需修改这一项即可迁移环境。
+
+  raw_data:
+    filtered_cycles_dir: "models/uf_rl/datasets/UF_xishan_data/processed/filter_segments"
+    cycle_file_pattern: "UF{unit}_filtered_cycles.csv"
+
+    enabled_units: [1, 2, 4]
+    disabled_units: [3]
+
+    __comment__: >
+      已完成清洗与周期切分的真实工厂数据。
+      每个 CSV 对应一个机组,文件内每一行是一个【物理周期】。
+      data_to_rl 阶段会:
+        - 读取 enabled_units 中所有机组
+        - 合并为统一数据集
+        - 机组编号仅用于筛选,不进入状态空间
+
+  data_to_rl:
+    output_dir: "models/uf_rl/datasets/UF_xishan_data/rl_ready/output"
+    cache_dir: "models/uf_rl/datasets/UF_xishan_data/rl_ready/cache"
+
+    __comment__: >
+      data_to_rl 模块输出内容:
+        - 全机组合并后的状态空间范围
+        - reset 初始状态分布
+        - reward 统计量
+
+
+# ============================================================
+# 数据层级定义(非常重要)
+# ============================================================
+
+DataHierarchy:
+
+  physical_cycle:
+    id_column: "seg_id"
+    time_columns:
+      start: "start_time"
+      end: "end_time"
+    __comment__: >
+      物理周期是最细粒度的数据层级:
+      - 用于物理模型验证
+      - 用于环境内部子步模拟
+      - 不直接暴露给强化学习算法
+
+  chemical_cycle:
+    id_column: "chem_cycle_id"
+    validity_column: "chem_cycle_valid"
+    step_definition: "one_rl_step_per_chemical_cycle"
+    __comment__: >
+      化学周期是强化学习的【唯一 step 单位】。
+      强化学习中:
+        - 一个 step = 一个化学周期
+        - 一个状态 = 一个化学周期的整体状态
+
+# ============================================================
+# 强化学习状态定义(工程师重点关注)
+# ============================================================
+
+RLState:
+  level: chemical_cycle
+  dimension: 8
+  __comment__: >
+    强化学习 observation 的定义。
+    所有变量在一个 step(化学周期)内保持不变。
+
+  variables:
+
+    q_UF:
+      source: "flow_mean"
+      aggregation: "mean_within_cycle"
+      unit: "m3_per_h"
+      description: "化学周期代表性进水流量"
+      __comment__: >
+        原始数据在物理周期层级变化,
+        这里使用化学周期内的平均值作为状态。
+
+    temp:
+      source: "temp_mean"
+      aggregation: "mean_within_cycle"
+      unit: "celsius"
+      description: "化学周期代表性水温"
+
+    TMP:
+      source: "tmp_start"
+      selection: "first_physical_cycle"
+      unit: "MPa"
+      description: "化学周期起始跨膜压差"
+      __comment__: >
+        虽然 TMP 在物理周期内会变化,
+        但强化学习只关心化学周期开始时的状态。
+
+    R:
+      source: "R_scaled_start"
+      selection: "first_physical_cycle"
+      unit: "scaled_resistance"
+      description: "化学周期起始膜阻力"
+
+    nuK:
+      source: "cycle_nuK"
+      unit: "scaled"
+      description: "短期污染增长系数"
+      __comment__: >
+        在同一化学周期内视为常数,
+        后续 step (下一化学周期)中根据动作进行经验性更新。
+
+    slope:
+      source: "cycle_long_a"
+      unit: "scaled"
+      description: "长期不可逆污染幂律系数 a"
+
+    power:
+      source: "cycle_long_b"
+      unit: "dimensionless"
+      description: "长期不可逆污染幂律指数 b"
+
+    ceb_removal:
+      source: "cycle_R_removed"
+      unit: "scaled"
+      description: "CEB 可去除的膜阻力"
+
+# ============================================================
+# 状态空间范围提取(reset 使用)
+# ============================================================
+
+StateSpaceExtraction:
+  enabled: true
+  level: chemical_cycle
+  include_only_valid_cycles: true
+
+  __comment__: >
+    用真实工厂数据统计【合理状态空间范围】,
+    用于:
+      - reset 初始状态采样
+      - 状态边界 sanity check
+
+  statistics:
+    method: "empirical"
+    percentiles: [1, 5, 50, 95, 99]
+    __comment__: >
+      使用经验分布统计,避免假设正态分布。
+
+  output:
+    save_csv: true
+    filename: "state_space_bounds.csv"
+
+# ============================================================
+# 物理周期层级的用途声明
+# ============================================================
+
+PhysicalCycleUsage:
+  enabled: true
+
+  purposes:
+    - internal_simulation
+    - parameter_sanity_check
+    - post_training_validation
+
+  variables_available:
+    - R_scaled_start
+    - R_scaled_end
+    - tmp_start
+    - tmp_end
+    - flow_mean
+    - flow_std
+    - temp_mean
+    - temp_std
+
+  __comment__: >
+    物理周期数据:
+      ✔ 可用于环境内部计算
+      ✔ 可用于训练后验证
+      ✘ 不可作为 RL observation
+
+# ============================================================
+# 真实数据在强化学习中的角色(工程边界声明)
+# ============================================================
+
+RealDataUsagePolicy:
+
+  reset_initialization:
+    enabled: true
+    source: "chemical_cycle_state_distribution"
+    __comment__: >
+      reset 时从真实化学周期状态分布中采样,
+      确保训练从“现实可达状态”开始。
+
+  training:
+    use_real_transitions: false
+    __comment__: >
+      真实数据不用于:
+        - 动力学拟合
+        - 监督策略
+      强化学习仍在模拟环境中进行。
+
+  validation:
+    enabled: true
+    compare_with_real_cycles: true
+    __comment__: >
+      训练完成后:
+        - 将策略运行结果
+        - 与真实化学周期统计特性进行对比
+      用于评估策略的现实可行性与安全性。
+
+# ============================================================
+# 方法论与工程风险声明
+# ============================================================
+
+MethodologyNotes:
+  - "强化学习的 step 定义严格等同于一个化学周期"
+  - "所有 RL 状态变量在一个 step 内视为常数"
+  - "物理周期层级仅用于环境内部模拟"
+  - "真实数据用于分布约束与验证,而非动力学建模"

+ 26 - 0
algorithm/uf_rl/config_and_model/xishan/dqn_config.yaml

@@ -0,0 +1,26 @@
+# ============================================================
+# DQN 超参数配置
+# ============================================================
+# 神经网络参数
+learning_rate: 0.0001           # 学习率 1e-4
+
+# 经验回放参数
+buffer_size: 100000              # 经验回放缓冲区大小
+learning_starts: 10000           # 开始训练前收集的步数
+batch_size: 32                   # 训练批次大小
+
+# 强化学习参数
+gamma: 0.95                      # 折扣因子
+train_freq: 4                    # 训练频率(步数)
+
+# 目标网络参数
+target_update_interval: 1        # 目标网络更新间隔
+tau: 0.005                       # 软更新系数
+
+# 探索策略参数
+exploration_initial_eps: 1.0     # 初始探索率
+exploration_fraction: 0.3        # 探索率衰减比例
+exploration_final_eps: 0.02      # 最终探索率
+
+# 日志参数
+remark: "uf_dqn_real_reset"      # 实验备注

+ 134 - 0
algorithm/uf_rl/config_and_model/xishan/env_config.yaml

@@ -0,0 +1,134 @@
+UFState:
+  # ===== 膜动态运行参数 =====
+  q_UF: 360.0
+  TMP: 0.03
+  temp: 25.0
+  R: 200.0
+
+  # ===== 膜阻力模型参数 =====
+  nuK: 170.0
+  slope: 2.0
+  power: 1.032
+  ceb_removal: 100.0
+
+
+UFPhysicsParams:
+  # ===== TMP 全局约束 =====
+  global_TMP_hard_limit: 0.08
+  global_TMP_soft_limit: 0.06
+
+  # ===== 物理反洗参数 =====
+  tau_bw_s: 20.0
+  gamma_t: 1.0
+  q_bw_m3ph: 1000.0
+
+  # ===== CEB 化学反洗参数 =====
+  T_ceb_interval_h: 48.0
+  T_ceb_interval_times: 48
+  v_ceb_m3: 20.0
+  t_ceb_s: 2400.0
+
+  # ===== 膜组件参数 =====
+  A: 5120.0
+
+  # ===== 吨水电耗查找表 =====
+  energy_lookup:
+    2700: 0.1088
+    2760: 0.1083
+    2820: 0.1078
+    2880: 0.1074
+    2940: 0.1070
+    3000: 0.1066
+    3060: 0.1062
+    3120: 0.1059
+    3180: 0.1055
+    3240: 0.1052
+    3300: 0.1049
+    3360: 0.1045
+    3420: 0.1042
+    3480: 0.1039
+    3540: 0.1036
+    3600: 0.1034
+    3660: 0.1031
+    3720: 0.1029
+    3780: 0.1026
+    3840: 0.1023
+    3900: 0.1021
+    3960: 0.1019
+    4020: 0.1017
+    4080: 0.1015
+    4140: 0.1012
+    4200: 0.1011
+    4260: 0.1008
+    4320: 0.1007
+    4380: 0.1005
+    4440: 0.1003
+    4500: 0.1001
+    4560: 0.0999
+    4620: 0.0998
+    4680: 0.0996
+    4740: 0.0995
+    4800: 0.0993
+
+  p_feed_kw_min: 15.0
+  p_feed_kw_max: 20.0
+  p_bw_kw: 15.0
+
+  dose_min: 0.10
+  dose_max: 0.30
+  dose_area: 0.56
+
+
+UFActionSpec:
+  L_min_s: 3800.0
+  L_max_s: 4800.0
+  t_bw_min_s: 40.0
+  t_bw_max_s: 60.0
+  L_step_s: 60.0
+  t_bw_step_s: 5.0
+
+
+UFRewardParams:
+  # ===== TMP 安全与惩罚 =====
+  global_TMP_hard_limit: 0.08
+  global_TMP_soft_limit: 0.06
+  w_tmp_hard: 5.0
+  w_tmp: 1.5
+  p: 3.0
+  w_trend: 1.0
+
+  # ===== 经济成本 =====
+  k_cost: 3.0
+  chemical_price: 13.0
+  energy_price: 0.667
+  cost_low: 0.08
+  cost_high:  0.12
+  w_cost: 1.0
+
+  # ===== 残余污染 =====
+  k_res: 3.0
+  residual_ref_ratio: null
+  w_res: 1.0
+
+
+UFStateBounds:
+  q_UF_min: 210.0
+  q_UF_max: 380.0
+
+  temp_min: 16.0
+  temp_max: 32.0
+
+  TMP0_min: 0.01
+  TMP0_max: 0.045
+  global_TMP_hard_limit: 0.08
+
+  nuK_min: 40.0
+  nuK_max: 260.0
+
+  slope_min: 0.03
+  slope_max: 27.0
+  power_min: 0.4
+  power_max: 2.2
+
+  ceb_removal_min: 60.0
+  ceb_removal_max: 250.0

+ 48 - 0
algorithm/uf_rl/config_and_model/xishan/uf_analyze_config.yaml

@@ -0,0 +1,48 @@
+UF:
+  units: ["UF1", "UF2", "UF3", "UF4"]
+  area_m2: 128 * 40
+
+  inlet_codes: [21.0, 26.0]
+  stable_inlet_code: [24.0, 26.0]
+
+  physical_bw_code: [40.0, 50.0]
+  chemical_bw_code: [90.0, 100.0]
+
+  # 列名
+  flow_col_template: "C.M.{unit}_FT_JS@out"
+  tmp_col: "C.M.{unit}_DB@press_PV"
+  press_col: "C.M.{unit}_PT_JS@out"
+  ctrl_col: "C.M.{unit}_DB@word_control"
+  temp_col: "C.M.RO_TT_ZJS@out"
+  orp_col: "C.M.UF_ORP_ZCS@out"
+  cond_col: "C.M.RO_Cond_ZJS@out"
+
+
+Params:
+  # 稳定段提取
+  min_stable_points: 30
+  initial_points: 10
+
+  # 阻力趋势计算
+  segment_head_n: 5
+  segment_tail_n: 5
+
+  scale_factor: 1e10
+
+
+Plot:
+  figsize: [12, 6]
+  dpi: 120
+  color_inlet: "#1f77b4"
+  color_bw_phys: "#ff7f0e"
+  color_bw_chem: "#d62728"
+
+Paths:
+  project_root: "E:/Greentech" # 请根据项目根目录修改相应路径
+
+  raw_data_path: "models/uf_rl/datasets/UF_xishan_data/raw"
+  output_path: "models/uf_rl/datasets/UF_xishan_data/processed/segments"
+  filter_output_path: "models/uf_rl/datasets/UF_xishan_data/processed/filter_segments"
+
+  output_format: "csv"
+

+ 55 - 0
algorithm/uf_rl/config_and_model/yancheng/uf_analyze_config.yaml

@@ -0,0 +1,55 @@
+UF:
+  units: [ "1", "2" ]
+  area_m2: 128 * 40
+
+  inlet_codes: [221.0, 300.0]
+  stable_codes: [221.0, 260.0]
+
+  physical_bw_code: [301.0, 340.0]
+  chemical_bw_code: [400.0, 660.0]
+
+  # 列名
+  column_formats:
+    flow_col: "ns=3;s={unit}#UF_JSFLOW_O"
+    tmp_col: "ns=3;s=UF{unit}_SSD_KMYC"
+    press_col: "ns=3;s={unit}#UF_JSPRESS_O"
+    ctrl_col: "ns=3;s=UF{unit}_STEP"
+    temp_col: "ns=3;s=ZJS_TEMP_O"
+    orp_col: "ns=3;s=RO_JSORP_O"
+    ph_col: "ns=3;s=RO_JSPH_O"
+    event_col: "event_type"
+    BWB_POWER_col: "ns=3;s=ZZ_{unit}#UFBWB_POWER"
+    GSB_POWER_col: "ns=3;s=ZZ_UFGSB_POWER"
+    NaClO_col: "ns=3;s=CN_LEVEL_O"
+    HCL_col: "ns=3;s=S_LEVEL_O"
+    NaOH_col: "ns=3;s=J_LEVEL_O"
+
+
+Params:
+  # 稳定段提取
+  min_stable_points: 30
+  initial_points: 10
+
+  # 阻力趋势计算
+  segment_head_n: 5
+  segment_tail_n: 5
+
+  scale_factor: 1e10
+
+
+Plot:
+  figsize: [12, 6]
+  dpi: 120
+  color_inlet: "#1f77b4"
+  color_bw_phys: "#ff7f0e"
+  color_bw_chem: "#d62728"
+
+Paths:
+  project_root: "E:/Greentech" # 请根据项目根目录修改相应路径
+
+  raw_data_path: "models/uf_rl/datasets/UF_yancheng_data/raw"
+  output_path: "models/uf_rl/datasets/UF_yancheng_data/processed/segments"
+  filter_output_path: "models/uf_rl/datasets/UF_yancheng_data/processed/filter_segments"
+
+  output_format: "csv"
+

+ 0 - 0
algorithm/uf_rl/data_to_rl/__init__.py


+ 83 - 0
algorithm/uf_rl/data_to_rl/data_splitter.py

@@ -0,0 +1,83 @@
+import pandas as pd
+from pathlib import Path
+from typing import Tuple
+
+
+class ResetStatePoolLoader:
+    """
+    Reset 初始状态池加载与划分类
+
+    功能:
+    - 从 CSV 文件读取 RL-ready 的 reset 状态池
+    - 做基础数据合法性检查
+    - 按比例划分为 train / val 两个状态池
+
+    设计原则:
+    - 不涉及 RL 算法
+    - 不涉及环境逻辑
+    - 仅负责「数据 → reset 可用状态池」
+    """
+
+    def __init__(
+        self,
+        csv_path: str | Path,
+        train_ratio: float = 0.8,
+        shuffle: bool = True,
+        random_state: int = 42,
+    ):
+        self.csv_path = Path(csv_path)
+        self.train_ratio = train_ratio
+        self.shuffle = shuffle
+        self.random_state = random_state
+
+        self._validate_path()
+
+    def _validate_path(self):
+        if not self.csv_path.exists():
+            raise FileNotFoundError(
+                f"Reset state pool 文件不存在: {self.csv_path}"
+            )
+
+    def load(self) -> pd.DataFrame:
+        """
+        读取 reset 状态池 CSV
+        """
+        df = pd.read_csv(self.csv_path)
+
+        if df.empty:
+            raise ValueError("reset_state_pool.csv 为空")
+
+        # 基础合法性检查
+        if df.isnull().any().any():
+            raise ValueError(
+                "reset_state_pool.csv 中存在 NaN,请在 data_to_rl 阶段处理"
+            )
+
+        return df
+
+    def split(self) -> Tuple[pd.DataFrame, pd.DataFrame]:
+        """
+        按比例划分 train / val reset 状态池
+
+        返回:
+            train_pool, val_pool
+        """
+        df = self.load()
+
+        if self.shuffle:
+            df = df.sample(
+                frac=1.0,
+                random_state=self.random_state
+            ).reset_index(drop=True)
+
+        split_idx = int(len(df) * self.train_ratio)
+
+        train_pool = df.iloc[:split_idx].reset_index(drop=True)
+        val_pool = df.iloc[split_idx:].reset_index(drop=True)
+
+        if len(train_pool) == 0 or len(val_pool) == 0:
+            raise ValueError(
+                f"数据量不足以划分 train/val,样本数={len(df)}"
+            )
+
+        return train_pool, val_pool

+ 34 - 0
algorithm/uf_rl/data_to_rl/get_reset_pool.py

@@ -0,0 +1,34 @@
+import yaml
+from pathlib import Path
+
+from loader import load_all_units_cycles, resolve_path
+from state_construction import build_chem_cycle_state, STATE_COLUMNS
+
+
+def load_config(path: Path) -> dict:
+    with open(path, "r", encoding="utf-8") as f:
+        return yaml.safe_load(f)
+
+
+def generate_reset_state_pool(config_path: Path):
+    cfg = load_config(config_path)
+    project_root = Path(cfg["Paths"]["project_root"])
+
+    output_dir = resolve_path(
+        project_root,
+        cfg["Paths"]["data_to_rl"]["output_dir"]
+    )
+    output_dir.mkdir(parents=True, exist_ok=True)
+
+    df = load_all_units_cycles(cfg)
+    df = df[df["chem_cycle_valid"] == True]
+
+    state_df = build_chem_cycle_state(df)
+
+    # 严格完整性过滤
+    state_df = state_df.dropna(subset=STATE_COLUMNS)
+
+    state_df.to_csv(
+        output_dir / "reset_state_pool.csv",
+        index=False
+    )

+ 33 - 0
algorithm/uf_rl/data_to_rl/loader.py

@@ -0,0 +1,33 @@
+import pandas as pd
+from pathlib import Path
+
+
+def resolve_path(project_root: Path, relative_path: str) -> Path:
+    return project_root / relative_path
+
+
+def load_all_units_cycles(cfg: dict) -> pd.DataFrame:
+    """
+    读取所有启用 UF 机组的 filtered_cycles 数据,
+    返回【物理周期层级】的合并 DataFrame。
+    """
+    project_root = Path(cfg["Paths"]["project_root"])
+    raw_cfg = cfg["Paths"]["raw_data"]
+
+    all_dfs = []
+
+    for unit in raw_cfg["enabled_units"]:
+        file_name = raw_cfg["cycle_file_pattern"].format(unit=unit)
+        file_path = resolve_path(
+            project_root,
+            raw_cfg["filtered_cycles_dir"]
+        ) / file_name
+
+        if not file_path.exists():
+            raise FileNotFoundError(f"未找到 UF{unit} 数据文件: {file_path}")
+
+        df_unit = pd.read_csv(file_path)
+        df_unit["unit_id"] = unit  # 仅用于分组,不进入状态
+        all_dfs.append(df_unit)
+
+    return pd.concat(all_dfs, ignore_index=True)

+ 78 - 0
algorithm/uf_rl/data_to_rl/run_data_to_rl_pipeline.py

@@ -0,0 +1,78 @@
+"""
+============================================================
+ data_to_rl | 全流程入口脚本
+------------------------------------------------------------
+ 功能:
+     串行执行 data_to_rl 模块的全部数据准备流程,包括:
+       1. 强化学习状态空间范围提取
+       2. 强化学习 reset 状态先验池生成
+
+ 设计原则:
+     - 只负责“调度”,不包含任何业务逻辑
+     - 所有参数均来自 data_to_rl_config.yaml
+     - 工程师只需运行本脚本即可完成全部准备工作
+
+ 使用方式:
+     python run_data_to_rl_pipeline.py
+============================================================
+"""
+
+from pathlib import Path
+import time
+
+from state_space_bounds import extract_state_space_bounds
+from get_reset_pool import generate_reset_state_pool
+
+THIS_FILE = Path(__file__).resolve()
+UF_RL_ROOT = THIS_FILE.parents[1]
+
+def main():
+    # --------------------------------------------------------
+    # 配置文件路径(统一入口)
+    # --------------------------------------------------------
+    config_path = UF_RL_ROOT / "lankao" / "data_to_rl_config.yaml"
+
+    print(f" 配置文件: {config_path}")
+    print("======================================================\n")
+
+    start_time = time.time()
+
+    # --------------------------------------------------------
+    # Step 1: 提取状态空间经验范围
+    # --------------------------------------------------------
+    print(">>> Step 1 / 2:提取强化学习状态空间范围")
+    step_start = time.time()
+
+    extract_state_space_bounds(config_path)
+
+    print(
+        f">>> Step 1 完成,用时 {time.time() - step_start:.2f} 秒\n"
+    )
+
+    # --------------------------------------------------------
+    # Step 2: 生成 reset 状态先验池
+    # --------------------------------------------------------
+    print(">>> Step 2 / 2:生成 RL reset 状态先验池")
+    step_start = time.time()
+
+    generate_reset_state_pool(config_path)
+
+    print(
+        f">>> Step 2 完成,用时 {time.time() - step_start:.2f} 秒\n"
+    )
+
+    # --------------------------------------------------------
+    # 总结
+    # --------------------------------------------------------
+    print("======================================================")
+    print(" data_to_rl 数据准备流水线执行完成 ")
+    print("------------------------------------------------------")
+    print(f" 总耗时: {time.time() - start_time:.2f} 秒")
+    print(" 输出内容包括:")
+    print("   - state_space_bounds.csv   (状态空间统计)")
+    print("   - reset_state_pool.csv     (reset 初始状态池)")
+    print("======================================================")
+
+
+if __name__ == "__main__":
+    main()

+ 28 - 0
algorithm/uf_rl/data_to_rl/state_construction.py

@@ -0,0 +1,28 @@
+import pandas as pd
+
+
+STATE_COLUMNS = [
+    "q_UF", "temp", "TMP", "R",
+    "nuK", "slope", "power", "ceb_removal"
+]
+
+
+def build_chem_cycle_state(df: pd.DataFrame) -> pd.DataFrame:
+    """
+    从【物理周期层级】数据中构造
+    【化学周期层级】的 RL 状态。
+    """
+    grouped = df.groupby(["unit_id", "chem_cycle_id"])
+
+    state_df = pd.DataFrame({
+        "q_UF": grouped["flow_mean"].mean(),
+        "temp": grouped["temp_mean"].mean(),
+        "TMP": grouped["tmp_start"].first(),
+        "R": grouped["R_scaled_start"].first(),
+        "nuK": grouped["cycle_nuK"].first(),
+        "slope": grouped["cycle_long_a"].first(),
+        "power": grouped["cycle_long_b"].first(),
+        "ceb_removal": grouped["cycle_R_removed"].first(),
+    })
+
+    return state_df.reset_index(drop=True)

+ 47 - 0
algorithm/uf_rl/data_to_rl/state_space_bounds.py

@@ -0,0 +1,47 @@
+import yaml
+import pandas as pd
+from pathlib import Path
+
+from loader import load_all_units_cycles, resolve_path
+from state_construction import build_chem_cycle_state, STATE_COLUMNS
+
+
+def load_config(path: Path) -> dict:
+    with open(path, "r", encoding="utf-8") as f:
+        return yaml.safe_load(f)
+
+
+def extract_state_space_bounds(config_path: Path):
+    cfg = load_config(config_path)
+    project_root = Path(cfg["Paths"]["project_root"])
+
+    output_dir = resolve_path(
+        project_root,
+        cfg["Paths"]["data_to_rl"]["output_dir"]
+    )
+    output_dir.mkdir(parents=True, exist_ok=True)
+
+    df = load_all_units_cycles(cfg)
+    df = df[df["chem_cycle_valid"] == True]
+
+    state_df = build_chem_cycle_state(df)
+
+    percentiles = cfg["StateSpaceExtraction"]["statistics"]["percentiles"]
+    stats = {}
+
+    for col in STATE_COLUMNS:
+        desc = state_df[col].describe(
+            percentiles=[p / 100 for p in percentiles]
+        )
+
+        stats[col] = {
+            "min": desc["min"],
+            "max": desc["max"],
+            "mean": desc["mean"],
+            "std": desc["std"],
+            **{f"p{p}": desc[f"{p}%"] for p in percentiles}
+        }
+
+    pd.DataFrame(stats).T.to_csv(
+        output_dir / "state_space_bounds.csv"
+    )

+ 113 - 0
algorithm/uf_rl/dqn_decide.py

@@ -0,0 +1,113 @@
+from typing import Dict
+from algorithm.base.algorithm_base import AlgorithmBase
+from pympler import asizeof
+import os
+import dotenv
+SCRIPT_DIR = os.path.dirname(__file__)  # 脚本所在路径
+dotenv.load_dotenv()  # 加载脚本所在目录的.env环境变量
+
+from algorithm.uf_rl.env.env_config_loader import EnvConfigLoader, create_env_params_from_yaml
+from algorithm.uf_rl.rl_model.DQN.uf_decide.run_dqn_decide import build_physics, replace, check_state_bounds
+
+# ========== 决策器 ==========
+from algorithm.uf_rl.rl_model.DQN.uf_decide.dqn_decider import UFDQNDecider
+
+import logging
+logger = logging.getLogger(__name__)
+
+class DQNDecide(AlgorithmBase):
+
+    def __init__(self, params: Dict = None):
+        super().__init__(params=None)
+
+        # 静态配置参数
+        # ========== 模型及配置路径指定 ==========
+        self.IS_TIMES = int(os.getenv("IS_TIMES", '0'))  # 外部指定变量,表示CEB间隔为时间控制/次数控制,T表示48次bw一次CEB,F表示48h一次CEB
+        self.PLANT = os.getenv("PLANT", None)
+        self.MODEL_PATH = os.path.join(SCRIPT_DIR, "config_and_model", self.PLANT, "48times_dqn_model.zip" if self.IS_TIMES else "48h_dqn_model.zip")  # 需根据IS_TIMES变量值指定模型为48h_dqn_model.zip/48times_dqn_model.zip
+        self.ENV_CONFIG_PATH = os.path.join(SCRIPT_DIR, "config_and_model", self.PLANT, "env_config.yaml")  # 环境配置路径
+
+        # ========== 模型及配置加载 ==========
+        config_loader = EnvConfigLoader(self.ENV_CONFIG_PATH)
+        config_loader.validate_config()
+        config_loader.print_config_summary()
+        (
+            self.uf_state_default,  # UFState默认值
+            phys_params,  # UFPhysicsParams
+            action_spec,  # UFActionSpec
+            reward_params,  # UFRewardParams
+            self.state_bounds  # UFStateBounds
+        ) = create_env_params_from_yaml(self.ENV_CONFIG_PATH)
+
+        # 构造决策器, 环境实例化,模型加载等功能放在UFDQNDecider类中
+        self.decider = UFDQNDecider(
+            physics=build_physics(self.IS_TIMES, phys_params, self.state_bounds),
+            action_spec=action_spec,
+            reward_params=reward_params,
+            state_bounds=self.state_bounds,
+            model_path=self.MODEL_PATH,
+            seed=0,
+        )
+
+
+    def __call__(self, x: Dict, *args, **kwargs)->Dict:
+
+        if not isinstance(x, dict):
+            logger.warning("输入不为字典", x)
+            return {}
+        # ========== 外部调用输入 ==========
+        # 轻量版,仅输入当前周期起始状态变量
+        units_to_run = x.get('units_to_run', None)  # 新增输入:本次调用的机组对象名
+        TMP0 = x.get('TMP0', None)  # 原始 TMP0
+        q_UF = x.get('q_UF', None)  # 进水流量
+        temp = x.get('temp', None)  # 进水温度
+
+        if None in [TMP0, q_UF, temp, units_to_run]:
+            logger.warning("输入不合法", x)
+            return {}
+
+        # ========== 调用模型生成模型指令 ==========
+        # 基于外部输入构建当前状态
+        current_state = replace(
+            self.uf_state_default,
+            TMP=TMP0,
+            q_UF=q_UF,
+            temp=temp
+        )
+
+        # 状态异常检查(仅检查,不中断,出现异常时后续归一化中将异常状态强制归一化至上下限)
+        for unit_name in units_to_run:
+            error_result = check_state_bounds(current_state, self.state_bounds, unit_name)
+            if error_result:
+                print(f"错误发生时间: {error_result['error_time']};错误特征量:{error_result['error_feature']}")
+
+        # 模型输出指令
+        decision = self.decider.decide(current_state)
+        # 生成plc指令放到业务层
+
+        return {
+            'action_id': decision["action_id"],
+            'model_L_s': decision["action_id"],
+            'model_t_bw_s': decision["t_bw_s"],
+        }
+
+    def set(self):
+        pass
+
+    def get(self):
+        pass
+
+    def __del__(self):
+        """显式执行清理"""
+        pass
+
+if __name__ == "__main__":
+    decider = DQNDecide()
+    res = decider({
+        'units_to_run': ["UF1"], # 新增输入:本次调用的机组对象名
+        'TMP0': 0.07,            # 原始 TMP0
+        'q_UF': 300,             # 进水流量
+        'temp': 20.0             # 进水温度
+    })
+
+    print(res)

+ 0 - 0
algorithm/uf_rl/dqn_decide_totalstate.py


+ 0 - 0
algorithm/uf_rl/env/__init__.py


BIN
algorithm/uf_rl/env/__pycache__/__init__.cpython-314.pyc


BIN
algorithm/uf_rl/env/__pycache__/env_config_loader.cpython-314.pyc


BIN
algorithm/uf_rl/env/__pycache__/env_params.cpython-314.pyc


BIN
algorithm/uf_rl/env/__pycache__/env_reset.cpython-314.pyc


BIN
algorithm/uf_rl/env/__pycache__/uf_env.cpython-314.pyc


BIN
algorithm/uf_rl/env/__pycache__/uf_physics.cpython-314.pyc


BIN
algorithm/uf_rl/env/__pycache__/uf_resistance_models_define.cpython-314.pyc


BIN
algorithm/uf_rl/env/__pycache__/uf_resistance_models_load.cpython-314.pyc


+ 256 - 0
algorithm/uf_rl/env/env_config_loader.py

@@ -0,0 +1,256 @@
+"""
+env_config_loader.py
+
+配置加载器,负责从YAML文件加载配置并实例化env_params中的参数类。
+"""
+
+import yaml
+from pathlib import Path
+from typing import Dict, Any, Union, Type, TypeVar
+
+# 改为绝对导入
+from algorithm.uf_rl.env.env_params import (
+    UFState,
+    UFPhysicsParams,
+    UFActionSpec,
+    UFRewardParams,
+    UFStateBounds
+)
+
+T = TypeVar('T')
+
+
+class EnvConfigLoader:
+    """环境配置加载器,从YAML加载并实例化参数类"""
+
+    # 参数类与配置节名的映射
+    SECTION_TO_CLASS = {
+        'UFState': UFState,
+        'UFPhysicsParams': UFPhysicsParams,
+        'UFActionSpec': UFActionSpec,
+        'UFRewardParams': UFRewardParams,
+        'UFStateBounds': UFStateBounds,
+    }
+
+    def __init__(self, config_path: Union[str, Path]):
+        """
+        初始化配置加载器
+
+        Args:
+            config_path: YAML配置文件路径
+        """
+        self.config_path = Path(config_path)
+        self._config = None
+
+    @property
+    def config(self) -> Dict[str, Any]:
+        """懒加载配置"""
+        if self._config is None:
+            self._config = self._load_yaml()
+        return self._config
+
+    def _load_yaml(self) -> Dict[str, Any]:
+        """加载YAML文件"""
+        if not self.config_path.exists():
+            raise FileNotFoundError(f"Config file not found: {self.config_path}")
+
+        with open(self.config_path, 'r', encoding='utf-8') as f:
+            return yaml.safe_load(f)
+
+    def _create_instance(self, cls: Type[T], section: str) -> T:
+        """
+        从配置的指定部分创建参数类实例
+        """
+        if section not in self.config:
+            raise KeyError(f"Section '{section}' not found in config. "
+                           f"Available sections: {list(self.config.keys())}")
+
+        section_config = self.config[section]
+
+        # 特殊处理:如果配置中有null值,需要转换为None
+        # YAML中的null会自动转换为Python的None,所以不需要额外处理
+
+        return cls(**section_config)
+
+    # ========== 加载各个参数类 ==========
+
+    def load_uf_state(self) -> UFState:
+        """加载UFState(环境动态状态)"""
+        return self._create_instance(UFState, 'UFState')
+
+    def load_physics_params(self) -> UFPhysicsParams:
+        """加载物理参数 UFPhysicsParams"""
+        return self._create_instance(UFPhysicsParams, 'UFPhysicsParams')
+
+    def load_action_spec(self) -> UFActionSpec:
+        """加载动作规范 UFActionSpec"""
+        return self._create_instance(UFActionSpec, 'UFActionSpec')
+
+    def load_reward_params(self) -> UFRewardParams:
+        """加载奖励参数 UFRewardParams"""
+        return self._create_instance(UFRewardParams, 'UFRewardParams')
+
+    def load_state_bounds(self) -> UFStateBounds:
+        """加载状态边界 UFStateBounds"""
+        return self._create_instance(UFStateBounds, 'UFStateBounds')
+
+    def load_all(self) -> Dict[str, Any]:
+        """加载所有参数类"""
+        return {
+            'uf_state': self.load_uf_state(),
+            'physics': self.load_physics_params(),
+            'action': self.load_action_spec(),
+            'reward': self.load_reward_params(),
+            'state_bounds': self.load_state_bounds(),
+        }
+
+    # ========== 工具方法 ==========
+
+    def get_raw_section(self, section: str) -> Dict[str, Any]:
+        """获取配置的原始字典部分"""
+        return self.config.get(section, {}).copy()
+
+    def validate_config(self) -> bool:
+        """
+        验证配置文件是否包含所有必需的配置节
+
+        Returns:
+            bool: 验证通过返回True,否则抛出异常
+        """
+        missing_sections = []
+        for section in self.SECTION_TO_CLASS.keys():
+            if section not in self.config:
+                missing_sections.append(section)
+
+        if missing_sections:
+            raise ValueError(f"Missing required sections in config: {missing_sections}")
+
+        print("✅ Config validation passed. All required sections present.")
+        return True
+
+    def print_config_summary(self):
+        """打印配置摘要信息"""
+        print("\n" + "=" * 50)
+        print("📋 配置加载摘要")
+        print("=" * 50)
+
+        for section in self.SECTION_TO_CLASS.keys():
+            if section in self.config:
+                print(f"✅ {section}: {len(self.config[section])} parameters")
+            else:
+                print(f"❌ {section}: MISSING")
+
+        print("=" * 50 + "\n")
+
+
+# ========== 便捷函数 ==========
+
+def load_env_config(config_path: Union[str, Path]) -> Dict[str, Any]:
+    """
+    便捷函数:一次性加载所有环境配置
+
+    Args:
+        config_path: 配置文件路径
+
+    Returns:
+        包含所有参数类实例的字典
+    """
+    loader = EnvConfigLoader(config_path)
+    return loader.load_all()
+
+
+def load_single_config(config_path: Union[str, Path], section: str):
+    """
+    便捷函数:加载单个配置节
+
+    Args:
+        config_path: 配置文件路径
+        section: 配置部分名称 (UFState, UFPhysicsParams, UFActionSpec, UFRewardParams, UFStateBounds)
+
+    Returns:
+        对应的参数类实例
+    """
+    loader = EnvConfigLoader(config_path)
+
+    mapping = {
+        'UFState': loader.load_uf_state,
+        'UFPhysicsParams': loader.load_physics_params,
+        'UFActionSpec': loader.load_action_spec,
+        'UFRewardParams': loader.load_reward_params,
+        'UFStateBounds': loader.load_state_bounds,
+    }
+
+    if section not in mapping:
+        raise ValueError(f"Unknown section: {section}. "
+                         f"Available: {list(mapping.keys())}")
+
+    return mapping[section]()
+
+
+def create_env_params_from_yaml(config_path: Union[str, Path]) -> tuple:
+    """
+    从YAML文件创建所有环境参数,返回元组方便解包
+
+    Args:
+        config_path: 配置文件路径
+
+    Returns:
+        (uf_state, physics_params, action_spec, reward_params, state_bounds)
+    """
+    loader = EnvConfigLoader(config_path)
+    return (
+        loader.load_uf_state(),
+        loader.load_physics_params(),
+        loader.load_action_spec(),
+        loader.load_reward_params(),
+        loader.load_state_bounds(),
+    )
+
+
+# ========== 测试 ==========
+if __name__ == "__main__":
+    # 测试配置加载
+    from pathlib import Path
+
+    # 假设配置文件在当前目录的上一级config文件夹中
+    default_config = Path(__file__).parent.parent.parent / "config" / "xishan_env_config.yaml"
+
+    if default_config.exists():
+        print(f"Testing config loading from: {default_config}")
+
+        # 测试加载器
+        loader = EnvConfigLoader(default_config)
+
+        # 验证配置
+        loader.validate_config()
+        loader.print_config_summary()
+
+        # 测试加载各个参数
+        try:
+            uf_state = loader.load_uf_state()
+            print(f"UFState: q_UF={uf_state.q_UF}, TMP={uf_state.TMP}")
+
+            physics = loader.load_physics_params()
+            print(f"UFPhysicsParams: A={physics.A}, T_hard_limit={physics.global_TMP_hard_limit}")
+
+            action = loader.load_action_spec()
+            print(f"UFActionSpec: L_range=[{action.L_min_s}, {action.L_max_s}]")
+
+            reward = loader.load_reward_params()
+            print(f"UFRewardParams: w_tmp={reward.w_tmp}, residual_ref_ratio={reward.residual_ref_ratio}")
+
+            bounds = loader.load_state_bounds()
+            print(f"UFStateBounds: TMP0_range=[{bounds.TMP0_min}, {bounds.TMP0_max}]")
+
+            # 测试一次性加载所有
+            all_params = loader.load_all()
+            print(f"\nAll {len(all_params)} parameter classes loaded successfully")
+
+        except Exception as e:
+            print(f"Error loading config: {e}")
+            import traceback
+
+            traceback.print_exc()
+    else:
+        print(f"Config file not found: {default_config}")
+        print("Please specify config path manually for testing.")

+ 367 - 0
algorithm/uf_rl/env/env_params.py

@@ -0,0 +1,367 @@
+"""
+env_params.py
+
+超滤(UF)强化学习环境的参数定义文件。
+
+本文件集中定义 UF 强化学习环境中使用的所有【参数类(Parameter Schemas)】。
+这些类仅用于描述系统配置、状态语义和约束边界,不包含任何数值计算、
+物理模型、奖励逻辑或 Gym 接口实现。
+
+本文件中的类只定义参数结构,不负责加载配置。具体数值应从YAML文件加载后在主程序中实例化。
+
+========================
+参数类总览
+========================
+
+1. UFState
+------------------------
+【环境动态状态】
+
+- 描述超滤系统在某一时刻的运行状态
+- 这些变量会在 env.step() 中随动作不断演化
+- 构成强化学习算法可观测的 observation
+- 生命周期:每个 step 更新一次
+
+包含内容:
+- 实时运行工况(流量、温度、TMP)
+- 膜污染状态参数(短期污染、长期污染)
+- CEB 去除能力等可随 episode 演化的量
+
+使用方:
+- 强化学习环境(step / reset)
+- 膜阻力物理模型(作为 forward 的 state 输入)
+- reward 计算逻辑
+
+
+2. UFPhysicsParams
+------------------------
+【物理与工艺固定参数】
+
+- 描述超滤系统的客观工艺与设备配置
+- 在整个 episode 甚至整个训练生命周期中保持不变
+- 典型为“物理常数 / 工艺设定值”
+
+设计约束:
+- 使用 frozen dataclass,防止运行时被意外修改
+
+包含内容:
+- 物理反洗效率时间尺度
+- 反洗流量
+- CEB 间隔与用水量
+- 膜组件面积等硬件参数
+
+使用方:
+- 膜阻力模型(上升 / 下降)
+- 回收率、能耗等物理量计算
+- 环境内部的物理约束判断
+
+
+3. UFActionSpec
+------------------------
+【动作空间定义】
+
+- 描述智能体“可以选什么动作”,而不是“如何执行动作”
+- 仅定义动作的物理含义、取值范围和离散粒度
+- 不负责构造 Gym action_space
+
+包含内容:
+- 过滤时长(L)范围与步长
+- 物理反洗时长(t_bw)范围与步长
+
+使用方:
+- 环境初始化时构建离散动作空间
+- 不同算法(DQN / Dueling / PPO)共用同一动作语义
+
+
+4. UFRewardSpec
+------------------------
+【奖励函数与安全约束参数】
+
+- 集中管理 reward 计算所需的权重、阈值与约束
+- 同时用于 episode 终止(terminated)判断
+- 不包含 reward 的计算公式本身
+
+包含内容:
+- TMP 硬 / 软安全上限
+- 回收率与污染惩罚的敏感度系数
+- 回收率合理区间与参考污染比例
+
+使用方:
+- reward 函数
+- 环境终止条件判断
+
+
+5. UFStateBounds
+------------------------
+【状态初始化约束】
+
+- 仅用于 reset() 阶段的随机初始化
+- 不参与 step() 中的状态演化
+- 用于保证初始状态物理合理、覆盖足够多工况
+
+包含内容:
+- TMP、流量、温度的初始化范围
+- 污染模型参数的合理区间
+- CEB 去除能力的上下限
+
+使用方:
+- env.reset()
+- 用于构造多样化初始工况,增强策略鲁棒性
+
+
+========================
+设计原则(请勿破坏)
+========================
+
+1. 本文件只描述“参数是什么”,不描述“参数如何使用”
+2. 禁止在本文件中出现:
+   - torch / numpy 数值计算
+   - Gym / Gymnasium space 定义
+   - step / forward / reward 等逻辑
+3. 若需要新增参数,请优先判断其生命周期:
+   - 是否随 step 变化 → UFState
+   - 是否全局不变 → UFPhysicsParams / Spec 类
+4. 若发现需要在此文件中写计算代码,说明模块边界已经被破坏,应重构调用方
+
+"""
+
+
+from dataclasses import dataclass, field
+from typing import Dict
+
+
+"""
+【说明:拆分 UFParams】
+
+原 UFParams 类同时承担了多种不同职责,包括:
+1. 环境状态(会在 step 中动态变化)
+2. reset 时的初始状态约束
+3. 超滤系统的物理 / 工艺固定参数
+4. 强化学习动作空间定义
+5. 奖励函数与安全约束参数
+
+在强化学习工程中,这种参数结构会带来以下问题:
+
+- 状态与参数混杂,容易在 step() 中被误修改
+- reset 初始化逻辑与运行逻辑强耦合,难以调试
+- 奖励函数修改会影响环境动力学,增加不可控风险
+- 不利于算法切换(DQN / Dueling Q / PPO)
+
+因此,现将 UFParams 拆分为多个“职责单一”的参数类,
+每个类只负责一类明确的功能,从而实现:
+
+- 语义清晰
+- 边界明确
+- 易于维护
+- 算法无关(一个环境,多种算法共用)
+
+【注意】
+本次拆分不改变任何物理意义、数值含义与默认参数,仅做结构重组。
+"""
+
+# ==================== 超滤系统参数配置类 ====================
+@dataclass
+class UFState:
+    """
+    【环境动态状态】
+    描述超滤系统在当前时刻的运行状态。
+    这些变量会在 step() 中随动作不断演化,并构成强化学习的 observation。
+    """
+
+    # ========== 膜动态运行参数 ==========
+    # 这些参数描述超滤膜的实时运行状态,在环境模拟中会动态变化
+
+    q_UF: float = 360.0
+    # 过滤进水流量(m³/h)
+    # 说明:影响膜通量,进而影响污染速率
+    # 典型范围:250-400 m³/h
+
+    TMP: float = 0.03
+    # 跨膜压差(MPa,兆帕)
+    # 说明:反映膜阻力状态,TMP 越高表示膜污染越严重
+    # 正常范围:0.01-0.035 MPa,超过 0.08 MPa 需停机检修
+
+    temp: float = 25.0
+    # 水温(摄氏度)
+    # 说明:影响水的粘度,进而影响跨膜压差
+    # 典型范围:10-40℃,25℃为标准温度
+
+    R: float = 200.0
+    # 膜阻力(缩放至1e2)
+    # 说明:实际上的膜阻力应该根据q_UF/TMP0/temp动态计算,此处仅为构造状态类占位使用
+
+    # ========== 膜阻力模型参数 ==========
+    # 这些参数描述膜污染的物理化学特性,基于历史数据拟合得到
+
+    nuK: float = 1.7e+02
+    # 过滤阶段膜阻力增长系数(缩放后单位)
+    # 说明:反映水质污染特性,nuK 越大表示水质越差、膜污染越快
+    # 物理意义:单位膜通量、单位时间的阻力增长速率
+
+    slope: float = 2
+    # 全周期不可逆污染增长斜率
+    # 说明:描述长期不可逆污染的累积速率(幂律模型的系数)
+
+    power: float = 1.032
+    # 全周期不可逆污染增长幂次
+    # 说明:描述长期污染的非线性特性(幂律模型的指数)
+    # power > 1 表示污染加速累积,power < 1 表示污染增速放缓
+
+    ceb_removal: float = 100
+    # 化学增强反洗(CEB)可去除的膜阻力(缩放后单位)
+    # 说明:CEB 比物理反洗更彻底,可去除部分不可逆污染
+
+@dataclass(frozen=True)
+class UFPhysicsParams:
+    """
+    【物理与工艺固定参数】
+    描述超滤系统的客观工艺条件,在整个 episode 中保持不变。
+    """
+    global_TMP_hard_limit: float = 0.08
+    # TMP 硬上限(MPa)
+    # 说明:超过此值将导致episode失败,需立即停机
+
+    global_TMP_soft_limit: float = 0.06
+    # TMP 软上限 (MPa)
+    # 说明:此上限用于指导奖励函数中膜阻力允许上升值,越接近该上限,系统对膜阻力上升控制的更严格
+
+    # ========== 反洗参数(固定配置) ==========
+    tau_bw_s: float = 20.0
+    # 物理反洗时长影响的时间尺度(秒)
+    # 说明:反洗效率的特征时间,当反洗时长 = tau 时,达到约 63% 效率
+
+    gamma_t: float = 1.0
+    # 物理反洗时长作用指数(保留参数,当前未使用)
+
+    q_bw_m3ph: float = 1000.0
+    # 物理反洗流量(m³/h)
+    # 说明:反洗流量通常为正常过滤流量的 2-3 倍
+
+    # ========== CEB 化学反洗参数 ==========
+    T_ceb_interval_h: float = 48.0
+    # CEB 间隔时间(小时)
+    # 说明:每运行约 60 小时执行一次化学增强反洗 TODO: PARAM 当前采用保守测试参数48h
+
+    T_ceb_interval_times: int = 48
+    # CEB 间隔次数
+    # 说明: 每执行48次物理反冲洗进行一次CEB,与T_ceb_interval_h为互斥参数
+
+    v_ceb_m3: float = 20.0
+    # CEB 用水体积(m³)
+
+    t_ceb_s: float = 40 * 60.0
+    # CEB 时长(秒,这里为 40 分钟)
+
+    # ========== 新增:膜面积 ==========
+    # 膜有效面积(锡山水厂配置:128组膜,每组40m²)
+    A: float = 5120.0  # [m²]
+
+    # 参考吨水电耗查找表
+    energy_lookup: Dict[int, float] = field(default_factory=lambda: {
+        2700: 0.1088, 2760: 0.1083, 2820: 0.1078, 2880: 0.1074,
+        2940: 0.1070, 3000: 0.1066, 3060: 0.1062, 3120: 0.1059,
+        3180: 0.1055, 3240: 0.1052, 3300: 0.1049, 3360: 0.1045,
+        3420: 0.1042, 3480: 0.1039, 3540: 0.1036, 3600: 0.1034,
+        3660: 0.1031, 3720: 0.1029, 3780: 0.1026, 3840: 0.1023,
+        3900: 0.1021, 3960: 0.1019, 4020: 0.1017, 4080: 0.1015,
+        4140: 0.1012, 4200: 0.1011, 4260: 0.1008, 4320: 0.1007,
+        4380: 0.1005, 4440: 0.1003, 4500: 0.1001, 4560: 0.0999,
+        4620: 0.0998, 4680: 0.0996, 4740: 0.0995, 4800: 0.0993,
+    })
+
+    # 实际吨水电耗计算指标
+    p_feed_kw_min: float = 15.0
+    p_feed_kw_max: float = 20.0
+    p_bw_kw: float = 15.0
+
+    # 实际吨水药耗计算指标
+    dose_min: float = 0.10
+    dose_max: float = 0.30
+    dose_area: float = 0.56
+
+
+@dataclass(frozen=True)
+class UFActionSpec:
+    """
+    【动作空间定义】
+    描述智能体可选的过滤-反洗策略组合。
+    """
+    # ========== 强化学习动作空间搜索范围 ==========
+    # 定义智能体可选择的动作范围(离散化)
+
+    L_min_s: float = 3800.0  # 过滤时长下限(秒,约 63 分钟)
+    L_max_s: float = 4800.0  # 过滤时长上限,当前为 4800s 80分钟
+    t_bw_min_s: float = 40.0  # 物理反洗时长下限(秒)
+    t_bw_max_s: float = 60.0  # 物理反洗时长上限(秒)
+
+    # ========== 动作离散化网格 ==========
+    L_step_s: float = 60.0  # 过滤时长步长(秒)
+    t_bw_step_s: float = 5.0  # 物理反洗时长步长(秒)
+
+@dataclass(frozen=True)
+class UFRewardParams:
+    """
+    【奖励函数与安全约束参数】用于 reward 计算和 episode 终止判断。
+    """
+    # TMP 限值
+    global_TMP_hard_limit: float = 0.08
+    # TMP 硬上限(MPa)
+    # 说明:超过此值将导致 episode 失败,需立即停机
+    global_TMP_soft_limit: float = 0.06     # TMP 软上限 (MPa)
+
+    w_tmp_hard: float = 5.0      # TMP超硬限固定惩罚
+    w_tmp: float = 1.5            # TMP软限惩罚
+    p: float = 3                  # TMP软限非线性指数
+    w_trend: float = 1.0          # TMP趋势惩罚权重
+
+    # 经济成本
+    k_cost: float = 3.0
+    chemical_price: float = 13.0
+    energy_price: float = 0.667
+    cost_low: float = 0.08
+    cost_high: float = 0.20
+    w_cost: float = 1.0
+
+    # 残余污染
+    k_res: float = 3.0
+    residual_ref_ratio: float = None   # 动态=1/max_episode_steps
+    w_res: float = 1.0
+
+
+
+
+@dataclass(frozen=True)
+class UFStateBounds:
+    """
+    【状态初始化约束】
+    仅用于 reset() 时随机初始化环境状态,
+    不参与 step() 中的状态演化。
+    """
+    # --- 流量约束 ---
+    q_UF_max: float = 380.0  # 进水流量上限(m³/h)
+    q_UF_min: float = 210.0  # 进水流量下限(m³/h)
+
+    # --- 温度约束 ---
+    temp_max: float = 32.0  # 温度上限(℃)
+    temp_min: float = 16.0  # 温度下限(℃)
+
+    # --- 初始TMP约束 ---
+    TMP0_max: float = 0.045  # 初始TMP上限(MPa)
+    TMP0_min: float = 0.01  # 初始TMP下限(MPa)
+
+    # --- TMP上限约束 ---
+    global_TMP_hard_limit: float = 0.08
+
+    # --- 短期污染模型参数约束 ---
+    nuK_max: float = 2.6e+02  # 阻力增长系数上限
+    nuK_min: float = 4e+01  # 阻力增长系数下限
+
+    # --- 长期污染模型参数约束 ---
+    slope_max: float = 27  # 不可逆污染斜率上限
+    slope_min: float = 0.03  # 不可逆污染斜率下限
+    power_max: float = 2.2  # 不可逆污染幂次上限
+    power_min: float = 0.4  # 不可逆污染幂次下限
+
+    # --- CEB去除能力约束 ---
+    ceb_removal_max: float = 250  # CEB去除阻力上限(缩放后)
+    ceb_removal_min: float = 40  # CEB去除阻力下限(缩放后)

+ 190 - 0
algorithm/uf_rl/env/env_reset.py

@@ -0,0 +1,190 @@
+import pandas as pd
+import numpy as np
+from typing import Optional
+
+class ResetSampler:
+    # ============================================================
+    # 状态定义(唯一真源)
+    # ============================================================
+    FREE_STATE_KEYS = [
+        "q_UF",
+        "temp",
+        "TMP",
+        "nuK",
+        "slope",
+        "power",
+        "ceb_removal",
+    ]
+
+    FULL_STATE_KEYS = [
+        "q_UF",
+        "temp",
+        "TMP",
+        "R",
+        "nuK",
+        "slope",
+        "power",
+        "ceb_removal",
+    ]
+
+    IDX_Q_UF = 0
+    IDX_TEMP = 1
+    IDX_TMP = 2
+
+    def __init__(
+        self,
+        bounds: "UFStateBounds",
+        physics,
+        real_state_pool=None,
+        max_resample_attempts: int = 50,
+        random_state=None,
+    ):
+        self.bounds = bounds
+        self.physics = physics
+        self.max_resample_attempts = max_resample_attempts
+        self.rng = random_state or np.random.RandomState()
+
+        # --- 自由变量边界(顺序必须与 FREE_STATE_KEYS 一致)---
+        self.low = np.array([
+            bounds.q_UF_min,
+            bounds.temp_min,
+            bounds.TMP0_min,
+            bounds.nuK_min,
+            bounds.slope_min,
+            bounds.power_min,
+            bounds.ceb_removal_min,
+        ])
+
+        self.high = np.array([
+            bounds.q_UF_max,
+            bounds.temp_max,
+            bounds.TMP0_max,
+            bounds.nuK_max,
+            bounds.slope_max,
+            bounds.power_max,
+            bounds.ceb_removal_max,
+        ])
+
+        self.state_dim = len(self.low)
+
+        # ========================================================
+        # 统一 real_state_pool 为 DataFrame(不含 R)
+        # ========================================================
+        if real_state_pool is not None:
+            if isinstance(real_state_pool, pd.DataFrame):
+                df = real_state_pool.copy()
+            else:
+                df = pd.DataFrame(real_state_pool, columns=self.FULL_STATE_KEYS)
+
+            if "R" in df.columns:
+                df = df.drop(columns=["R"])
+
+            df = df[self.FREE_STATE_KEYS]
+            self.real_state_pool = df.reset_index(drop=True)
+        else:
+            self.real_state_pool = None
+
+    # ============================================================
+    # 对外接口
+    # ============================================================
+    def sample(self, progress: float) -> pd.Series:
+        cfg = self._get_sampling_config(progress)
+
+        sources, weights = [], []
+
+        if self.real_state_pool is not None:
+            sources += ["real", "perturb"]
+            weights += [cfg["w_real"], cfg["w_perturb"]]
+
+        sources.append("virtual")
+        weights.append(cfg["w_virtual"])
+
+        source = self.rng.choice(sources, p=self._normalize(weights))
+
+        # ========================================================
+        # 1. 采样自由变量(DataFrame 单行)
+        # ========================================================
+        if source == "real":
+            state_df = self._sample_real()  # 必须返回 DataFrame(1 row)
+
+        elif source == "perturb":
+            base = self._sample_real()
+            noise = self.rng.normal(0.0, cfg["perturb_scale"], size=self.state_dim)
+            vec = base.values.squeeze() + noise
+            vec = np.clip(vec, self.low, self.high)
+            state_df = pd.DataFrame([vec], columns=self.FREE_STATE_KEYS)
+
+        else:
+            vec = self.rng.uniform(self.low, self.high)
+            state_df = pd.DataFrame([vec], columns=self.FREE_STATE_KEYS)
+
+        # ========================================================
+        # 2. 物理派生:计算 R
+        # ========================================================
+        TMP = state_df.at[state_df.index[0], "TMP"]
+        q_UF = state_df.at[state_df.index[0], "q_UF"]
+        temp = state_df.at[state_df.index[0], "temp"]
+
+        R = self.physics.resistance_from_tmp(tmp=TMP, q_UF=q_UF, temp=temp)
+
+        if not np.isfinite(R):
+            raise RuntimeError("Invalid resistance computed during reset sampling.")
+
+        # ========================================================
+        # 3. 插入 R,形成完整状态(仍是 DataFrame)
+        # ========================================================
+        state_df.insert(3, "R", R)
+        state_df = state_df[self.FULL_STATE_KEYS]
+
+        # ========================================================
+        # 4. DataFrame(1 row) → Series(对外唯一返回)
+        # ========================================================
+        state = state_df.iloc[0]
+        state.name = "reset_state"  # 可选,但强烈推荐
+
+        return state
+
+    # ============================================================
+    # 内部方法
+    # ============================================================
+    def _sample_real(self) -> pd.DataFrame:
+        for _ in range(self.max_resample_attempts):
+            idx = self.rng.randint(len(self.real_state_pool))
+            row = self.real_state_pool.iloc[[idx]].reset_index(drop=True)
+
+            vec = row.values.squeeze()
+            if np.all(vec >= self.low) and np.all(vec <= self.high):
+                return row
+
+        raise RuntimeError("No valid real reset state within bounds.")
+
+    @staticmethod
+    def _normalize(w):
+        s = sum(w)
+        return [x / s for x in w]
+
+    def _get_sampling_config(self, progress: float) -> dict:
+        progress = np.clip(progress, 0.0, 1.0)
+
+        # # -------------------------
+        # # 阶段权重设计(非线性 + 提高虚拟工况)
+        # # -------------------------
+        # w_real = (1.0 - progress) ** 1.2  # 历史工况逐渐衰减
+        # w_perturb = 0.5 * progress  # 周边扰动按线性增加
+        # w_virtual = 0.3 * progress ** 1.5  # 虚拟工况加快增长,后期最大约 0.3
+        #
+        # # perturb 扰动幅度
+        # perturb_scale = 0.02 + 0.04 * progress
+        w_real = 0.0
+        w_perturb = 0.0
+        w_virtual = 1.0
+
+        # perturb 扰动幅度
+        perturb_scale = 0.0
+        return dict(
+            w_real=w_real,
+            w_perturb=w_perturb,
+            w_virtual=w_virtual,
+            perturb_scale=perturb_scale,
+        )
+

+ 110 - 0
algorithm/uf_rl/env/env_visual.py

@@ -0,0 +1,110 @@
+import numpy as np
+from stable_baselines3.common.callbacks import BaseCallback
+
+
+
+class UFEpisodeRecorder:
+    """记录episode中的决策和结果"""
+
+    def __init__(self):
+        self.episode_data = []
+        self.current_episode = []
+
+    def record_step(self, obs, action, reward, done, info):
+        """记录单步信息"""
+        step_data = {
+            "obs": obs.copy(),
+            "action": action.copy(),
+            "reward": reward,
+            "done": done,
+            "info": info.copy() if info else {}
+        }
+        self.current_episode.append(step_data)
+
+        if done:
+            self.episode_data.append(self.current_episode)
+            self.current_episode = []
+
+    def get_episode_stats(self, episode_idx=-1):
+        """获取episode统计信息"""
+        if not self.episode_data:
+            return {}
+
+        episode = self.episode_data[episode_idx]
+        total_reward = sum(step["reward"] for step in episode)
+        avg_recovery = np.mean([step["info"].get("recovery", 0) for step in episode if "recovery" in step["info"]])
+        feasible_steps = sum(1 for step in episode if step["info"].get("feasible", False))
+
+        return {
+            "total_reward": total_reward,
+            "avg_recovery": avg_recovery,
+            "feasible_steps": feasible_steps,
+            "total_steps": len(episode)
+        }
+
+
+# ==== 定义强化学习训练回调器 ====
+class UFTrainingCallback(BaseCallback):
+    """
+    强化学习训练回调,用于记录每一步的数据到 recorder。
+    1. 不依赖环境内部 last_* 属性
+    2. 使用环境接口提供的 obs、actions、rewards、dones、infos
+    3. 自动处理 episode 结束时的统计
+    """
+
+    def __init__(self, recorder, verbose=0):
+        super(UFTrainingCallback, self).__init__(verbose)
+        self.recorder = recorder
+
+    def _on_step(self) -> bool:
+        try:
+            new_obs = self.locals.get("new_obs")
+            actions = self.locals.get("actions")
+            rewards = self.locals.get("rewards")
+            dones = self.locals.get("dones")
+            infos = self.locals.get("infos")
+
+            if len(new_obs) > 0:
+                step_obs = new_obs[0]
+                step_action = actions[0] if actions is not None else None
+                step_reward = rewards[0] if rewards is not None else 0.0
+                step_done = dones[0] if dones is not None else False
+                step_info = infos[0] if infos is not None else {}
+                L_s = step_info["L_s"]
+                t_bw_s = step_info["t_bw_s"]
+                initial_tmp = step_info["initial_tmp"]
+                tmp_after_ceb = step_info["tmp_after_ceb"]
+                max_TMP_during_filtration = step_info["max_TMP_during_filtration"]
+                tmp_penalty = step_info["tmp_penalty"]
+
+                residual_ratio =step_info["residual_ratio"]
+                res_penalty = step_info["res_penalty"]
+
+                econ_reward = step_info["econ_reward"]
+                recovery = step_info["recovery"]
+
+
+                # 打印当前 step 的信息
+                if self.verbose:
+                    print(f"[Step {self.num_timesteps}] 动作={step_action}, 奖励={step_reward:.3f}, Done={step_done}, L_s={L_s}, t_bw_s={t_bw_s},"
+                          f"residual_ratio = {residual_ratio:.4f}, res_penalty = {res_penalty:.4f},"
+                          f"recovery = {recovery:.4f},econ_reward  = {econ_reward :.4f},"
+                          f"initial_tmp = {initial_tmp:.4f}, tmp_after_ceb = {tmp_after_ceb:.4f}, max_TMP_during_filtration ={max_TMP_during_filtration:.4f}, tmp_penalty = {tmp_penalty:.4f}")
+
+                # 记录数据
+                self.recorder.record_step(
+                    obs=step_obs,
+                    action=step_action,
+                    reward=step_reward,
+                    done=step_done,
+                    info=step_info,
+                )
+
+        except Exception as e:
+            if self.verbose:
+                print(f"[Callback Error] {e}")
+
+        return True
+
+
+

BIN
algorithm/uf_rl/env/resistance_model_bw.pth


BIN
algorithm/uf_rl/env/resistance_model_fp.pth


+ 487 - 0
algorithm/uf_rl/env/uf_env.py

@@ -0,0 +1,487 @@
+"""
+超滤强化学习环境模块
+========================
+本模块定义了超滤系统的强化学习环境,包括:
+1. UFParams: 超滤系统参数配置类
+2. 膜阻力与跨膜压差转换函数
+3. simulate_one_supercycle: 超级周期模拟函数
+4. calculate_reward: 奖励函数
+5. is_dead_cycle: 失败判定函数
+6. UFSuperCycleEnv: Gymnasium环境类
+
+模块设计说明:
+- 基于 Gymnasium (原OpenAI Gym) 标准接口
+- 模拟超滤膜的"超级周期"运行(多次物理反洗 + 一次化学反洗)
+- 强化学习智能体通过优化过滤时长和反洗时长来最大化回收率并控制污染累积
+"""
+
+import numpy as np
+import gymnasium as gym
+from gymnasium import spaces
+from algorithm.uf_rl.env.env_params import UFState, UFStateBounds, UFRewardParams, UFActionSpec
+from algorithm.uf_rl.env.uf_physics import UFPhysicsModel
+from algorithm.uf_rl.env.env_reset import ResetSampler
+import copy
+
+
+
+
+
+class UFSuperCycleEnv(gym.Env):
+    """
+    超滤系统强化学习环境(Gymnasium标准接口)
+    
+    功能:
+    - 模拟超滤膜的超级周期运行
+    - 智能体在每个超级周期选择过滤时长和反洗时长
+    - 目标:最大化回收率同时控制污染累积
+    
+    状态空间 (8维,归一化到 [0,1]):
+        1. TMP0: 初始跨膜压差
+        2. q_UF: 过滤流量
+        3. temp: 水温
+        4. R0: 初始膜阻力
+        5. nuK: 短期污染系数
+        6. slope: 长期污染斜率
+        7. power: 长期污染幂次
+        8. ceb_removal: CEB去除能力
+    
+    动作空间 (离散):
+        - 二维离散动作组合:(过滤时长, 反洗时长)
+        - 过滤时长: L_min_s ~ L_max_s,步长 L_step_s
+        - 反洗时长: t_bw_min_s ~ t_bw_max_s,步长 t_bw_step_s
+        - 总动作数 = len(L_values) × len(t_bw_values)
+    
+    奖励机制:
+        - 基于回收率和残余污染的平衡
+        - 失败 (TMP超限、回收率过低、污染过快) 时给予大负奖励 (-10)
+    
+    终止条件:
+        - terminated: 违反运行约束(失败)
+        - truncated: 达到最大步数 (max_episode_steps)
+    """
+
+    metadata = {"render_modes": ["human"]}
+
+    def __init__(
+            self,
+            physics: UFPhysicsModel,
+            reward_params: UFRewardParams,
+            action_spec:UFActionSpec,
+            statebounds:UFStateBounds,
+            real_state_pool,
+            max_episode_steps: int = 45,
+            RANDOM_SEED = 1024
+    ):
+        """
+        超滤强化学习环境
+
+        参数:
+            physics(UFPhysicsModel): 超滤物理模型
+            reward_params(UFRewardParams): 奖励函数参数
+            max_episode_steps (int): 每个episode的最大步数,默认45
+                注:每步代表一个超级周期(约2-3天),45步约三个月
+        """
+
+        super(UFSuperCycleEnv, self).__init__()
+
+        self.RANDOM_SEED = RANDOM_SEED
+        self.physics = physics
+        self.reward_params = reward_params
+        self.max_episode_steps = max_episode_steps
+
+        self.current_step = 0
+
+
+        # -------- 动作空间 --------
+        self.action_spec = action_spec
+
+        self.L_values = np.arange(
+            self.action_spec.L_min_s,
+            self.action_spec.L_max_s ,
+            self.action_spec.L_step_s,
+        )
+
+        self.t_bw_values = np.arange(
+            self.action_spec.t_bw_min_s,
+            self.action_spec.t_bw_max_s,
+            self.action_spec.t_bw_step_s,
+        )
+
+        self.num_L = len(self.L_values)
+        self.num_bw = len(self.t_bw_values)
+
+        self.action_space = spaces.Discrete(self.num_L * self.num_bw)
+
+        # -------- 状态空间 --------
+        self.observation_space = spaces.Box(
+            low=0.0,
+            high=1.0,
+            shape=(8,),
+            dtype=np.float32,
+        )
+
+        self.state_bounds = statebounds # 状态边界
+        self.real_state_pool = real_state_pool
+
+        self.reset_sampler = ResetSampler(
+            bounds=self.state_bounds,
+            physics=physics,
+            real_state_pool=self.real_state_pool,
+            max_resample_attempts=50,
+            random_state=np.random.RandomState(RANDOM_SEED)
+        )
+
+
+    def _generate_initial_state(self) -> UFState | None:
+        """
+        在 UFStateBounds 定义的范围内采样一个【合法】初始状态。
+        若采样失败(约束不满足)返回 None,由 reset() 负责重试。
+        """
+
+        b = self.state_bounds
+        A = 128 * 40.0  # 有效膜面积
+
+        # ---- 1. 基础工况 ----
+        # ---- 随机生成 TMP、q_UF、温度 ----
+        TMP0 = np.random.uniform(b.TMP0_min, b.TMP0_max)
+        q_UF = np.random.uniform(b.q_UF_min, b.q_UF_max)
+        temp = np.random.uniform(b.temp_min, b.temp_max)
+
+        # ---- 2. 污染增长参数 ----
+        slope = np.random.uniform(b.slope_min, b.slope_max)
+        power = np.random.uniform(b.power_min, b.power_max)
+
+        # ---- 3. 约束:污染增长速率可实现 ----
+        t_max = 60 if power >= 1 else 1
+        required_nuK_min = slope * power * (t_max ** (power - 1)) * (A / q_UF)
+
+        # 若 required_nuK_min 超过可选范围 → 初始状态非法
+        if required_nuK_min > b.nuK_max:
+            return None
+        # 在可行范围中采样 nuK
+        nuK = np.random.uniform(
+            max(required_nuK_min, b.nuK_min),
+            b.nuK_max
+        )
+
+        # ---- 4. CEB 去除率 ----
+        ceb_removal = np.random.uniform(
+            b.ceb_removal_min,
+            b.ceb_removal_max
+        )
+
+        # ---- 5. 初始膜阻力(物理模型) ----
+        R0 = self.physics.calculate_initial_resistance(
+            TMP=TMP0,
+            q_UF=q_UF,
+            temp=temp
+        )
+
+        return UFState(
+            TMP=TMP0,
+            q_UF=q_UF,
+            temp=temp,
+            R=R0,
+            slope=slope,
+            power=power,
+            nuK=nuK,
+            ceb_removal=ceb_removal,
+        )
+
+    def _get_training_progress(self) -> float:
+        """
+        返回训练进度,用于 reset_sampler 的 curriculum sampling
+        """
+        return min(1.0, self.current_step / self.max_episode_steps )
+
+    def reset(self, seed=None, options=None, max_attempts: int = 10000):
+        super().reset(seed=seed)
+
+        progress = self._get_training_progress()
+
+        for _ in range(max_attempts):
+            state = self.reset_sampler.sample(progress)
+            if state is None:
+                continue
+
+            ok_run = self.physics.check_dead_initial_state(
+                init_state=state,
+                max_steps=self.max_episode_steps,
+                L_s=self.action_spec.L_min_s,
+                t_bw_s=self.action_spec.t_bw_max_s
+            )
+
+            if ok_run:
+                self.state = state
+                break
+        else:
+            raise RuntimeError("无法生成可行初始状态")
+
+        self.current_step = 0
+        self.tmp_over_limit_flag = False
+        self.last_action = None
+        self.max_TMP_during_filtration = self.state.TMP
+
+        return self.get_obs(self.state), {}
+
+    def _get_state_copy(self):
+        return copy.deepcopy(self.state)
+
+    def get_obs(self, state):
+        """
+        构建当前环境归一化状态向量
+        """
+        # === 1. 从 state 读取动态参数 ===
+        TMP = state.TMP
+        q_UF = state.q_UF
+        temp = state.temp
+
+        # === 2. 计算本周期初始膜阻力 ===
+        R = state.R
+
+        # === 3. 从 self.state 读取膜阻力增长模型参数 ===
+        nuk = state.nuK
+        slope = state.slope
+        power = state.power
+        ceb_removal = state.ceb_removal
+
+        # === 4. 从 current_params 动态读取上下限 ===
+        TMP0_min, TMP0_max = self.state_bounds.TMP0_min, self.state_bounds.global_TMP_hard_limit
+        q_UF_min, q_UF_max = self.state_bounds.q_UF_min, self.state_bounds.q_UF_max
+        temp_min, temp_max = self.state_bounds.temp_min, self.state_bounds.temp_max
+        nuK_min, nuK_max = self.state_bounds.nuK_min, self.state_bounds.nuK_max
+        slope_min, slope_max = self.state_bounds.slope_min, self.state_bounds.slope_max
+        power_min, power_max = self.state_bounds.power_min, self.state_bounds.power_max
+        ceb_min, ceb_max = self.state_bounds.ceb_removal_min, self.state_bounds.ceb_removal_max
+
+        # === 5. 归一化计算(clip防止越界) ===
+        TMP0_norm = np.clip((TMP - TMP0_min) / (TMP0_max - TMP0_min), 0, 1)
+        q_UF_norm = np.clip((q_UF - q_UF_min) / (q_UF_max - q_UF_min), 0, 1)
+        temp_norm = np.clip((temp - temp_min) / (temp_max - temp_min), 0, 1)
+
+        # R0 不在 current_params 中定义上下限,设定经验范围
+        R0_norm = np.clip((R - 100.0) / (800.0 - 100.0), 0, 1)
+
+        short_term_norm = np.clip((nuk - nuK_min) / (nuK_max - nuK_min), 0, 1)
+        long_term_slope_norm = np.clip((slope - slope_min) / (slope_max - slope_min), 0, 1)
+        long_term_power_norm = np.clip((power - power_min) / (power_max - power_min), 0, 1)
+        ceb_removal_norm = np.clip((ceb_removal - ceb_min) / (ceb_max - ceb_min), 0, 1)
+
+        # === 6. 构建观测向量 ===
+        obs = np.array([
+            TMP0_norm,
+            q_UF_norm,
+            temp_norm,
+            R0_norm,
+            short_term_norm,
+            long_term_slope_norm,
+            long_term_power_norm,
+            ceb_removal_norm
+        ], dtype=np.float32)
+
+        return obs
+
+    def get_action_values(self, action):
+        """
+        将动作还原为实际时长
+        """
+        L_idx = action // self.num_bw
+        t_bw_idx = action % self.num_bw
+        return self.L_values[L_idx], self.t_bw_values[t_bw_idx]
+
+    def step(self, action):
+        self.current_step += 1
+        L_s, t_bw_s = self.get_action_values(action)
+        L_s = np.clip(L_s, self.action_spec.L_min_s, self.action_spec.L_max_s)
+        t_bw_s = np.clip(t_bw_s, self.action_spec.t_bw_min_s, self.action_spec.t_bw_max_s)
+
+        # 模拟超级周期
+        info, next_state = self.physics.simulate_one_supercycle(state=self.state,L_s=L_s, t_bw_s=t_bw_s)
+        # 根据 info 判断是否成功
+        feasible = self.physics.is_dead_cycle(info)  # True 表示成功循环,False 表示失败
+
+        if info["max_TMP_during_filtration"] >= self.reward_params.global_TMP_hard_limit:
+            self.tmp_over_limit_flag = True
+
+        # ================== 孤立观察下一周期 ==================
+        info_next = None
+        if info["max_TMP_during_filtration"] > self.reward_params.global_TMP_soft_limit:
+            info_next, _ = self.physics.simulate_one_supercycle(state=next_state,L_s=L_s,t_bw_s=t_bw_s)
+
+        reward,tmp_penalty,econ_reward,res_penalty= self._calculate_reward(info, info_next)
+        info["tmp_penalty"] = tmp_penalty
+        info["econ_reward"] = econ_reward
+        info["res_penalty"] = res_penalty
+
+        self.state = next_state
+        terminated = False
+
+        # 判断是否到达最大步数
+        truncated = self.current_step >= self.max_episode_steps
+
+        self.last_action = (L_s, t_bw_s)
+        next_obs = self.get_obs(next_state)
+
+        info["feasible"] = feasible
+        info["step"] = self.current_step
+        info["L_s"] = L_s.copy()
+        info["t_bw_s"] = t_bw_s.copy()
+
+        # # ===================== 测试终末奖励:鼓励 TMP 接近初始状态 =====================
+        # # 仅在 episode 自然结束(满步但未提前失败)时触发
+        # if truncated and not terminated:
+        #     TMP_initial = self.TMP0  # reset 时记录的初始 TMP
+        #     TMP_final = next_obs[0]  # next_obs 提供的最终 TMP
+        #
+        #     delta_ratio = abs((TMP_final - TMP_initial) / TMP_initial)
+        #
+        #     alpha = 4.0  # TMP 偏差敏感度
+        #     gamma = 5.0  # 奖励幅度
+        #     stability_reward = gamma * (np.exp(-alpha * delta_ratio) - 1) # 量级在0到-5之间
+        #
+        #     reward += stability_reward
+        #     terminated = True  # episode 正式结束
+
+        # # ===================== 测试结果 =====================
+        # 增加该奖励后强化学习依然能保证奖励收敛,但是损失函数在2-3之间反复震荡,无法降低,见reward_test&loss_test
+        # 原设想是只能听在大额偏移发生前能通过该奖励学习到提前减小偏移步伐,但是实际训练时该惩罚反复被触发
+        # 推测是终末的大额奖惩无法有效传递回过往时间步引导智能体学习,可能由于状态中缺少预测值,智能体会将其观测为不可控事件,暂时不添加该奖励,TODO:等待优化
+
+        return next_obs, reward, terminated, truncated, info
+
+    def _calculate_reward(self, info: dict, info_next=None):
+        """
+        计算强化学习奖励函数(经济性 + 系统稳定性)
+
+        奖励结构:
+            Reward = 经济奖励 + 污染控制奖励 + TMP风险惩罚
+
+        经济奖励:
+            基于吨水电耗 + 吨水药耗
+
+        稳定性奖励:
+            - 残余污染控制
+            - TMP软限制
+            - TMP增长趋势
+
+        返回:
+            total_reward,
+            tmp_penalty,
+            econ_reward,
+            res_penalty
+        """
+
+        # ==============================
+        # TMP 状态惩罚
+        # ==============================
+        # ==============================
+        # TMP 状态惩罚(第一阶段优化版)
+        # ==============================
+
+        tmp = info["max_TMP_during_filtration"]
+        tmp_soft = self.reward_params.global_TMP_soft_limit
+        tmp_hard = self.reward_params.global_TMP_hard_limit
+
+        HARD_PENALTY = 2.0  # 替代原来的5
+
+        if self.tmp_over_limit_flag:
+            tmp_state_penalty = -HARD_PENALTY
+
+        elif tmp <= tmp_soft:
+            tmp_state_penalty = 0.0
+
+        elif tmp < tmp_hard:
+            x = (tmp - tmp_soft) / (tmp_hard - tmp_soft)
+
+            tmp_state_penalty = -self.reward_params.w_tmp * (
+                    x ** self.reward_params.p  # p建议=4
+            )
+
+        else:
+            tmp_state_penalty = -HARD_PENALTY
+
+        # ==============================
+        # TMP 趋势惩罚(加死区)
+        # ==============================
+
+        tmp_trend_penalty = 0.0
+
+        if info_next is not None:
+            delta_tmp = info_next["max_TMP_during_filtration"] - tmp
+
+            delta_tmp = max(delta_tmp, 0)
+
+            # 死区(关键)
+            if delta_tmp < 0.001:
+                delta_tmp = 0.0
+
+            tmp_trend_penalty = -self.reward_params.w_trend * delta_tmp
+
+        tmp_penalty = tmp_state_penalty + tmp_trend_penalty
+
+        # ==============================
+        # 残余污染惩罚
+        # ==============================
+        delta_R = info["delta_R"]
+        delta_R_allow = info["delta_R_allow"]
+
+        # 归一化
+        ratio = delta_R / (delta_R_allow + 1e-6)
+
+        # 基础响应
+        k_res = self.reward_params.k_res  # 2~4
+        base = np.tanh(-k_res * ratio)
+
+        # 污染阶段调制
+        c = 50  # 控制“污染敏感度”
+        severity = np.tanh(c / (delta_R_allow + 1e-6))
+
+        # 最终奖励
+        res_penalty = base * (1 + 0.5 * severity)
+        # ==============================
+        # 经济成本(电耗 + 药耗)
+        # ==============================
+
+        energy = info["ton_water_energy"]
+        chemical = info["ton_water_chem"]
+
+        chemical_price = self.reward_params.chemical_price
+        energy_price =self.reward_params.energy_price
+
+        cost = energy * energy_price + chemical * chemical_price * 100
+
+        # 成本归一化范围
+        cost_low = self.reward_params.cost_low
+        cost_high = self.reward_params.cost_high
+
+        cost_norm = (
+                (cost - cost_low) /
+                (cost_high - cost_low)
+        )
+
+        econ_reward = -np.tanh(
+            self.reward_params.k_cost *
+            (cost_norm - 0.5)
+        )
+
+        # ==============================
+        # 总奖励
+        # ==============================
+
+        total_reward = (
+                econ_reward
+                + 1.5 * res_penalty
+                + tmp_penalty
+        )
+
+        total_reward += 1
+
+        return (
+            total_reward,
+            tmp_penalty,
+            econ_reward,
+            res_penalty
+        )
+
+

+ 588 - 0
algorithm/uf_rl/env/uf_physics.py

@@ -0,0 +1,588 @@
+"""
+uf_physics_48h.py
+
+超滤(UF)系统物理模型与确定性计算规则模块。
+
+本模块定义:
+- 与强化学习算法无关的物理规律
+- 与环境状态更新相关的确定性计算
+- 超滤系统中的基础物理量转换关系
+
+设计原则:
+- 不依赖 env / agent / trainer
+- 不包含强化学习语义
+- 不负责参数加载策略(由上层控制)
+
+该模块应可被:
+- 强化学习环境
+- 离线仿真
+- 工艺分析脚本
+独立复用。
+"""
+
+import numpy as np
+import copy
+from algorithm.uf_rl.env.env_params import UFState, UFPhysicsParams, UFStateBounds
+
+
+class UFPhysicsModel:
+    """
+    超滤系统无状态物理模型(Physical Rules)
+
+    说明:
+    - 本类不保存任何动态状态
+    - 所有计算结果仅依赖输入参数
+    - 表达的是“物理规律”,而不是“设备实例”
+    """
+
+    def __init__(
+            self,
+            phys_params: UFPhysicsParams,
+            state_bounds: UFStateBounds,
+            resistance_model_fp=None,
+            resistance_model_bw=None,
+            IS_TIMES: bool = False,
+    ):
+        """
+        参数:
+            phys_params: 物理/工艺固定参数
+            resistance_model_fp: 过滤阶段阻力增长模型
+            resistance_model_bw: 反洗阶段阻力下降模型
+            IS_TIMES: CEB是否为固定次数,T为固定次数
+        """
+        self.p = phys_params
+        self.state_bounds = state_bounds
+        self.model_fp = resistance_model_fp
+        self.model_bw = resistance_model_bw
+        self.IS_TIMES = IS_TIMES
+
+    # ==========================================================
+    # 水温-粘度关系
+    # ==========================================================
+    def viscosity(self, temp: float) -> float:
+        """
+        锡山水厂水温粘度修正公式
+
+        功能:根据水温计算水的动力粘度(考虑温度影响)
+
+        参数:
+            temp (float): 水温(摄氏度)
+
+        返回:
+            float: 水的动力粘度 μ (Pa·s)
+
+        原理:
+        - 水的粘度随温度升高而降低
+        - 25℃时纯水粘度约为 0.00089 Pa·s
+        - 本公式基于锡山水厂PLC系统的经验修正因子
+
+        注意:
+        - 本公式基于纯水粘度修正
+        - 实际水厂水质与纯水有差异,对粘度有一定影响
+        - 未来可根据实际水质进一步校准
+        """
+        # 温度归一化(相对于300K)
+        x = (temp + 273.15) / 300  # 摄氏度转开尔文
+
+        # 温度修正因子(经验公式,基于锡山水厂PLC)
+        factor = 890 / (
+                280.68 * x ** -1.9 +
+                511.45 * x ** -7.7 +
+                61.131 * x ** -19.6 +
+                0.45903 * x ** -40
+        )
+
+        # 计算修正后的粘度(25℃标准粘度 / 修正因子)
+        mu = 0.00089 / factor  # [Pa·s]
+
+        return mu
+
+    # ==========================================================
+    # TMP → 膜阻力
+    # ==========================================================
+    def resistance_from_tmp(
+        self,
+        tmp: float,
+        q_UF: float,
+        temp: float
+    ) -> float:
+        """
+        由跨膜压差计算膜阻力
+
+        功能:根据 Darcy 定律,由跨膜压差反推膜阻力
+
+        参数:
+            tmp (float): 跨膜压差 TMP (MPa)
+            q_UF (float): 过滤流量 (m³/h)
+            temp (float): 水温 (℃)
+
+        返回:
+            float: 膜阻力 R(已缩放 1e10)
+
+        原理:
+            Darcy 定律:J = TMP / (μ × R)
+            其中:
+            - J: 膜通量 [m/s]
+            - TMP: 跨膜压差 [Pa]
+            - μ: 水的动力粘度 [Pa·s]
+            - R: 膜阻力 [m⁻¹]
+
+            反解得:R = TMP / (J × μ)
+
+        注意:
+            - 超滤膜阻力实际量级为 1e12 m⁻¹
+            - 为便于数值计算,已缩放 1e10 倍至 1e2 量级
+        """
+        # 温度修正后的水粘度
+        mu = self.viscosity(temp) # [Pa·s]
+        # 膜有效面积(在参数文件中配置)
+        A = self.p.A
+
+        # 跨膜压差单位转换:MPa → Pa
+        TMP_Pa = tmp * 1e6  # [Pa]
+
+        # 计算膜通量:流量 / 面积
+        J = q_UF / A / 3600  # [m³/h] → [m³/(m²·s)] = [m/s]
+
+        # 物理约束检查:通量和粘度必须为正
+        if J <= 0 or mu <= 0:
+            return np.nan
+
+        # 根据 Darcy 定律计算膜阻力并缩放
+        R = TMP_Pa / (J * mu) / 1e10  # [m⁻¹] → [缩放单位]
+
+        return float(R)
+
+    # ==========================================================
+    # 膜阻力 → TMP
+    # ==========================================================
+    def tmp_from_resistance(
+        self,
+        R: float,
+        q_UF: float,
+        temp: float
+    ) -> float:
+        """
+        由膜阻力计算跨膜压差
+
+        功能:根据 Darcy 定律,由膜阻力计算跨膜压差(_calculate_resistance 的逆运算)
+
+        参数:
+            R (float): 膜阻力(已缩放 1e10)
+            q_UF (float): 过滤流量 (m³/h)
+            temp (float): 水温 (℃)
+
+        返回:
+            float: 跨膜压差 TMP (MPa)
+
+        原理:
+            Darcy 定律:TMP = J × μ × R
+            其中:
+            - J: 膜通量 [m/s]
+            - μ: 水的动力粘度 [Pa·s]
+            - R: 膜阻力 [m⁻¹]
+        """
+        # 温度修正后的水粘度
+        mu = self.viscosity(temp) # [Pa·s]
+        # 膜有效面积(在参数文件中配置)
+        A = self.p.A
+
+        # 计算膜通量
+        J = q_UF / A / 3600  # [m/s]
+
+        # 根据 Darcy 定律计算跨膜压差(还原缩放)
+        TMP_Pa = R * J * mu * 1e10  # [缩放单位] → [Pa]
+
+        # 单位转换:Pa → MPa
+        tmp = TMP_Pa / 1e6  # [MPa]
+
+        return float(tmp)
+
+    def delta_resistance_filter(
+        self,
+        state,
+        L_s: float
+    ) -> float:
+        """
+        过滤阶段膜阻力上升量(数据驱动模型)
+
+        参数:
+            state (UFState): 当前运行状态
+            L_s (float): 过滤时长 [s]
+
+        返回:
+            float: 阻力增量 ΔR
+        """
+        if self.model_fp is None:
+            raise RuntimeError("过滤阶段阻力模型未注入")
+
+        return float(self.model_fp(state, L_s))
+
+    def delta_resistance_backwash(
+        self,
+        state,
+        R0: float,
+        R_end: float,
+        L_h_next_start: float,
+        t_bw_s: float
+    ) -> float:
+        """
+        物理反洗可去除的膜阻力(数据驱动模型)
+
+        返回:
+            float: 可去除阻力
+        """
+        if self.model_bw is None:
+            raise RuntimeError("反洗阶段阻力模型未注入")
+
+        return float(
+            self.model_bw(
+                state,
+                R0,
+                R_end,
+                L_h_next_start,
+                t_bw_s
+            )
+        )
+
+    def backwash_water_volume(self, t_bw_s: float) -> float:
+        """
+        计算物理反洗水耗
+
+        参数:
+            t_bw_s (float): 反洗时长 [s]
+
+        返回:
+            float: 反洗水耗 [m³]
+        """
+        return float(self.p.q_bw_m3ph * t_bw_s / 3600.0)
+
+    def simulate_one_supercycle(self, state: UFState, L_s: float, t_bw_s: float) -> UFState:
+        """
+        模拟一个超级周期(Super Cycle)
+        返回 info 字典 + 更新后的 UFState
+
+        IS_TIMES: False CEB执行是否采用次数控制,默认为 False, 采用时长控制
+        """
+        # ========== 初始化周期参数 ==========
+        L_h = float(L_s) / 3600.0  # 过滤时长转换:秒 → 小时
+
+        # 初始状态(统一用 TMP / R,与当前 state 保持一致)
+        initial_tmp = state.TMP # 记录周期初始跨膜压差
+        initial_R = state.R # 记录周期初始膜阻力
+        tmp = initial_tmp # 当前跨膜压差
+
+
+        # 跟踪变量(用于记录周期内的极值)
+        max_tmp_during_filtration = tmp  # 周期内最大TMP
+        min_tmp_during_filtration = tmp  # 周期内最小TMP
+        max_residual_increase = 0.0  # 周期内最大残余污染增量
+
+        # ========== 计算小周期数量 ==========
+        # 小周期时长 = 过滤时长 + 物理反洗时长
+        t_small_cycle_h = (L_s + t_bw_s) / 3600.0  # [小时]
+
+        # 计算一个超级周期内包含多少个小周期
+        if self.IS_TIMES: # 采用次数控制
+            k_bw_per_ceb = self.p.T_ceb_interval_times
+        else: # 采用时长控制
+            # k = floor(CEB间隔时间 / 小周期时长)
+            k_bw_per_ceb = int(np.floor(self.p.T_ceb_interval_h / t_small_cycle_h))
+            if k_bw_per_ceb < 1:
+                k_bw_per_ceb = 1  # 至少包含1个小周期
+
+        # ========== 循环模拟每个小周期(过滤 + 物理反洗) ==========
+        for idx in range(k_bw_per_ceb):
+            # --- 小周期开始状态 ---
+            tmp_run_start = tmp # 本次过滤开始时的TMP
+            q_UF = state.q_UF # 过滤流量
+            temp = state.temp # 水温
+
+            # --- 过滤阶段:膜阻力上升 ---
+            R_run_start = self.resistance_from_tmp(tmp_run_start, q_UF, temp) # 过滤开始时的膜阻力
+            d_R = self.delta_resistance_filter(state, L_s) # 过滤阶段膜阻力增量
+            R_peak = R_run_start + d_R # 过滤结束时的膜阻力(峰值)
+            tmp_peak = self.tmp_from_resistance(R_peak, q_UF, temp) # 过滤结束时的TMP(峰值)
+
+            # 更新TMP极值记录
+            max_tmp_during_filtration = max(max_tmp_during_filtration, tmp_peak)
+            min_tmp_during_filtration = min(min_tmp_during_filtration, tmp_run_start)
+
+            # --- 物理反洗阶段:膜阻力下降 ---
+            # 计算累积运行时间(用于长期污染模型)
+            L_h_next_start = (L_s + t_bw_s) / 3600.0 * (idx + 1)  # 下一小周期起始时间
+            # 调用膜阻力下降模型,计算物理反洗可去除的阻力
+            reversible_R = self.delta_resistance_backwash(state, initial_R, R_peak, L_h_next_start, t_bw_s)
+
+            # 物理反洗后的膜阻力
+            R_after_bw = R_peak - reversible_R
+            tmp_after_bw = self.tmp_from_resistance(R_after_bw, q_UF, temp)
+
+            # 计算残余污染增量(反洗后的TMP相对本次开始的增加)
+            residual_inc = tmp_after_bw - tmp_run_start
+            max_residual_increase = max(max_residual_increase, residual_inc)
+            # 更新TMP(作为下一小周期的起始TMP)
+            tmp = tmp_after_bw
+
+        # ========== 化学增强反洗 (CEB) ==========
+        # CEB比物理反洗更彻底,可去除部分不可逆污染
+        R_after_ceb = R_peak - state.ceb_removal # CEB后的膜阻力
+        tmp_after_ceb = self.tmp_from_resistance(R_after_ceb, q_UF, temp) # CEB后的TMP
+
+        # ============================================================
+        # 计算周期性能指标
+        # ============================================================
+
+        # ========== 水量平衡计算 ==========
+        # 进水总量(所有小周期的过滤进水之和)
+        V_feed_super = k_bw_per_ceb * state.q_UF * L_h
+
+        # 损失水量(物理反洗 + 化学反洗)
+        V_loss_super = k_bw_per_ceb * self.backwash_water_volume(t_bw_s) + self.p.v_ceb_m3
+        V_net = max(0.0, V_feed_super - V_loss_super)
+
+        # 回收率(净产水 / 进水总量)
+        # 加1e-12避免除零,max确保非负
+        recovery = max(0.0, V_net / max(V_feed_super, 1e-12))
+
+        # ========== 时间与能耗计算 ==========
+        # 超级周期总时长
+        T_super_h = k_bw_per_ceb * (L_s + t_bw_s) / 3600.0 + self.p.t_ceb_s / 3600.0  # [小时]
+
+        # 日均产水时间(24小时内实际产水的时间)
+        daily_prod_time_h = k_bw_per_ceb * L_h / T_super_h * 24.0  # [小时]
+
+        # 参考吨水电耗(从查找表获取最接近的值)
+        # 从物理参数类中获取查找表
+        closest_L = min(self.p.energy_lookup.keys(), key=lambda x: abs(float(x) - L_s))
+        refer_ton_water_energy = self.p.energy_lookup[closest_L]  # [kWh/m³]
+
+        # 实际吨水电耗计算
+        # 进水时间(小时)
+        t_feed_total_h = k_bw_per_ceb * L_h
+        # 反洗时间(小时)
+        t_bw_total_h = k_bw_per_ceb * t_bw_s / 3600.0
+
+        # 实际泵功率(kW)
+        simple_avg_tmp = (tmp_run_start + max_tmp_during_filtration) / 2
+        tmp_min = self.state_bounds.TMP0_min
+        tmp_max = self.state_bounds.global_TMP_hard_limit
+        simple_avg_tmp_clip = np.clip(simple_avg_tmp, tmp_min, tmp_max)
+
+        p_feed_kw_min = self.p.p_feed_kw_min
+        p_feed_kw_max = self.p.p_feed_kw_max
+        p_feed_kw = p_feed_kw_min + (simple_avg_tmp_clip - tmp_min) / (tmp_max - tmp_min) * (p_feed_kw_max - p_feed_kw_min)
+
+        # 总能耗 (kWh)
+        E_total = t_feed_total_h * p_feed_kw+ t_bw_total_h * self.p.p_bw_kw
+
+        # 吨水电耗 (kWh/吨)
+        ton_water_energy = E_total / max(V_net, 1e-12)
+
+        # 吨水药耗计算
+        # ========== 吨水药耗计算 ==========
+        R_removed = state.ceb_removal
+
+        # 参数
+        R_min = self.state_bounds.ceb_removal_min
+        R_max = self.state_bounds.ceb_removal_max
+
+        dose_min = self.p.dose_min
+        dose_max = self.p.dose_max
+
+        # 防止越界
+        R_removed_clip = np.clip(R_removed, R_min, R_max)
+
+        # 线性映射计算加药量
+        dose = dose_min + (R_removed_clip - R_min) / (R_max - R_min) * (dose_max - dose_min)
+
+        # 吨水药耗
+        ton_water_chem = dose / max(V_net, 1e-12)
+
+        # ===== 新指标:膜阻力允许上升空间 =====
+        # 该指标根据当前最大跨膜压差距离软约束跨膜压差的距离,动态计算当前周期允许上升的膜阻力值,用于后续清洗效果奖励计算
+        delta_R = R_after_ceb - initial_R
+        delta_R_allow = max(
+            self.resistance_from_tmp(self.p.global_TMP_hard_limit, state.q_UF, state.temp) -
+            self.resistance_from_tmp(max_tmp_during_filtration, state.q_UF, state.temp),
+            1e-6
+        )
+        if delta_R_allow > 50:
+            residual_ratio = max(delta_R / delta_R_allow, 0.0)
+        else:
+            residual_ratio = 1.0
+
+        # ========== 构建性能指标字典 ==========
+        info = {
+            # 运行参数
+            "q_UF": state.q_UF,  # 过滤流量
+            "temp": state.temp,  # 水温
+
+            # 水量指标
+            "recovery": recovery,  # 回收率
+            "V_feed_super_m3": V_feed_super,  # 进水总量
+            "V_loss_super_m3": V_loss_super,  # 损失水量
+            "V_net_super_m3": V_net,  # 净产水量
+
+            # 时间指标
+            "supercycle_time_h": T_super_h,  # 超级周期时长
+            "daily_prod_time_h": daily_prod_time_h,  # 日均产水时间
+            "k_bw_per_ceb": k_bw_per_ceb,  # 小周期数量
+
+            # TMP指标
+            "max_TMP_during_filtration": max_tmp_during_filtration,  # 周期内最大TMP
+            "min_TMP_during_filtration": min_tmp_during_filtration,  # 周期内最小TMP
+            "initial_tmp": initial_tmp,  # 周期初始TMP
+            "tmp_after_ceb": tmp_after_ceb,  # CEB后TMP
+
+            # 膜阻力指标
+            "initial_R": initial_R,  # 周期初始膜阻力
+            "R_after_ceb": R_after_ceb,  # CEB后膜阻力
+            "max_residual_increase_per_run": max_residual_increase,  # 最大残余污染增量
+            "delta_R_allow": delta_R_allow,  # 污染允许增长空间
+            "delta_R": delta_R, # 污染上升值
+            "residual_ratio" : residual_ratio, # 污染上升比例
+
+            # 能耗指标
+            "refer_ton_water_energy": refer_ton_water_energy,  # 参考吨水电耗
+            "ton_water_energy": ton_water_energy,  # 吨水电耗
+            "ton_water_chem": ton_water_chem,  # 吨水药耗
+        }
+
+        # 更新 state
+        next_state = copy.deepcopy(state)
+        next_state.TMP = tmp_after_ceb
+        next_state.R = R_after_ceb
+
+        # ========== 可选更新的参数 ==========
+        # 这些参数可根据实际情况动态调整,预留扩展接口
+        next_state.nuK = state.nuK  # 短期污染系数
+        next_state.slope = state.slope  # 长期污染斜率
+        next_state.power = state.power  # 长期污染幂次
+        next_state.ceb_removal = state.ceb_removal  # CEB去除能力
+        next_state.q_UF = state.q_UF  # 过滤流量
+        next_state.temp = state.temp  # 水温
+
+        return info, next_state
+
+    def is_dead_cycle(self, info: dict) -> bool:
+        """
+            判断当前超级周期是否成功(可行)
+
+            功能:
+            - 检查超级周期是否违反运行约束
+            - 用于强化学习环境单步step的失败判定(terminated条件)
+            - True表示成功,False表示失败
+
+            参数:
+                info (dict): simulate_one_supercycle() 返回的性能指标字典
+
+            返回:
+                bool: True表示成功周期,False表示失败周期
+
+            失败条件(任一满足即失败):
+            1. TMP超限:max_TMP > global_TMP_limit
+               - 原因:TMP过高会损坏膜或影响产水质量
+               - 阈值:0.08 MPa(可配置)
+
+            2. 回收率过低:recovery < 0.75
+               - 原因:回收率太低说明反洗水耗过大,经济性差
+               - 阈值:75%(可调整)
+
+            3. 残余污染累积过快:(R_after_ceb - R0) / R0 > 0.05
+               - 原因:单个超级周期污染增长超过5%,长期运行不可持续
+               - 阈值:10%(可调整)
+            """
+        # ========== 获取关键指标 ==========
+        TMP_limit = self.p.global_TMP_hard_limit  # TMP硬约束上限
+        max_tmp = info.get("max_TMP_during_filtration", 0)  # 周期内最大TMP
+        recovery = info.get("recovery", 1.0)  # 回收率
+        R_after_ceb = info.get("R_after_ceb", 0)  # CEB后膜阻力
+        R0 = info.get("initial_R", 1e-6)  # 初始膜阻力
+        delta_R_allow = info.get("delta_R_allow", 1e-6)  # 允许上升的膜阻力(加小值避免除零)
+
+        # ========== 失败条件检查 ==========
+        # 条件1:TMP超限
+        if max_tmp > TMP_limit:
+            return False  # 失败
+
+        # 条件2:回收率过低
+        if recovery < 0.75:
+            return False  # 失败
+
+        # 条件3:污染增长比例超过容许范围
+        residual_increase = (R_after_ceb - R0) / delta_R_allow
+        if residual_increase > 1 / 20:
+            return False  # 失败
+
+        # 所有条件通过
+        return True  # 成功
+
+    def check_dead_initial_state(
+            self,
+            init_state: UFState,
+            max_steps: int = 45,
+            L_s: float = 3800.0,
+            t_bw_s: float = 60.0
+    ) -> bool:
+        """
+        判断给定初始状态在指定动作策略下,是否为物理可行(non-dead)。
+
+        从 init_state 出发,使用固定动作 (L_s, t_bw_s)
+        连续模拟 max_steps 个 supercycle:
+
+            - 任意一次 is_dead_cycle(info) 为 False → 必死
+            - 任意一步 TMP0 < 0 → 必死
+            - 任意异常 → 必死
+
+        参数:
+            init_state:
+                初始 UFState(不会被原地修改)
+            max_steps:
+                前向模拟的最大步数
+            L_s:
+                过滤时长(秒)
+            t_bw_s:
+                物理反洗时长(秒)
+
+        返回:
+            bool:
+                True  → 物理可行(non-dead)
+                False → 必死状态
+        """
+        import copy
+
+        # 使用局部副本,确保 physics 无副作用
+        curr_state = copy.deepcopy(init_state)
+
+        for step in range(max_steps):
+            try:
+                info, next_state = self.simulate_one_supercycle(
+                    curr_state,
+                    L_s=L_s,
+                    t_bw_s=t_bw_s
+
+                )
+            except Exception:
+                # 任何异常都视为物理不可行
+                return False
+
+            # 单步物理可行性判定
+            if not self.is_dead_cycle(info):
+                return False
+
+            # 物理硬约束:TMP 不允许为负
+            if next_state.TMP < 0:
+                return False
+
+            # 进入下一步
+            curr_state = next_state
+
+        return True
+
+
+
+
+
+
+

+ 169 - 0
algorithm/uf_rl/env/uf_resistance_models_define.py

@@ -0,0 +1,169 @@
+"""
+超滤膜阻力模型模块
+====================
+本模块定义了超滤膜阻力的动态变化模型,包括:
+1. ResistanceIncreaseModel: 过滤阶段膜阻力上升模型
+2. ResistanceDecreaseModel: 反洗阶段膜阻力下降模型
+
+这些模型用于模拟超滤膜在运行过程中的阻力变化,是强化学习环境的核心组件。
+"""
+
+import torch
+import numpy as np
+from algorithm.uf_rl.env.env_params import UFState, UFPhysicsParams
+
+
+# ==================== 膜阻力上升模型 ====================
+class ResistanceIncreaseModel(torch.nn.Module):
+    """
+    过滤阶段膜阻力上升模型
+    
+    功能说明:
+    - 计算在过滤阶段膜阻力的增长量 ΔR
+    - 膜阻力上升主要由污染物在膜表面的累积引起
+    - 阻力增长速率与膜通量(J)和过滤时长(L_s)相关
+    
+    模型公式:
+        ΔR = nuK × J × L_s
+        其中:
+        - nuK: 膜阻力增长系数(反映水质污染特性)
+        - J: 膜通量 = q_UF / A / 3600 [m/s]
+        - L_s: 过滤时长 [秒]
+    """
+
+    def __init__(self, phys: UFPhysicsParams):
+        """初始化膜阻力上升模型"""
+        super().__init__()
+        self.phys = phys
+
+    def forward(self, state: UFState, L_s: float) -> float:
+        """
+        前向传播:计算膜阻力上升量
+        
+        参数:
+            state (UFState): 超滤运行状态变量对象,包含:
+                - q_UF: 过滤进水流量 [m³/h]
+                - nuK: 膜阻力增长系数 [m⁻¹/s]
+            L_s (float): 过滤时长 [秒]
+        
+        返回:
+            float: 膜阻力上升量 ΔR(已缩放1e10)
+        
+        注意:
+            - 实际膜阻力量级为1e12,为便于数值计算已缩放至1e2量级
+            - 膜面积 A = 128组 × 40 m²/组 = 5120 m²,现已优化为 UFPhysicsParams类配置
+        """
+        # 加载膜有效面积(锡山水厂配置:128组膜,每组40m²)
+        A = self.phys.A
+        
+        # 计算膜通量 J = 流量 / 面积 / 时间单位转换
+        # q_UF [m³/h] → J [m³/(m²·s)]
+        J = state.q_UF / A / 3600.0  # [m/s]
+        
+        # 膜阻力上升模型(线性模型,已缩放)
+        # nuK: 阻力增长速率,反映水质污染特性
+        # J: 膜通量,通量越大污染速率越快
+        # L_s: 过滤时间,时间越长累积污染越多
+        dR = state.nuK * J * L_s  # [缩放后的阻力单位]
+        
+        return float(dR)
+
+
+# ==================== 膜阻力下降模型 ====================
+class ResistanceDecreaseModel(torch.nn.Module):
+    """
+    反洗阶段膜阻力下降模型
+    
+    功能说明:
+    - 计算物理反冲洗能够去除的膜阻力量
+    - 区分可逆污染和不可逆污染
+    - 反洗时长影响去除效率
+    
+    模型原理:
+    1. 膜污染分为两类:
+       - 可逆污染:可通过物理反洗去除(如表面颗粒物)
+       - 不可逆污染:无法通过物理反洗去除(如孔内吸附污染)
+    
+    2. 不可逆污染累积模型:
+       R_irr = R0 + slope × t^power
+       其中 t 为累积运行时间
+    
+    3. 反洗效率模型:
+       time_gain = 1 - exp(-t_bw / τ)
+       反洗时间越长,去除效率越高,但存在上限
+    """
+
+    def __init__(self, phys: UFPhysicsParams):
+        """初始化膜阻力下降模型(无需训练参数)"""
+        super().__init__()
+        self.phys = phys
+
+    def forward(self,state: UFState, R0, R_end, L_h_next_start, t_bw_s):
+        """
+        前向传播:计算物理反洗能够去除的膜阻力
+        
+        参数:
+            state (UFState): 超滤运行状态变量对象,包含:
+                - slope: 不可逆污染增长斜率
+                - power: 不可逆污染增长幂次
+            phys  (UFPhysicsParams): 超滤运行固定参数对象,包含:
+                - tau_bw_s: 反洗时长影响的时间尺度
+
+            R0 (float): 本超级周期初始膜阻力
+            R_end (float): 过滤结束时的膜阻力(峰值)
+            L_h_next_start (float): 下一小周期起始时的累积运行时间 [小时]
+            t_bw_s (float): 物理反洗时长 [秒]
+        
+        返回:
+            float: 物理反洗实际去除的膜阻力量
+        
+        计算步骤:
+            1. 基于长期污染模型计算本周期的不可逆污染增量
+            2. 计算可逆污染量 = 当前总污染 - 不可逆污染
+            3. 应用时间因子(反洗时长的影响)
+            4. 返回实际去除的阻力(不超过可逆污染量)
+        """
+        # ========== 步骤1:计算不可逆污染累积 ==========
+        # 使用幂律模型描述长期不可逆污染的累积
+        # R_irr(t) = R0 + slope × t^power
+
+        # 下一小周期开始时的理论膜阻力
+        delta_R = state.slope * (L_h_next_start ** state.power)
+        R_next_start = R0 + delta_R
+
+        # ========== 步骤2:计算可逆污染 ==========
+        # 可逆污染 = 当前污染 - 理论长期污染
+        reversible_R = max(R_end - R_next_start, 0.0)
+        
+        # ========== 步骤3:计算反洗时间效率因子 ==========
+        # 使用指数衰减模型:time_gain = 1 - exp(-t_bw / τ)
+        # τ (tau_bw_s): 时间尺度参数
+        # - 反洗时间 t_bw = 0 时,time_gain = 0(无去除效果)
+        # - 反洗时间 t_bw → ∞ 时,time_gain → 1(达到最大效率)
+        # - 反洗时间 t_bw = τ 时,time_gain ≈ 0.632(去除63.2%)
+        # τ参考计算: 以60s去除效率高于95%的要求推算得τ取20s
+        # 该取值下:
+        # - 反洗时间 t_bw = 40s 时,time_gain ≈ 0.865(去除86.5%)
+        # - 反洗时间 t_bw = 60s 时,time_gain ≈ 0.950(去除95.0%)
+        tau = self.phys.tau_bw_s
+        time_gain = 1.0 - np.exp(-t_bw_s / tau)
+        
+        # ========== 步骤4:计算实际去除的膜阻力 ==========
+        # 实际去除量 = 可逆污染量 × 时间效率因子
+        dR_bw = reversible_R * time_gain
+        
+        # 确保去除量不超过可逆污染总量(物理约束)
+        return float(np.clip(dR_bw, 0.0, reversible_R))
+
+
+# ===== 主程序 =====
+if __name__ == "__main__":
+    phys = UFPhysicsParams()
+
+    model_fp = ResistanceIncreaseModel(phys)
+    model_bw = ResistanceDecreaseModel(phys)
+
+    torch.save(model_fp.state_dict(), "resistance_model_fp.pth")
+    torch.save(model_bw.state_dict(), "resistance_model_bw.pth")
+
+    print("模型已安全保存为 resistance_model_fp.pth、resistance_model_bw.pth")

+ 58 - 0
algorithm/uf_rl/env/uf_resistance_models_load.py

@@ -0,0 +1,58 @@
+import torch
+from pathlib import Path
+from algorithm.uf_rl.env.uf_resistance_models_define import ResistanceDecreaseModel, ResistanceIncreaseModel
+
+
+# ==================== 膜阻力模型加载函数 ====================
+def load_resistance_models(phys):
+    """
+    加载膜阻力预测模型(单例模式)
+
+    功能:
+    - 加载预训练的膜阻力上升模型和下降模型
+    - 使用全局变量实现单例模式,避免重复加载
+    - 仅在首次调用时执行加载操作
+
+    返回:
+        tuple: (resistance_model_fp, resistance_model_bw)
+            - resistance_model_fp: 过滤阶段阻力上升模型
+            - resistance_model_bw: 反洗阶段阻力下降模型
+
+    注意:
+    - 模型文件必须与本脚本位于同一目录
+    - 模型已设置为推理模式(eval),不会更新参数
+    """
+    # 声明全局变量(实现单例模式)
+    global resistance_model_fp, resistance_model_bw
+
+    # 检查模型是否已加载(避免重复加载)
+    if "resistance_model_fp" in globals() and resistance_model_fp is not None:
+        return resistance_model_fp, resistance_model_bw
+
+    print("🔄 正在加载膜阻力模型...")
+
+    # 初始化模型对象
+    resistance_model_fp = ResistanceIncreaseModel(phys)
+    resistance_model_bw = ResistanceDecreaseModel(phys)
+
+    # 获取当前脚本所在目录
+    base_dir = Path(__file__).resolve().parent
+
+    # 构造模型文件路径
+    fp_path = base_dir / "resistance_model_fp.pth"   # 过滤阶段模型
+    bw_path = base_dir / "resistance_model_bw.pth"   # 反洗阶段模型
+
+    # 检查模型文件是否存在
+    assert fp_path.exists(), f"缺少膜阻力上升模型文件: {fp_path.name}"
+    assert bw_path.exists(), f"缺少膜阻力下降模型文件: {bw_path.name}"
+
+    # 加载模型权重(map_location="cpu" 确保在没有GPU的环境也能运行)
+    resistance_model_fp.load_state_dict(torch.load(fp_path, map_location="cpu"))
+    resistance_model_bw.load_state_dict(torch.load(bw_path, map_location="cpu"))
+
+    # 设置为推理模式(禁用 dropout、batchnorm 等训练特性)
+    resistance_model_fp.eval()
+    resistance_model_bw.eval()
+
+    print("✅ 膜阻力模型加载成功!")
+    return resistance_model_fp, resistance_model_bw

+ 500 - 0
algorithm/uf_rl/rl_model/DQN/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模型是一个**决策优化系统**,通过深度强化学习学习在不同跨膜压差下的最优运行策略。相比传统方法:
+- **更智能**:能适应不同状态,无需人工调参
+- **更高效**:训练后推理快速
+- **更全面**:平衡多个矛盾目标
+
+但同时也需要:
+- **准确的模拟器**:保证学到的策略有效
+- **充分的训练**:探索足够多的状态-动作组合
+- **谨慎的部署**:实际应用前充分验证
+

+ 0 - 0
algorithm/uf_rl/rl_model/DQN/__init__.py


BIN
algorithm/uf_rl/rl_model/DQN/__pycache__/__init__.cpython-314.pyc


+ 0 - 0
algorithm/uf_rl/rl_model/DQN/dqn_model/__init__.py


+ 181 - 0
algorithm/uf_rl/rl_model/DQN/dqn_model/dqn_config_loader.py

@@ -0,0 +1,181 @@
+"""
+dqn_config_loader.py
+
+DQN 配置加载器,负责从YAML文件加载DQN超参数。
+与dqn_params.py同级,保持参数类的纯净性。
+"""
+
+import yaml
+from pathlib import Path
+from typing import Dict, Any, Union
+from rl_model.DQN.dqn_model.dqn_params import DQNParams
+
+
+
+
+class DQNConfigLoader:
+    """DQN配置加载器,从YAML加载并创建DQNParams实例"""
+
+    # DQNParams的字段列表(用于过滤)
+    DQN_FIELDS = [
+        'learning_rate',
+        'buffer_size',
+        'learning_starts',
+        'batch_size',
+        'gamma',
+        'train_freq',
+        'target_update_interval',
+        'tau',
+        'exploration_initial_eps',
+        'exploration_fraction',
+        'exploration_final_eps',
+        'remark'
+    ]
+
+    def __init__(self, config_path: Union[str, Path]):
+        """
+        初始化DQN配置加载器
+
+        Args:
+            config_path: YAML配置文件路径
+        """
+        self.config_path = Path(config_path)
+        self._config = None
+
+    @property
+    def config(self) -> Dict[str, Any]:
+        """懒加载配置"""
+        if self._config is None:
+            self._config = self._load_yaml()
+        return self._config
+
+    def _load_yaml(self) -> Dict[str, Any]:
+        """加载YAML文件"""
+        if not self.config_path.exists():
+            raise FileNotFoundError(f"DQN config file not found: {self.config_path}")
+
+        with open(self.config_path, 'r', encoding='utf-8') as f:
+            return yaml.safe_load(f)
+
+    def _filter_dqn_params(self, raw_config: Dict[str, Any]) -> Dict[str, Any]:
+        """
+        过滤出DQNParams需要的参数
+
+        Args:
+            raw_config: 原始配置字典
+
+        Returns:
+            只包含DQNParams字段的字典
+        """
+        return {k: v for k, v in raw_config.items() if k in self.DQN_FIELDS}
+
+    def load_params(self) -> DQNParams:
+        """
+        从YAML配置加载DQN参数
+
+        Returns:
+            DQNParams实例
+        """
+        filtered_config = self._filter_dqn_params(self.config)
+        return DQNParams(**filtered_config)
+
+    def validate_config(self) -> bool:
+        """
+        验证配置文件
+
+        Returns:
+            bool: 验证通过返回True
+        """
+        # 检查是否有未知参数(可选)
+        unknown_params = set(self.config.keys()) - set(self.DQN_FIELDS)
+        if unknown_params:
+            print(f"⚠️ Warning: Unknown parameters in DQN config: {unknown_params}")
+
+        # 检查必需参数(所有参数都有默认值,所以都是可选的)
+        print("✅ DQN config loaded successfully")
+        return True
+
+    def print_config_summary(self):
+        """打印配置摘要"""
+        params = self.load_params()
+        print("\n" + "=" * 50)
+        print("🤖 DQN 超参数配置(从YAML加载)")
+        print("=" * 50)
+        print(f"学习率 (learning_rate): {params.learning_rate}")
+        print(f"缓冲区大小 (buffer_size): {params.buffer_size}")
+        print(f"预热步数 (learning_starts): {params.learning_starts}")
+        print(f"批次大小 (batch_size): {params.batch_size}")
+        print(f"折扣因子 (gamma): {params.gamma}")
+        print(f"训练频率 (train_freq): {params.train_freq}")
+        print(f"目标网络更新间隔: {params.target_update_interval}")
+        print(f"软更新系数 (tau): {params.tau}")
+        print(f"初始探索率: {params.exploration_initial_eps}")
+        print(f"探索率衰减比例: {params.exploration_fraction}")
+        print(f"最终探索率: {params.exploration_final_eps}")
+        print(f"实验备注: {params.remark}")
+        print("=" * 50)
+
+
+# ========== 便捷函数 ==========
+
+def load_dqn_config(config_path: Union[str, Path]) -> DQNParams:
+    """
+    便捷函数:从YAML文件加载DQN配置
+
+    Args:
+        config_path: 配置文件路径
+
+    Returns:
+        DQNParams实例
+    """
+    loader = DQNConfigLoader(config_path)
+    return loader.load_params()
+
+
+def load_dqn_config_with_validation(config_path: Union[str, Path]) -> DQNParams:
+    """
+    加载并验证DQN配置
+
+    Args:
+        config_path: 配置文件路径
+
+    Returns:
+        DQNParams实例
+    """
+    loader = DQNConfigLoader(config_path)
+    loader.validate_config()
+    return loader.load_params()
+
+
+# ========== 测试代码 ==========
+if __name__ == "__main__":
+    # 测试配置加载
+    from pathlib import Path
+
+    # 假设配置文件在项目根目录的config文件夹中
+    current_dir = Path(__file__).parent
+    project_root = current_dir.parent.parent  # uf_rl
+    default_config = Path(__file__).parent.parent.parent.parent / "config" / "xishan_dqn_config.yaml"
+
+    if default_config.exists():
+        print(f"Testing DQN config loading from: {default_config}")
+
+        # 测试加载器
+        loader = DQNConfigLoader(default_config)
+
+        # 验证配置
+        loader.validate_config()
+
+        # 加载参数
+        dqn_params = loader.load_params()
+
+        # 打印摘要
+        loader.print_config_summary()
+
+        print("\n✅ DQN config loaded successfully!")
+        print(f"   Learning rate: {dqn_params.learning_rate}")
+        print(f"   Buffer size: {dqn_params.buffer_size}")
+        print(f"   Remark: {dqn_params.remark}")
+    else:
+        print(f"Config file not found: {default_config}")
+        print("Please create a DQN config file first.")

+ 94 - 0
algorithm/uf_rl/rl_model/DQN/dqn_model/dqn_params.py

@@ -0,0 +1,94 @@
+from dataclasses import dataclass
+
+# ==================== DQN超参数配置类 ====================
+@dataclass
+class DQNParams:
+    """
+    DQN 超参数配置类
+
+    功能:统一管理DQN算法的所有超参数
+
+    超参数说明:
+    - learning_rate: 神经网络学习率,控制梯度下降的步长
+    - buffer_size: 经验回放缓冲区大小,存储历史经验
+    - learning_starts: 开始训练前先收集的经验数量(warm-up)
+    - batch_size: 每次训练采样的batch大小
+    - gamma: 折扣因子,权衡即时奖励和长期奖励
+    - train_freq: 训练频率,每隔多少步训练一次
+    - target_update_interval: 目标网络更新频率
+    - tau: 软更新系数(soft update)
+    - exploration_*: ε-贪心策略的探索率参数
+    """
+    # ========== 神经网络参数 ==========
+    learning_rate: float = 1e-4
+    # 学习率,控制神经网络权重更新的步长
+    # 典型范围:1e-5 ~ 1e-3
+    # 过大:训练不稳定;过小:收敛慢
+
+    # ========== 经验回放参数 ==========
+    buffer_size: int = 100000
+    # 经验回放缓冲区大小(可存储的transition数量)
+    # 作用:打破样本间的时间相关性,提高训练稳定性
+    # 建议:至少存储几个完整episode的经验
+
+    learning_starts: int = 10000
+    # 开始训练前先收集的步数(预填充缓冲区)
+    # 作用:确保缓冲区有足够的多样性样本再开始训练
+    # 建议:设为buffer_size的10%-20%
+
+    batch_size: int = 32
+    # 每次训练从缓冲区采样的样本数量
+    # 典型值:32, 64, 128, 256
+    # 过大:显存占用高,训练慢;过小:梯度估计不准确
+
+    # ========== 强化学习参数 ==========
+    gamma: float = 0.95
+    # 折扣因子(discount factor),γ ∈ [0, 1]
+    # 作用:权衡即时奖励和长期奖励
+    # γ=0:只考虑当前奖励(短视)
+    # γ=1:完全考虑未来奖励(长视)
+    # 通常设为0.9-0.99
+
+    train_freq: int = 4
+    # 训练频率:每收集多少步执行一次训练
+    # 作用:平衡数据收集和网络更新
+    # 典型值:1(每步训练)或4-16(批量训练)
+
+    # ========== 目标网络参数 ==========
+    target_update_interval: int = 1
+    # 目标网络更新间隔(硬更新)
+    # 作用:目标网络每隔多少次训练更新一次
+    # 注:使用软更新(tau)时此参数通常设为1
+
+    tau: float = 0.005
+    # 软更新系数(soft update)
+    # θ_target = τ×θ + (1-τ)×θ_target
+    # τ=1:硬更新(完全复制)
+    # τ<<1:软更新(平滑过渡,更稳定)
+    # 典型值:0.001 - 0.01
+
+    # ========== 探索策略参数(ε-greedy) ==========
+    exploration_initial_eps: float = 1.0
+    # 初始探索率 ε_0
+    # ε=1:完全随机探索
+    # ε=0:完全利用已学知识
+
+    exploration_fraction: float = 0.3
+    # 探索率衰减比例
+    # 表示训练总步数的前30%进行ε衰减
+    # 例:总共10万步,前3万步ε从1.0衰减到0.02
+
+    exploration_final_eps: float = 0.02
+    # 最终探索率 ε_final
+    # 衰减结束后保持此值(保留小概率探索)
+    # 典型值:0.01 - 0.05
+
+    # ========== 日志参数 ==========
+    remark: str = "default"
+    # 实验备注,用于区分不同训练实验
+    # 会自动添加到TensorBoard日志目录名中
+
+
+
+
+

+ 332 - 0
algorithm/uf_rl/rl_model/DQN/dqn_model/dqn_statebuilder.py

@@ -0,0 +1,332 @@
+from typing import Dict
+from dataclasses import replace
+
+import numpy as np
+import pandas as pd
+from sklearn.metrics import r2_score
+
+from env.env_params import UFState
+
+from uf_data_process.load import UFConfigLoader
+from uf_data_process.label import UFEventClassifier, PostBackwashInletMarker
+from uf_data_process.filter import ConstantFlowFilter,EventQualityFilter, InletSegmentFilter,FlowOutlierFilter
+from uf_data_process.calculate import UFResistanceCalculator, UFResistanceAnalyzer
+from uf_data_process.fit import ShortTermCycleFoulingFitter, LongTermFoulingFitter
+
+class DQNStateBuilder:
+    """
+    在 DQN 决策前构建状态的工具类
+
+    相关数据:
+        * CSV1 = 上一完整化学周期
+        * CSV2 = 新周期初始进水段
+        * CSV3 = 新周期预测进水段
+    """
+
+    def __init__(self, config_path: str):
+        """
+        Parameters
+        ----------
+        config_path : str
+            uf_analyze_config.yaml 路径
+        """
+        self.cfg = UFConfigLoader(config_path)
+        uf_cfg = self.cfg.uf
+        params = self.cfg.params
+
+
+        self.units = uf_cfg.get("units", ["UF1", "UF2", "UF3", "UF4"])
+        self.stable_inlet_code = uf_cfg.get("stable_inlet_code", [24.0, 26.0])
+
+        column_formats = uf_cfg.get("column_formats", {})
+        self.ctrl_format = column_formats.get("ctrl_col", "C.M.{unit}_DB@word_control")
+        self.flow_format = column_formats.get("flow_col", "C.M.{unit}_FT_JS@out")
+        self.tmp_format = column_formats.get("tmp_col", "C.M.{unit}_DB@press_PV")
+        self.per_format = column_formats.get("per_col", "{unit}Per")
+        self.temp_col = column_formats.get("temp_col", "C.M.RO_TT_ZJS@out")
+
+
+        # 过滤器
+        self.min_points = params.get("min_points", 20)
+        self.initial_points = params.get("initial_points", 10)
+
+        self.quality_filter = EventQualityFilter(min_points=self.min_points)
+        self.flow_filter = FlowOutlierFilter(n_sigma=3)
+        self.initial_label = PostBackwashInletMarker(n_points=self.initial_points)
+
+        # 阻力计算器
+        self.res_calc = UFResistanceCalculator(self.units, area_m2=uf_cfg["area_m2"], scale_factor=params.get("scale_factor", 1e10))
+        self.segment_head_n = params.get("segment_head_n", 10)
+        self.segment_tail_n = params.get("segment_tail_n", 10)
+
+    # ======================================================================
+    # 对外主接口
+    # ======================================================================
+
+    def build_from_csv_pair(
+        self,
+        unit_name,
+        uf_state_default,
+        state_bounds,
+        prev_cycle_csv: str,
+        init_cycle_csv: str,
+        predict_cycle_csv: str,
+    ) -> UFState:
+        """
+        使用【上一完整化学周期 CSV】+【当前周期初始 CSV】+ 【当前周期预测 CSV】构建 UFState
+        """
+
+        df_prev = pd.read_csv(prev_cycle_csv)
+        df_init = pd.read_csv(init_cycle_csv)
+        df_predict = pd.read_csv(predict_cycle_csv)
+
+        # 分别处理两个 CSV
+        prev_features = self._analyze_previous_cycle_csv(df_prev, unit_name, uf_state_default, state_bounds)
+        init_features = self._analyze_init_cycle_csv(df_init, unit_name, uf_state_default, state_bounds)
+
+        # 化学清洗去除阻力(上一周期末 - 当前初始)
+        ceb_removal = max(
+            prev_features["R_end"] - init_features["R_start"],
+            0.0
+        )
+
+        # 使用df_predict修正 nuk
+        corrected_nuk = self._correct_nuk_with_predict(
+            df_predict=df_predict,
+            unit_name=unit_name,
+            R_start=init_features["R_start"],
+            q_mean=init_features["q_mean"],
+            temp_celsius=init_features["temp_mean"],
+            base_nuk=prev_features["nuK"],
+            uf_state_default=uf_state_default,
+        )
+
+        # 构建 UFState
+        current_state = replace(
+            uf_state_default,
+            TMP=init_features["tmp_mean"],
+            q_UF=init_features["q_mean"],
+            temp=init_features["temp_mean"],
+            R = init_features["R_start"],
+            nuK = corrected_nuk,
+            slope=prev_features["slope"],
+            power=prev_features["power"],
+            ceb_removal=ceb_removal,
+        )
+
+        return current_state
+
+    # ======================================================================
+    # 上一完整化学周期分析
+    # ======================================================================
+
+    def _analyze_previous_cycle_csv(
+        self,
+        df,
+        unit_name,
+        uf_state_default,
+        state_bounds
+    ) -> Dict[str, float]:
+        """
+        上一完整化学周期分析逻辑
+
+        步骤:
+        1. 事件标注
+        2. 进水段过滤(质量过滤)
+        3. 膜阻力计算
+        4. 提取周期末稳定阻力
+        5. 拟合 nuK
+        6. 拟合长期不可逆污染(slope / power)
+        """
+
+        ctrl_col = self.ctrl_format.format(unit=unit_name)
+        flow_col = self.flow_format.format(unit=unit_name)
+        tmp_col = self.tmp_format.format(unit=unit_name)
+
+        # 事件标注
+        event_clf = UFEventClassifier(unit_name, self.cfg.uf["inlet_codes"],
+                                      self.cfg.uf["physical_bw_code"], self.cfg.uf["chemical_bw_code"],
+                                      ctrl_col)
+        df_unit = event_clf.classify(df)  # 产生 event_type 列
+        df_unit_mark = self.initial_label.mark(df_unit)  # 标记反冲洗事件后的前 N 个进水点
+        seg_df = event_clf.segment(df_unit_mark)  # 根据 event_type 列编号事件段落
+
+        # 对 seg_df 进行按 segment 分组后逐段过滤:
+        const_flow_filter = ConstantFlowFilter(flow_col=flow_col, repeat_len=20)
+        segments = const_flow_filter.filter(seg_df)  # 去除出现网络错误的进水段
+        segments = self.quality_filter.filter(segments)  # 去除时间过短的进水段
+        # 提取稳定进水段
+        stable_extractor = InletSegmentFilter(ctrl_col, stable_codes=self.stable_inlet_code, min_points=self.min_points)
+        stable_segments = stable_extractor.extract(segments)  # 提取稳定进水数据
+
+        if len(stable_segments) == 0:
+            raise ValueError("上一周期无有效稳定进水段,无法构建状态,请使用run_dqn_decide.py")
+
+        # 膜阻力计算
+        stable_segments = self.res_calc.calculate_for_segments(
+            stable_segments,
+            temp_col=self.temp_col,
+            flow_col=flow_col,
+            tmp_col=tmp_col,
+        )
+
+        # -------- 膜阻力统计 --------
+        res_col = f"{unit_name}_R_scaled"
+        ura = UFResistanceAnalyzer(
+            resistance_col=res_col,
+            head_n=self.segment_head_n,
+            tail_n=self.segment_tail_n
+        )
+        stable_segments = ura.analyze_segments(stable_segments)
+        df_all = stable_segments[-1]
+        R_end = df_all["R_scaled_end"].iloc[0]
+
+        # ===== 确保 time 为 datetime =====
+        for i, seg in enumerate(stable_segments):
+            if not pd.api.types.is_datetime64_any_dtype(seg["time"]):
+                seg = seg.copy()
+                seg["time"] = pd.to_datetime(seg["time"], errors="coerce")
+                seg = seg.dropna(subset=["time"])
+                stable_segments[i] = seg
+
+        # -------- 5️⃣ 短期污染拟合(nuK)--------
+        st_fitter = ShortTermCycleFoulingFitter(unit_name)
+        nuK, st_r2 = st_fitter.fit_cycle(stable_segments)
+        if (
+                pd.isna(nuK)
+                or pd.isna(st_r2)
+                or not np.isfinite(nuK)
+                or st_r2 < 0.4
+        ):
+            nuK = uf_state_default.nuK
+
+        # -------- 6️⃣ 长期不可逆污染拟合 --------
+        lt_fitter = LongTermFoulingFitter(unit_name)
+        slope, power, lt_r2 = lt_fitter.fit_cycle(stable_segments)
+        if (
+                pd.isna(slope)
+                or pd.isna(power)
+                or pd.isna(lt_r2)
+                or not np.isfinite(slope)
+                or not np.isfinite(power)
+                or lt_r2 < 0.4
+        ):
+            slope = uf_state_default.slope
+            power = uf_state_default.power
+
+        return {
+            "R_end": R_end,
+            "nuK": float(nuK),
+            "slope": float(slope),
+            "power": float(power),
+        }
+
+    # ======================================================================
+    # 当前周期初始进水段分析
+    # ======================================================================
+
+    def _analyze_init_cycle_csv(
+        self,
+        df,
+        unit_name,
+        uf_state_default,
+        state_bounds
+    ) -> Dict[str, float]:
+        """
+        当前周期初始进水段分析
+        """
+
+        flow_col = self.flow_format.format(unit=unit_name)
+        tmp_col = self.tmp_format.format(unit=unit_name)
+        temp_col = self.temp_col
+        res_col = f"{unit_name}_R_scaled"
+
+        segments = [df]
+        segments = self.res_calc.calculate_for_segments(
+            segments,
+            temp_col=self.temp_col,
+            flow_col=flow_col,
+            tmp_col=tmp_col,
+        )
+        df = segments[-1]
+
+        return {
+            "q_mean": float(df[flow_col].mean()),
+            "tmp_mean": float(df[tmp_col].mean()),
+            "temp_mean": float(df[temp_col].mean()),
+            "R_start": float(df[res_col].mean()),
+        }
+
+    def _correct_nuk_with_predict(
+            self,
+            df_predict,
+            unit_name,
+            R_start,
+            q_mean,
+            temp_celsius,
+            base_nuk,
+            uf_state_default,
+    ):
+        """
+        使用 predict_csv 中预测的渗透率,
+        对短期污染参数 nuK 进行在线修正。
+        """
+
+        per_col = self.per_format.format(unit=unit_name)
+        if per_col not in df_predict.columns:
+            return base_nuk
+        per = pd.to_numeric(
+            df_predict[per_col],
+            errors="coerce"
+        )
+        mask = (
+                np.isfinite(per)
+                & (per > 0)
+        )
+        per = per[mask]
+
+        if len(per) < 5:
+            return base_nuk
+
+
+        mu = self.res_calc.xishan_viscosity(temp_celsius)
+        R = 3.6e11 / (mu * per)
+        R_scaled = R / self.res_calc.scale_factor
+        R_scaled = (R_scaled- R_scaled.iloc[0]+ R_start)
+        t = np.arange(len(R_scaled)) * 60.0
+        J_const = q_mean / self.res_calc.A / 3600.0
+
+        if not np.isfinite(J_const) or J_const <= 0:
+            return base_nuk
+
+        x = J_const * t
+
+        try:
+            coef = np.polyfit(x, R_scaled, 1)
+            predict_nuk = float(coef[0])
+            pred = np.polyval(coef, x)
+            r2 = r2_score(R_scaled, pred)
+
+        except Exception:
+            return base_nuk
+
+        if (
+                not np.isfinite(r2)
+                or not np.isfinite(predict_nuk)
+                or predict_nuk <= 0
+                or r2 < 0.4
+        ):
+            return base_nuk
+
+        corrected_nuk = (
+                0.7 * base_nuk
+                + 0.3 * predict_nuk
+        )
+
+        if (
+                not np.isfinite(corrected_nuk)
+                or corrected_nuk <= 0
+        ):
+            return uf_state_default.nuK
+
+        return float(corrected_nuk)

+ 0 - 0
algorithm/uf_rl/rl_model/DQN/uf_decide/__init__.py


BIN
algorithm/uf_rl/rl_model/DQN/uf_decide/__pycache__/__init__.cpython-314.pyc


BIN
algorithm/uf_rl/rl_model/DQN/uf_decide/__pycache__/dqn_decider.cpython-314.pyc


BIN
algorithm/uf_rl/rl_model/DQN/uf_decide/__pycache__/run_dqn_decide.cpython-314.pyc


+ 186 - 0
algorithm/uf_rl/rl_model/DQN/uf_decide/dqn_decider.py

@@ -0,0 +1,186 @@
+"""
+UF 超滤系统 DQN 决策脚本(与当前 DQNTrainer 严格对齐)
+
+功能定位:
+- 加载已训练好的 DQN 模型
+- 构造与训练阶段完全一致的环境
+- 执行单步动作推理(predict)
+- 输出模型建议的工程动作参数(L_s, t_bw_s)
+
+注意:
+- 本脚本【不 step 环境】
+- 不计算 reward
+- 不进行 episode rollout
+"""
+
+from pathlib import Path
+import numpy as np
+
+# ============================================================
+# 1. UF 环境与物理模型
+# ============================================================
+from algorithm.uf_rl.env.uf_env import UFSuperCycleEnv
+from algorithm.uf_rl.env.env_params import (
+    UFRewardParams,
+    UFActionSpec,
+    UFStateBounds,
+)
+
+# ============================================================
+# 2. Stable-Baselines3
+# ============================================================
+from stable_baselines3 import DQN
+
+
+# ============================================================
+# 3. DQN 决策器
+# ============================================================
+class UFDQNDecider:
+    """
+    UF 超滤 DQN 决策器(Inference Only)
+
+    设计原则:
+    1. 与训练环境参数级一致
+    2. 决策侧不推进环境
+    3. 不依赖 Trainer 内部状态
+    """
+
+    def __init__(
+        self,
+        physics,
+        action_spec,
+        reward_params,
+        state_bounds,
+        model_path,
+        seed: int = 0,
+    ):
+        """
+        Parameters
+        ----------
+        model_path
+            dqn_model.zip 的路径
+        reset_state_pool :
+            ResetStatePoolLoader.split() 得到的 pool(train / val 均可)
+        seed : int
+            随机种子(推理阶段主要用于 env.reset)
+        """
+
+        self.action_spec = action_spec
+        reward_params = reward_params
+        state_bounds = state_bounds
+
+        self.env = UFSuperCycleEnv(
+            physics=physics,
+            reward_params=reward_params,
+            action_spec=self.action_spec,
+            statebounds=state_bounds,
+            real_state_pool=None,
+            RANDOM_SEED=seed,
+        )
+
+        model_path = Path(model_path)
+        if not model_path.exists():
+            raise FileNotFoundError(f"DQN 模型不存在: {model_path}")
+
+        self.model = DQN.load(
+            path=str(model_path),
+            env=self.env,          # ⚠ 必须提供 env
+        )
+
+    # ========================================================
+    # 对外决策接口
+    # ========================================================
+    def decide(self, state: np.ndarray | None = None) -> dict:
+        """
+        单步决策(不 step 环境)
+
+        Parameters
+        ----------
+        state : np.ndarray | None
+            - None:env.reset() 从 reset_state_pool 抽样状态
+            - 非 None:使用外部系统提供的状态
+
+        Returns
+        -------
+        dict
+            {
+                "action_id": int,
+                "L_s": float,
+                "t_bw_s": float,
+            }
+        """
+
+        # ----------------------------------------------------
+        # 4.1 获取观测状态
+        # ----------------------------------------------------
+        if state is None:
+            obs = self.env.reset()
+        else:
+            obs = self.env.get_obs(state) # 获取归一化状态作为策略网络输入
+
+        # ----------------------------------------------------
+        # 4.2 DQN 推理(确定性)
+        # ----------------------------------------------------
+        action, _ = self.model.predict(obs, deterministic=True)
+        action_id = int(action)
+
+
+        # ----------------------------------------------------
+        # 4.3 动作解码(工程语义)
+        # ----------------------------------------------------
+        L_s, t_bw_s = self.env.get_action_values(action_id)
+
+        return {
+            "action_id": action_id,
+            "L_s": L_s,
+            "t_bw_s": t_bw_s,
+        }
+
+
+# ============================================================
+# 5. 示例调用(调试用)
+# ============================================================
+if __name__ == "__main__":
+
+    from data_to_rl import ResetStatePoolLoader
+
+    # --------------------------------------------------------
+    # 模型路径(来自 Trainer.save())
+    # --------------------------------------------------------
+    MODEL_PATH = Path(
+        "models/uf_rl/model_result/uf_dqn_tensorboard/xxx/dqn_model.zip"
+    )
+
+    # --------------------------------------------------------
+    # Reset state pool
+    # --------------------------------------------------------
+    RESET_STATE_CSV = Path(
+        "datasets/rl_ready/output/reset_state_pool.csv"
+    )
+
+    loader = ResetStatePoolLoader(
+        csv_path=RESET_STATE_CSV,
+        train_ratio=0.8,
+        shuffle=False,
+        random_state=2025,
+    )
+
+    _, val_pool = loader.split()
+
+    # --------------------------------------------------------
+    # 初始化决策器
+    # --------------------------------------------------------
+    decider = UFDQNDecider(
+        model_path=MODEL_PATH,
+        reset_state_pool=val_pool,
+    )
+
+    # --------------------------------------------------------
+    # 执行一次决策
+    # --------------------------------------------------------
+    decision = decider.decide()
+
+    print("===== DQN 决策结果 =====")
+    print(f"Action ID : {decision['action_id']}")
+    print(f"L_s       : {decision['L_s']} s")
+    print(f"t_bw_s    : {decision['t_bw_s']} s")

+ 334 - 0
algorithm/uf_rl/rl_model/DQN/uf_decide/run_dqn_decide.py

@@ -0,0 +1,334 @@
+"""
+run_dqn_decide.py
+
+UF 超滤 DQN 决策主入口(Inference / Online Assist)
+
+职责:
+1. 构造物理世界(physics)
+2. 实例化决策器(UFDQNDecider)
+3. 构造当前工厂状态(observation)
+4. 调用模型给出策略建议
+5. 生成 PLC 下发指令(限幅 / 限速)
+6. 评估该指令在物理模型下的效果(只评估,不下发)
+"""
+
+from pathlib import Path
+from dataclasses import replace
+
+
+# ============================================================
+# 导入模块
+# ============================================================
+CURRENT_DIR = Path(__file__).resolve().parent
+
+UF_RL_ROOT = CURRENT_DIR.parents[2]     # uf_train  # uf_rl
+
+# ========== 参数 / 物理 ==========
+from algorithm.uf_rl.env.uf_resistance_models_load import load_resistance_models
+from algorithm.uf_rl.env.uf_physics import UFPhysicsModel
+from algorithm.uf_rl.env.env_params import UFState, UFActionSpec
+from algorithm.uf_rl.env.env_config_loader import EnvConfigLoader, create_env_params_from_yaml
+
+# ========== 决策器 ==========
+from algorithm.uf_rl.rl_model.DQN.uf_decide.dqn_decider import UFDQNDecider
+
+
+def build_physics(IS_TIMES, phys_params,state_bounds):
+    """
+    构造与训练一致的物理模型(只做一次)
+    """
+    res_fp, res_bw = load_resistance_models(phys_params)
+
+    physics = UFPhysicsModel(
+        phys_params=phys_params,
+        state_bounds=state_bounds,
+        resistance_model_fp=res_fp,
+        resistance_model_bw=res_bw,
+        IS_TIMES = IS_TIMES
+    )
+    return physics
+
+
+def check_state_bounds(current_state, state_bounds, unit_name):
+    """
+    检查当前状态是否在边界范围内
+
+    参数:
+        current_state: UFState对象,包含TMP, q_UF, temp
+        state_bounds: 状态边界对象
+        unit_name: 机组名称(如 "UF1")
+
+    返回:
+        dict: 错误信息字典,格式 {"error_time": str, "error_feature": str}
+              如果没有错误,返回 None
+    """
+    from datetime import datetime
+
+    error_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+
+    # 检查各项参数是否在边界范围内
+    TMP0_min = state_bounds.TMP0_min
+    TMP0_max = state_bounds.TMP0_max
+    if not (TMP0_min <= current_state.TMP <= TMP0_max):
+        return {
+            "error_time": error_time,
+            "error_feature": f"{unit_name}Per"
+        }
+
+    return None
+
+
+def generate_plc_instructions(action_spec,current_L_s, current_t_bw_s, model_prev_L_s, model_prev_t_bw_s, model_L_s, model_t_bw_s):
+    """
+    根据工厂当前值、模型上一轮决策值和模型当前轮决策值,生成PLC指令。
+
+    新增功能:
+    1. 处理None值情况:如果模型上一轮值为None,则使用工厂当前值;
+       如果工厂当前值也为None,则返回None并提示错误。
+    """
+
+    action_spec = action_spec
+    adjustment_threshold = 1.0
+
+    # 处理None值情况
+    if model_prev_L_s is None:
+        if current_L_s is None:
+            print("错误: 过滤时长的工厂当前值和模型上一轮值均为None")
+            return None, None
+        else:
+            # 使用工厂当前值作为基准
+            effective_current_L = current_L_s
+            source_L = "工厂当前值(模型上一轮值为None)"
+    else:
+        # 模型上一轮值不为None,继续检查工厂当前值
+        if current_L_s is None:
+            effective_current_L = model_prev_L_s
+            source_L = "模型上一轮值(工厂当前值为None)"
+        else:
+            effective_current_L = model_prev_L_s
+            source_L = "模型上一轮值"
+
+    # 对反洗时长进行同样的处理
+    if model_prev_t_bw_s is None:
+        if current_t_bw_s is None:
+            print("错误: 反洗时长的工厂当前值和模型上一轮值均为None")
+            return None, None
+        else:
+            effective_current_t_bw = current_t_bw_s
+            source_t_bw = "工厂当前值(模型上一轮值为None)"
+    else:
+        if current_t_bw_s is None:
+            effective_current_t_bw = model_prev_t_bw_s
+            source_t_bw = "模型上一轮值(工厂当前值为None)"
+        else:
+            effective_current_t_bw = model_prev_t_bw_s
+            source_t_bw = "模型上一轮值"
+
+    # 检测所有输入值是否在规定范围内(只对非None值进行检查)
+    # 工厂当前值检查(警告)
+    if current_L_s is not None and not (action_spec.L_min_s <= current_L_s <= action_spec.L_max_s):
+        print(f"警告: 当前过滤时长 {current_L_s} 秒不在允许范围内 [{action_spec.L_min_s}, {action_spec.L_max_s}]")
+    if current_t_bw_s is not None and not (action_spec.t_bw_min_s <= current_t_bw_s <= action_spec.t_bw_max_s):
+        print(f"警告: 当前反洗时长 {current_t_bw_s} 秒不在允许范围内 [{action_spec.t_bw_min_s}, {action_spec.t_bw_max_s}]")
+
+    # 模型上一轮决策值检查(警告)
+    if model_prev_L_s is not None and not (action_spec.L_min_s <= model_prev_L_s <= action_spec.L_max_s):
+        print(f"警告: 模型上一轮过滤时长 {model_prev_L_s} 秒不在允许范围内 [{action_spec.L_min_s}, {action_spec.L_max_s}]")
+    if model_prev_t_bw_s is not None and not (action_spec.t_bw_min_s <= model_prev_t_bw_s <= action_spec.t_bw_max_s):
+        print(f"警告: 模型上一轮反洗时长 {model_prev_t_bw_s} 秒不在允许范围内 [{action_spec.t_bw_min_s}, {action_spec.t_bw_max_s}]")
+
+    # 模型当前轮决策值检查
+    if model_L_s is None:
+        raise ValueError("错误: 决策模型建议的过滤时长不能为None")
+    model_L_s = max(action_spec.L_min_s, min(model_L_s, action_spec.L_max_s))
+
+    if model_t_bw_s is None:
+        raise ValueError("错误: 决策模型建议的反洗时长不能为None")
+    model_t_bw_s = max(action_spec.t_bw_min_s, min(model_t_bw_s, action_spec.t_bw_max_s))
+
+
+    print(f"过滤时长基准: {source_L}, 值: {effective_current_L}")
+    print(f"反洗时长基准: {source_t_bw}, 值: {effective_current_t_bw}")
+
+    # 使用选定的基准值进行计算调整
+    L_diff = model_L_s - effective_current_L
+    L_adjustment = 0
+    if abs(L_diff) >= adjustment_threshold * action_spec.L_step_s:
+        if L_diff >= 0:
+            L_adjustment = action_spec.L_step_s
+        else:
+            L_adjustment = -action_spec.L_step_s
+    next_L_s = effective_current_L + L_adjustment
+
+    t_bw_diff = model_t_bw_s - effective_current_t_bw
+    t_bw_adjustment = 0
+    if abs(t_bw_diff) >= adjustment_threshold * action_spec.t_bw_step_s:
+        if t_bw_diff >= 0:
+            t_bw_adjustment = action_spec.t_bw_step_s
+        else:
+            t_bw_adjustment = -action_spec.t_bw_step_s
+    next_t_bw_s = effective_current_t_bw + t_bw_adjustment
+
+    return next_L_s, next_t_bw_s
+
+
+
+def calc_uf_cycle_metrics(current_state, max_tmp_during_filtration, min_tmp_during_filtration, L_s: float, t_bw_s: float):
+    """
+    计算 UF 超滤系统的核心性能指标
+
+    参数:
+
+        L_s (float): 单次过滤时间(秒)
+        t_bw_s (float): 单次反洗时间(秒)
+
+    返回:
+        dict: {
+            "k_bw_per_ceb": 小周期次数,
+            "ton_water_energy_kWh_per_m3": 吨水电耗,
+            "recovery": 回收率,
+            "net_delivery_rate_m3ph": 净供水率 (m³/h),
+            "daily_prod_time_h": 日均产水时间 (小时/天)
+            "max_permeability": 全周期最高渗透率(lmh/bar)
+        }
+    """
+
+    # 模拟该参数下的超级周期
+    info, next_state = physics.simulate_one_supercycle(current_state, L_s=L_s, t_bw_s=t_bw_s)
+
+    # 获得模型模拟周期信息
+    k_bw_per_ceb = info["k_bw_per_ceb"]
+    refer_ton_water_energy = info["refer_ton_water_energy"]
+    ton_water_energy = info["ton_water_energy"]
+    recovery = info["recovery"]
+    daily_prod_time_h = info["daily_prod_time_h"]
+
+    # 获得模型模拟周期内最高跨膜压差/最低跨膜压差
+    if max_tmp_during_filtration is None:
+        max_tmp_during_filtration = info["max_TMP_during_filtration"]
+    if min_tmp_during_filtration is None:
+        min_tmp_during_filtration = info["min_TMP_during_filtration"]
+
+    # 计算最高渗透率
+    max_permeability = 100 * current_state.q_UF / (128*40) / min_tmp_during_filtration
+
+
+    return {
+        "k_bw_per_ceb": k_bw_per_ceb,
+        "refer_ton_water_energy": refer_ton_water_energy,
+        "ton_water_energy": ton_water_energy,
+        "recovery": recovery,
+        "daily_prod_time_h": daily_prod_time_h,
+        "max_permeability": max_permeability
+    }
+
+def run_dqn_decide(
+    model_path: Path,
+    physics,
+    action_spec,
+    reward_params,
+    state_bounds,
+# -------- 工厂当前值 --------
+    current_state: UFState
+):
+    """
+    单轮 DQN 决策流程
+    """
+
+    # 构造决策器
+    decider = UFDQNDecider(
+        physics=physics,
+        action_spec=action_spec,
+        reward_params=reward_params,
+        state_bounds=state_bounds,
+        model_path=model_path,
+        seed=0,
+    )
+
+    # 模型决策
+    decision = decider.decide(current_state)
+    action_id = decision["action_id"]
+    model_L_s = decision["L_s"]
+    model_t_bw_s = decision["t_bw_s"]
+
+    return action_id, model_L_s, model_t_bw_s
+
+
+# ==============================
+# 示例调用
+# ==============================
+if __name__ == "__main__":
+
+    # ========== 模型及配置路径指定 ==========
+    IS_TIMES = False # 外部指定变量,表示CEB间隔为时间控制/次数控制,T表示48次bw一次CEB,F表示48h一次CEB
+    MODEL_PATH = UF_RL_ROOT / "config_and_model" / "anzhen" / "48h_dqn_model.zip" # 需根据IS_TIMES变量值指定模型为48h_dqn_model.zip/48times_dqn_model.zip
+    ENV_CONFIG_PATH = UF_RL_ROOT / "config_and_model" / "anzhen" / "env_config.yaml" # 环境配置路径
+
+    # ========== 外部调用输入 ==========
+    # 轻量版,仅输入当前周期起始状态变量
+    units_to_run = ["UF1"] # 新增输入:本次调用的机组对象名
+    TMP0 = 0.07  # 原始 TMP0
+    q_UF = 300 # 进水流量
+    temp = 20.0 #进水温度
+
+    # ========== 模型及配置加载 ==========
+    config_loader = EnvConfigLoader(ENV_CONFIG_PATH)
+    config_loader.validate_config()
+    config_loader.print_config_summary()
+    (
+        uf_state_default,  # UFState默认值
+        phys_params,  # UFPhysicsParams
+        action_spec,  # UFActionSpec
+        reward_params,  # UFRewardParams
+        state_bounds  # UFStateBounds
+    ) = create_env_params_from_yaml(ENV_CONFIG_PATH)
+    physics = build_physics(IS_TIMES, phys_params,state_bounds)
+
+    # ========== 调用模型生成模型指令 ==========
+    # 基于外部输入构建当前状态
+    current_state = replace(
+        uf_state_default,
+        TMP=TMP0,
+        q_UF=q_UF,
+        temp=temp
+    )
+
+    # 状态异常检查(仅检查,不中断,出现异常时后续归一化中将异常状态强制归一化至上下限)
+    for unit_name in units_to_run:
+        error_result = check_state_bounds(current_state, state_bounds, unit_name)
+        if error_result:
+            print(f"错误发生时间: {error_result['error_time']};错误特征量:{error_result['error_feature']}")
+
+    # 模型输出指令
+    action_id, model_L_s, model_t_bw_s = run_dqn_decide(
+        model_path=MODEL_PATH,
+        physics=physics,
+        action_spec=action_spec,
+        reward_params=reward_params,
+        state_bounds=state_bounds,
+        current_state=current_state,
+    ) # 环境实例化,模型加载等功能放在UFDQNDecider类中
+
+    # ========== 生成工厂下发指令 ==========
+    current_L_s = 3800
+    current_t_bw_s = 40
+    model_prev_L_s = 4040
+    model_prev_t_bw_s = 60
+    L_s, t_bw_s = generate_plc_instructions(action_spec, current_L_s, current_t_bw_s, model_prev_L_s, model_prev_t_bw_s, model_L_s,
+                                            model_t_bw_s)  # 获取模型下发指令
+
+    # ========== 生成指令模拟执行结果 ==========
+    max_tmp_during_filtration = 0.050176 # 新增工厂数据接口:周期最高/最低跨膜压差,无工厂数据接入时传入None,calc_uf_cycle_metrics()自动获取模拟周期中的跨膜压差最值
+    min_tmp_during_filtration = 0.012496
+    execution_result = calc_uf_cycle_metrics(current_state, max_tmp_during_filtration, min_tmp_during_filtration, L_s, t_bw_s)
+    print("\n===== 单步决策结果 =====")
+    print(f"模型选择的动作: {action_id}")
+    print(f"模型选择的L_s: {model_L_s} 秒, 模型选择的t_bw_s: {model_t_bw_s} 秒")
+    print(f"指令下发的L_s: {L_s} 秒, 指令下发的t_bw_s: {t_bw_s} 秒")
+    print(f"指令对应的反洗次数: {execution_result['k_bw_per_ceb']}")
+    print(f"指令对应的理论参考吨水电耗: {execution_result['refer_ton_water_energy']}")
+    print(f"指令对应的计算吨水电耗: {execution_result['ton_water_energy']}")
+    print(f"指令对应的回收率: {execution_result['recovery']}")
+    print(f"指令对应的日均产水时间: {execution_result['daily_prod_time_h']}")
+    print(f"指令对应的最高渗透率: {execution_result['max_permeability']}")
+

+ 377 - 0
algorithm/uf_rl/rl_model/DQN/uf_decide/run_dqn_decide_totalstate.py

@@ -0,0 +1,377 @@
+"""
+run_dqn_decide_totalstate.py
+
+全状态 UF 超滤 DQN 决策主入口(Inference / Online Assist)
+
+要求数据:
+1. UF_prev_cycle.csv 示例数据,实际路径可更改,水厂上一周期完整数据,必须包含上一周期全部数据,不能包含其他周期数据,分辨率1min,至少包含时间,控制字/步序,进水流量,跨膜压差,温度
+2. UF_init_cycle.csv 示例数据,实际路径可更改,水厂当前周期初始实际数据,在水厂当前周期进水稳定后读取并传入,至少包含时间,控制字/步序,进水流量,跨膜压差,温度
+3. UF_predict_cycle.csv 示例数据,实际路径可更改,水厂当前预测数据,在调用决策模型前调用预测模型生成,包含未来20min的时间及渗透率
+
+职责:
+1. 构造物理世界(physics)
+2. 实例化决策器(UFDQNDecider)
+3. 根据水厂csv数据构造当前工厂状态(observation)
+4. 调用模型生成模型指令
+5. 生成 PLC 下发指令
+6. 评估该指令在物理模型下的效果
+"""
+
+from pathlib import Path
+from dataclasses import replace
+
+
+# ============================================================
+# 导入模块
+# ============================================================
+CURRENT_DIR = Path(__file__).resolve().parent
+
+UF_RL_ROOT = CURRENT_DIR.parents[2]     # uf_train  # uf_rl
+
+# ========== 参数 / 物理 ==========
+from algorithm.uf_rl.env.uf_resistance_models_load import load_resistance_models
+from algorithm.uf_rl.env.uf_physics import UFPhysicsModel
+from algorithm.uf_rl.env.env_params import UFState, UFActionSpec
+from algorithm.uf_rl.env.env_config_loader import EnvConfigLoader, create_env_params_from_yaml
+
+# ========== 决策器 ==========
+from algorithm.uf_rl.rl_model.DQN.uf_decide.dqn_decider import UFDQNDecider
+
+# ========== 决策状态构建器 ==========
+from algorithm.uf_rl.rl_model.DQN.dqn_model.dqn_statebuilder import DQNStateBuilder
+
+
+def build_physics(IS_TIMES, phys_params,state_bounds):
+    """
+    构造与训练一致的物理模型(只做一次)
+    """
+    res_fp, res_bw = load_resistance_models(phys_params)
+
+    physics = UFPhysicsModel(
+        phys_params=phys_params,
+        state_bounds=state_bounds,
+        resistance_model_fp=res_fp,
+        resistance_model_bw=res_bw,
+        IS_TIMES = IS_TIMES
+    )
+    return physics
+
+
+def check_state_bounds(current_state, state_bounds, unit_name):
+    """
+    检查当前状态是否在边界范围内
+
+    参数:
+        current_state: UFState对象,包含TMP, q_UF, temp
+        state_bounds: 状态边界对象
+        unit_name: 机组名称(如 "UF1")
+
+    返回:
+        dict: 错误信息字典,格式 {"error_time": str, "error_feature": str}
+              如果没有错误,返回 None
+    """
+    from datetime import datetime
+
+    error_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+
+    # 检查各项参数是否在边界范围内
+    TMP0_min = state_bounds.TMP0_min
+    TMP0_max = state_bounds.TMP0_max
+    if not (TMP0_min <= current_state.TMP <= TMP0_max):
+        return {
+            "error_time": error_time,
+            "error_feature": f"{unit_name}Per"
+        }
+
+    nuK_min = state_bounds.nuK_min
+    nuK_max = state_bounds.nuK_max
+    if not (nuK_min <= current_state.nuK <= nuK_max):
+        return {
+            "error_time": error_time,
+            "error_feature": f"{unit_name}Per"
+        }
+
+    slope_min = state_bounds.slope_min
+    slope_max = state_bounds.slope_max
+    if not (slope_min <= current_state.slope <= slope_max):
+        return {
+            "error_time": error_time,
+            "error_feature": f"{unit_name}Per"
+        }
+
+    power_min = state_bounds.power_min
+    power_max = state_bounds.power_max
+    if not (power_min <= current_state.power <= power_max):
+        return {
+            "error_time": error_time,
+            "error_feature": f"{unit_name}Per"
+        }
+
+    ceb_removal_min = state_bounds.ceb_removal_min
+    ceb_removal_max = state_bounds.ceb_removal_max
+    if not (ceb_removal_min <= current_state.ceb_removal <= ceb_removal_max):
+        return {
+            "error_time": error_time,
+            "error_feature": f"{unit_name}Per"
+        }
+
+    return None
+
+
+def generate_plc_instructions(action_spec,current_L_s, current_t_bw_s, model_prev_L_s, model_prev_t_bw_s, model_L_s, model_t_bw_s):
+    """
+    根据工厂当前值、模型上一轮决策值和模型当前轮决策值,生成PLC指令。
+
+    新增功能:
+    1. 处理None值情况:如果模型上一轮值为None,则使用工厂当前值;
+       如果工厂当前值也为None,则返回None并提示错误。
+    """
+
+    action_spec = action_spec
+    adjustment_threshold = 1.0
+
+    # 处理None值情况
+    if model_prev_L_s is None:
+        if current_L_s is None:
+            print("错误: 过滤时长的工厂当前值和模型上一轮值均为None")
+            return None, None
+        else:
+            # 使用工厂当前值作为基准
+            effective_current_L = current_L_s
+            source_L = "工厂当前值(模型上一轮值为None)"
+    else:
+        # 模型上一轮值不为None,继续检查工厂当前值
+        if current_L_s is None:
+            effective_current_L = model_prev_L_s
+            source_L = "模型上一轮值(工厂当前值为None)"
+        else:
+            effective_current_L = model_prev_L_s
+            source_L = "模型上一轮值"
+
+    # 对反洗时长进行同样的处理
+    if model_prev_t_bw_s is None:
+        if current_t_bw_s is None:
+            print("错误: 反洗时长的工厂当前值和模型上一轮值均为None")
+            return None, None
+        else:
+            effective_current_t_bw = current_t_bw_s
+            source_t_bw = "工厂当前值(模型上一轮值为None)"
+    else:
+        if current_t_bw_s is None:
+            effective_current_t_bw = model_prev_t_bw_s
+            source_t_bw = "模型上一轮值(工厂当前值为None)"
+        else:
+            effective_current_t_bw = model_prev_t_bw_s
+            source_t_bw = "模型上一轮值"
+
+    # 检测所有输入值是否在规定范围内(只对非None值进行检查)
+    # 工厂当前值检查(警告)
+    if current_L_s is not None and not (action_spec.L_min_s <= current_L_s <= action_spec.L_max_s):
+        print(f"警告: 当前过滤时长 {current_L_s} 秒不在允许范围内 [{action_spec.L_min_s}, {action_spec.L_max_s}]")
+    if current_t_bw_s is not None and not (action_spec.t_bw_min_s <= current_t_bw_s <= action_spec.t_bw_max_s):
+        print(f"警告: 当前反洗时长 {current_t_bw_s} 秒不在允许范围内 [{action_spec.t_bw_min_s}, {action_spec.t_bw_max_s}]")
+
+    # 模型上一轮决策值检查(警告)
+    if model_prev_L_s is not None and not (action_spec.L_min_s <= model_prev_L_s <= action_spec.L_max_s):
+        print(f"警告: 模型上一轮过滤时长 {model_prev_L_s} 秒不在允许范围内 [{action_spec.L_min_s}, {action_spec.L_max_s}]")
+    if model_prev_t_bw_s is not None and not (action_spec.t_bw_min_s <= model_prev_t_bw_s <= action_spec.t_bw_max_s):
+        print(f"警告: 模型上一轮反洗时长 {model_prev_t_bw_s} 秒不在允许范围内 [{action_spec.t_bw_min_s}, {action_spec.t_bw_max_s}]")
+
+    # 模型当前轮决策值检查
+    if model_L_s is None:
+        raise ValueError("错误: 决策模型建议的过滤时长不能为None")
+    model_L_s = max(action_spec.L_min_s, min(model_L_s, action_spec.L_max_s))
+
+    if model_t_bw_s is None:
+        raise ValueError("错误: 决策模型建议的反洗时长不能为None")
+    model_t_bw_s = max(action_spec.t_bw_min_s, min(model_t_bw_s, action_spec.t_bw_max_s))
+
+
+    print(f"过滤时长基准: {source_L}, 值: {effective_current_L}")
+    print(f"反洗时长基准: {source_t_bw}, 值: {effective_current_t_bw}")
+
+    # 使用选定的基准值进行计算调整
+    L_diff = model_L_s - effective_current_L
+    L_adjustment = 0
+    if abs(L_diff) >= adjustment_threshold * action_spec.L_step_s:
+        if L_diff >= 0:
+            L_adjustment = action_spec.L_step_s
+        else:
+            L_adjustment = -action_spec.L_step_s
+    next_L_s = effective_current_L + L_adjustment
+
+    t_bw_diff = model_t_bw_s - effective_current_t_bw
+    t_bw_adjustment = 0
+    if abs(t_bw_diff) >= adjustment_threshold * action_spec.t_bw_step_s:
+        if t_bw_diff >= 0:
+            t_bw_adjustment = action_spec.t_bw_step_s
+        else:
+            t_bw_adjustment = -action_spec.t_bw_step_s
+    next_t_bw_s = effective_current_t_bw + t_bw_adjustment
+
+    return next_L_s, next_t_bw_s
+
+
+
+def calc_uf_cycle_metrics(current_state, max_tmp_during_filtration, min_tmp_during_filtration, L_s: float, t_bw_s: float):
+    """
+    计算 UF 超滤系统的核心性能指标
+
+    参数:
+
+        L_s (float): 单次过滤时间(秒)
+        t_bw_s (float): 单次反洗时间(秒)
+
+    返回:
+        dict: {
+            "k_bw_per_ceb": 小周期次数,
+            "ton_water_energy_kWh_per_m3": 吨水电耗,
+            "recovery": 回收率,
+            "net_delivery_rate_m3ph": 净供水率 (m³/h),
+            "daily_prod_time_h": 日均产水时间 (小时/天)
+            "max_permeability": 全周期最高渗透率(lmh/bar)
+        }
+    """
+
+    # 模拟该参数下的超级周期
+    info, next_state = physics.simulate_one_supercycle(current_state, L_s=L_s, t_bw_s=t_bw_s)
+
+    # 获得模型模拟周期信息
+    k_bw_per_ceb = info["k_bw_per_ceb"]
+    refer_ton_water_energy = info["refer_ton_water_energy"]
+    ton_water_energy = info["ton_water_energy"]
+    recovery = info["recovery"]
+    daily_prod_time_h = info["daily_prod_time_h"]
+
+    # 获得模型模拟周期内最高跨膜压差/最低跨膜压差
+    if max_tmp_during_filtration is None:
+        max_tmp_during_filtration = info["max_TMP_during_filtration"]
+    if min_tmp_during_filtration is None:
+        min_tmp_during_filtration = info["min_TMP_during_filtration"]
+
+    # 计算最高渗透率
+    max_permeability = 100 * current_state.q_UF / (128*40) / min_tmp_during_filtration
+
+
+    return {
+        "k_bw_per_ceb": k_bw_per_ceb,
+        "refer_ton_water_energy": refer_ton_water_energy,
+        "ton_water_energy": ton_water_energy,
+        "recovery": recovery,
+        "daily_prod_time_h": daily_prod_time_h,
+        "max_permeability": max_permeability
+    }
+
+def run_dqn_decide(
+    model_path: Path,
+    physics,
+    action_spec,
+    reward_params,
+    state_bounds,
+# -------- 工厂当前值 --------
+    current_state: UFState
+):
+    """
+    单轮 DQN 决策流程
+    """
+
+    # 构造决策器
+    decider = UFDQNDecider(
+        physics=physics,
+        action_spec=action_spec,
+        reward_params=reward_params,
+        state_bounds=state_bounds,
+        model_path=model_path,
+        seed=0,
+    )
+
+    # 模型决策
+    decision = decider.decide(current_state)
+    action_id = decision["action_id"]
+    model_L_s = decision["L_s"]
+    model_t_bw_s = decision["t_bw_s"]
+
+    return action_id, model_L_s, model_t_bw_s
+
+
+# ==============================
+# 示例调用
+# ==============================
+if __name__ == "__main__":
+
+    # ========== 模型及配置路径指定 ==========
+    IS_TIMES = False # 外部指定变量,表示CEB间隔为时间控制/次数控制,T表示48次bw一次CEB,F表示48h一次CEB
+    DATA_CONFIG_PATH = UF_RL_ROOT / "config_and_model" / "xishan" / "uf_analyze_config.yaml"  # 配置
+    MODEL_PATH = UF_RL_ROOT / "config_and_model" / "xishan" / "48h_dqn_model.zip" # 需根据IS_TIMES变量值指定模型为48h_dqn_model.zip/48times_dqn_model.zip
+    ENV_CONFIG_PATH = UF_RL_ROOT / "config_and_model" / "xishan" / "env_config.yaml" # 环境配置路径
+
+    # ========== 外部调用输入 ==========
+    unit_name = "UF1"
+    prev_cycle_csv = CURRENT_DIR / "test_online_datasets" / "UF_prev_cycle.csv"  # todo: 上一个周期数据。从控制字确定上一个周期起始时间,传上一个周期所有数据(1分钟一个)
+    init_cycle_csv = CURRENT_DIR / "test_online_datasets" / "UF_init_cycle.csv"  # todo: 当前周期前10分钟数据
+    predict_cycle_csv = CURRENT_DIR / "test_online_datasets" / "UF_predict_cycle.csv"  # todo: 不需要从系统里面读,从预测模型生成的,注释
+
+    # ========== 模型及配置加载 ==========
+    config_loader = EnvConfigLoader(ENV_CONFIG_PATH)
+    config_loader.validate_config()
+    config_loader.print_config_summary()
+    (
+        uf_state_default,  # UFState默认值
+        phys_params,  # UFPhysicsParams
+        action_spec,  # UFActionSpec
+        reward_params,  # UFRewardParams
+        state_bounds  # UFStateBounds
+    ) = create_env_params_from_yaml(ENV_CONFIG_PATH)
+    physics = build_physics(IS_TIMES, phys_params,state_bounds)  # 环境依赖
+
+    # ========== 调用模型生成模型指令 ==========
+    # 基于外部输入构建当前状态
+    state_builder = DQNStateBuilder(config_path=DATA_CONFIG_PATH)
+    current_state: UFState = state_builder.build_from_csv_pair(
+        unit_name,
+        uf_state_default,
+        state_bounds,
+        prev_cycle_csv=prev_cycle_csv,
+        init_cycle_csv=init_cycle_csv,
+        predict_cycle_csv=predict_cycle_csv
+    )
+
+    # 状态异常检查(仅检查,不中断,出现异常时后续归一化中将异常状态强制归一化至上下限)
+    error_result = check_state_bounds(current_state, state_bounds, unit_name)
+    if error_result:
+        print(f"错误发生时间: {error_result['error_time']};错误特征量:{error_result['error_feature']}")
+
+    # 模型输出指令
+    action_id, model_L_s, model_t_bw_s = run_dqn_decide(
+        model_path=MODEL_PATH,
+        physics=physics,
+        action_spec=action_spec,
+        reward_params=reward_params,
+        state_bounds=state_bounds,
+        current_state=current_state,
+    ) # 环境实例化,模型加载等功能放在UFDQNDecider类中
+
+    # ========== 生成工厂下发指令 ==========
+    # todo: 超滤,CEB周期 化学冲洗
+    current_L_s = 3800  # todo:需要从从水厂传 搜“过滤时间 设定”
+    current_t_bw_s = 40 # todo:需要从水厂传   搜“反洗时间 设定”
+    model_prev_L_s = 4040 # 上一轮的值
+    model_prev_t_bw_s = 60  # 上一轮的值
+    L_s, t_bw_s = generate_plc_instructions(action_spec, current_L_s, current_t_bw_s, model_prev_L_s, model_prev_t_bw_s, model_L_s,
+                                            model_t_bw_s)  # 获取模型下发指令
+
+    # ========== 生成指令模拟执行结果 ==========
+    max_tmp_during_filtration = 0.050176 # 新增工厂数据接口:周期最高/最低跨膜压差,无工厂数据接入时传入None,calc_uf_cycle_metrics()自动获取模拟周期中的跨膜压差最值
+    min_tmp_during_filtration = 0.012496
+    execution_result = calc_uf_cycle_metrics(current_state, max_tmp_during_filtration, min_tmp_during_filtration, L_s, t_bw_s)
+    print("\n===== 单步决策结果 =====")
+    print(f"模型选择的动作: {action_id}")
+    print(f"模型选择的L_s: {model_L_s} 秒, 模型选择的t_bw_s: {model_t_bw_s} 秒")
+    print(f"指令下发的L_s: {L_s} 秒, 指令下发的t_bw_s: {t_bw_s} 秒")  # 模型输出
+    print(f"指令对应的反洗次数: {execution_result['k_bw_per_ceb']}")
+    print(f"指令对应的理论参考吨水电耗: {execution_result['refer_ton_water_energy']}")
+    print(f"指令对应的计算吨水电耗: {execution_result['ton_water_energy']}")
+    print(f"指令对应的回收率: {execution_result['recovery']}")
+    print(f"指令对应的日均产水时间: {execution_result['daily_prod_time_h']}")
+    print(f"指令对应的最高渗透率: {execution_result['max_permeability']}")
+

+ 11 - 0
algorithm/uf_rl/rl_model/DQN/uf_decide/test_online_datasets/UF_init_cycle.csv

@@ -0,0 +1,11 @@
+time,C.M.UF1_DB@press_PV,C.M.UF1_FT_JS@out,C.M.UF1_DB@word_control,C.M.RO_TT_ZJS@out
+2024-09-28 08:47:00,0.0366965667,258.2537652833,26.0,29.85100995
+2024-09-28 08:48:00,0.0377313667,265.0496630667,26.0,29.8446019
+2024-09-28 08:49:00,0.0458058333,309.5450154333,26.0,29.8467378333
+2024-09-28 08:50:00,0.0513884237,342.4652041017,26.0,29.8405016102
+2024-09-28 08:51:00,0.0512495667,340.76577755,26.0,29.8419317333
+2024-09-28 08:52:00,0.0514033333,340.2851627667,26.0,29.8397956
+2024-09-28 08:53:00,0.0513394333,340.5895517333,26.0,29.8323191333
+2024-09-28 08:54:00,0.0513937333,339.8750400667,26.0,29.8328531667
+2024-09-28 08:55:00,0.0514371356,340.3113914237,26.0,29.8361569322
+2024-09-28 08:56:00,0.0514675167,339.8237709833,26.0,29.8323191333

+ 22 - 0
algorithm/uf_rl/rl_model/DQN/uf_decide/test_online_datasets/UF_predict_cycle.csv

@@ -0,0 +1,22 @@
+time,UF1Per
+2024/9/28 8:57,128.7106581
+2024/9/28 8:58,128.523688
+2024/9/28 8:59,128.7225444
+2024/9/28 9:00,128.7960679
+2024/9/28 9:01,128.3732001
+2024/9/28 9:02,128.3547406
+2024/9/28 9:03,128.2604308
+2024/9/28 9:04,128.1878302
+2024/9/28 9:05,128.2315896
+2024/9/28 9:06,128.0728985
+2024/9/28 9:07,127.9547203
+2024/9/28 9:08,127.6789771
+2024/9/28 9:09,127.9532075
+2024/9/28 9:10,129.3650869
+2024/9/28 9:11,128.9794077
+2024/9/28 9:12,129.6007399
+2024/9/28 9:13,129.3473523
+2024/9/28 9:14,129.2578081
+2024/9/28 9:15,129.3611611
+2024/9/28 9:16,129.3308854
+2024/9/28 9:17,128.870858

+ 2511 - 0
algorithm/uf_rl/rl_model/DQN/uf_decide/test_online_datasets/UF_prev_cycle.csv

@@ -0,0 +1,2511 @@
+time,C.M.UF1_DB@press_PV,C.M.UF1_FT_JS@out,C.M.UF1_DB@word_control,C.M.RO_TT_ZJS@out
+2024-09-26 14:39:00,0.0339027167,277.7282948167,26.0,29.6598306833
+2024-09-26 14:40:00,0.0337968833,277.1739793667,26.0,29.6614328833
+2024-09-26 14:41:00,0.0336879167,275.6007716833,26.0,29.6667731333
+2024-09-26 14:42:00,0.0336815833,275.9532182667,26.0,29.6576946667
+2024-09-26 14:43:00,0.0366570847,295.6646874746,26.0,29.6650894915
+2024-09-26 14:44:00,0.04137775,327.3245774167,26.0,29.6603648333
+2024-09-26 14:45:00,0.04115985,326.0012781333,26.0,29.6587627667
+2024-09-26 14:46:00,0.0412655667,326.84716195,26.0,29.6566266333
+2024-09-26 14:47:00,0.0412047667,325.33482965,26.0,29.6571606833
+2024-09-26 14:48:00,0.0411183333,324.8990706,26.0,29.6560926333
+2024-09-26 14:49:00,0.0411438333,325.2483147833,26.0,29.6598308
+2024-09-26 14:50:00,0.0409836167,323.7359857,26.0,29.6625009667
+2024-09-26 14:51:00,0.0412368333,325.3572580333,26.0,29.6550246
+2024-09-26 14:52:00,0.0412273,324.96635575,26.0,29.66410295
+2024-09-26 14:53:00,0.0413552667,325.1938470333,26.0,29.6592967667
+2024-09-26 14:54:00,0.04136815,325.3188079,26.0,29.6635689
+2024-09-26 14:55:00,0.04119835,324.6427406833,26.0,29.66730715
+2024-09-26 14:56:00,0.0413649333,324.6523552833,26.0,29.66356905
+2024-09-26 14:57:00,0.0414387,324.7580896167,26.0,29.6705113167
+2024-09-26 14:58:00,0.0415475167,326.1262384833,26.0,29.6630349833
+2024-09-26 14:59:00,0.0413842,324.9919915833,26.0,29.6721132833
+2024-09-26 15:00:00,0.0519833167,389.4264657333,26.0,29.6603648833
+2024-09-26 15:01:00,0.0543095167,405.7769921333,26.0,29.6592967667
+2024-09-26 15:02:00,0.04679275,359.62512195,26.0,29.6715793667
+2024-09-26 15:03:00,0.0413393167,322.6914438333,26.0,29.6603647833
+2024-09-26 15:04:00,0.0413681167,323.2745939333,26.0,29.6592967833
+2024-09-26 15:05:00,0.04134895,322.9477731333,26.0,29.6576947167
+2024-09-26 15:06:00,0.0413426,322.4319101667,26.0,29.6608988667
+2024-09-26 15:07:00,0.0414225833,323.0887530667,26.0,29.6683752167
+2024-09-26 15:08:00,0.04149625,323.4283889167,26.0,29.6731815167
+2024-09-26 15:09:00,0.04141625,322.57289325,26.0,29.66623915
+2024-09-26 15:10:00,0.0413393333,322.0506226167,26.0,29.6726474167
+2024-09-26 15:11:00,0.0530054167,392.4927917667,26.0,29.6683752
+2024-09-26 15:12:00,0.0571803333,419.9903853333,26.0,29.6619669167
+2024-09-26 15:13:00,0.0471979831,358.8289236271,26.0,29.6667188814
+2024-09-26 15:14:00,0.0414546,320.8522895333,26.0,29.6662390333
+2024-09-26 15:15:00,0.041461,320.6312053167,26.0,29.6576947167
+2024-09-26 15:16:00,0.041493,320.66004245,26.0,29.6646370667
+2024-09-26 15:17:00,0.0413970167,320.0897146,26.0,29.6678411667
+2024-09-26 15:18:00,0.0414371356,320.7902684068,26.0,29.6667187966
+2024-09-26 15:19:00,0.0491519483,366.7690493966,26.0,29.6727395517
+2024-09-26 15:20:00,0.0570521667,418.9298290167,26.0,29.6694432833
+2024-09-26 15:21:00,0.0523069833,391.7270125333,26.0,29.6635689833
+2024-09-26 15:22:00,0.041429,318.6318538333,26.0,29.6763854833
+2024-09-26 15:23:00,0.0413777667,319.2053844333,26.0,29.6715793833
+2024-09-26 15:24:00,0.041461,319.6251207,26.0,29.6678410667
+2024-09-26 15:25:00,0.0413842,318.71195225,26.0,29.67051125
+2024-09-26 15:26:00,0.0414482333,319.2566493833,26.0,29.6673071
+2024-09-26 15:27:00,0.0414706333,318.8529353833,26.0,29.6763855667
+2024-09-26 15:28:00,0.041531678,318.9492715593,26.0,29.6764941017
+2024-09-26 15:29:00,0.04154745,319.0419780833,26.0,29.6715792833
+2024-09-26 15:30:00,0.0416210833,319.0964479,26.0,29.67531745
+2024-09-26 15:31:00,0.0415795667,319.1252818833,26.0,29.6721132833
+2024-09-26 15:32:00,0.0416948333,319.5930803,26.0,29.6646371167
+2024-09-26 15:33:00,0.041688,318.8580400847,26.0,29.6748648983
+2024-09-26 15:34:00,0.0415283667,317.89490505,26.0,29.6785216
+2024-09-26 15:35:00,0.04158585,317.38545525,26.0,29.67264735
+2024-09-26 15:36:00,0.041682,318.7728318,26.0,29.6721133167
+2024-09-26 15:37:00,0.0417301333,318.971486,26.0,29.6779875333
+2024-09-26 15:38:00,0.0418552,319.2470384,26.0,29.6806576667
+2024-09-26 15:39:00,0.04173015,318.4492167333,26.0,29.6785216
+2024-09-26 15:40:00,0.0419224333,318.8753642833,26.0,29.6801236333
+2024-09-26 15:41:00,0.0417910167,318.59980775,26.0,29.6827938
+2024-09-26 15:42:00,0.0419321333,318.7568110833,26.0,29.6795897
+2024-09-26 15:43:00,0.0418777167,318.5036891333,26.0,29.6758515667
+2024-09-26 15:44:00,0.04183275,318.46844335,26.0,29.6801236333
+2024-09-26 15:45:00,0.0420345333,318.9362401667,26.0,29.6817257333
+2024-09-26 15:46:00,0.0336207333,261.9064431667,29.7,29.6817257833
+2024-09-26 15:47:00,-0.0870234,0.1249599,44.9833333333,29.6817257667
+2024-09-26 15:48:00,-0.06835305,0.8522908833,40.3833333333,29.6827938333
+2024-09-26 15:49:00,0.0384587667,302.5728953667,25.95,29.6753175333
+2024-09-26 15:50:00,0.0410860833,327.0746537,26.0,29.6763855
+2024-09-26 15:51:00,0.0412464,325.5879510833,26.0,29.6811917833
+2024-09-26 15:52:00,0.0413457167,326.8055053667,26.0,29.6758514833
+2024-09-26 15:53:00,0.04130095,325.38289025,26.0,29.6795897667
+2024-09-26 15:54:00,0.0413426,325.7353398167,26.0,29.6779875667
+2024-09-26 15:55:00,0.0411469833,324.4633167167,26.0,29.6769196167
+2024-09-26 15:56:00,0.0415219833,325.5591165833,26.0,29.6854639333
+2024-09-26 15:57:00,0.04142905,325.1489892333,26.0,29.6833278667
+2024-09-26 15:58:00,0.04161485,325.4661961167,26.0,29.6795896333
+2024-09-26 15:59:00,0.0415059,325.0849084333,26.0,29.6908041333
+2024-09-26 16:00:00,0.0415635833,325.2931741333,26.0,29.6908042
+2024-09-26 16:01:00,0.04153155,325.0400506333,26.0,29.6876001
+2024-09-26 16:02:00,0.0417077667,324.71643505,26.0,29.6806576667
+2024-09-26 16:03:00,0.0416884333,325.1938454833,26.0,29.6795896
+2024-09-26 16:04:00,0.0417045667,325.0176212167,26.0,29.68386185
+2024-09-26 16:05:00,0.04174295,325.38929895,26.0,29.6865319333
+2024-09-26 16:06:00,0.0417942833,325.3476434333,26.0,29.6902702
+2024-09-26 16:07:00,0.0415634667,323.34828785,26.0,29.6806577
+2024-09-26 16:08:00,0.0409291,318.8529307667,26.0,29.6854639833
+2024-09-26 16:09:00,0.0399133667,311.8583825833,26.0,29.6876000833
+2024-09-26 16:10:00,0.0390931,305.7801971833,26.0,29.6827938
+2024-09-26 16:11:00,0.0384555333,301.42582225,26.0,29.6892021
+2024-09-26 16:12:00,0.0475841333,358.91381025,26.0,29.69027015
+2024-09-26 16:13:00,0.05041315,377.3021494333,26.0,29.6886681167
+2024-09-26 16:14:00,0.0459532667,350.5703312167,26.0,29.69080415
+2024-09-26 16:15:00,0.0401345833,311.7878871333,26.0,29.6870660167
+2024-09-26 16:16:00,0.0408715333,316.03652445,26.0,29.6881340667
+2024-09-26 16:17:00,0.0413585167,318.5773814333,26.0,29.6924062
+2024-09-26 16:18:00,0.04175585,322.2044193333,26.0,29.6934743167
+2024-09-26 16:19:00,0.0418520333,323.0503050333,26.0,29.6913382667
+2024-09-26 16:20:00,0.0417974333,322.6433833,26.0,29.6950764333
+2024-09-26 16:21:00,0.0418807,322.6882450833,26.0,29.69507635
+2024-09-26 16:22:00,0.0419578,322.0410102333,26.0,29.6892021167
+2024-09-26 16:23:00,0.0558218667,407.1964079667,26.0,29.6934743833
+2024-09-26 16:24:00,0.0577636,418.9746871333,26.0,29.6913382
+2024-09-26 16:25:00,0.04628645,350.4902273833,26.0,29.6929402
+2024-09-26 16:26:00,0.04183605,320.3524517833,26.0,29.6918722833
+2024-09-26 16:27:00,0.0416307833,319.3367503833,26.0,29.6881341333
+2024-09-26 16:28:00,0.04188395,319.5674480833,26.0,29.687066
+2024-09-26 16:29:00,0.0418167,319.9487362167,26.0,29.6929403167
+2024-09-26 16:30:00,0.04194175,320.3396333833,26.0,29.6897361333
+2024-09-26 16:31:00,0.0505382333,372.508814,26.0,29.6961444
+2024-09-26 16:32:00,0.0574593833,417.91092785,26.0,29.68813405
+2024-09-26 16:33:00,0.0506943898,377.1770241695,26.0,29.6938725593
+2024-09-26 16:34:00,0.0418391167,318.3530976,26.0,29.6913383
+2024-09-26 16:35:00,0.0418680167,318.1896845333,26.0,29.6950763167
+2024-09-26 16:36:00,0.0418872,317.8756836,26.0,29.6934742833
+2024-09-26 16:37:00,0.0418647833,317.6097431667,26.0,29.6870659833
+2024-09-26 16:38:00,0.0420040847,318.9916320508,26.0,29.6971309661
+2024-09-26 16:39:00,0.0419994333,318.971486,26.0,29.6982806
+2024-09-26 16:40:00,0.0419961333,318.3562999167,26.0,29.6913380833
+2024-09-26 16:41:00,0.0420185667,317.8981129667,26.0,29.69454235
+2024-09-26 16:42:00,0.0420217833,317.8180104167,26.0,29.6966784667
+2024-09-26 16:43:00,0.0420507167,317.9045176333,26.0,29.6972124167
+2024-09-26 16:44:00,0.04186475,316.61006365,26.0,29.6961444
+2024-09-26 16:45:00,0.0422652333,318.1832738,26.0,29.69400835
+2024-09-26 16:46:00,0.0421916,318.2794022,26.0,29.6918722333
+2024-09-26 16:47:00,0.0421852,318.1800709667,26.0,29.6913381667
+2024-09-26 16:48:00,0.0421409831,318.0304027966,26.0,29.6982172203
+2024-09-26 16:49:00,0.0422748333,318.0358876167,26.0,29.6961443333
+2024-09-26 16:50:00,0.0423293333,318.66388855,26.0,29.6897361333
+2024-09-26 16:51:00,0.04238055,318.5933995667,26.0,29.6902701833
+2024-09-26 16:52:00,0.0423645333,318.5517508,26.0,29.69400845
+2024-09-26 16:53:00,0.0423966333,318.4588292667,26.0,29.6924062667
+2024-09-26 16:54:00,0.0423325167,318.0999715167,26.0,29.6950763167
+2024-09-26 16:55:00,0.0424798833,318.1640538667,26.0,29.6924061833
+2024-09-26 16:56:00,0.04202495,315.3700720333,26.0,29.69400835
+2024-09-26 16:57:00,0.0411309333,309.8237726833,26.0,29.69828055
+2024-09-26 16:58:00,0.0327202,251.0829866667,29.6,29.70148475
+2024-09-26 16:59:00,-0.0878532667,0.1409804,45.0,29.6998826667
+2024-09-26 17:00:00,-0.06536375,24.7741116667,41.1833333333,29.7046887833
+2024-09-26 17:01:00,0.0401698333,316.3857732333,26.0,29.7004166667
+2024-09-26 17:02:00,0.0409900667,319.6379355167,26.0,29.69507645
+2024-09-26 17:03:00,0.04148025,321.8647849,26.0,29.6988145
+2024-09-26 17:04:00,0.0418200167,323.7135546833,26.0,29.7030867667
+2024-09-26 17:05:00,0.0419449333,324.4344791167,26.0,29.6950763333
+2024-09-26 17:06:00,0.04193215,324.0243534167,26.0,29.7052229167
+2024-09-26 17:07:00,0.04215955,324.70682665,26.0,29.6988145667
+2024-09-26 17:08:00,0.0420796333,323.9218234167,26.0,29.7036207833
+2024-09-26 17:09:00,0.0421531667,324.527397,26.0,29.6972125333
+2024-09-26 17:10:00,0.0419769667,322.1691753,26.0,29.69988265
+2024-09-26 17:11:00,0.0415154333,319.45530255,26.0,29.6988145333
+2024-09-26 17:12:00,0.0408074833,314.7612950833,26.0,29.7046889167
+2024-09-26 17:13:00,0.0401569667,309.41685285,26.0,29.7036208333
+2024-09-26 17:14:00,0.03938155,305.1586025833,26.0,29.7052228833
+2024-09-26 17:15:00,0.03891695,301.1246389,26.0,29.7004166667
+2024-09-26 17:16:00,0.03874395,300.2178787667,26.0,29.70949505
+2024-09-26 17:17:00,0.0387277833,299.4585088333,26.0,29.706825
+2024-09-26 17:18:00,0.0387086333,299.4969564333,26.0,29.7025527167
+2024-09-26 17:19:00,0.0386861833,299.2246109667,26.0,29.7036207833
+2024-09-26 17:20:00,0.0386701333,299.2854854333,26.0,29.7126993333
+2024-09-26 17:21:00,0.03876635,299.5642396,26.0,29.71109715
+2024-09-26 17:22:00,0.0387856167,299.3880168333,26.0,29.7116312167
+2024-09-26 17:23:00,0.03868615,299.3655889333,26.0,29.7132333167
+2024-09-26 17:24:00,0.0469529,349.1733388167,26.0,29.7062908
+2024-09-26 17:25:00,0.05118545,377.0201821333,26.0,29.71002915
+2024-09-26 17:26:00,0.0480357,358.2505599833,26.0,29.7068249667
+2024-09-26 17:27:00,0.0409003667,311.9512984333,26.0,29.7089610833
+2024-09-26 17:28:00,0.0416243667,316.7510431667,26.0,29.7084271
+2024-09-26 17:29:00,0.0421532,320.6247962667,26.0,29.7036208833
+2024-09-26 17:30:00,0.0424317333,320.9804520833,26.0,29.70468885
+2024-09-26 17:31:00,0.0425792167,321.84235495,26.0,29.7009507
+2024-09-26 17:32:00,0.04248945,321.3841692333,26.0,29.7052229333
+2024-09-26 17:33:00,0.0426112833,322.0442139167,26.0,29.7046888833
+2024-09-26 17:34:00,0.0428933833,322.2813192667,26.0,29.7089610667
+2024-09-26 17:35:00,0.0575651,411.2592127333,26.0,29.71056325
+2024-09-26 17:36:00,0.0585358,417.53604735,26.0,29.708427
+2024-09-26 17:37:00,0.0448156,337.0906800167,26.0,29.7073589833
+2024-09-26 17:38:00,0.04242225,319.0291581,26.0,29.7062909333
+2024-09-26 17:39:00,0.0425055667,319.214998,26.0,29.706825
+2024-09-26 17:40:00,0.0425151167,318.9939154167,26.0,29.7078930167
+2024-09-26 17:41:00,0.0423932833,318.9041987833,26.0,29.7094950833
+2024-09-26 17:42:00,0.0425664,319.1060563,26.0,29.70629085
+2024-09-26 17:43:00,0.0531912833,382.7523253833,26.0,29.7089609833
+2024-09-26 17:44:00,0.0581417,415.3668685,26.0,29.70095075
+2024-09-26 17:45:00,0.0491509333,360.7657764167,26.0,29.7110971833
+2024-09-26 17:46:00,0.04247035,317.5648833833,26.0,29.7126992667
+2024-09-26 17:47:00,0.0423966833,316.9913501167,26.0,29.7110970833
+2024-09-26 17:48:00,0.0424382333,317.1803908667,26.0,29.7110971833
+2024-09-26 17:49:00,0.0423932833,316.7414290167,26.0,29.7110972167
+2024-09-26 17:50:00,0.0426210167,317.4527403667,26.0,29.7100292167
+2024-09-26 17:51:00,0.0427395833,317.9077255,26.0,29.7126993
+2024-09-26 17:52:00,0.0426146167,317.1355314333,26.0,29.7148353667
+2024-09-26 17:53:00,0.042419,316.3985921,26.0,29.7068249667
+2024-09-26 17:54:00,0.0425985167,317.1932093,26.0,29.7100291667
+2024-09-26 17:55:00,0.0426498333,317.0169818667,26.0,29.7148354
+2024-09-26 17:56:00,0.0427460333,316.9400847833,26.0,29.7153694167
+2024-09-26 17:57:00,0.04280045,318.0294785167,26.0,29.7073591167
+2024-09-26 17:58:00,0.04281335,317.0426142,26.0,29.7137672833
+2024-09-26 17:59:00,0.0429542,317.6193516667,26.0,29.7153693833
+2024-09-26 18:00:00,0.04288055,316.97212515,26.0,29.7126991833
+2024-09-26 18:01:00,0.0429189,317.0682478667,26.0,29.7137673167
+2024-09-26 18:02:00,0.04293815,317.3373910333,26.0,29.7094951667
+2024-09-26 18:03:00,0.0427363,315.9564260167,26.0,29.7078930667
+2024-09-26 18:04:00,0.04295735,316.60365235,26.0,29.7094951
+2024-09-26 18:05:00,0.0430854833,317.3213699333,26.0,29.7068250833
+2024-09-26 18:06:00,0.0430310833,316.8824069667,26.0,29.7062909167
+2024-09-26 18:07:00,0.0430182,316.4690820833,26.0,29.7105631
+2024-09-26 18:08:00,0.04228445,312.0698538667,26.0,29.7132332667
+2024-09-26 18:09:00,0.0413328833,306.4786947333,26.0,29.7137673333
+2024-09-26 18:10:00,0.0225536833,199.6155089667,32.7666666667,29.7041550333
+2024-09-26 18:11:00,-0.1177156,0.1153476,45.0,29.71163125
+2024-09-26 18:12:00,-0.0648125333,46.2960589,39.1666666667,29.7143013
+2024-09-26 18:13:00,0.0412431,317.9942306,26.0,29.7201755167
+2024-09-26 18:14:00,0.0423293667,323.1720605667,26.0,29.7153693667
+2024-09-26 18:15:00,0.0425183667,323.98590525,26.0,29.7148353667
+2024-09-26 18:16:00,0.0426915667,323.1880842167,26.0,29.7143013167
+2024-09-26 18:17:00,0.0425344667,322.9445709167,26.0,29.71002915
+2024-09-26 18:18:00,0.0425697167,322.9477721333,26.0,29.72177755
+2024-09-26 18:19:00,0.0426434333,323.5469424167,26.0,29.7148353667
+2024-09-26 18:20:00,0.0428292,324.1781487667,26.0,29.7180395
+2024-09-26 18:21:00,0.0426916333,323.6751031167,26.0,29.7148353
+2024-09-26 18:22:00,0.04275555,322.8965041667,26.0,29.7217776167
+2024-09-26 18:23:00,0.0427075167,322.3582178833,26.0,29.7207095333
+2024-09-26 18:24:00,0.04297015,322.6978530167,26.0,29.7143013
+2024-09-26 18:25:00,0.0427845,322.8324268333,26.0,29.71002925
+2024-09-26 18:26:00,0.0426144833,321.00929275,26.0,29.7180394667
+2024-09-26 18:27:00,0.0420217167,316.6549192833,26.0,29.7212435
+2024-09-26 18:28:00,0.0409163,309.7180369333,26.0,29.7217775667
+2024-09-26 18:29:00,0.0399358167,303.54694215,26.0,29.7191074833
+2024-09-26 18:30:00,0.0393655333,298.7920578333,26.0,29.7169714667
+2024-09-26 18:31:00,0.0393429333,298.4812577333,26.0,29.7132333
+2024-09-26 18:32:00,0.0392052667,297.850049,26.0,29.7255156167
+2024-09-26 18:33:00,0.0392821167,297.8949088667,26.0,29.7223115833
+2024-09-26 18:34:00,0.0392212667,297.4110866167,26.0,29.72818565
+2024-09-26 18:35:00,0.0393431,298.2217245,26.0,29.7185735167
+2024-09-26 18:36:00,0.0462159167,338.6638868833,26.0,29.71964145
+2024-09-26 18:37:00,0.05177815,374.8702320833,26.0,29.7126992333
+2024-09-26 18:38:00,0.0499327167,365.33482875,26.0,29.7169713667
+2024-09-26 18:39:00,0.0410348833,307.6898432,26.0,29.7153693833
+2024-09-26 18:40:00,0.04176545,312.0794611667,26.0,29.7153693833
+2024-09-26 18:41:00,0.0422331667,315.32841745,26.0,29.7132332833
+2024-09-26 18:42:00,0.0428164833,317.9077224,26.0,29.7153694167
+2024-09-26 18:43:00,0.0429894333,319.40724195,26.0,29.7233795667
+2024-09-26 18:44:00,0.0431944,320.1217534833,26.0,29.7169714333
+2024-09-26 18:45:00,0.0432490333,319.4264634,26.0,29.7164374167
+2024-09-26 18:46:00,0.0445177667,326.1326498333,26.0,29.71483535
+2024-09-26 18:47:00,0.0591926667,416.3601401167,26.0,29.7126991
+2024-09-26 18:48:00,0.0593271833,415.96283225,26.0,29.7148352833
+2024-09-26 18:49:00,0.0445338167,329.9423247667,26.0,29.7164374167
+2024-09-26 18:50:00,0.0430054,318.1416239333,26.0,29.7153694167
+2024-09-26 18:51:00,0.04301505,317.97500965,26.0,29.7137673167
+2024-09-26 18:52:00,0.0431785167,317.79237355,26.0,29.7100291333
+2024-09-26 18:53:00,0.0431207667,317.5360483167,26.0,29.7217775333
+2024-09-26 18:54:00,0.0431847,318.2729893667,26.0,29.7201755
+2024-09-26 18:55:00,0.0562448833,395.32842095,26.0,29.7084271
+2024-09-26 18:56:00,0.0592247333,414.98878675,26.0,29.7260495333
+2024-09-26 18:57:00,0.04775395,347.6065346667,26.0,29.7207095333
+2024-09-26 18:58:00,0.0430150333,315.7032988833,26.0,29.7191075
+2024-09-26 18:59:00,0.042967,315.9596282,26.0,29.7132332667
+2024-09-26 19:00:00,0.0428836833,314.8894561667,26.0,29.7217775333
+2024-09-26 19:01:00,0.0430952167,315.98205625,26.0,29.7228456
+2024-09-26 19:02:00,0.0430759,315.44376675,26.0,29.7217775
+2024-09-26 19:03:00,0.0431080167,314.7292509833,26.0,29.7212435167
+2024-09-26 19:04:00,0.0432489167,316.1518706667,26.0,29.7223115333
+2024-09-26 19:05:00,0.0434220167,316.5684106167,26.0,29.72284555
+2024-09-26 19:06:00,0.0432970333,315.6167896333,26.0,29.72551555
+2024-09-26 19:07:00,0.0431751833,314.9759704,26.0,29.7276517
+2024-09-26 19:08:00,0.0432808667,314.68439685,26.0,29.7271176333
+2024-09-26 19:09:00,0.0434828333,316.2704221833,26.0,29.72070955
+2024-09-26 19:10:00,0.04329055,315.2739501833,26.0,29.71964155
+2024-09-26 19:11:00,0.04339635,316.1807102833,26.0,29.7196415
+2024-09-26 19:12:00,0.0433098333,315.6135849167,26.0,29.7281857667
+2024-09-26 19:13:00,0.04344765,315.9051563667,26.0,29.72604955
+2024-09-26 19:14:00,0.0434445167,315.8442793667,26.0,29.7271176
+2024-09-26 19:15:00,0.0435406333,316.1775044,26.0,29.71643745
+2024-09-26 19:16:00,0.0436784,316.4306323,26.0,29.7233796167
+2024-09-26 19:17:00,0.043547,315.9692355667,26.0,29.7153693833
+2024-09-26 19:18:00,0.0435694167,315.2835611167,26.0,29.7169714
+2024-09-26 19:19:00,0.0431272333,312.3806493833,26.0,29.7260496167
+2024-09-26 19:20:00,0.0422171667,306.9432902,26.0,29.7271175667
+2024-09-26 19:21:00,0.04119835,300.0512651,26.0,29.7271176
+2024-09-26 19:22:00,0.01734385,183.04069215,33.4833333333,29.7260495833
+2024-09-26 19:23:00,-0.1085229667,0.1473886,45.0,29.7276516833
+2024-09-26 19:24:00,-0.0373406,89.2886904667,36.2666666667,29.7233795167
+2024-09-26 19:25:00,0.0411149167,311.5091318667,26.0,29.7249815667
+2024-09-26 19:26:00,0.04176875,314.1525142167,26.0,29.72711755
+2024-09-26 19:27:00,0.0425921833,318.4396052833,26.0,29.7292537
+2024-09-26 19:28:00,0.0429446667,320.29798035,26.0,29.72070945
+2024-09-26 19:29:00,0.0431430833,321.6052502167,26.0,29.72070955
+2024-09-26 19:30:00,0.0432649667,321.2784318333,26.0,29.7175054333
+2024-09-26 19:31:00,0.0432618667,321.2079408,26.0,29.7217774667
+2024-09-26 19:32:00,0.04336115,321.31367705,26.0,29.7265835167
+2024-09-26 19:33:00,0.0423549167,315.1489895,26.0,29.7276517
+2024-09-26 19:34:00,0.03438325,263.8288978333,26.0,29.7276516667
+2024-09-26 19:35:00,0.0331433167,254.9247032,26.0,29.7287195333
+2024-09-26 19:36:00,0.0325857,251.1694965667,26.0,29.7276517333
+2024-09-26 19:37:00,0.0319641667,245.8250562167,26.0,29.7271174833
+2024-09-26 19:38:00,0.03124325,241.5636043,26.0,29.7377978
+2024-09-26 19:39:00,0.03058635,237.0842688167,26.0,29.7319236833
+2024-09-26 19:40:00,0.02981085,232.3870574667,26.0,29.7367298
+2024-09-26 19:41:00,0.0297628,231.7238105833,26.0,29.7313897333
+2024-09-26 19:42:00,0.0299326,231.7494425667,26.0,29.73245785
+2024-09-26 19:43:00,0.0298653667,231.00608975,26.0,29.72925365
+2024-09-26 19:44:00,0.0299101333,232.12752625,26.0,29.7383317167
+2024-09-26 19:45:00,0.0299935,232.0025665667,26.0,29.7431378333
+2024-09-26 19:46:00,0.0302402333,233.9186167667,26.0,29.7308556333
+2024-09-26 19:47:00,0.0316308667,244.4152501667,26.0,29.7335257667
+2024-09-26 19:48:00,0.03527705,266.3985898167,26.0,29.7329918
+2024-09-26 19:49:00,0.0407689333,303.4219838667,26.0,29.73779785
+2024-09-26 19:50:00,0.0408938333,304.4408854,26.0,29.73779785
+2024-09-26 19:51:00,0.03980125,295.0881134833,26.0,29.7260495833
+2024-09-26 19:52:00,0.0428067667,314.8317874833,26.0,29.7335257
+2024-09-26 19:53:00,0.0433963667,318.3018259667,26.0,29.72765165
+2024-09-26 19:54:00,0.0437072,320.0384451667,26.0,29.72818565
+2024-09-26 19:55:00,0.0437969,320.0128170833,26.0,29.7233795667
+2024-09-26 19:56:00,0.0438256667,319.91989765,26.0,29.72391355
+2024-09-26 19:57:00,0.0438161,319.79173295,26.0,29.7233795833
+2024-09-26 19:58:00,0.04631855,332.5152192333,26.0,29.7185734667
+2024-09-26 19:59:00,0.06023075,416.3505275167,26.0,29.7185735167
+2024-09-26 20:00:00,0.0593367,413.7359822667,26.0,29.7212435667
+2024-09-26 20:01:00,0.0438609833,318.7760328833,26.0,29.7175054333
+2024-09-26 20:02:00,0.0435726333,317.0778588167,26.0,29.7329916
+2024-09-26 20:03:00,0.0436847167,317.1291237333,26.0,29.7260496
+2024-09-26 20:04:00,0.0437392,317.09387915,26.0,29.7244475333
+2024-09-26 20:05:00,0.0437648333,318.1480352167,26.0,29.7292537
+2024-09-26 20:06:00,0.0441877,319.0900380667,26.0,29.72444755
+2024-09-26 20:07:00,0.0590836667,408.6254429167,26.0,29.7297876833
+2024-09-26 20:08:00,0.0598815333,413.4059615333,26.0,29.72498155
+2024-09-26 20:09:00,0.04623525,333.0727311333,26.0,29.7233796
+2024-09-26 20:10:00,0.0437423833,315.7161166167,26.0,29.7287195667
+2024-09-26 20:11:00,0.0438001167,315.7641784833,26.0,29.7244476333
+2024-09-26 20:12:00,0.0433642167,313.2713925333,26.0,29.7207094833
+2024-09-26 20:13:00,0.0438096667,315.8282573333,26.0,29.7196415
+2024-09-26 20:14:00,0.0438160833,315.6648498667,26.0,29.7239136333
+2024-09-26 20:15:00,0.0439250167,316.53315755,26.0,29.72444755
+2024-09-26 20:16:00,0.0436815,314.1076604667,26.0,29.7255156167
+2024-09-26 20:17:00,0.0439057167,315.86030325,26.0,29.7201754667
+2024-09-26 20:18:00,0.0439345333,315.9852609333,26.0,29.7196414833
+2024-09-26 20:19:00,0.043816,315.3668661167,26.0,29.7228455333
+2024-09-26 20:20:00,0.0439634333,315.56552065,26.0,29.7217775333
+2024-09-26 20:21:00,0.0440691833,315.8763218667,26.0,29.72604955
+2024-09-26 20:22:00,0.0440692167,315.7385450167,26.0,29.72391355
+2024-09-26 20:23:00,0.044082,315.4758112,26.0,29.7223115
+2024-09-26 20:24:00,0.0439858333,314.9727641333,26.0,29.7217775
+2024-09-26 20:25:00,0.0442295,315.6328097833,26.0,29.72284545
+2024-09-26 20:26:00,0.0441814,315.9596246667,26.0,29.7217775333
+2024-09-26 20:27:00,0.044159,315.7161147833,26.0,29.7207095
+2024-09-26 20:28:00,0.0443447667,316.3889789833,26.0,29.7265836167
+2024-09-26 20:29:00,0.0441974,315.40852145,26.0,29.7292537167
+2024-09-26 20:30:00,0.0437425,311.90964295,26.0,29.7233796333
+2024-09-26 20:31:00,0.0428772167,307.21883695,26.0,29.7228455167
+2024-09-26 20:32:00,0.0417206167,299.97116,26.0,29.7233796333
+2024-09-26 20:33:00,0.0409130667,295.1906405833,26.0,29.7249815667
+2024-09-26 20:34:00,-0.0121473729,129.1013849153,37.2203389831,29.7291721695
+2024-09-26 20:35:00,-0.1222077833,0.1409804,45.0,29.7265835833
+2024-09-26 20:36:00,-0.0120538667,148.9362384333,33.55,29.7292535667
+2024-09-26 20:37:00,0.0421691833,312.9061168333,26.0,29.7313896167
+2024-09-26 20:38:00,0.0430824,317.7475138667,26.0,29.7233796
+2024-09-26 20:39:00,0.0436911333,321.0189048,26.0,29.7255156
+2024-09-26 20:40:00,0.0435693667,320.8234551167,26.0,29.72444755
+2024-09-26 20:41:00,0.0437488333,320.0897147667,26.0,29.7249816
+2024-09-26 20:42:00,0.04370405,319.3784038333,26.0,29.72017545
+2024-09-26 20:43:00,0.0439730667,320.2883658333,26.0,29.72391365
+2024-09-26 20:44:00,0.0438544333,320.4165316,26.0,29.7287197333
+2024-09-26 20:45:00,0.0439666167,320.57673515,26.0,29.7271175833
+2024-09-26 20:46:00,0.0439314,319.9903881833,26.0,29.7313897167
+2024-09-26 20:47:00,0.0439955,320.0961245,26.0,29.7265836333
+2024-09-26 20:48:00,0.0441172667,319.9903866333,26.0,29.7223115833
+2024-09-26 20:49:00,0.04359505,317.1996150167,26.0,29.7281855833
+2024-09-26 20:50:00,0.0429381,312.8868983333,26.0,29.7228455167
+2024-09-26 20:51:00,0.0419385,306.5203440833,26.0,29.7223114833
+2024-09-26 20:52:00,0.0410701667,300.9676361167,26.0,29.7292536167
+2024-09-26 20:53:00,0.0402756,296.2095443,26.0,29.73085575
+2024-09-26 20:54:00,0.0402178167,295.7097091,26.0,29.73299155
+2024-09-26 20:55:00,0.0402562667,295.2002593,26.0,29.7233796333
+2024-09-26 20:56:00,0.04029795,295.4149300833,26.0,29.72711755
+2024-09-26 20:57:00,0.0403428167,295.6007684333,26.0,29.7271174667
+2024-09-26 20:58:00,0.0403555833,295.5591129167,26.0,29.7292536167
+2024-09-26 20:59:00,0.0405671333,296.3088734667,26.0,29.7260496333
+2024-09-26 21:00:00,0.0442069667,316.145464,26.0,29.7239136167
+2024-09-26 21:01:00,0.0531271667,372.6754232833,26.0,29.7276516667
+2024-09-26 21:02:00,0.05412355,377.7731511333,26.0,29.7340596333
+2024-09-26 21:03:00,0.0430118333,311.6501115,26.0,29.7276516667
+2024-09-26 21:04:00,0.0433001333,312.0153772833,26.0,29.7297876167
+2024-09-26 21:05:00,0.0441364667,316.8087151667,26.0,29.7239135833
+2024-09-26 21:06:00,0.0440115167,317.0394087667,26.0,29.7212435167
+2024-09-26 21:07:00,0.0442583,317.6097442167,26.0,29.7244474833
+2024-09-26 21:08:00,0.0442937667,317.3566172167,26.0,29.7244475
+2024-09-26 21:09:00,0.04417185,317.3630258,26.0,29.7271175833
+2024-09-26 21:10:00,0.0491734667,342.8260155333,26.0,29.7223116333
+2024-09-26 21:11:00,0.0609452167,413.4956765333,26.0,29.71964155
+2024-09-26 21:12:00,0.05928565,405.40211835,26.0,29.7244475
+2024-09-26 21:13:00,0.04414935,315.2226871667,26.0,29.7281857667
+2024-09-26 21:14:00,0.0442198833,315.3027876833,26.0,29.7340597167
+2024-09-26 21:15:00,0.0441717167,314.7292536,26.0,29.7297876667
+2024-09-26 21:16:00,0.0440532333,314.18135425,26.0,29.7228456333
+2024-09-26 21:17:00,0.04400185,314.2710678167,26.0,29.7239134667
+2024-09-26 21:18:00,0.0460749833,323.8449222333,26.0,29.7191074333
+2024-09-26 21:19:00,0.0605128167,410.5094488667,26.0,29.7169714167
+2024-09-26 21:20:00,0.0602019833,409.6699773833,26.0,29.72231155
+2024-09-26 21:21:00,0.0442390833,316.5171417167,26.0,29.7233796167
+2024-09-26 21:22:00,0.0439923,312.4511364,26.0,29.7372637833
+2024-09-26 21:23:00,0.04421025,313.55014595,26.0,29.7239136
+2024-09-26 21:24:00,0.0441782,313.8096782667,26.0,29.7217775167
+2024-09-26 21:25:00,0.0441398,312.7843634833,26.0,29.7191074333
+2024-09-26 21:26:00,0.0442615667,313.6558799833,26.0,29.7260496167
+2024-09-26 21:27:00,0.0442806833,312.8420376167,26.0,29.7249815333
+2024-09-26 21:28:00,0.0443192333,312.7170793167,26.0,29.7335258
+2024-09-26 21:29:00,0.0444953833,313.4636379167,26.0,29.7276517167
+2024-09-26 21:30:00,0.0443351833,313.4251888333,26.0,29.7287196333
+2024-09-26 21:31:00,0.0445819333,313.8417216167,26.0,29.7292537
+2024-09-26 21:32:00,0.0443255167,312.99583285,26.0,29.72498165
+2024-09-26 21:33:00,0.0443896833,312.931752,26.0,29.7255156667
+2024-09-26 21:34:00,0.0446661864,314.042114339,26.0,29.7172248983
+2024-09-26 21:35:00,0.0445850833,313.4476165833,26.0,29.72337955
+2024-09-26 21:36:00,0.0446362667,313.7840430167,26.0,29.7244475333
+2024-09-26 21:37:00,0.0445883333,313.7840460667,26.0,29.7271175167
+2024-09-26 21:38:00,0.0446810833,313.9923142667,26.0,29.7233795833
+2024-09-26 21:39:00,0.0448356949,313.5957104237,26.0,29.7237416102
+2024-09-26 21:40:00,0.0447676,313.05350965,26.0,29.7228455
+2024-09-26 21:41:00,0.0447676333,313.1688599667,26.0,29.7292536
+2024-09-26 21:42:00,0.0448062,312.84524355,26.0,29.7276515833
+2024-09-26 21:43:00,0.04477105,313.7199651667,26.0,29.7297876833
+2024-09-26 21:44:00,0.0448830167,313.4636318,26.0,29.7255155833
+2024-09-26 21:45:00,0.0441365667,309.4424863833,26.0,29.7191075167
+2024-09-26 21:46:00,-0.0322045167,85.5430939,40.1666666667,29.7233795667
+2024-09-26 21:47:00,-0.1095516333,0.1217558,45.0166666667,29.7217774167
+2024-09-26 21:48:00,0.0170169667,204.0595956,29.9833333333,29.7233795333
+2024-09-26 21:49:00,0.0441717833,319.4841417,26.0,29.72818555
+2024-09-26 21:50:00,0.0443896333,319.8462032833,26.0,29.7175053833
+2024-09-26 21:51:00,0.0443000333,319.18936475,26.0,29.72177755
+2024-09-26 21:52:00,0.04431605,318.9458537833,26.0,29.7239136333
+2024-09-26 21:53:00,0.04441535,319.4873445,26.0,29.7244476667
+2024-09-26 21:54:00,0.0444602,318.4972783667,26.0,29.7260494667
+2024-09-26 21:55:00,0.0445146167,318.5677689,26.0,29.7244475833
+2024-09-26 21:56:00,0.0446266333,319.3848140833,26.0,29.7217775833
+2024-09-26 21:57:00,0.0446907667,318.7984640167,26.0,29.7207095167
+2024-09-26 21:58:00,0.0444762,317.9910318833,26.0,29.7276516167
+2024-09-26 21:59:00,0.0439955833,314.4985545667,26.0,29.72124345
+2024-09-26 22:00:00,0.0429861,308.1095795333,26.0,29.7239136
+2024-09-26 22:01:00,0.0422397667,303.0983657333,26.0,29.7287197
+2024-09-26 22:02:00,0.0413105,297.4911876,26.0,29.7217775667
+2024-09-26 22:03:00,0.0408938167,294.9150912667,26.0,29.7244476167
+2024-09-26 22:04:00,0.0409578667,294.09804815,26.0,29.7287195833
+2024-09-26 22:05:00,0.04082975,294.0628028833,26.0,29.7271176167
+2024-09-26 22:06:00,0.0409611833,294.0531899333,26.0,29.7287195833
+2024-09-26 22:07:00,0.0408553333,294.5273959167,26.0,29.7281854667
+2024-09-26 22:08:00,0.0411054333,294.8926644333,26.0,29.7260495667
+2024-09-26 22:09:00,0.0410298136,294.0257096271,26.0,29.7221125085
+2024-09-26 22:10:00,0.04095785,293.70714905,26.0,29.7281856
+2024-09-26 22:11:00,0.0411951167,294.2710711667,26.0,29.7175054167
+2024-09-26 22:12:00,0.0433226667,305.4117303333,26.0,29.7212434667
+2024-09-26 22:13:00,0.0540146833,371.2015356167,26.0,29.7207095167
+2024-09-26 22:14:00,0.0543864167,373.8769613,26.0,29.7255156
+2024-09-26 22:15:00,0.0445978833,315.7353423833,26.0,29.7265837167
+2024-09-26 22:16:00,0.04344125,307.3822458667,26.0,29.72871975
+2024-09-26 22:17:00,0.0441589333,311.5860301,26.0,29.7239136333
+2024-09-26 22:18:00,0.0446652333,314.3319467667,26.0,29.7292536333
+2024-09-26 22:19:00,0.04497615,316.36654795,26.0,29.7276516167
+2024-09-26 22:20:00,0.0448830833,316.1006128667,26.0,29.7255155667
+2024-09-26 22:21:00,0.0448542333,314.8029474667,26.0,29.7233795333
+2024-09-26 22:22:00,0.0507627,346.4979140833,26.0,29.7185735167
+2024-09-26 22:23:00,0.0618806167,411.6244801667,26.0,29.7212435833
+2024-09-26 22:24:00,0.05774755,389.6763844167,26.0,29.7228455667
+2024-09-26 22:25:00,0.04488625,313.95386605,26.0,29.7292536833
+2024-09-26 22:26:00,0.0448830833,313.8032722167,26.0,29.7249815
+2024-09-26 22:27:00,0.0448862833,313.6270458167,26.0,29.7201754667
+2024-09-26 22:28:00,0.0450144333,314.6683762167,26.0,29.7233796167
+2024-09-26 22:29:00,0.0451458667,313.8224950833,26.0,29.7201755667
+2024-09-26 22:30:00,0.0491990667,335.6135891667,26.0,29.71750535
+2024-09-26 22:31:00,0.0610638,406.9016315667,26.0,29.7260495
+2024-09-26 22:32:00,0.0594938,399.50336625,26.0,29.7217775667
+2024-09-26 22:33:00,0.04506255,312.5921210333,26.0,29.72284555
+2024-09-26 22:34:00,0.0448291695,312.1424560508,26.0,29.7226555085
+2024-09-26 22:35:00,0.0450913667,312.4415233,26.0,29.7180395
+2024-09-26 22:36:00,0.04492155,312.2108272333,26.0,29.7116312667
+2024-09-26 22:37:00,0.0449375667,311.646904,26.0,29.7239136
+2024-09-26 22:38:00,0.0451202333,312.3870529333,26.0,29.7228455333
+2024-09-26 22:39:00,0.0451331167,312.2204412667,26.0,29.7223115833
+2024-09-26 22:40:00,0.0450658,312.1948116833,26.0,29.7191074833
+2024-09-26 22:41:00,0.0450945833,312.0890742833,26.0,29.7207095333
+2024-09-26 22:42:00,0.0452548,312.1467474667,26.0,29.7233796333
+2024-09-26 22:43:00,0.0452450833,312.30374405,26.0,29.7249816167
+2024-09-26 22:44:00,0.0451972333,312.3486086,26.0,29.7271176333
+2024-09-26 22:45:00,0.0453862167,312.54405735,26.0,29.7212434833
+2024-09-26 22:46:00,0.0451811333,312.0089686833,26.0,29.7228454833
+2024-09-26 22:47:00,0.0452388167,311.8007029833,26.0,29.7260496
+2024-09-26 22:48:00,0.0453541,312.45754505,26.0,29.7260495667
+2024-09-26 22:49:00,0.04503075,310.7689823667,26.0,29.72711765
+2024-09-26 22:50:00,0.04528035,311.9160481833,26.0,29.7255155833
+2024-09-26 22:51:00,0.0455335167,312.7715496,26.0,29.7265835667
+2024-09-26 22:52:00,0.0454597667,312.1948065167,26.0,29.7313897
+2024-09-26 22:53:00,0.0454438167,311.9833364667,26.0,29.72444755
+2024-09-26 22:54:00,0.0449696167,308.6895221833,26.0,29.7121652667
+2024-09-26 22:55:00,0.0439795,302.4639546333,26.0,29.7207095333
+2024-09-26 22:56:00,0.0427652,296.05895225,26.0,29.7196414667
+2024-09-26 22:57:00,0.0418197833,290.6824752667,26.5,29.7265836
+2024-09-26 22:58:00,-0.0564915,32.7202818167,42.8,29.7239135167
+2024-09-26 22:59:00,-0.1008217288,0.1270778644,45.2033898305,29.7264569492
+2024-09-26 23:00:00,0.0347068167,248.13841755,26.6166666667,29.7244476167
+2024-09-26 23:01:00,0.0431400333,307.6610061833,26.0,29.7159033
+2024-09-26 23:02:00,0.0438449667,311.0285166333,26.0,29.7212435667
+2024-09-26 23:03:00,0.04438,313.89939475,26.0,29.7223115167
+2024-09-26 23:04:00,0.0448510167,316.24479255,26.0,29.7249815667
+2024-09-26 23:05:00,0.0450817167,317.5680902333,26.0,29.7223115833
+2024-09-26 23:06:00,0.0451681017,317.5546739661,26.0,29.7313443898
+2024-09-26 23:07:00,0.045242,317.355546037,26.0,29.7287195741
+2024-09-26 23:08:00,0.0451017241,317.2491180517,26.0,29.7208015345
+2024-09-26 23:09:00,0.0450850333,316.2800382833,26.0,29.7212435
+2024-09-26 23:10:00,0.0445338,312.77475395,26.0,29.72978765
+2024-09-26 23:11:00,0.0441269,309.9487343833,26.0,29.71750545
+2024-09-26 23:12:00,0.0401505333,287.1996174833,26.0,29.7201755333
+2024-09-26 23:13:00,0.0328998167,240.8907402333,26.0,29.7265836333
+2024-09-26 23:14:00,0.0321467167,235.70970655,26.0,29.7303215833
+2024-09-26 23:15:00,0.0315443833,232.1948119667,26.0,29.7303216167
+2024-09-26 23:16:00,0.0314673833,231.4226232333,26.0,29.7335258333
+2024-09-26 23:17:00,0.03145465,231.1246427167,26.0,29.7356617333
+2024-09-26 23:18:00,0.03156685,231.3232978,26.0,29.7308557667
+2024-09-26 23:19:00,0.0315090833,231.5059308667,26.0,29.7377978333
+2024-09-26 23:20:00,0.0315667,231.2912559,26.0,29.7292537167
+2024-09-26 23:21:00,0.0316430526,231.5148676842,26.0,29.7357460526
+2024-09-26 23:22:00,0.0315122833,230.84909015,26.0,29.7377976667
+2024-09-26 23:23:00,0.0315891833,231.4194219,26.0,29.7388658
+2024-09-26 23:24:00,0.0321692,233.5341233,26.0,29.7303216
+2024-09-26 23:25:00,0.04040365,286.2640125167,26.0,29.73352575
+2024-09-26 23:26:00,0.0414034833,291.1726993333,26.0,29.7276516
+2024-09-26 23:27:00,0.0366741833,263.3771232667,26.0,29.7303217333
+2024-09-26 23:28:00,0.0362897,260.7785969667,26.0,29.7303216333
+2024-09-26 23:29:00,0.03720265,265.8122379167,26.0,29.7249815667
+2024-09-26 23:30:00,0.0375808,267.6481899333,26.0,29.7292536333
+2024-09-26 23:31:00,0.0374655,267.3117616,26.0,29.7319237333
+2024-09-26 23:32:00,0.0375936333,267.4943945167,26.0,29.7329917333
+2024-09-26 23:33:00,0.0390547167,275.0977254667,26.0,29.7297876833
+2024-09-26 23:34:00,0.0546075,364.0179401,26.0,29.7239136
+2024-09-26 23:35:00,0.06297345,412.69144875,26.0,29.7249815833
+2024-09-26 23:36:00,0.0571065333,381.9096450167,26.0,29.7239136167
+2024-09-26 23:37:00,0.0455654833,314.3671896,26.0,29.7239136
+2024-09-26 23:38:00,0.0453733167,313.56616585,26.0,29.72444755
+2024-09-26 23:39:00,0.0395578333,278.8016653,26.0,29.7201755
+2024-09-26 23:40:00,0.0361325833,258.09996585,26.0,29.7265836333
+2024-09-26 23:41:00,0.03620965,258.4492171833,26.0,29.7287197
+2024-09-26 23:42:00,0.04074335,284.5818654333,26.0,29.7260496167
+2024-09-26 23:43:00,0.0474433,326.1134226333,26.0,29.7233796167
+2024-09-26 23:44:00,0.045303,314.9343161333,26.0,29.7255155
+2024-09-26 23:45:00,0.0359660833,256.07497855,26.0,29.7217776167
+2024-09-26 23:46:00,0.0361166167,256.1358521333,26.0,29.7276516333
+2024-09-26 23:47:00,0.0361070333,256.1550768,26.0,29.7244476
+2024-09-26 23:48:00,0.0360749333,256.3889762667,26.0,29.7249815833
+2024-09-26 23:49:00,0.0361006,255.9019537,26.0,29.7276515833
+2024-09-26 23:50:00,0.0361095932,256.1271664407,26.0,29.7280860678
+2024-09-26 23:51:00,0.03609095,256.6933656667,26.0,29.7244475667
+2024-09-26 23:52:00,0.0361934333,256.2063424,26.0,29.7260495667
+2024-09-26 23:53:00,0.0362929333,256.61646895,26.0,29.7335257833
+2024-09-26 23:54:00,0.03625115,256.07497675,26.0,29.72978775
+2024-09-26 23:55:00,0.03624795,256.2608136167,26.0,29.7319237167
+2024-09-26 23:56:00,0.0363345,256.0429359,26.0,29.7265836167
+2024-09-26 23:57:00,0.03635055,256.5491801167,26.0,29.72498155
+2024-09-26 23:58:00,0.0362895833,256.4562624333,26.0,29.7271176
+2024-09-26 23:59:00,0.0363602667,256.5716085167,26.0,29.7361958833
+2024-09-27 00:00:00,0.0363730667,256.65491735,26.0,29.7335257333
+2024-09-27 00:01:00,0.03631515,256.10381405,26.0,29.7345938
+2024-09-27 00:02:00,0.0363409333,255.9147705833,26.0,29.7281856167
+2024-09-27 00:03:00,0.03635365,256.1582821833,26.0,29.72604965
+2024-09-27 00:04:00,0.0363376833,256.10701525,26.0,29.7313896833
+2024-09-27 00:05:00,0.0363538333,256.7254112167,26.0,29.7340598667
+2024-09-27 00:06:00,0.0355271167,250.5511064667,26.0,29.72391355
+2024-09-27 00:07:00,0.0346364667,245.0464606,26.0,29.7255154667
+2024-09-27 00:08:00,0.0337585167,239.3623832167,26.0,29.7265836667
+2024-09-27 00:09:00,0.0281736,206.8920196,28.7333333333,29.7292536833
+2024-09-27 00:10:00,-0.0864531333,0.1217558,44.9666666667,29.7329917167
+2024-09-27 00:11:00,-0.0750529333,16.7862862167,41.35,29.7308556833
+2024-09-27 00:12:00,0.0428325167,303.1239979333,26.0,29.7239135667
+2024-09-27 00:13:00,0.043643,308.4492106,26.0,29.7217775833
+2024-09-27 00:14:00,0.0442391167,311.4194178833,26.0,29.7292536
+2024-09-27 00:15:00,0.0447293,313.6462678167,26.0,29.7303215833
+2024-09-27 00:16:00,0.04513625,315.2771545,26.0,29.7164373833
+2024-09-27 00:17:00,0.0454342,316.7286146667,26.0,29.72070955
+2024-09-27 00:18:00,0.0454917167,317.4815784667,26.0,29.7260496667
+2024-09-27 00:19:00,0.0456649167,317.2220427,26.0,29.7228456
+2024-09-27 00:20:00,0.0455175,317.00416455,26.0,29.7329918
+2024-09-27 00:21:00,0.0452516833,314.4088480167,26.0,29.7212435667
+2024-09-27 00:22:00,0.0447997,311.4963114333,26.0,29.73192385
+2024-09-27 00:23:00,0.0443255667,309.0900378333,26.0,29.7265836333
+2024-09-27 00:24:00,0.0436014667,304.7068274667,26.0,29.7244475667
+2024-09-27 00:25:00,0.0429572667,300.0416515167,26.0,29.7228455667
+2024-09-27 00:26:00,0.0424447,296.5235468333,26.0,29.72765175
+2024-09-27 00:27:00,0.0418359333,292.93495875,26.0,29.7180394
+2024-09-27 00:28:00,0.0419226,292.5728959167,26.0,29.7292537
+2024-09-27 00:29:00,0.0419064833,291.6821524333,26.0,29.7303216833
+2024-09-27 00:30:00,0.0419672833,292.8003864833,26.0,29.7271176167
+2024-09-27 00:31:00,0.0420121833,292.4863814333,26.0,29.7228456
+2024-09-27 00:32:00,0.0419834667,292.2364599333,26.0,29.7292537
+2024-09-27 00:33:00,0.0419064167,292.27170525,26.0,29.7255156333
+2024-09-27 00:34:00,0.0419480833,292.4991988667,26.0,29.72551565
+2024-09-27 00:35:00,0.0420698667,292.4030762333,26.0,29.72925365
+2024-09-27 00:36:00,0.0425312833,293.8417171,26.0,29.7233795167
+2024-09-27 00:37:00,0.0549567667,365.7065077667,26.0,29.7228455333
+2024-09-27 00:38:00,0.0559851667,371.68535645,26.0,29.71910745
+2024-09-27 00:39:00,0.0482505833,329.21820025,26.0,29.7287196333
+2024-09-27 00:40:00,0.0448542667,307.8148020333,26.0,29.7255157
+2024-09-27 00:41:00,0.0457770333,312.9798147,26.0,29.72284555
+2024-09-27 00:42:00,0.0459692667,313.6110254667,26.0,29.7308556333
+2024-09-27 00:43:00,0.0460526,314.0115358667,26.0,29.7207095833
+2024-09-27 00:44:00,0.04608785,313.7744350167,26.0,29.7319238
+2024-09-27 00:45:00,0.0461551167,314.5786601333,26.0,29.7287196333
+2024-09-27 00:46:00,0.05685035,371.6885606333,26.0,29.7281858167
+2024-09-27 00:47:00,0.0635244333,410.1762254667,26.0,29.7249816167
+2024-09-27 00:48:00,0.0542774,360.403711,26.0,29.7249815833
+2024-09-27 00:49:00,0.0458219167,311.37455595,26.0,29.7244476667
+2024-09-27 00:50:00,0.0457770167,310.8939416333,26.0,29.7265836
+2024-09-27 00:51:00,0.0460845333,312.90291565,26.0,29.7287197333
+2024-09-27 00:52:00,0.0460879333,312.0346018833,26.0,29.72658365
+2024-09-27 00:53:00,0.0460685833,312.4959942333,26.0,29.72498155
+2024-09-27 00:54:00,0.05491825,359.3944229333,26.0,29.7212435333
+2024-09-27 00:55:00,0.0632168333,407.3502069833,26.0,29.7292537
+2024-09-27 00:56:00,0.0560429833,371.0958012,26.0,29.72871975
+2024-09-27 00:57:00,0.0458348,309.9487369667,26.0,29.7201755667
+2024-09-27 00:58:00,0.0458796333,309.9871845167,26.0,29.7191075333
+2024-09-27 00:59:00,0.0460269,310.53187855,26.0,29.7324577333
+2024-09-27 01:00:00,0.0460141833,310.310797,26.0,29.7255156333
+2024-09-27 01:01:00,0.0460366,310.6279987167,26.0,29.7244475667
+2024-09-27 01:02:00,0.0461134667,310.1698149,26.0,29.74100185
+2024-09-27 01:03:00,0.04599815,310.5254657167,26.0,29.7239135833
+2024-09-27 01:04:00,0.0460013167,309.9006743,26.0,29.7249815167
+2024-09-27 01:05:00,0.0461135333,310.2114668,26.0,29.7292536333
+2024-09-27 01:06:00,0.0461038833,310.1570006,26.0,29.73138975
+2024-09-27 01:07:00,0.0461550833,310.40692285,26.0,29.7239136333
+2024-09-27 01:08:00,0.0463794167,310.5863488833,26.0,29.7255156333
+2024-09-27 01:09:00,0.0463890667,310.6792652167,26.0,29.72871965
+2024-09-27 01:10:00,0.0463921667,310.8747197167,26.0,29.7404678167
+2024-09-27 01:11:00,0.0464113833,310.5863463,26.0,29.7377978167
+2024-09-27 01:12:00,0.0463441333,310.4581836667,26.0,29.73566185
+2024-09-27 01:13:00,0.0464274167,310.3652632,26.0,29.7313896833
+2024-09-27 01:14:00,0.0463057,309.7244479667,26.0,29.7308556833
+2024-09-27 01:15:00,0.0464371167,310.38128495,26.0,29.7239135333
+2024-09-27 01:16:00,0.0464466333,309.8430001333,26.0,29.7271176667
+2024-09-27 01:17:00,0.0464917,310.2659392,26.0,29.72925365
+2024-09-27 01:18:00,0.0466133833,309.8782417833,26.0,29.7319237
+2024-09-27 01:19:00,0.0465909333,310.0576735,26.0,29.7319236667
+2024-09-27 01:20:00,0.0464243667,308.9138091,26.0,29.7324577
+2024-09-27 01:21:00,0.0299039,212.4575456833,32.1666666667,29.7372638333
+2024-09-27 01:22:00,-0.1044666,0.1441845,45.0,29.7367298
+2024-09-27 01:23:00,-0.0469721167,68.59980845,38.2666666667,29.7308557333
+2024-09-27 01:24:00,0.0456776167,315.7481561,26.0,29.7239136333
+2024-09-27 01:25:00,0.0459309167,316.7542459,26.0,29.7361958833
+2024-09-27 01:26:00,0.0461134833,316.7125914667,26.0,29.73459385
+2024-09-27 01:27:00,0.0461904333,317.0490232833,26.0,29.7351277333
+2024-09-27 01:28:00,0.04627365,316.25119665,26.0,29.7335258
+2024-09-27 01:29:00,0.0461871,315.5591139833,26.0,29.7324577
+2024-09-27 01:30:00,0.0463281,316.0621566167,26.0,29.7287196333
+2024-09-27 01:31:00,0.04633785,315.8635055,26.0,29.7329916167
+2024-09-27 01:32:00,0.0463089667,315.7481546667,26.0,29.7329917333
+2024-09-27 01:33:00,0.0463217667,316.0140960333,26.0,29.7329918833
+2024-09-27 01:34:00,0.0455527333,310.8490802667,26.0,29.7345937333
+2024-09-27 01:35:00,0.0448748136,306.1372122542,26.0,29.7297152542
+2024-09-27 01:36:00,0.0439025333,300.34284305,26.0,29.72925365
+2024-09-27 01:37:00,0.0431143167,295.5527057667,26.0,29.7351277833
+2024-09-27 01:38:00,0.0424863333,291.5571901,26.0,29.73566175
+2024-09-27 01:39:00,0.04255055,291.73021105,26.0,29.74367185
+2024-09-27 01:40:00,0.04252485,291.51874195,26.0,29.7329917667
+2024-09-27 01:41:00,0.0426017167,291.8679891833,26.0,29.7324577333
+2024-09-27 01:42:00,0.0424863167,290.7177141,26.0,29.7361958667
+2024-09-27 01:43:00,0.0425632333,291.0669623667,26.0,29.7393998333
+2024-09-27 01:44:00,0.0426049333,292.0281968,26.0,29.7329916833
+2024-09-27 01:45:00,0.0427780667,291.2816383667,26.0,29.7399338833
+2024-09-27 01:46:00,0.0426434333,291.1726965833,26.0,29.7345937333
+2024-09-27 01:47:00,0.0427395167,291.1951270833,26.0,29.7415357
+2024-09-27 01:48:00,0.0427204,291.4963147333,26.0,29.7372638167
+2024-09-27 01:49:00,0.0548126167,358.2153167,26.0,29.7329918
+2024-09-27 01:50:00,0.0566677333,368.9682803833,26.0,29.73512785
+2024-09-27 01:51:00,0.04993595,333.2745884833,26.0,29.7367298
+2024-09-27 01:52:00,0.0448029833,302.4511377333,26.0,29.7383318
+2024-09-27 01:53:00,0.0455591667,306.8023072167,26.0,29.7351279
+2024-09-27 01:54:00,0.0461135333,310.2114678333,26.0,29.7345939
+2024-09-27 01:55:00,0.0466485833,312.31336175,26.0,29.7313897333
+2024-09-27 01:56:00,0.0468024,313.2810036167,26.0,29.7297877
+2024-09-27 01:57:00,0.0469049333,313.4412069833,26.0,29.7329918167
+2024-09-27 01:58:00,0.06042935,384.0788223,26.0,29.73939995
+2024-09-27 01:59:00,0.06398595,406.3505267667,26.0,29.7356618
+2024-09-27 02:00:00,0.052307,344.1973708667,26.0,29.7420699
+2024-09-27 02:01:00,0.0465941,310.9868549167,26.0,29.7490119667
+2024-09-27 02:02:00,0.04675115,311.7270075833,26.0,29.7340596833
+2024-09-27 02:03:00,0.0467672,310.8971486167,26.0,29.7388658333
+2024-09-27 02:04:00,0.0466998833,310.6728596167,26.0,29.7399338833
+2024-09-27 02:05:00,0.0468472,311.49951785,26.0,29.7361958167
+2024-09-27 02:06:00,0.0583402833,372.5857069833,26.0,29.7393998
+2024-09-27 02:07:00,0.0641267,405.5495004,26.0,29.73833195
+2024-09-27 02:08:00,0.0542198667,354.5081719333,26.0,29.73459375
+2024-09-27 02:09:00,0.0465941667,308.6863223333,26.0,29.73779785
+2024-09-27 02:10:00,0.0466711,308.8785667667,26.0,29.73619585
+2024-09-27 02:11:00,0.0466902833,308.69914075,26.0,29.7361957667
+2024-09-27 02:12:00,0.0467446333,308.5325228667,26.0,29.7351277333
+2024-09-27 02:13:00,0.0467062833,308.75681135,26.0,29.73619575
+2024-09-27 02:14:00,0.0468505333,309.1797539333,26.0,29.73352575
+2024-09-27 02:15:00,0.0467960333,308.6062167833,26.0,29.7399338333
+2024-09-27 02:16:00,0.0468536,308.5837872833,26.0,29.7351278333
+2024-09-27 02:17:00,0.0470329167,309.1925677167,26.0,29.73672995
+2024-09-27 02:18:00,0.0469786333,309.0003171333,26.0,29.7319237
+2024-09-27 02:19:00,0.0469496333,308.8657508833,26.0,29.7426039
+2024-09-27 02:20:00,0.047017,309.3207314167,26.0,29.73192365
+2024-09-27 02:21:00,0.0469465167,308.2121127,26.0,29.7442059
+2024-09-27 02:22:00,0.0471612333,309.2918963667,26.0,29.73833185
+2024-09-27 02:23:00,0.0472511333,309.6123081167,26.0,29.7319237667
+2024-09-27 02:24:00,0.0473214667,309.5546303333,26.0,29.7388659167
+2024-09-27 02:25:00,0.04712605,308.5517494333,26.0,29.7383319
+2024-09-27 02:26:00,0.04732805,309.2694685,26.0,29.7410019167
+2024-09-27 02:27:00,0.0473439333,309.6892016667,26.0,29.7442059167
+2024-09-27 02:28:00,0.047376,308.7824445,26.0,29.73993395
+2024-09-27 02:29:00,0.0474016667,309.3399534,26.0,29.74100195
+2024-09-27 02:30:00,0.0474272667,308.6959343667,26.0,29.7431379167
+2024-09-27 02:31:00,0.0473086333,308.02948095,26.0,29.7388658
+2024-09-27 02:32:00,0.04620315,302.7651388667,26.0,29.7335257167
+2024-09-27 02:33:00,0.0047548833,157.34379845,35.2166666667,29.7431379
+2024-09-27 02:34:00,-0.12441525,0.1473886,45.0,29.7361958833
+2024-09-27 02:35:00,-0.0405639,94.98558095,36.6833333333,29.7377978667
+2024-09-27 02:36:00,0.0462896167,315.0112146667,26.0,29.7372637333
+2024-09-27 02:37:00,0.0466326,315.9532184,26.0,29.7377977667
+2024-09-27 02:38:00,0.04639215,312.9349576333,26.0,29.7404679333
+2024-09-27 02:39:00,0.0468985333,315.23870485,26.0,29.7399338833
+2024-09-27 02:40:00,0.0468216,315.27394905,26.0,29.7447399667
+2024-09-27 02:41:00,0.0469785833,315.7673800833,26.0,29.7313898667
+2024-09-27 02:42:00,0.0469337333,315.0144189833,26.0,29.74046795
+2024-09-27 02:43:00,0.0469048,314.7933335,26.0,29.74046795
+2024-09-27 02:44:00,0.0470715167,315.4918274167,26.0,29.7431379
+2024-09-27 02:45:00,0.0470362333,315.00480835,26.0,29.7335257333
+2024-09-27 02:46:00,0.0472029833,315.1842376,26.0,29.7399339333
+2024-09-27 02:47:00,0.0473087167,315.13617745,26.0,29.745808
+2024-09-27 02:48:00,0.0471932333,315.1457870167,26.0,29.743138
+2024-09-27 02:49:00,0.04660695,310.8458816833,26.0,29.7431379667
+2024-09-27 02:50:00,0.0456648833,305.3316269333,26.0,29.740468
+2024-09-27 02:51:00,0.0445178,298.1992976833,26.0,29.73833185
+2024-09-27 02:52:00,0.0434860667,292.6946497167,26.0,29.7426039667
+2024-09-27 02:53:00,0.0432296,290.97724785,26.0,29.749546
+2024-09-27 02:54:00,0.0432040667,290.7721854667,26.0,29.7436719167
+2024-09-27 02:55:00,0.0432970667,290.4549791,26.0,29.7329916667
+2024-09-27 02:56:00,0.0433194167,290.52867915,26.0,29.7399339333
+2024-09-27 02:57:00,0.04322965,290.5607143333,26.0,29.7420699167
+2024-09-27 02:58:00,0.04344125,290.6055721833,26.0,29.746876
+2024-09-27 02:59:00,0.0435021667,290.71771565,26.0,29.7367299
+2024-09-27 03:00:00,0.0435214667,290.5927553,26.0,29.744206
+2024-09-27 03:01:00,0.0540019333,347.1066941667,26.0,29.73993385
+2024-09-27 03:02:00,0.0576418333,368.2633756333,26.0,29.74473995
+2024-09-27 03:03:00,0.0527202333,343.3803277833,26.0,29.7426038667
+2024-09-27 03:04:00,0.04592115,303.2105067167,26.0,29.7415358667
+2024-09-27 03:05:00,0.0466999167,307.5744929167,26.0,29.7393998833
+2024-09-27 03:06:00,0.0472317833,310.9836588333,26.0,29.7345938
+2024-09-27 03:07:00,0.0475649833,312.1563579333,26.0,29.7420698
+2024-09-27 03:08:00,0.04734395,311.2495992167,26.0,29.738866
+2024-09-27 03:09:00,0.0474431833,310.86510615,26.0,29.7404678667
+2024-09-27 03:10:00,0.0638448833,396.2640202833,26.0,29.7388658667
+2024-09-27 03:11:00,0.065524,406.7061848667,26.0,29.73779795
+2024-09-27 03:12:00,0.0504935333,329.34956745,26.0,29.7442059167
+2024-09-27 03:13:00,0.0473983333,309.9711643167,26.0,29.74260395
+2024-09-27 03:14:00,0.0424447167,283.4828565333,26.0,29.741536
+2024-09-27 03:15:00,0.0377953833,255.2899694167,26.0,29.74313795
+2024-09-27 03:16:00,0.03785305,255.4533759667,26.0,29.745808
+2024-09-27 03:17:00,0.0378563667,255.5366854333,26.0,29.7468760333
+2024-09-27 03:18:00,0.0472829333,308.4972789,26.0,29.751148
+2024-09-27 03:19:00,0.0498941833,323.61743145,26.0,29.73833175
+2024-09-27 03:20:00,0.0424894833,284.3319457167,26.0,29.7404678833
+2024-09-27 03:21:00,0.0375679833,253.3546925667,26.0,29.7436719167
+2024-09-27 03:22:00,0.0375423167,252.7555274333,26.0,29.74741
+2024-09-27 03:23:00,0.0375904,253.3803253333,26.0,29.743672
+2024-09-27 03:24:00,0.0376640833,253.1336101667,26.0,29.74367195
+2024-09-27 03:25:00,0.0375615667,253.05030385,26.0,29.747944
+2024-09-27 03:26:00,0.0375744333,252.8612624833,26.0,29.7420699667
+2024-09-27 03:27:00,0.0376705,253.28099845,26.0,29.7506140833
+2024-09-27 03:28:00,0.0377152333,253.8545308333,26.0,29.7356619
+2024-09-27 03:29:00,0.0387117333,259.1605227167,26.0,29.74206995
+2024-09-27 03:30:00,0.03922455,262.1916027,26.0,29.745808
+2024-09-27 03:31:00,0.03910915,261.3553326833,26.0,29.7420699667
+2024-09-27 03:32:00,0.0390067,260.8907381,26.0,29.7458079667
+2024-09-27 03:33:00,0.0389554333,260.5575125667,26.0,29.7399339167
+2024-09-27 03:34:00,0.0389555167,260.7016974667,26.0,29.7436719
+2024-09-27 03:35:00,0.0441877833,288.4235809167,26.0,29.749012
+2024-09-27 03:36:00,0.0479494833,310.3844887833,26.0,29.74207
+2024-09-27 03:37:00,0.0478885167,309.2566500667,26.0,29.745274
+2024-09-27 03:38:00,0.0479270167,309.31753065,26.0,29.73886595
+2024-09-27 03:39:00,0.0478468667,308.3627033167,26.0,29.7377979667
+2024-09-27 03:40:00,0.0470171667,304.4793320167,26.0,29.7377979667
+2024-09-27 03:41:00,0.0459371833,297.4303115667,26.0,29.7393998167
+2024-09-27 03:42:00,0.0448575667,291.4162125833,26.0,29.7420699167
+2024-09-27 03:43:00,0.0440210833,285.9916690833,26.0,29.746342
+2024-09-27 03:44:00,0.04395075,285.8506890833,26.0,29.751682
+2024-09-27 03:45:00,-0.0110573667,121.90003245,37.4,29.7372639
+2024-09-27 03:46:00,-0.1126690167,0.0897148,45.0,29.74634195
+2024-09-27 03:47:00,-0.00449845,152.3934623,32.5,29.74794405
+2024-09-27 03:48:00,0.0453445667,305.1297660167,26.0,29.745274
+2024-09-27 03:49:00,0.04602705,307.9750076167,26.0,29.73993395
+2024-09-27 03:50:00,0.0467735,310.778598,26.0,29.7356617667
+2024-09-27 03:51:00,0.0472958667,313.89939125,26.0,29.7410018833
+2024-09-27 03:52:00,0.0475073167,314.2422342667,26.0,29.74741
+2024-09-27 03:53:00,0.0475232667,314.5434197667,26.0,29.7404678167
+2024-09-27 03:54:00,0.0474978,313.4091639333,26.0,29.7415358333
+2024-09-27 03:55:00,0.04764825,313.8128855167,26.0,29.745808
+2024-09-27 03:56:00,0.04776025,314.5338052667,26.0,29.7415359167
+2024-09-27 03:57:00,0.0475169167,313.4380020667,26.0,29.7474099667
+2024-09-27 03:58:00,0.046828,310.2979806333,26.0,29.7410019333
+2024-09-27 03:59:00,0.0464787167,306.3280996167,26.0,29.745274
+2024-09-27 04:00:00,0.0457673667,302.0378071667,26.0,29.7415358833
+2024-09-27 04:01:00,0.0450176333,298.250559,26.0,29.7356617
+2024-09-27 04:02:00,0.0442775167,293.6654930667,26.0,29.7410019167
+2024-09-27 04:03:00,0.0438288333,291.2431877167,26.0,29.7420699167
+2024-09-27 04:04:00,0.0439089667,291.2688204167,26.0,29.744206
+2024-09-27 04:05:00,0.0440627833,291.3072699667,26.0,29.746342
+2024-09-27 04:06:00,0.0439410667,290.2723482,26.0,29.7420699167
+2024-09-27 04:07:00,0.0439602,290.781798,26.0,29.7484778833
+2024-09-27 04:08:00,0.0440884833,291.3937781833,26.0,29.7458078667
+2024-09-27 04:09:00,0.0440755667,290.5799404833,26.0,29.745274
+2024-09-27 04:10:00,0.0440724167,290.4133291833,26.0,29.746876
+2024-09-27 04:11:00,0.04411735,290.2018576667,26.0,29.7431379667
+2024-09-27 04:12:00,0.0441428333,290.5190619667,26.0,29.742604
+2024-09-27 04:13:00,0.0538096333,341.0990077333,26.0,29.74473995
+2024-09-27 04:14:00,0.0583533,367.2701100333,26.0,29.7420699667
+2024-09-27 04:15:00,0.0548991167,349.8269781,26.0,29.7393998667
+2024-09-27 04:16:00,0.0465332833,302.8452408333,26.0,29.744206
+2024-09-27 04:17:00,0.0474208167,308.31784315,26.0,29.74420595
+2024-09-27 04:18:00,0.0477187667,309.5994865833,26.0,29.748478
+2024-09-27 04:19:00,0.0481897667,311.9384817667,26.0,29.741002
+2024-09-27 04:20:00,0.0481514167,311.5187382667,26.0,29.74420595
+2024-09-27 04:21:00,0.05002905,318.7215601,26.0,29.7426039167
+2024-09-27 04:22:00,0.0663730167,406.49151255,26.0,29.7335257333
+2024-09-27 04:23:00,0.06611665,406.5652054,26.0,29.74046785
+2024-09-27 04:24:00,0.0483308167,312.1948093,26.0,29.73993375
+2024-09-27 04:25:00,0.0479527167,309.6475487333,26.0,29.74046775
+2024-09-27 04:26:00,0.0480585,310.0897165,26.0,29.7442059667
+2024-09-27 04:27:00,0.0481802333,309.8494062833,26.0,29.7383319
+2024-09-27 04:28:00,0.0481705167,309.8429980667,26.0,29.7356617667
+2024-09-27 04:29:00,0.0481257667,309.8782428167,26.0,29.7404679167
+2024-09-27 04:30:00,0.0637616,390.9163731333,26.0,29.7351278
+2024-09-27 04:31:00,0.0658217667,403.1784707333,26.0,29.74046795
+2024-09-27 04:32:00,0.0539700333,342.47036085,26.0,29.7399338
+2024-09-27 04:33:00,0.04790135,307.0970830667,26.0,29.74100185
+2024-09-27 04:34:00,0.04801355,308.13521475,26.0,29.7410018
+2024-09-27 04:35:00,0.0480168333,307.65780135,26.0,29.7452739167
+2024-09-27 04:36:00,0.0480809,307.5841043833,26.0,29.7431379667
+2024-09-27 04:37:00,0.0481994167,308.53892895,26.0,29.7367299
+2024-09-27 04:38:00,0.0481867167,308.0134581667,26.0,29.73779795
+2024-09-27 04:39:00,0.0481096333,307.3373918333,26.0,29.7410019
+2024-09-27 04:40:00,0.04822835,308.1832768167,26.0,29.7404679333
+2024-09-27 04:41:00,0.0484781333,308.32745855,26.0,29.7404679
+2024-09-27 04:42:00,0.0483500167,308.138418,26.0,29.7447399667
+2024-09-27 04:43:00,0.0483596667,307.98461755,26.0,29.7458079
+2024-09-27 04:44:00,0.0483949333,307.7987823,26.0,29.7452739667
+2024-09-27 04:45:00,0.0484460167,308.01345475,26.0,29.74153585
+2024-09-27 04:46:00,0.0485197167,308.035888,26.0,29.74153595
+2024-09-27 04:47:00,0.04826035,306.46267115,26.0,29.7404678833
+2024-09-27 04:48:00,0.0485934833,307.7891738167,26.0,29.7340596833
+2024-09-27 04:49:00,0.04870885,308.3082355167,26.0,29.7393998667
+2024-09-27 04:50:00,0.0485742,307.3405925167,26.0,29.7436718833
+2024-09-27 04:51:00,0.0486447167,307.4687555667,26.0,29.74473995
+2024-09-27 04:52:00,0.04870565,307.72829135,26.0,29.74100185
+2024-09-27 04:53:00,0.0486672,307.6385752333,26.0,29.7468759167
+2024-09-27 04:54:00,0.04888515,308.5197056,26.0,29.7410019167
+2024-09-27 04:55:00,0.04891075,307.8820957167,26.0,29.74206995
+2024-09-27 04:56:00,0.04824745,304.6747818333,26.0,29.746342
+2024-09-27 04:57:00,-0.0346524333,80.61198425,40.2,29.74260395
+2024-09-27 04:58:00,-0.1127138,0.1249599,45.0333333333,29.74260395
+2024-09-27 04:59:00,0.0219545167,209.7596945167,29.6333333333,29.739934
+2024-09-27 05:00:00,0.0481770333,315.1297638,26.0,29.7436719667
+2024-09-27 05:01:00,0.0481995333,314.8958663333,26.0,29.74313795
+2024-09-27 05:02:00,0.0483275833,314.7196425333,26.0,29.74100185
+2024-09-27 05:03:00,0.0483819667,314.8958649,26.0,29.7329918833
+2024-09-27 05:04:00,0.0484141167,314.370396,26.0,29.743672
+2024-09-27 05:05:00,0.0483949333,313.5661688,26.0,29.7393999667
+2024-09-27 05:06:00,0.0485646833,314.7260488,26.0,29.7436719667
+2024-09-27 05:07:00,0.04865115,314.4312743833,26.0,29.7410018833
+2024-09-27 05:08:00,0.0485485333,314.107659,26.0,29.743672
+2024-09-27 05:09:00,0.0484973167,313.5052862333,26.0,29.74420595
+2024-09-27 05:10:00,0.0475007833,308.0006398333,26.0,29.7452739667
+2024-09-27 05:11:00,0.0466774333,303.04709575,26.0,29.74473985
+2024-09-27 05:12:00,0.0458507,297.6257609,26.0,29.743672
+2024-09-27 05:13:00,0.0449439333,292.4030777167,26.0,29.743138
+2024-09-27 05:14:00,0.0445210833,289.8942628,26.0,29.7404678
+2024-09-27 05:15:00,0.0446073667,290.0128170333,26.0,29.74473995
+2024-09-27 05:16:00,0.0446394833,290.3075955333,26.0,29.745808
+2024-09-27 05:17:00,0.0446651667,290.0768988667,26.0,29.7393998
+2024-09-27 05:18:00,0.0446427167,289.8077556833,26.0,29.7361957833
+2024-09-27 05:19:00,0.0446042833,289.7853283333,26.0,29.7426039667
+2024-09-27 05:20:00,0.0447163833,289.4200624,26.0,29.745808
+2024-09-27 05:21:00,0.0446362333,289.1893647,26.0,29.745808
+2024-09-27 05:22:00,0.04471945,289.1124675333,26.0,29.748478
+2024-09-27 05:23:00,0.0447741167,290.0032039833,26.0,29.742604
+2024-09-27 05:24:00,0.0448381667,289.1381002667,26.0,29.74367195
+2024-09-27 05:25:00,0.0524832167,327.9493765833,26.0,29.7484779167
+2024-09-27 05:26:00,0.0590390333,364.9471279833,26.0,29.7468759667
+2024-09-27 05:27:00,0.0570554667,356.5395701333,26.0,29.744206
+2024-09-27 05:28:00,0.0468696333,299.8974676167,26.0,29.7447399667
+2024-09-27 05:29:00,0.0477506667,304.8349864833,26.0,29.7436719167
+2024-09-27 05:30:00,0.04860635,309.4649128333,26.0,29.7420699667
+2024-09-27 05:31:00,0.0488306167,310.22108295,26.0,29.7442059333
+2024-09-27 05:32:00,0.0489780833,310.5318780333,26.0,29.74741
+2024-09-27 05:33:00,0.0528933,329.1893584333,26.0,29.7410019167
+2024-09-27 05:34:00,0.0673469833,405.3668688833,26.0,29.73833195
+2024-09-27 05:35:00,0.0665043333,401.9673185667,26.0,29.7319238167
+2024-09-27 05:36:00,0.0485774333,308.2537609167,26.0,29.7394
+2024-09-27 05:37:00,0.0485615333,307.1932043167,26.0,29.7367298
+2024-09-27 05:38:00,0.0486159,307.6994571667,26.0,29.74313795
+2024-09-27 05:39:00,0.0486446833,307.69305115,26.0,29.7431379167
+2024-09-27 05:40:00,0.0487761,308.2281308667,26.0,29.7511480667
+2024-09-27 05:41:00,0.0488017333,307.6257603667,26.0,29.7345938667
+2024-09-27 05:42:00,0.06471325,388.76000865,26.0,29.7324577
+2024-09-27 05:43:00,0.06660035,400.5478989,26.0,29.74100195
+2024-09-27 05:44:00,0.0524832667,329.4681199167,26.0,29.7393997167
+2024-09-27 05:45:00,0.0485550167,305.71931625,26.0,29.7393999167
+2024-09-27 05:46:00,0.0485998,305.5655224833,26.0,29.7308557833
+2024-09-27 05:47:00,0.04867045,305.7705853333,26.0,29.7404678833
+2024-09-27 05:48:00,0.0486704333,305.3572593667,26.0,29.73726385
+2024-09-27 05:49:00,0.0487922333,305.5463005167,26.0,29.74313785
+2024-09-27 05:50:00,0.0488884333,305.8186464833,26.0,29.7393997833
+2024-09-27 05:51:00,0.0485197333,304.1621303333,26.0,29.7410018667
+2024-09-27 05:52:00,0.0487666167,305.4726056333,26.0,29.7404679
+2024-09-27 05:53:00,0.04887565,306.1678906,26.0,29.747944
+2024-09-27 05:54:00,0.0488916833,305.2098665,26.0,29.7474099667
+2024-09-27 05:55:00,0.0489171,305.2771526833,26.0,29.7399339333
+2024-09-27 05:56:00,0.0490068167,305.37327595,26.0,29.7383319
+2024-09-27 05:57:00,0.0491413667,305.9532168,26.0,29.7458079333
+2024-09-27 05:58:00,0.0490069333,305.546301,26.0,29.7410018833
+2024-09-27 05:59:00,0.0491862,305.04966605,26.0,29.7393997833
+2024-09-27 06:00:00,0.04915745,305.4373572667,26.0,29.7388658
+2024-09-27 06:01:00,0.0491414,304.9887890833,26.0,29.7436719167
+2024-09-27 06:02:00,0.0493334833,305.4469687667,26.0,29.7388658833
+2024-09-27 06:03:00,0.04920865,305.7994198333,26.0,29.7361958167
+2024-09-27 06:04:00,0.0491605667,305.5783373,26.0,29.7420699167
+2024-09-27 06:05:00,0.04927585,305.9051556333,26.0,29.7356617833
+2024-09-27 06:06:00,0.0493207333,305.5014391,26.0,29.7399338333
+2024-09-27 06:07:00,0.0495129667,306.1486655667,26.0,29.74420595
+2024-09-27 06:08:00,0.04932715,305.6840725833,26.4166666667,29.747944
+2024-09-27 06:09:00,-0.0597372,34.0916375667,42.9333333333,29.7415358833
+2024-09-27 06:10:00,-0.1055814833,0.1441845,45.2333333333,29.7394
+2024-09-27 06:11:00,0.0409615085,260.5977031017,24.593220339,29.7373182034
+2024-09-27 06:12:00,0.0486128,313.3771246833,26.0,29.745808
+2024-09-27 06:13:00,0.0487183833,312.6049374667,26.0,29.7436719667
+2024-09-27 06:14:00,0.0486800333,311.9865377167,26.0,29.7399338167
+2024-09-27 06:15:00,0.0488466,312.1467479833,26.0,29.7367298333
+2024-09-27 06:16:00,0.0489396333,312.14354105,26.0,29.7335257
+2024-09-27 06:17:00,0.0488947667,312.3261802167,26.0,29.7287196333
+2024-09-27 06:18:00,0.0428388167,279.4168517667,26.0,29.7420699667
+2024-09-27 06:19:00,0.039074,256.5844256667,26.0,29.7426038833
+2024-09-27 06:20:00,0.0390739667,256.6997719,26.0,29.741536
+2024-09-27 06:21:00,0.0384587833,252.7363024167,26.0,29.745274
+2024-09-27 06:22:00,0.0375550667,247.6321688333,26.0,29.745808
+2024-09-27 06:23:00,0.0365522833,241.7654592333,26.0,29.745808
+2024-09-27 06:24:00,0.0355720167,235.6071771333,26.0,29.75008
+2024-09-27 06:25:00,0.0345628333,229.7308566333,26.0,29.742604
+2024-09-27 06:26:00,0.0337872667,225.1842313833,26.0,29.746342
+2024-09-27 06:27:00,0.0337519333,225.0208251333,26.0,29.7538181
+2024-09-27 06:28:00,0.0338897167,225.1714162,26.0,29.75755605
+2024-09-27 06:29:00,0.0338063667,225.2066626667,26.0,29.74474
+2024-09-27 06:30:00,0.0340052333,226.1935325167,26.0,29.7564881833
+2024-09-27 06:31:00,0.0339122333,225.2034590667,26.0,29.7447399667
+2024-09-27 06:32:00,0.0339187167,225.5847474667,26.0,29.749546
+2024-09-27 06:33:00,0.0353893333,233.6494735167,26.0,29.747944
+2024-09-27 06:34:00,0.03567155,235.5559111667,26.0,29.75008005
+2024-09-27 06:35:00,0.0356169833,234.8670274,26.0,29.7479439833
+2024-09-27 06:36:00,0.0355144667,233.915409,26.0,29.751682
+2024-09-27 06:37:00,0.0401890833,259.7084268833,26.0,29.749546
+2024-09-27 06:38:00,0.0463953667,295.0368457167,26.0,29.743672
+2024-09-27 06:39:00,0.0538033833,333.8160822333,26.0,29.746342
+2024-09-27 06:40:00,0.0471804333,297.6449855833,26.0,29.742604
+2024-09-27 06:41:00,0.0477156167,301.4834995833,26.0,29.74474
+2024-09-27 06:42:00,0.04839165,304.7516832,26.0,29.7458079667
+2024-09-27 06:43:00,0.0487923167,306.7991018333,26.0,29.7388658667
+2024-09-27 06:44:00,0.0492118167,308.7055438333,26.0,29.7388658667
+2024-09-27 06:45:00,0.0554533667,337.33098585,26.0,29.7351277833
+2024-09-27 06:46:00,0.0683594833,405.82185205,26.0,29.7372638333
+2024-09-27 06:47:00,0.0640949,386.5555906833,26.0,29.7372637833
+2024-09-27 06:48:00,0.0493528333,308.2089044333,26.0,29.7367298333
+2024-09-27 06:49:00,0.0493302,308.3595030167,26.0,29.7313897667
+2024-09-27 06:50:00,0.0493622333,308.0903568167,26.0,29.7447398833
+2024-09-27 06:51:00,0.0494263833,308.6606860333,26.0,29.7308557667
+2024-09-27 06:52:00,0.0494199667,308.0038441333,26.0,29.74100195
+2024-09-27 06:53:00,0.0508617667,313.4155705,26.0,29.7399339667
+2024-09-27 06:54:00,0.0672604167,399.5514256333,26.0,29.73886585
+2024-09-27 06:55:00,0.0674560667,400.8426777333,26.0,29.7415359
+2024-09-27 06:56:00,0.0499552667,313.1400183167,26.0,29.73779795
+2024-09-27 06:57:00,0.0493111333,305.8474835333,26.0,29.7410019667
+2024-09-27 06:58:00,0.0493495167,306.1070157,26.0,29.74046795
+2024-09-27 06:59:00,0.0493143333,306.13905445,26.0,29.744206
+2024-09-27 07:00:00,0.04940075,305.9307889333,26.0,29.7377978667
+2024-09-27 07:01:00,0.0494136,305.6264004833,26.0,29.7287196833
+2024-09-27 07:02:00,0.0495097,305.95642215,26.0,29.7452739667
+2024-09-27 07:03:00,0.0494840667,306.0717678667,26.0,29.746342
+2024-09-27 07:04:00,0.0495512333,305.8410733333,26.0,29.743138
+2024-09-27 07:05:00,0.0497468833,306.6100526667,26.0,29.745808
+2024-09-27 07:06:00,0.0497339167,306.4498522167,26.0,29.73726385
+2024-09-27 07:07:00,0.0495897833,306.1102144,26.0,29.7436719167
+2024-09-27 07:08:00,0.0497340167,306.32168905,26.0,29.7372638833
+2024-09-27 07:09:00,0.0496378833,305.7193173333,26.0,29.7410018833
+2024-09-27 07:10:00,0.0498321053,305.3677080526,26.0,29.7357462105
+2024-09-27 07:12:00,0.0497555625,305.6832755,26.0,29.7480775
+2024-09-27 07:13:00,0.0498462333,305.6392184,26.0,29.7399339
+2024-09-27 07:14:00,0.0499903,305.64882825,26.0,29.74794405
+2024-09-27 07:15:00,0.0499230333,305.4117235,26.0,29.73833195
+2024-09-27 07:16:00,0.0499871167,305.7833986167,26.0,29.7313897333
+2024-09-27 07:17:00,0.05007685,306.0621585,26.0,29.7415359
+2024-09-27 07:18:00,0.05003835,305.5687268333,26.0,29.745808
+2024-09-27 07:19:00,0.04989405,304.37039555,26.0,29.7474099667
+2024-09-27 07:20:00,0.04167565,263.0791421,28.8,29.7404679
+2024-09-27 07:21:00,-0.0872285333,0.22108295,44.9833333333,29.7506140833
+2024-09-27 07:22:00,-0.0724094,17.5488623,41.8166666667,29.74527395
+2024-09-27 07:23:00,0.0483499833,307.8019850667,26.0,29.7468759667
+2024-09-27 07:24:00,0.0492119167,312.44152535,26.0,29.7442059167
+2024-09-27 07:25:00,0.0495481,312.74271255,26.0,29.74100185
+2024-09-27 07:26:00,0.0494680333,312.32617815,26.0,29.7426038833
+2024-09-27 07:27:00,0.0495801167,312.6273632333,26.0,29.74847795
+2024-09-27 07:28:00,0.0496635833,312.5344479,26.0,29.7393999
+2024-09-27 07:29:00,0.0496218167,312.4190984333,26.0,29.7490120333
+2024-09-27 07:30:00,0.04958975,311.60204775,26.0,29.7410020333
+2024-09-27 07:31:00,0.0496538167,312.0185847833,26.0,29.74634195
+2024-09-27 07:32:00,0.0494168333,309.5962808167,26.0,29.74527395
+2024-09-27 07:33:00,0.04845895,305.0592784333,26.0,29.7442059833
+2024-09-27 07:34:00,0.04750405,299.5065678667,26.0,29.7511479833
+2024-09-27 07:35:00,0.0465620167,294.3543756167,26.0,29.749012
+2024-09-27 07:36:00,0.0458667667,289.9423208167,26.0,29.74580795
+2024-09-27 07:37:00,0.0455462833,287.3213703,26.0,29.7463419667
+2024-09-27 07:38:00,0.0455430333,288.1960911167,26.0,29.746876
+2024-09-27 07:39:00,0.04552065,287.7891676,26.0,29.7484779667
+2024-09-27 07:40:00,0.0455623167,287.8949059833,26.0,29.74420595
+2024-09-27 07:41:00,0.0455687167,287.3918623833,26.0,29.74367195
+2024-09-27 07:42:00,0.04567135,287.3534142333,26.0,29.7479440333
+2024-09-27 07:43:00,0.0455527333,286.3953799833,26.0,29.7458079667
+2024-09-27 07:44:00,0.0457514667,287.1547570167,26.0,29.74741
+2024-09-27 07:45:00,0.0458893167,287.1387348167,26.0,29.7500801
+2024-09-27 07:46:00,0.0457162,286.6741433333,26.0,29.753818
+2024-09-27 07:47:00,0.0458667667,287.6033360167,26.0,29.752216
+2024-09-27 07:48:00,0.0458571833,287.6481927667,26.0,29.746876
+2024-09-27 07:49:00,0.0508780333,311.2880493,26.0,29.74741
+2024-09-27 07:50:00,0.0605222833,363.5917974667,26.0,29.75061405
+2024-09-27 07:51:00,0.0609323667,366.1935277833,26.0,29.751148
+2024-09-27 07:52:00,0.0479590333,298.2249257,26.0,29.75114805
+2024-09-27 07:53:00,0.0487120333,301.4931111,26.0,29.74473995
+2024-09-27 07:54:00,0.0494263833,305.3860923167,26.0,29.75275005
+2024-09-27 07:55:00,0.0499998667,307.4879847333,26.0,29.745808
+2024-09-27 07:56:00,0.05012815,308.2249298167,26.0,29.7554201
+2024-09-27 07:57:00,0.0589138167,349.3976282333,26.0,29.745808
+2024-09-27 07:58:00,0.0693721333,404.0628040167,26.0,29.74901205
+2024-09-27 07:59:00,0.0621339167,370.2435119667,26.0,29.748478
+2024-09-27 08:00:00,0.0494936667,304.52418985,26.0,29.75008
+2024-09-27 08:01:00,0.0498942167,306.0717674167,26.0,29.74741
+2024-09-27 08:02:00,0.049909,306.2349570678,26.0,29.748179322
+2024-09-27 08:03:00,0.0499583667,306.6004457167,26.0,29.7495460333
+2024-09-27 08:04:00,0.0500608333,306.3953823833,26.0,29.75221605
+2024-09-27 08:05:00,0.0536975333,322.7683431833,26.0,29.748478
+2024-09-27 08:06:00,0.0681768667,398.8881753667,26.0,29.74741
+2024-09-27 08:07:00,0.0680359167,398.5485416167,26.0,29.75114805
+2024-09-27 08:08:00,0.0501601333,307.9782125167,26.0,29.7538180833
+2024-09-27 08:09:00,0.0496539167,304.0083339333,26.0,29.7580901667
+2024-09-27 08:10:00,0.0496442,303.2777974167,26.0,29.7490120333
+2024-09-27 08:11:00,0.0499102833,304.0243510333,26.0,29.7490120333
+2024-09-27 08:12:00,0.04988145,303.6590840667,26.0,29.7463419667
+2024-09-27 08:13:00,0.0499167667,303.9891073167,26.0,29.750614
+2024-09-27 08:14:00,0.04977895,303.6142283333,26.0,29.746876
+2024-09-27 08:15:00,0.0499935667,303.8993963667,26.0,29.745808
+2024-09-27 08:16:00,0.05016645,303.9987214,26.0,29.74741
+2024-09-27 08:17:00,0.0500425424,303.6477899322,26.0,29.7530669153
+2024-09-27 08:18:00,0.05017935,303.8737605333,26.0,29.748478
+2024-09-27 08:19:00,0.0501120833,303.47645575,26.0,29.749546
+2024-09-27 08:20:00,0.0503235333,304.2422344333,26.0,29.753284
+2024-09-27 08:21:00,0.0502818333,303.79686285,26.0,29.74741
+2024-09-27 08:22:00,0.05042275,304.1397024667,26.0,29.74794405
+2024-09-27 08:23:00,0.0502881167,303.3546987167,26.0,29.7602261333
+2024-09-27 08:24:00,0.0504260167,304.00192575,26.0,29.7538181
+2024-09-27 08:25:00,0.0505638167,303.7488012167,26.0,29.7564881667
+2024-09-27 08:26:00,0.0504966167,303.4892716,26.0,29.753284
+2024-09-27 08:27:00,0.05047405,303.5693695,26.0,29.7532841333
+2024-09-27 08:28:00,0.0503394333,303.5501454667,26.0,29.7559541167
+2024-09-27 08:29:00,0.0506311833,303.5853897,26.0,29.7543520333
+2024-09-27 08:30:00,0.0506471667,303.7103526333,26.0,29.7580900833
+2024-09-27 08:31:00,0.0507786,304.50496685,26.0,29.7479440167
+2024-09-27 08:32:00,0.03595,221.8487676167,31.65,29.7484780167
+2024-09-27 08:33:00,-0.10431915,0.1057353,45.0,29.7580900333
+2024-09-27 08:34:00,-0.0514836,58.7600131333,38.9333333333,29.749546
+2024-09-27 08:35:00,0.0496955333,312.3582164667,26.0,29.754352
+2024-09-27 08:36:00,0.0499198167,311.2111475333,26.0,29.753818
+2024-09-27 08:37:00,0.0500319667,311.3681482833,26.0,29.7564881833
+2024-09-27 08:38:00,0.04992,310.2723468667,26.0,29.7564881167
+2024-09-27 08:39:00,0.0499936,310.3652668167,26.0,29.7580901667
+2024-09-27 08:40:00,0.0500191333,310.44536575,26.0,29.7623621333
+2024-09-27 08:41:00,0.0500223667,310.1441863,26.0,29.7575561333
+2024-09-27 08:42:00,0.0500577833,309.9231001,26.0,29.7564880333
+2024-09-27 08:43:00,0.0501408,309.2470385667,26.0,29.7532841
+2024-09-27 08:44:00,0.05022085,309.8141625333,26.0,29.75542
+2024-09-27 08:45:00,0.0501727833,309.1893654333,26.0,29.7570221167
+2024-09-27 08:46:00,0.0502786,309.4969577167,26.0,29.7602261833
+2024-09-27 08:47:00,0.0503491667,309.1156731333,26.0,29.7682363
+2024-09-27 08:48:00,0.0494681167,304.8510101667,26.0,29.7554201833
+2024-09-27 08:49:00,0.0482795667,298.5164978667,26.0,29.7661002
+2024-09-27 08:50:00,0.0472414,291.92246205,26.0,29.7591581333
+2024-09-27 08:51:00,0.0462353,286.8952259833,26.0,29.7591581
+2024-09-27 08:52:00,0.0461390833,286.2479938833,26.0,29.7612942
+2024-09-27 08:53:00,0.0462159833,286.65171145,26.0,29.7650322667
+2024-09-27 08:54:00,0.0463056667,286.0493432167,26.0,29.7618281333
+2024-09-27 08:55:00,0.0462767667,285.9788512167,26.0,29.7602262167
+2024-09-27 08:56:00,0.0463153,285.9275858167,26.0,29.7714404333
+2024-09-27 08:57:00,0.0463602,285.9692391667,26.0,29.7596921333
+2024-09-27 08:58:00,0.0464434667,286.0044824333,26.0,29.7586241667
+2024-09-27 08:59:00,0.04632505,285.9436039333,26.0,29.76236225
+2024-09-27 09:00:00,0.0464275167,285.8763188333,26.0,29.7730424667
+2024-09-27 09:01:00,0.0496764833,300.2851632667,26.0,29.7564881167
+2024-09-27 09:02:00,0.0610798,361.0893944833,26.0,29.7682362667
+2024-09-27 09:03:00,0.0617526667,363.74559215,26.0,29.7591580833
+2024-09-27 09:04:00,0.0494008833,303.3546904667,26.0,29.7682363167
+2024-09-27 09:05:00,0.0486703167,297.01057325,26.0,29.7735764167
+2024-09-27 09:06:00,0.04933995,300.34604295,26.0,29.7570221833
+2024-09-27 09:07:00,0.0501088333,303.8769628167,26.0,29.7703724
+2024-09-27 09:08:00,0.0505444833,305.4886237333,26.0,29.7714403333
+2024-09-27 09:09:00,0.0621917333,361.2463944167,26.0,29.7602261167
+2024-09-27 09:10:00,0.0701667167,402.7202851,26.0,29.7661002667
+2024-09-27 09:11:00,0.0618519667,363.80006815,26.0,29.7666341833
+2024-09-27 09:12:00,0.0504453333,304.4985628,26.0,29.7677022
+2024-09-27 09:13:00,0.0505093833,304.9887844333,26.0,29.7628962333
+2024-09-27 09:14:00,0.05056705,304.8221737333,26.0,29.77197435
+2024-09-27 09:15:00,0.05062145,304.8381918333,26.0,29.77464435
+2024-09-27 09:16:00,0.0505766833,304.9311087167,26.0,29.7623623167
+2024-09-27 09:17:00,0.05486705,323.82249005,26.0,29.7661002333
+2024-09-27 09:18:00,0.0689972167,397.5232299667,26.0,29.7661002833
+2024-09-27 09:19:00,0.0671720169,390.0542534576,26.0,29.767186339
+2024-09-27 09:20:00,0.0502336333,302.73630055,26.0,29.7703723167
+2024-09-27 09:21:00,0.0502016167,302.5825072,26.0,29.7650322
+2024-09-27 09:22:00,0.0503074333,302.29092995,26.0,29.7580901
+2024-09-27 09:23:00,0.0503170667,302.65940385,26.0,29.7741103167
+2024-09-27 09:24:00,0.0505863667,302.7234887833,26.0,29.7762464167
+2024-09-27 09:25:00,0.0504806,302.7907723833,26.0,29.7666343
+2024-09-27 09:26:00,0.0505318333,302.4158893167,26.0,29.7730423167
+2024-09-27 09:27:00,0.0506151,302.76193535,26.0,29.76716815
+2024-09-27 09:28:00,0.05069855,303.0342869667,26.0,29.7644982
+2024-09-27 09:29:00,0.0507722,302.8196099833,26.0,29.7725082333
+2024-09-27 09:30:00,0.05083935,303.3803278333,26.0,29.7687703667
+2024-09-27 09:31:00,0.0508714,302.1499504833,26.0,29.7735763333
+2024-09-27 09:32:00,0.0506599167,302.3518115667,26.0,29.7661002333
+2024-09-27 09:33:00,0.0509418833,302.92855065,26.0,29.7719743667
+2024-09-27 09:34:00,0.0509322,302.7683445167,26.0,29.7687702667
+2024-09-27 09:35:00,0.0510027667,302.6049341167,26.0,29.76610015
+2024-09-27 09:36:00,0.0510827667,302.40948315,26.0,29.7687703333
+2024-09-27 09:37:00,0.0510298814,302.7517254068,26.0,29.7688154746
+2024-09-27 09:38:00,0.0511021333,302.3229698833,26.0,29.77250825
+2024-09-27 09:39:00,0.0510635333,302.5120167167,26.0,29.7666341833
+2024-09-27 09:40:00,0.0511084833,302.5024051667,26.0,29.7741104333
+2024-09-27 09:41:00,0.05134565,303.0150629333,26.0,29.7693042833
+2024-09-27 09:42:00,0.05128165,302.111505,26.0,29.7767803667
+2024-09-27 09:43:00,0.05130725,302.2941363667,26.0,29.7634302167
+2024-09-27 09:44:00,0.01636655,178.4652379333,34.3333333333,29.7703722833
+2024-09-27 09:45:00,-0.1269946167,0.1025312,45.0,29.7794504667
+2024-09-27 09:46:00,-0.05292535,71.8295421,38.3333333333,29.7746443667
+2024-09-27 09:47:00,0.0502049167,311.6853536,26.0,29.7714402
+2024-09-27 09:48:00,0.0503587833,311.28484035,26.0,29.7810524667
+2024-09-27 09:49:00,0.05033625,311.1566766833,26.0,29.77571235
+2024-09-27 09:50:00,0.0504581167,310.9708409167,26.0,29.7741103667
+2024-09-27 09:51:00,0.0505125167,310.2755507,26.0,29.7693044
+2024-09-27 09:52:00,0.0506216333,310.1954455667,26.0,29.7671681833
+2024-09-27 09:53:00,0.0506215833,310.4902287167,26.0,29.7719743
+2024-09-27 09:54:00,0.0505990833,310.03844895,26.0,29.7735763667
+2024-09-27 09:55:00,0.0505158333,309.6667727667,26.0,29.7661002333
+2024-09-27 09:56:00,0.0507626,310.0929172167,26.0,29.7725083333
+2024-09-27 09:57:00,0.0508553167,310.512654,26.0,29.7714402667
+2024-09-27 09:58:00,0.0508458,310.25632925,26.0,29.7783823833
+2024-09-27 09:59:00,0.0508330167,309.7436725167,26.0,29.7687702833
+2024-09-27 10:00:00,0.0506502667,308.8753590833,26.0,29.7767804
+2024-09-27 10:01:00,0.04945855,302.2396724167,26.0,29.7757123
+2024-09-27 10:02:00,0.0484076333,295.79301705,26.0,29.7741103667
+2024-09-27 10:03:00,0.04720605,289.9647533333,26.0,29.7799844333
+2024-09-27 10:04:00,0.0465332667,286.1903218,26.0,29.7874607667
+2024-09-27 10:05:00,0.0464371167,285.11694855,26.0,29.7826546333
+2024-09-27 10:06:00,0.04666145,286.3312992,26.0,29.7789164
+2024-09-27 10:07:00,0.0467128,286.4979134833,26.0,29.7815864833
+2024-09-27 10:08:00,0.0466806333,285.9436050333,26.0,29.7746443667
+2024-09-27 10:09:00,0.0467415667,286.1454638833,26.0,29.7799844333
+2024-09-27 10:10:00,0.0467383,286.2640154667,26.0,29.7794504167
+2024-09-27 10:11:00,0.04687605,285.7898076333,26.0,29.7815864833
+2024-09-27 10:12:00,0.04686325,286.1550749833,26.0,29.77731435
+2024-09-27 10:13:00,0.0479878833,290.6504316333,26.0,29.7789164
+2024-09-27 10:14:00,0.06154425,360.4197334333,26.0,29.7783824333
+2024-09-27 10:15:00,0.0620088167,361.8904174667,26.0,29.7810524667
+2024-09-27 10:16:00,0.0517878667,312.96699825,26.0,29.7901308
+2024-09-27 10:17:00,0.04897795,296.9561015333,26.0,29.7810524833
+2024-09-27 10:18:00,0.0496763,299.9038796333,26.0,29.7773143833
+2024-09-27 10:19:00,0.0502722333,302.4959969833,26.0,29.7789164667
+2024-09-27 10:20:00,0.05067595,305.32200945,26.0,29.7778483667
+2024-09-27 10:21:00,0.0635180833,365.0624787667,26.0,29.78051845
+2024-09-27 10:22:00,0.0706857333,401.78788805,26.0,29.77411025
+2024-09-27 10:23:00,0.0596314333,350.8651113167,26.0,29.7778484167
+2024-09-27 10:24:00,0.0508553667,305.4053153167,26.0,29.7789164
+2024-09-27 10:25:00,0.0507016167,304.6074972667,26.0,29.77731435
+2024-09-27 10:26:00,0.0510412,305.3925005,26.0,29.7773144
+2024-09-27 10:27:00,0.0509258,304.7420717,26.0,29.7794504167
+2024-09-27 10:28:00,0.0510700667,305.1746211833,26.0,29.7783823833
+2024-09-27 10:29:00,0.0580037833,337.17718715,26.0,29.7826545167
+2024-09-27 10:30:00,0.0700000333,398.55495185,26.0,29.7783824333
+2024-09-27 10:31:00,0.0651682333,376.7959003333,26.0,29.78746075
+2024-09-27 10:32:00,0.0509643167,302.9317524167,26.0,29.7847906333
+2024-09-27 10:33:00,0.0507464576,302.2075684237,26.0,29.7867366271
+2024-09-27 10:34:00,0.0508714,303.0406946833,26.0,29.7895968333
+2024-09-27 10:35:00,0.0510507667,303.1944894833,26.0,29.7901308833
+2024-09-27 10:36:00,0.0509899667,303.2489629167,26.0,29.7815865333
+2024-09-27 10:37:00,0.0510571167,302.9029194667,26.0,29.7906649167
+2024-09-27 10:38:00,0.0512559167,303.4091706,26.0,29.7858587333
+2024-09-27 10:39:00,0.0510604,302.3966693667,26.0,29.7842566167
+2024-09-27 10:40:00,0.0511533667,302.8676736833,26.0,29.7842565667
+2024-09-27 10:41:00,0.0511886,302.9541834333,26.0,29.7821205333
+2024-09-27 10:42:00,0.0512815333,302.8132023167,26.0,29.7858587
+2024-09-27 10:43:00,0.0513296833,302.7234898167,26.0,29.7863926333
+2024-09-27 10:44:00,0.05126545,302.4991997833,26.0,29.7863927333
+2024-09-27 10:45:00,0.0514066167,303.2617772167,26.0,29.78479065
+2024-09-27 10:46:00,0.0513681833,302.6529972167,26.0,29.7911989333
+2024-09-27 10:47:00,0.0513586,302.5664855833,26.0,29.7890627833
+2024-09-27 10:48:00,0.0516308,303.16565705,26.0,29.7960051833
+2024-09-27 10:49:00,0.0515219333,303.19449,26.0,29.7885288
+2024-09-27 10:50:00,0.0512303,301.3553380167,26.0,29.7901309
+2024-09-27 10:51:00,0.0436782167,263.0983675167,26.0,29.79386905
+2024-09-27 10:52:00,0.0414898,250.5639224833,26.0,29.7901309833
+2024-09-27 10:53:00,0.0409131833,248.1896809833,26.0,29.7949371
+2024-09-27 10:54:00,0.0400416167,243.1592430333,26.0,29.8029474833
+2024-09-27 10:55:00,0.0389361167,237.22845095,26.0,29.8077536
+2024-09-27 10:56:00,0.0022813,125.2579300333,35.2166666667,29.7965391667
+2024-09-27 10:57:00,-0.1292695167,0.1153476,45.0,29.79600515
+2024-09-27 10:58:00,-0.0330983833,95.1586039667,35.4666666667,29.7933349833
+2024-09-27 10:59:00,0.0391477,249.5257922167,26.0,29.7992092833
+2024-09-27 11:00:00,0.03998085,253.2777955167,26.0,29.7949370833
+2024-09-27 11:01:00,0.04071125,256.5716084667,26.0,29.7981411833
+2024-09-27 11:02:00,0.0410637833,258.5421366333,26.0,29.7960051333
+2024-09-27 11:03:00,0.0411567,258.7792414333,26.0,29.8029474333
+2024-09-27 11:04:00,0.0411727,258.2505625167,26.0,29.8056175667
+2024-09-27 11:05:00,0.04112785,258.0519063,26.0,29.7960051833
+2024-09-27 11:06:00,0.0421723667,263.5565529667,26.0,29.794403
+2024-09-27 11:07:00,0.04196085,262.2492812833,26.0,29.7944030833
+2024-09-27 11:08:00,0.0412175167,257.9653934667,26.0,29.7992093
+2024-09-27 11:09:00,0.0404356167,253.2297331833,26.0,29.8034815
+2024-09-27 11:10:00,0.0393943,247.53284275,26.0,29.8002773333
+2024-09-27 11:11:00,0.03835945,241.9577049667,26.0,29.7992092833
+2024-09-27 11:12:00,0.04391855,270.2915700333,26.0,29.8066855833
+2024-09-27 11:13:00,0.0468665667,287.7731511,26.0,29.80561755
+2024-09-27 11:14:00,0.04681845,287.20602455,26.0,29.7986754167
+2024-09-27 11:15:00,0.0469337333,286.7894911167,26.0,29.8002772833
+2024-09-27 11:16:00,0.04706515,287.5040027167,26.0,29.7960052333
+2024-09-27 11:17:00,0.0470522167,287.3502083333,26.0,29.7970731
+2024-09-27 11:18:00,0.0471804333,287.542458,26.0,29.8056175667
+2024-09-27 11:19:00,0.0472477167,287.6610045667,26.0,29.80508365
+2024-09-27 11:20:00,0.0471677,287.0201821333,26.0,29.79814125
+2024-09-27 11:21:00,0.0472766167,287.4271066167,26.0,29.8029473
+2024-09-27 11:22:00,0.0474175667,287.7763533333,26.0,29.8034814833
+2024-09-27 11:23:00,0.0474305333,287.1707772333,26.0,29.8029474833
+2024-09-27 11:24:00,0.0473184,287.5873132333,26.0,29.7992093167
+2024-09-27 11:25:00,0.0476707167,288.16084585,26.0,29.80615165
+2024-09-27 11:26:00,0.0613424167,356.78628785,26.0,29.8024133833
+2024-09-27 11:27:00,0.0620761667,361.8327474,26.0,29.7960051167
+2024-09-27 11:28:00,0.0442102833,273.3963437,26.0,29.8082877167
+2024-09-27 11:29:00,0.0380998167,238.1512334667,26.0,29.8098897667
+2024-09-27 11:30:00,0.0388689,242.72989455,26.0,29.8066855833
+2024-09-27 11:31:00,0.0496314833,296.7414269333,26.0,29.8072196
+2024-09-27 11:32:00,0.0537391167,319.8526125,26.0,29.80454955
+2024-09-27 11:33:00,0.0541491833,321.4130061833,26.0,29.80081135
+2024-09-27 11:34:00,0.0542742333,321.0733727167,26.0,29.7997433333
+2024-09-27 11:35:00,0.0543287,321.0990017333,26.0,29.7976072667
+2024-09-27 11:36:00,0.05433195,321.0861838667,26.0,29.8077537
+2024-09-27 11:37:00,0.0544153,320.5543113333,26.0,29.7938690667
+2024-09-27 11:38:00,0.05429675,319.9166927833,26.0,29.8002772333
+2024-09-27 11:39:00,0.0533481333,315.1073405333,26.0,29.8088217167
+2024-09-27 11:40:00,0.0523998333,310.6952881,26.0,29.8093557333
+2024-09-27 11:41:00,0.0511118,304.1429047667,26.0,29.8120258667
+2024-09-27 11:42:00,0.0501185,298.1255998,26.0,29.8114918333
+2024-09-27 11:43:00,0.0499421833,297.571289,26.0,29.8082876333
+2024-09-27 11:44:00,0.0492053,293.322656,26.0,29.8179002333
+2024-09-27 11:45:00,0.04921815,292.8676741667,26.0,29.8200363667
+2024-09-27 11:46:00,0.0485262,289.4585042167,26.0,29.8173662
+2024-09-27 11:47:00,0.04666795,280.3876944167,26.0,29.8162981333
+2024-09-27 11:48:00,0.0393016167,239.0483828,26.0,29.814162
+2024-09-27 11:49:00,0.0497052,296.8151209833,26.0,29.8152300667
+2024-09-27 11:50:00,0.0515347333,305.3828882333,26.0,29.8077536
+2024-09-27 11:51:00,0.0400447667,246.6164699667,26.0,29.8018793667
+2024-09-27 11:52:00,0.0408264833,248.0711297833,26.0,29.8173662
+2024-09-27 11:53:00,0.0409161167,248.3210496833,26.0,29.8152300667
+2024-09-27 11:54:00,0.0408938167,248.5389278667,26.0,29.8152300667
+2024-09-27 11:55:00,0.0408137833,248.1416208333,26.0,29.8189683
+2024-09-27 11:56:00,0.0410411833,248.7760330833,26.0,29.8173662
+2024-09-27 11:57:00,0.0534796667,311.5924372167,26.0,29.8146960333
+2024-09-27 11:58:00,0.044739,269.7372602833,26.0,29.8189683
+2024-09-27 11:59:00,0.0426370167,257.4943936667,26.0,29.8162981333
+2024-09-27 12:00:00,0.04284855,258.0102507,26.0,29.8120258667
+2024-09-27 12:01:00,0.0426850667,257.0682414333,26.0,29.8109578
+2024-09-27 12:02:00,0.04266595,256.6100587667,26.0,29.8125599
+2024-09-27 12:03:00,0.0433836167,259.8718301167,26.0,29.814162
+2024-09-27 12:04:00,0.0513072333,300.8490871167,26.0,29.8157641
+2024-09-27 12:05:00,0.0520217167,305.1009279,26.0,29.8184342667
+2024-09-27 12:06:00,0.0518903667,304.05639195,26.0,29.8152300667
+2024-09-27 12:07:00,0.0520313333,303.7295802833,26.0,29.8162981333
+2024-09-27 12:08:00,-0.0369862034,77.6804495254,40.4745762712,29.8171489661
+2024-09-27 12:09:00,-0.11487025,0.1121435,45.05,29.8098897333
+2024-09-27 12:10:00,0.0315410667,238.1960925833,29.3333333333,29.8221725
+2024-09-27 12:11:00,0.0509738667,311.51233215,26.0,29.8114918333
+2024-09-27 12:12:00,0.0512111,311.1727025667,26.0,29.8136279667
+2024-09-27 12:13:00,0.05108605,310.30118395,26.0,29.8157641
+2024-09-27 12:14:00,0.0512304,310.4005115667,26.0,29.8125599
+2024-09-27 12:15:00,0.0512784,310.2114668,26.0,29.8189683
+2024-09-27 12:16:00,0.0512880333,309.7789157167,26.0,29.8130939333
+2024-09-27 12:17:00,0.0512109833,309.8237740333,26.0,29.8152300667
+2024-09-27 12:18:00,0.0513200167,309.2085894667,26.0,29.8088216667
+2024-09-27 12:19:00,0.0514321833,309.9839801667,26.0,29.8157641
+2024-09-27 12:20:00,0.0514899,309.5610374833,26.0,29.8216384667
+2024-09-27 12:21:00,0.0516083833,309.7981444,26.0,29.8211044333
+2024-09-27 12:22:00,0.05155385,309.64114365,26.0,29.8162981333
+2024-09-27 12:23:00,0.0517076,309.4873421167,26.0,29.8136279667
+2024-09-27 12:24:00,0.0513617333,307.95578205,26.0,29.8157641
+2024-09-27 12:25:00,0.0516628333,309.5738559167,26.0,29.8195023333
+2024-09-27 12:26:00,0.0518550333,310.0064064833,26.0,29.8173662
+2024-09-27 12:27:00,0.0518454167,309.7468789333,26.0,29.8146960333
+2024-09-27 12:28:00,0.0517814167,309.30471175,26.0,29.8162981333
+2024-09-27 12:29:00,0.0509035333,304.8990703333,26.0,29.8195023333
+2024-09-27 12:30:00,0.0495673833,297.8564544667,26.0,29.8221725
+2024-09-27 12:31:00,0.04852615,292.28452505,26.0,29.8211044333
+2024-09-27 12:32:00,0.0476676,287.2572879333,26.0,29.8237746
+2024-09-27 12:33:00,0.0474625167,286.0877928,26.0,29.8227065333
+2024-09-27 12:34:00,0.047549,286.7157926333,26.0,29.8291149333
+2024-09-27 12:35:00,0.0475392667,286.0877908167,26.0,29.8232405667
+2024-09-27 12:36:00,0.0476994833,286.4850987167,26.0,29.8243086333
+2024-09-27 12:37:00,0.047802,287.1098930167,26.0,29.8227065333
+2024-09-27 12:38:00,0.054066,316.9753289333,26.0,29.8243086333
+2024-09-27 12:39:00,0.0627169667,361.4194152333,26.0,29.8355233333
+2024-09-27 12:40:00,0.0619449167,358.9907099333,26.0,29.8323191333
+2024-09-27 12:41:00,0.04946175,294.5209832167,26.0,29.8264447667
+2024-09-27 12:42:00,0.0502626167,298.1480291167,26.0,29.8328531667
+2024-09-27 12:43:00,0.0510189333,302.72348575,26.0,29.8333872
+2024-09-27 12:44:00,0.0516692167,305.02723095,26.0,29.8296489667
+2024-09-27 12:45:00,0.0519191167,306.97532565,26.0,29.8243086333
+2024-09-27 12:46:00,0.0520185667,306.8311404167,26.0,29.8397956
+2024-09-27 12:47:00,0.0520698333,306.7606508667,26.0,29.8285809
+2024-09-27 12:48:00,0.0520504833,306.4017914833,26.0,29.8371254333
+2024-09-27 12:49:00,0.0521530833,306.1646867667,26.0,29.8323191333
+2024-09-27 12:50:00,0.0522493167,307.0265925333,26.0,29.8307170333
+2024-09-27 12:51:00,0.0516595667,303.4508204333,26.0,29.8360573667
+2024-09-27 12:52:00,0.0512398833,300.9644332667,26.0,29.8397956
+2024-09-27 12:53:00,0.0502338167,295.9980777667,26.0,29.8408636667
+2024-09-27 12:54:00,0.0493078833,291.73982855,26.0,29.8381935
+2024-09-27 12:55:00,0.0486831333,287.6353759,26.0,29.8360573667
+2024-09-27 12:56:00,0.0481097333,284.7901331333,26.0,29.8371254333
+2024-09-27 12:57:00,0.0481002167,284.4889479833,26.0,29.8408636667
+2024-09-27 12:58:00,0.0480745,284.5241944167,26.0,29.8360573667
+2024-09-27 12:59:00,0.0481834667,284.7901336167,26.0,29.8381935
+2024-09-27 13:00:00,0.0518520333,300.5799408167,26.0,29.8413977
+2024-09-27 13:01:00,0.0658218,373.1207963333,26.0,29.8381935
+2024-09-27 13:02:00,0.0657513167,374.1941676,26.0,29.8429998
+2024-09-27 13:03:00,0.05528345,319.2310171,26.0,29.846204
+2024-09-27 13:04:00,0.0516147833,300.9516193667,26.0,29.8376594667
+2024-09-27 13:05:00,0.0523229167,304.6235190167,26.0,29.8413977
+2024-09-27 13:06:00,0.0524767167,305.15540185,26.0,29.8413977
+2024-09-27 13:07:00,0.0551329167,315.23870865,26.0,29.8429998
+2024-09-27 13:08:00,0.072573,400.52226205,26.0,29.8408636667
+2024-09-27 13:09:00,0.0718263167,398.5773757667,26.0,29.8429998
+2024-09-27 13:10:00,0.0572988833,328.3146386667,26.0,29.8446019
+2024-09-27 13:11:00,0.0521884,302.4575469333,26.0,29.8504760167
+2024-09-27 13:12:00,0.05213705,302.6465901167,26.0,29.8467380333
+2024-09-27 13:13:00,0.05225895,302.8612655,26.0,29.8467380333
+2024-09-27 13:14:00,0.0522045,302.7971821167,26.0,29.85421425
+2024-09-27 13:15:00,0.0523133667,302.5760979833,26.0,29.8520781167
+2024-09-27 13:16:00,0.0524477833,303.0535135833,26.0,29.8515439833
+2024-09-27 13:17:00,0.0524447167,303.3354741667,26.0,29.8494078
+2024-09-27 13:18:00,0.0525278833,303.0791458,26.0,29.849408
+2024-09-27 13:19:00,0.0520954,302.3774475,26.6666666667,29.8515443333
+2024-09-27 13:20:00,-0.064662,24.9182950333,43.2166666667,29.8499422333
+2024-09-27 13:21:00,-0.10329065,0.1217558,45.3666666667,29.85902025
+2024-09-27 13:22:00,0.0497758333,290.00640985,25.9166666667,29.8558166
+2024-09-27 13:23:00,0.0515636333,310.7850025667,26.0,29.8611565333
+2024-09-27 13:24:00,0.0517813833,310.8715138167,26.0,29.8568842667
+2024-09-27 13:25:00,0.0518295167,309.8846515167,26.0,29.85101
+2024-09-27 13:26:00,0.0519191,310.59595935,26.0,29.86222455
+2024-09-27 13:27:00,0.0519800667,310.31399825,26.0,29.8542141
+2024-09-27 13:28:00,0.05202165,309.9359185333,26.0,29.8552822667
+2024-09-27 13:29:00,0.0520280833,309.8429991,26.0,29.8600884667
+2024-09-27 13:30:00,0.0522300667,310.5478982333,26.0,29.85901985
+2024-09-27 13:31:00,0.0522653333,310.0000004167,26.0,29.86863255
+2024-09-27 13:32:00,0.0521884333,310.2915729667,26.0,29.8664965167
+2024-09-27 13:33:00,0.0518326833,308.0871524667,26.0,29.8659623833
+2024-09-27 13:34:00,0.0521916667,309.2021803,26.0,29.8606221
+2024-09-27 13:35:00,0.0523678,309.724449,26.0,29.8600880667
+2024-09-27 13:36:00,0.05240945,309.4424863833,26.0,29.8600885167
+2024-09-27 13:37:00,0.0524189,309.1957695167,26.0,29.8675642333
+2024-09-27 13:38:00,0.0525855333,309.58026255,26.0,29.8648945167
+2024-09-27 13:39:00,0.05257595,309.52579325,26.0,29.8622245
+2024-09-27 13:40:00,0.0525407333,309.4264640833,26.0,29.8686325
+2024-09-27 13:41:00,0.0527939667,309.5642418333,26.0,29.8697003167
+2024-09-27 13:42:00,0.0526625167,309.69881575,26.0,29.8595542333
+2024-09-27 13:43:00,0.0526722667,309.3719973667,26.0,29.8654285
+2024-09-27 13:44:00,0.0524094,307.4174927,26.0,29.8697005667
+2024-09-27 13:45:00,0.05141285,302.2012138167,26.0,29.8787783833
+2024-09-27 13:46:00,0.05004145,295.1874383667,26.0,29.8739726333
+2024-09-27 13:47:00,0.04873765,288.8657485333,26.0,29.8723706833
+2024-09-27 13:48:00,0.04835,286.12944425,26.0,29.86542875
+2024-09-27 13:49:00,0.0483051667,286.0781792333,26.0,29.87023445
+2024-09-27 13:50:00,0.048462,286.16148665,26.0,29.8718367
+2024-09-27 13:51:00,0.0483532167,285.8763208167,26.0,29.8691666833
+2024-09-27 13:52:00,0.05937505,338.3530939667,26.0,29.8718366
+2024-09-27 13:53:00,0.0637711,360.794613,26.0,29.8777105167
+2024-09-27 13:54:00,0.05896825,339.0676066667,26.0,29.8771765333
+2024-09-27 13:55:00,0.0505670167,295.5014404333,26.0,29.8761085667
+2024-09-27 13:56:00,0.0514257667,299.9935923167,26.0,29.87504045
+2024-09-27 13:57:00,0.0521819833,303.8353124667,26.0,29.8713026167
+2024-09-27 13:58:00,0.0526240667,305.6328076333,26.0,29.87984645
+2024-09-27 13:59:00,0.0529189,306.8023032167,26.0,29.8830509
+2024-09-27 14:00:00,0.05303755,307.2028183167,26.0,29.8771766833
+2024-09-27 14:01:00,0.0530535333,306.8471604833,26.0,29.8793126167
+2024-09-27 14:02:00,0.0530567333,306.4242200333,26.0,29.8771765333
+2024-09-27 14:03:00,0.05310155,306.7157899333,26.0,29.8782445
+2024-09-27 14:04:00,0.0529958,306.0749742667,26.0,29.8835849333
+2024-09-27 14:05:00,0.0523742333,302.7683409,26.0,29.8846526
+2024-09-27 14:06:00,0.0518583167,300.2371064333,26.0,29.88305045
+2024-09-27 14:07:00,0.0509324167,295.1265597667,26.0,29.8809144167
+2024-09-27 14:08:00,0.050032,290.7657801833,26.0,29.8787784833
+2024-09-27 14:09:00,0.0493462667,286.9272679667,26.0,29.8889243167
+2024-09-27 14:10:00,0.0487663,283.3066383167,26.0,29.8835844333
+2024-09-27 14:11:00,0.0487023,283.8449255167,26.0,29.8958668
+2024-09-27 14:12:00,0.05454655,309.3143255667,26.0,29.8899925833
+2024-09-27 14:13:00,0.06679275,372.6337748833,26.0,29.8862547
+2024-09-27 14:14:00,0.0652804167,365.9019578,26.0,29.8969348167
+2024-09-27 14:15:00,0.0557321,317.2540878333,26.0,29.8937304667
+2024-09-27 14:16:00,0.0524286,300.4421665,26.0,29.89426445
+2024-09-27 14:17:00,0.05301185,303.8289058333,26.0,29.8921289167
+2024-09-27 14:18:00,0.0529158,304.0179469833,26.0,29.8857209667
+2024-09-27 14:19:00,0.0581961167,325.5046488833,26.0,29.8964004833
+2024-09-27 14:20:00,0.07336435,398.6254418333,26.0,29.8910606
+2024-09-27 14:21:00,0.0704708667,386.1422610833,26.0,29.8958669
+2024-09-27 14:22:00,0.0575841,324.1941725667,26.0,29.8867887833
+2024-09-27 14:23:00,0.0527233833,301.3361072667,26.0,29.8947987333
+2024-09-27 14:24:00,0.0529253333,301.90963715,26.0,29.907081
+2024-09-27 14:25:00,0.0528709167,301.5379657833,26.0,29.9012068333
+2024-09-27 14:26:00,0.0526176667,299.8750401,26.0,29.8937307167
+2024-09-27 14:27:00,0.0528484167,301.0028808333,26.0,29.89746915
+2024-09-27 14:28:00,0.0529959167,301.4130064667,26.0,29.8931965833
+2024-09-27 14:29:00,0.0530663833,301.8071114333,26.0,29.9028085333
+2024-09-27 14:30:00,0.05318155,302.0538278167,26.0,29.9028086833
+2024-09-27 14:31:00,0.04692725,276.9785301,28.2166666667,29.9012066833
+2024-09-27 14:32:00,-0.0836815833,1.05414925,44.9333333333,29.9097509667
+2024-09-27 14:33:00,-0.0800161,8.4844597667,42.45,29.9070808
+2024-09-27 14:34:00,0.0563600667,330.5350841667,26.0,29.9022745
+2024-09-27 14:35:00,0.052371,310.1537941833,26.0,29.9028089333
+2024-09-27 14:36:00,0.0523454,309.5706489833,26.0,29.89586625
+2024-09-27 14:37:00,0.05242225,309.3335482667,26.0,29.9097509667
+2024-09-27 14:38:00,0.0525119333,308.9907102,26.0,29.9038767
+2024-09-27 14:39:00,0.0525503,309.3944267833,26.0,29.9113530667
+2024-09-27 14:40:00,0.0525438333,309.0291598167,26.0,29.910285
+2024-09-27 14:41:00,0.05269455,308.8945875,26.0,29.9060127333
+2024-09-27 14:42:00,0.0526593667,308.4331993833,26.0,29.9145571667
+2024-09-27 14:43:00,0.05257605,307.1835917833,26.0,29.9108190333
+2024-09-27 14:44:00,0.05297345,309.5706505333,26.0,29.9113530667
+2024-09-27 14:45:00,0.0528868167,308.8657483,26.0,29.9108190333
+2024-09-27 14:46:00,0.0529093167,308.3723188667,26.0,29.91669335
+2024-09-27 14:47:00,0.05306965,308.68952455,26.0,29.9161593667
+2024-09-27 14:48:00,0.0530728333,308.4972802167,26.0,29.9086828667
+2024-09-27 14:49:00,0.0532200333,308.7023425167,26.0,29.9140232333
+2024-09-27 14:50:00,0.0531848333,308.49407685,26.0,29.9124210833
+2024-09-27 14:51:00,0.0533322167,309.28548565,26.0,29.9129551167
+2024-09-27 14:52:00,0.0532745,308.8561357667,26.0,29.910819
+2024-09-27 14:53:00,0.0533033167,308.2858050833,26.0,29.91348915
+2024-09-27 14:54:00,0.0533322333,308.37552015,26.0,29.9193634167
+2024-09-27 14:55:00,0.05337055,307.7475193,26.0,29.9193635333
+2024-09-27 14:56:00,0.0528419667,305.99487465,26.0,29.9198975667
+2024-09-27 14:57:00,0.0517653,299.891064,26.0,29.9284418
+2024-09-27 14:58:00,0.05041325,292.95097855,26.0,29.9279079
+2024-09-27 14:59:00,0.04945205,287.8692738333,26.0,29.9236357167
+2024-09-27 15:00:00,0.0490484,285.5879520667,26.0,29.9407244167
+2024-09-27 15:01:00,0.0490419667,285.7129124,26.0,29.9263058
+2024-09-27 15:02:00,0.0490739833,285.2899689667,26.0,29.9273738167
+2024-09-27 15:03:00,0.0491028,285.0881148167,26.0,29.9279078
+2024-09-27 15:04:00,0.0490259667,285.0945191667,26.0,29.9247037333
+2024-09-27 15:05:00,0.0497885167,287.00096325,26.0,29.9295099167
+2024-09-27 15:06:00,0.0641524,357.1771855667,26.0,29.9327140833
+2024-09-27 15:07:00,0.0648670333,360.3844914333,26.0,29.9385882833
+2024-09-27 15:08:00,0.0553860667,316.8247390833,26.0,29.9353842333
+2024-09-27 15:09:00,0.04811925,279.96796185,26.0,29.9380543167
+2024-09-27 15:10:00,0.0470072833,271.64690575,26.0,29.93858835
+2024-09-27 15:11:00,0.05329385,304.3127167333,26.0,29.93805435
+2024-09-27 15:12:00,0.05410445,307.5937220333,26.0,29.93698625
+2024-09-27 15:13:00,0.05443765,309.6539579833,26.0,29.9364522
+2024-09-27 15:14:00,0.0545723,310.13457015,26.0,29.92630585
+2024-09-27 15:15:00,0.0547580333,309.7532855667,26.0,29.9364522167
+2024-09-27 15:16:00,0.05462355,309.5802672,26.0,29.9343161667
+2024-09-27 15:17:00,0.0543927833,308.3947438,26.0,29.93645225
+2024-09-27 15:18:00,0.0536269333,303.9794989167,26.0,29.9375202833
+2024-09-27 15:19:00,0.05297005,300.8138418,26.0,29.9375203167
+2024-09-27 15:20:00,0.051868,294.9919875167,26.0,29.9396563833
+2024-09-27 15:21:00,0.0507399333,289.22460485,26.0,29.9391224
+2024-09-27 15:22:00,0.049734,284.0115347667,26.0,29.9417924333
+2024-09-27 15:23:00,0.0487409,279.49054845,26.0,29.9460645667
+2024-09-27 15:24:00,0.0481191167,274.7004171,26.0,29.94499655
+2024-09-27 15:25:00,0.0537840167,304.0852300667,26.0,29.9524726833
+2024-09-27 15:26:00,0.053486,303.9666769333,26.0,29.9514045333
+2024-09-27 15:27:00,0.0414867167,241.5123356,26.0,29.94873455
+2024-09-27 15:28:00,0.04259205,246.4883045,26.0,29.9508707333
+2024-09-27 15:29:00,0.0436014833,251.9961540167,26.0,29.9498025667
+2024-09-27 15:30:00,0.0462127167,263.4379994167,26.0,29.9503366667
+2024-09-27 15:31:00,0.0632105167,343.4347966333,26.0,29.95033665
+2024-09-27 15:32:00,0.07438625,398.5068901833,26.0,29.9482006833
+2024-09-27 15:33:00,0.0697692333,377.7314951,26.0,29.9433945333
+2024-09-27 15:34:00,0.0565395167,317.6321695833,26.0,29.9546087833
+2024-09-27 15:35:00,0.0532841833,300.8490814333,26.0,29.95033665
+2024-09-27 15:36:00,0.0533609333,300.5222690833,26.0,29.9465985
+2024-09-27 15:37:00,0.0532968667,300.3396336333,26.0,29.9487346
+2024-09-27 15:38:00,0.0534057833,300.0384500667,26.0,29.9514047
+2024-09-27 15:39:00,0.0534090667,300.4069198167,26.0,29.9524727167
+2024-09-27 15:40:00,0.0535372833,300.7016942833,26.0,29.9465985667
+2024-09-27 15:41:00,0.0534795333,300.1986545667,26.0,29.9465985167
+2024-09-27 15:42:00,0.05358865,300.5671247667,26.0,29.9626188167
+2024-09-27 15:43:00,0.03252485,200.7882059,32.6166666667,29.9540748
+2024-09-27 15:44:00,-0.10959935,0.1153476,45.0,29.9631529167
+2024-09-27 15:45:00,-0.0413905,79.7725100333,36.4,29.9615507833
+2024-09-27 15:46:00,0.0560749167,329.3079166833,26.0,29.9535406333
+2024-09-27 15:47:00,0.0525438,309.3816109333,26.0,29.9540746667
+2024-09-27 15:48:00,0.05260795,308.6190356,26.0,29.9556767
+2024-09-27 15:49:00,0.05272665,308.1832766667,26.0,29.96208485
+2024-09-27 15:50:00,0.0527842833,308.3498935333,26.0,29.96582285
+2024-09-27 15:51:00,0.0528163333,307.6770258333,26.0,29.96582295
+2024-09-27 15:52:00,0.0526048667,306.5716087,26.0,29.9636869667
+2024-09-27 15:53:00,0.0528932833,307.7282907833,26.0,29.9652889167
+2024-09-27 15:54:00,0.0530566667,308.5100936,26.0,29.971697
+2024-09-27 15:55:00,0.0530311167,308.7407880833,26.0,29.971163
+2024-09-27 15:56:00,0.0530728,308.2569710667,26.0,29.97009495
+2024-09-27 15:57:00,0.0532072833,308.2954217667,26.0,29.96902695
+2024-09-27 15:58:00,0.053313,308.2761976667,26.0,29.9658228167
+2024-09-27 15:59:00,0.0534508,309.0964455333,26.0,29.975969
+2024-09-27 16:00:00,0.05337385,308.0134549667,26.0,29.972231
+2024-09-27 16:01:00,0.0533097333,307.5680857833,26.0,29.9754350667
+2024-09-27 16:02:00,0.0533257333,308.0038496833,26.0,29.9716969667
+2024-09-27 16:03:00,0.0533322833,307.4014720333,26.0,29.972765
+2024-09-27 16:04:00,0.0533835,307.4559392667,26.0,29.970629
+2024-09-27 16:05:00,0.0535789333,307.9045200333,26.0,29.9781051
+2024-09-27 16:06:00,0.0535020333,307.39185935,26.0,29.973833
+2024-09-27 16:07:00,0.05359185,308.1448260833,26.0,29.9759691
+2024-09-27 16:08:00,0.0535917667,307.8116006667,26.0,29.9759691
+2024-09-27 16:09:00,0.05222365,301.4162150333,26.0,29.9823771667
+2024-09-27 16:10:00,0.0448637333,262.3261789333,26.0,29.9807752333
+2024-09-27 16:11:00,0.0505030667,291.0637609167,26.0,29.9807753667
+2024-09-27 16:12:00,0.0489491167,282.8228124,26.0,29.974367
+2024-09-27 16:13:00,0.0479365833,277.1355323333,26.0,29.9845132333
+2024-09-27 16:14:00,0.0478660667,277.0265916833,26.0,29.9834451
+2024-09-27 16:15:00,0.0479655,276.6421007,26.0,29.9909213667
+2024-09-27 16:16:00,0.04790465,276.7574495333,26.0,29.9829111
+2024-09-27 16:17:00,0.0479397833,276.8439577667,26.0,29.9797071667
+2024-09-27 16:18:00,0.0479398,277.0778555833,26.0,29.9882513
+2024-09-27 16:19:00,0.0481482667,276.67414165,26.0,29.9866493333
+2024-09-27 16:20:00,0.0481290167,276.5716103167,26.0,29.9967956667
+2024-09-27 16:21:00,0.0481778621,276.7680508621,26.0,29.993701931
+2024-09-27 16:22:00,0.0482698833,276.9849382333,26.0,29.9839791667
+2024-09-27 16:23:00,0.0481706333,276.8055050833,26.0,29.9887852667
+2024-09-27 16:24:00,0.0485966833,278.6831122,26.0,29.9882513333
+2024-09-27 16:25:00,0.0397788667,233.7744326667,26.0,29.9941254667
+2024-09-27 16:26:00,0.0392885,230.7850059167,26.0,30.0058739667
+2024-09-27 16:27:00,0.03917645,230.29157135,26.0,30.0032038
+2024-09-27 16:28:00,0.0391188167,230.2050594833,26.0,30.0112142
+2024-09-27 16:29:00,0.0391636,230.2178789667,26.0,30.0032037667
+2024-09-27 16:30:00,0.0403908833,235.70650205,26.0,30.0048059
+2024-09-27 16:31:00,0.0494841333,282.1691773167,26.0,30.0042719
+2024-09-27 16:32:00,0.0498557667,284.18456075,26.0,30.0090780833
+2024-09-27 16:33:00,0.0499584,284.3864176667,26.0,30.0053399167
+2024-09-27 16:34:00,0.0499775167,284.3351506833,26.0,30.0010676667
+2024-09-27 16:35:00,0.04984945,283.1047756667,26.0,30.0042718667
+2024-09-27 16:36:00,0.05864145,322.9637945667,26.0,30.0016017667
+2024-09-27 16:37:00,0.0687792,374.9247058167,26.0,30.0026698667
+2024-09-27 16:38:00,0.0648767,358.1159906,26.0,30.0069419833
+2024-09-27 16:39:00,0.0515411667,291.3265005667,26.0,30.0048059
+2024-09-27 16:40:00,0.0523261167,295.4790155333,26.0,30.0048059
+2024-09-27 16:41:00,0.0528708333,298.0519088,26.0,30.0074760167
+2024-09-27 16:42:00,0.0534539833,300.42294005,26.0,30.0069420333
+2024-09-27 16:43:00,0.06782115,364.5882700333,26.0,30.0026697667
+2024-09-27 16:44:00,0.0753443667,400.5062433833,26.0,30.0106801667
+2024-09-27 16:45:00,0.0691797333,374.43127245,26.0,30.0192246167
+2024-09-27 16:46:00,0.0540243167,302.1339271333,26.0,30.0138843667
+2024-09-27 16:47:00,0.05469075,304.1813527,26.0,30.0058739333
+2024-09-27 16:48:00,0.07070835,376.97532565,26.0,30.0074760333
+2024-09-27 16:49:00,0.07108935,380.4261451833,26.0,30.0154864333
+2024-09-27 16:50:00,0.0611727833,337.1643717167,26.0,30.0144184167
+2024-09-27 16:51:00,0.0536526167,299.6507547667,26.0,30.0101462
+2024-09-27 16:52:00,0.0536654333,299.7949352667,26.0,30.0202925833
+2024-09-27 16:53:00,0.0537968167,299.83979445,26.0,30.0176225333
+2024-09-27 16:54:00,0.0537872333,300.1185491333,26.0,30.0192245333
+2024-09-27 16:55:00,-0.01105095,115.8699130167,38.2166666667,30.02349685
+2024-09-27 16:56:00,-0.1092054667,0.128164,45.0,30.0170885
+2024-09-27 16:57:00,0.0113264833,189.7661006167,31.4833333333,30.0213606667
+2024-09-27 16:58:00,0.0529253333,308.1063784833,26.0,30.0208266833
+2024-09-27 16:59:00,0.0529638167,307.7635360333,26.0,30.03043915
+2024-09-27 17:00:00,0.0532425,307.8083952833,26.0,30.0218947833
+2024-09-27 17:01:00,0.0530437,306.9432887667,26.0,30.0186906
+2024-09-27 17:02:00,0.05311115,306.7670610333,26.0,30.0245648
+2024-09-27 17:03:00,0.0533449667,307.8404370833,26.0,30.0186906667
+2024-09-27 17:04:00,0.0534441833,307.3341863667,26.0,30.0224287667
+2024-09-27 17:05:00,0.0535628667,307.85325515,26.0,30.0283030833
+2024-09-27 17:06:00,0.0534186333,306.9817313667,26.0,30.0309731833
+2024-09-27 17:07:00,0.0535821,307.0874686167,26.0,30.0299051333
+2024-09-27 17:08:00,0.05363025,307.2765147333,26.0,30.02776905
+2024-09-27 17:09:00,0.053679,307.3916966949,26.0,30.0332903559
+2024-09-27 17:10:00,0.0538161167,307.0297973833,26.0,30.0357794167
+2024-09-27 17:11:00,0.0537968667,307.6545979833,26.0,30.0373814833
+2024-09-27 17:12:00,0.0539026333,308.17045785,26.0,30.03524535
+2024-09-27 17:13:00,0.053909,307.3309804833,26.0,30.0336432833
+2024-09-27 17:14:00,0.0537231667,306.7318132167,26.0,30.0363133833
+2024-09-27 17:15:00,0.0540788,306.94649005,26.0,30.0320413
+2024-09-27 17:16:00,0.0540498333,307.34379575,26.0,30.0352454
+2024-09-27 17:17:00,0.05410435,307.1131057833,26.0,30.0389835167
+2024-09-27 17:18:00,0.0540659333,306.74783695,26.0,30.0448578
+2024-09-27 17:19:00,0.0541523667,307.0426108,26.0,30.0405856333
+2024-09-27 17:20:00,0.0544152667,307.3149632333,26.0,30.0427216667
+2024-09-27 17:21:00,0.0543704167,307.4463282833,26.0,30.0416536
+2024-09-27 17:22:00,0.0543447667,306.8952261833,26.0,30.0571404333
+2024-09-27 17:23:00,0.0544055833,306.6645256,26.0,30.0566064167
+2024-09-27 17:24:00,0.05447935,307.4174916833,26.0,30.0475278833
+2024-09-27 17:25:00,0.0544537,306.9496935,26.0,30.0624806833
+2024-09-27 17:26:00,0.0545114333,306.7093849,26.0,30.0475279
+2024-09-27 17:27:00,0.0547388,307.2124289333,26.0,30.0443237333
+2024-09-27 17:28:00,0.0547004333,306.9304659,26.0,30.0491300167
+2024-09-27 17:29:00,0.0546843667,307.4591465833,26.0,30.0507320333
+2024-09-27 17:30:00,0.0547933333,307.1771878,26.0,30.0491299833
+2024-09-27 17:31:00,0.0547964333,306.94008545,26.0,30.0443238
+2024-09-27 17:32:00,0.0548927167,307.28612725,26.0,30.05500415
+2024-09-27 17:33:00,0.0539601333,302.4158945333,26.0,30.0630147167
+2024-09-27 17:34:00,0.05268495,296.4178156,26.0,30.0598104167
+2024-09-27 17:35:00,0.05157945,290.5382909833,26.0,30.0662188333
+2024-09-27 17:36:00,0.0505092833,285.46619855,26.0,30.0614124833
+2024-09-27 17:37:00,0.0504516667,284.4985587167,26.0,30.0587423833
+2024-09-27 17:38:00,0.0504934,284.47613275,26.0,30.0678208333
+2024-09-27 17:39:00,0.0504324333,284.3159272333,26.0,30.0614124833
+2024-09-27 17:40:00,0.0504293,284.2486394667,26.0,30.0624805
+2024-09-27 17:41:00,0.05071445,285.0432576167,26.0,30.0763650667
+2024-09-27 17:42:00,0.0506984833,284.7837234833,26.0,30.0651506667
+2024-09-27 17:43:00,0.0507177333,284.9919902,26.0,30.0747628667
+2024-09-27 17:44:00,0.0508778667,284.9695617333,26.0,30.0763652167
+2024-09-27 17:45:00,0.0507946,284.7644994667,26.0,30.0731611667
+2024-09-27 17:46:00,0.0509002333,285.5014418667,26.0,30.0678208333
+2024-09-27 17:47:00,0.0509067,284.70682385,26.0,30.0790348333
+2024-09-27 17:48:00,0.0509227,284.2742731833,26.0,30.0747631667
+2024-09-27 17:49:00,0.0610573,330.4870229333,26.0,30.0720931
+2024-09-27 17:50:00,0.0702755667,376.0589533,26.0,30.0731611667
+2024-09-27 17:51:00,0.06438005,351.6084585333,26.0,30.08490915
+2024-09-27 17:52:00,0.0523838333,291.6757446833,26.0,30.0811711667
+2024-09-27 17:53:00,0.0533289167,295.5879510333,26.0,30.072093
+2024-09-27 17:54:00,0.0538096,298.0390878833,26.0,30.07689915
+2024-09-27 17:55:00,0.07124315,375.5911591167,26.0,30.0758312333
+2024-09-27 17:56:00,0.0764561667,400.9163727667,26.0,30.0774331333
+2024-09-27 17:57:00,0.0611950833,334.2037774833,26.0,30.0790351833
+2024-09-27 17:58:00,0.0547932667,302.6081430333,26.0,30.0795692667
+2024-09-27 17:59:00,0.0559627833,306.51714525,26.0,30.0801029
+2024-09-27 18:00:00,0.0718264333,379.11246205,26.0,30.0859772667
+2024-09-27 18:01:00,0.0721307333,379.8814460333,26.0,30.0865111
+2024-09-27 18:02:00,0.0611791333,333.5533484167,26.0,30.08010315
+2024-09-27 18:03:00,0.0543864,299.3752011,26.0,30.0811712667
+2024-09-27 18:04:00,0.0545018833,300.1345723833,26.0,30.0854432333
+2024-09-27 18:05:00,0.05462675,300.7433502167,26.0,30.08651145
+2024-09-27 18:06:00,0.0546362667,300.3172067667,26.25,30.0902492833
+2024-09-27 18:07:00,-0.0612080167,42.1243184833,42.3833333333,30.08971535
+2024-09-27 18:08:00,-0.1249214667,0.1249599,45.0333333333,30.0907833667
+2024-09-27 18:09:00,0.0329124833,231.0349211167,28.9833333333,30.08811315
+2024-09-27 18:10:00,0.05362375,309.6795917167,26.0,30.08971505
+2024-09-27 18:11:00,0.0534122833,307.5136148833,26.0,30.09131745
+2024-09-27 18:12:00,0.0539923,309.6411400333,26.0,30.09452145
+2024-09-27 18:13:00,0.0540050667,309.6699771167,26.0,30.1052015667
+2024-09-27 18:14:00,0.05399865,309.5001594833,26.0,30.1003956667
+2024-09-27 18:15:00,0.0542005833,309.3175285833,26.0,30.0918513833
+2024-09-27 18:16:00,0.0542548833,309.47773265,26.0,30.0950553833
+2024-09-27 18:17:00,0.0542647,309.4008349667,26.0,30.0982594833
+2024-09-27 18:18:00,0.0543415,309.298303,26.0,30.1014635333
+2024-09-27 18:19:00,0.0545050333,309.1477104667,26.0,30.1019973667
+2024-09-27 18:20:00,0.0543287167,308.7311780333,26.0,30.0961237
+2024-09-27 18:21:00,0.05443445,308.79205755,26.0,30.1030654833
+2024-09-27 18:22:00,0.0545114,308.8689547167,26.0,30.0993276
+2024-09-27 18:23:00,0.0545818667,308.67350235,26.0,30.0993276
+2024-09-27 18:24:00,0.0547132667,309.0804227333,26.0,30.0993275
+2024-09-27 18:25:00,0.0547164833,308.9650734667,26.0,30.1019975167
+2024-09-27 18:26:00,0.0547291833,308.8176866667,26.0,30.0993275
+2024-09-27 18:27:00,0.0549182,308.9522616167,26.0,30.0987933167
+2024-09-27 18:28:00,0.0547900833,308.5453391667,26.0,30.0998615833
+2024-09-27 18:29:00,0.0550239667,308.7888506667,26.0,30.1057354
+2024-09-27 18:30:00,0.0549246333,308.3050321667,26.0,30.10092955
+2024-09-27 18:31:00,0.0550240333,308.78884965,26.0,30.0987935167
+2024-09-27 18:32:00,0.0550943833,308.8753619167,26.0,30.0998614333
+2024-09-27 18:33:00,0.05510735,308.21211385,26.0,30.1030654833
+2024-09-27 18:34:00,0.0553764833,308.9650744167,26.0,30.1105414
+2024-09-27 18:35:00,0.05537325,309.0836312667,26.0,30.1110753833
+2024-09-27 18:36:00,0.0553828833,308.2345372,26.0,30.1052015167
+2024-09-27 18:37:00,0.0553733,308.6542804,26.0,30.1019975667
+2024-09-27 18:38:00,0.0553893833,308.8881797833,26.0,30.10733735
+2024-09-27 18:39:00,0.0554983,308.62223845,26.0,30.1062693833
+2024-09-27 18:40:00,0.0555558833,309.0676105,26.0,30.1046675833
+2024-09-27 18:41:00,0.055604,308.7568107833,26.0,30.1062695333
+2024-09-27 18:42:00,0.055604,308.42999195,26.0,30.1041336
+2024-09-27 18:43:00,0.0555398667,308.06152035,26.0,30.1025316
+2024-09-27 18:44:00,0.0553476167,306.7446264833,26.0,30.1100072667
+2024-09-27 18:45:00,0.0557673167,308.7792396667,26.0,30.1132115667
+2024-09-27 18:46:00,0.0558987167,308.6863197333,26.0,30.1084056167
+2024-09-27 18:47:00,0.0557834,308.0999709,26.0,30.10733745
+2024-09-27 18:48:00,0.0552066167,305.3316229667,26.0,30.1116095167
+2024-09-27 18:49:00,0.0539313833,299.1156677333,26.0,30.1030656333
+2024-09-27 18:50:00,0.05278745,292.6017318833,26.0,30.1126775833
+2024-09-27 18:51:00,0.0517203833,287.5264332333,26.0,30.1174833833
+2024-09-27 18:52:00,0.05147065,286.356933,26.0,30.1201537
+2024-09-27 18:53:00,0.0516052,286.7510385333,26.0,30.1206873833
+2024-09-27 18:54:00,0.0515827,286.24799735,26.0,30.1094735333
+2024-09-27 18:55:00,0.0516276,286.2768295667,26.0,30.10733775
+2024-09-27 18:56:00,0.0516789333,286.2095438,26.0,30.1270956833
+2024-09-27 18:57:00,0.0517653,286.3985889167,26.0,30.1254936833
+2024-09-27 18:58:00,0.0516403,286.2960554833,26.0,30.1222896333
+2024-09-27 18:59:00,0.05180375,286.6549157167,26.0,30.11534755
+2024-09-27 19:00:00,0.05203135,286.7926907833,26.0,30.1148133167
+2024-09-27 19:01:00,0.0574207333,309.3912169833,26.0,30.1148135667
+2024-09-27 19:02:00,0.07107985,376.4370412,26.0,30.1201534
+2024-09-27 19:03:00,0.07069855,376.2287719,26.0,30.1292319667
+2024-09-27 19:04:00,0.0533130167,292.9253474167,26.0,30.1260276667
+2024-09-27 19:05:00,0.0541877,297.2060242,26.0,30.1169496
+2024-09-27 19:06:00,0.0549470667,301.0765755667,26.0,30.1153476
+2024-09-27 19:07:00,0.0692342,362.35501565,26.0,30.1217557
+2024-09-27 19:08:00,0.0775392667,401.6917658667,26.0,30.1313678
+2024-09-27 19:09:00,0.0698878333,371.34251815,26.0,30.1238916333
+2024-09-27 19:10:00,0.0552387,301.52514995,26.0,30.1190858333
+2024-09-27 19:11:00,0.0570489,306.99135075,26.0,30.1212213167
+2024-09-27 19:12:00,0.0730151333,380.1185497833,26.0,30.1238916833
+2024-09-27 19:13:00,0.0732266833,381.6405003167,26.0,30.11855165
+2024-09-27 19:14:00,0.0622397167,335.12976435,26.0,30.1228237667
+2024-09-27 19:15:00,0.05544385,300.6600428667,26.0,30.1169494
+2024-09-27 19:16:00,0.0555976,300.9323897,26.0,30.1180175667
+2024-09-27 19:17:00,0.0555783667,300.89714555,26.0,30.1222896833
+2024-09-27 19:18:00,0.0546459,299.08041995,26.75,30.1196192667
+2024-09-27 19:19:00,-0.0747933667,19.0483816667,43.85,30.12335755
+2024-09-27 19:20:00,-0.1100545,0.1505927,44.65,30.1244256667
+2024-09-27 19:21:00,0.0521435833,289.1413011,25.9166666667,30.1335041333
+2024-09-27 19:22:00,0.05470685,311.0829838667,26.0,30.1222897833
+2024-09-27 19:23:00,0.0548060333,310.6279976833,26.0,30.12656165
+2024-09-27 19:24:00,0.0548924667,311.2367787167,26.0,30.1222897833
+2024-09-27 19:25:00,0.0547868833,310.0256362,26.0,30.1233574
+2024-09-27 19:26:00,0.05500165,310.75937035,26.0,30.1302997333
+2024-09-27 19:27:00,0.0550913167,310.5382877667,26.0,30.1270959333
+2024-09-27 19:28:00,0.0552163167,310.4293507167,26.0,30.1286976333
+2024-09-27 19:29:00,0.05524195,310.4421645,26.0,30.12816375
+2024-09-27 19:30:00,0.0552997333,310.6504271,26.0,30.1313679
+2024-09-27 19:31:00,0.0552996833,310.2242873,26.0,30.12495985
+2024-09-27 19:32:00,0.0553829167,309.9647556167,26.0,30.1302998333
+2024-09-27 19:33:00,0.0554694667,310.3332253833,26.0,30.1276296667
+2024-09-27 19:34:00,0.0554725333,309.7789193333,26.0,30.1249597
+2024-09-27 19:35:00,0.05540535,309.2182010167,26.0,30.1270955333
+2024-09-27 19:36:00,0.0556007167,309.8814482,26.0,30.1297657
+2024-09-27 19:37:00,0.0556263167,309.9230995833,26.0,30.1302998333
+2024-09-27 19:38:00,0.0557481667,310.2403044,26.0,30.1313679
+2024-09-27 19:39:00,0.05586025,309.9615486833,26.0,30.1244256167
+2024-09-27 19:40:00,0.0558667333,310.2595305,26.0,30.1265616
+2024-09-27 19:41:00,0.05593405,310.33322745,26.0,30.1340379667
+2024-09-27 19:42:00,0.0559627833,310.5222655,26.0,30.13083375
+2024-09-27 19:43:00,0.0560686167,310.3716739667,26.0,30.1319019167
+2024-09-27 19:44:00,0.0561648,309.9647561333,26.0,30.1324358667
+2024-09-27 19:45:00,0.0560590167,309.4841392833,26.0,30.1302997333
+2024-09-27 19:46:00,0.0562736833,310.3140029,26.0,30.1286978333
+2024-09-27 19:47:00,0.05611995,309.51938765,26.0,30.1228237667
+2024-09-27 19:48:00,0.0560334333,308.561362,26.0,30.1302997333
+2024-09-27 19:49:00,0.05627035,310.0448581667,26.0,30.1270956833
+2024-09-27 19:50:00,0.0563409,309.4456912167,26.0,30.1324358667
+2024-09-27 19:51:00,0.056469,309.987184,26.0,30.1313678
+2024-09-27 19:52:00,0.0565074167,309.5225883833,26.0,30.1324361167
+2024-09-27 19:53:00,0.0565747833,310.2467151667,26.0,30.1297657
+2024-09-27 19:54:00,0.0565459333,309.743672,26.0,30.1276295667
+2024-09-27 19:55:00,0.0567798667,309.9102847667,26.0,30.1335039333
+2024-09-27 19:56:00,0.0567607333,309.8333855333,26.0,30.1324358667
+2024-09-27 19:57:00,0.0569145167,309.8910607333,26.0,30.1302998833
+2024-09-27 19:58:00,0.05678955,309.4168573167,26.0,30.13296995
+2024-09-27 19:59:00,0.0560108833,305.5430938833,26.0,30.1324359667
+2024-09-27 20:00:00,0.054594678,298.7438850339,26.0,30.1315851356
+2024-09-27 20:01:00,0.0533835833,292.0602381833,26.0,30.1313678
+2024-09-27 20:02:00,0.0525023167,288.4940724167,26.0,30.1313679167
+2024-09-27 20:03:00,0.05244145,287.65139295,26.0,30.1367081333
+2024-09-27 20:04:00,0.0523485,287.3982680333,26.0,30.1335039333
+2024-09-27 20:05:00,0.0525182333,287.8724770833,26.0,30.1329699
+2024-09-27 20:06:00,0.0525407,287.3758406,26.0,30.1351061333
+2024-09-27 20:07:00,0.05259515,287.8308257,26.0,30.1340379667
+2024-09-27 20:08:00,0.0525791667,287.8340295333,26.0,30.1399122833
+2024-09-27 20:09:00,0.0527682333,287.4206974,26.0,30.1302997333
+2024-09-27 20:10:00,0.05270095,287.8756809167,26.0,30.1335039333
+2024-09-27 20:11:00,0.0527778,287.5776954833,26.0,30.135106
+2024-09-27 20:12:00,0.0529093667,287.7603367167,26.0,30.13617405
+2024-09-27 20:13:00,0.0528675667,287.5905165333,26.0,30.1308338667
+2024-09-27 20:14:00,0.0646525333,339.9423294333,26.0,30.1308337667
+2024-09-27 20:15:00,0.0724224,378.30823525,26.0,30.1329699
+2024-09-27 20:16:00,0.0654246,348.0999679333,26.0,30.1356400167
+2024-09-27 20:17:00,0.0549632,297.08747365,26.0,30.1335039333
+2024-09-27 20:18:00,0.0559660167,302.0634363,26.0,30.1260274667
+2024-09-27 20:19:00,0.0659437167,344.0756200667,26.0,30.1276298167
+2024-09-27 20:20:00,0.0787472667,402.6081386333,26.0,30.1340379667
+2024-09-27 20:21:00,0.0754565667,387.96219075,26.0,30.1260274667
+2024-09-27 20:22:00,0.0587857167,316.0397344833,26.0,30.1281637
+2024-09-27 20:23:00,0.0575584167,306.9625159167,26.0,30.1319018333
+2024-09-27 20:24:00,0.0738802167,379.0131359167,26.0,30.1281636
+2024-09-27 20:25:00,0.0742294333,381.4963150667,26.0,30.1254938833
+2024-09-27 20:26:00,0.0647933833,342.0538294833,26.0,30.12495955
+2024-09-27 20:27:00,0.0560236833,300.2787532,26.0,30.12335745
+2024-09-27 20:28:00,0.05639215,301.6917621833,26.0,30.1351060333
+2024-09-27 20:29:00,0.0563024833,301.3649432,26.0,30.12175535
+2024-09-27 20:30:00,0.0485101,267.3790405167,28.7166666667,30.1244256167
+2024-09-27 20:31:00,-0.0893206667,0.4101249,44.9666666667,30.12495955
+2024-09-27 20:32:00,-0.0771772333,15.7577698333,41.3333333333,30.1297657
+2024-09-27 20:33:00,0.058808,326.10701295,26.0,30.1228236667
+2024-09-27 20:34:00,0.0555110333,311.1726953333,26.0,30.1313678
+2024-09-27 20:35:00,0.0555142,310.7433491167,26.0,30.12495965
+2024-09-27 20:36:00,0.0557033,310.6344089667,26.0,30.1335039333
+2024-09-27 20:37:00,0.0557257167,310.1473865167,26.0,30.1324358667
+2024-09-27 20:38:00,0.05581865,310.2306970333,26.0,30.1238913333
+2024-09-27 20:39:00,0.0559405,310.0352477,26.0,30.1372421167
+2024-09-27 20:40:00,0.0559788667,310.3364307667,26.0,30.12976595
+2024-09-27 20:41:00,0.0560527,310.2467151667,26.0,30.1260277667
+2024-09-27 20:42:00,0.0560847667,309.82056865,26.0,30.1292316667
+2024-09-27 20:43:00,0.0562320333,310.2338977667,26.0,30.1260277167
+2024-09-27 20:44:00,0.0561167167,309.7372607167,26.0,30.1254935833
+2024-09-27 20:45:00,0.0562,309.8397973333,26.0,30.1254935833
+2024-09-27 20:46:00,0.0563121,310.2306934167,26.0,30.1254934333
+2024-09-27 20:47:00,0.0564625833,309.8750400167,26.0,30.1244256667
+2024-09-27 20:48:00,0.0564819,310.5543048667,26.0,30.134572
+2024-09-27 20:49:00,0.05646905,309.9743660833,26.0,30.1319019833
+2024-09-27 20:50:00,0.05670305,310.3748772833,26.0,30.1244254667
+2024-09-27 20:51:00,0.05667745,310.1602034,26.0,30.1260277667
+2024-09-27 20:52:00,0.0566005,309.1284849667,26.0,30.1260276667
+2024-09-27 20:53:00,0.0565461,309.0836272333,26.0,30.1164157167
+2024-09-27 20:54:00,0.0568313167,309.4713229167,26.0,30.1286976333
+2024-09-27 20:55:00,0.0568984667,310.1345717,26.0,30.1302999833
+2024-09-27 20:56:00,0.0569465167,310.1345711833,26.0,30.12656165
+2024-09-27 20:57:00,0.0568312167,309.6090991167,26.0,30.1302998333
+2024-09-27 20:58:00,0.0570553333,309.5610416167,26.0,30.1270956333
+2024-09-27 20:59:00,0.0570234167,309.7596958167,26.0,30.1297658
+2024-09-27 21:00:00,0.0592053667,317.3149631,26.0,30.1142799333
+2024-09-27 21:01:00,0.0652067,347.9782146,26.0,30.1270958333
+2024-09-27 21:02:00,0.05780525,312.64018015,26.0,30.1292317667
+2024-09-27 21:03:00,0.0580359167,312.9798184833,26.0,30.1212214667
+2024-09-27 21:04:00,0.0580616667,312.71707935,26.0,30.1217555
+2024-09-27 21:05:00,0.05816725,313.2681850833,26.0,30.1217553
+2024-09-27 21:06:00,0.05820255,312.71067165,26.0,30.1217556
+2024-09-27 21:07:00,0.0581609333,313.05671445,26.0,30.1222895333
+2024-09-27 21:08:00,0.0582251,312.6017331333,26.0,30.1297658
+2024-09-27 21:09:00,0.0582859333,313.0791403333,26.0,30.1190853833
+2024-09-27 21:10:00,0.05835315,312.6241573833,26.0,30.1190857333
+2024-09-27 21:11:00,0.05832445,312.43511675,26.0,30.11855145
+2024-09-27 21:12:00,0.0585294,312.8260225667,26.0,30.12175565
+2024-09-27 21:13:00,0.0585614,313.1912849,26.0,30.1238917833
+2024-09-27 21:14:00,0.0585965833,312.7715501833,26.0,30.1206874833
+2024-09-27 21:15:00,0.0586863667,312.7619370833,26.0,30.1254940333
+2024-09-27 21:16:00,0.0587728667,312.50560775,26.0,30.1185517
+2024-09-27 21:17:00,0.0588529833,313.0374920333,26.0,30.1228235167
+2024-09-27 21:18:00,0.0588402333,312.7170798667,26.0,30.1164156167
+2024-09-27 21:19:00,0.05885615,312.9157345,26.0,30.1196194167
+2024-09-27 21:20:00,0.05898125,312.9958366,26.0,30.10893935
+2024-09-27 21:21:00,0.0590582833,312.8484515167,26.0,30.11534775
+2024-09-27 21:22:00,0.0592184,313.6718990167,26.0,30.1201535
+2024-09-27 21:23:00,0.0588273833,311.4931101833,26.0,30.1185514
+2024-09-27 21:24:00,0.0501473,271.65011115,26.0,30.12175565
+2024-09-27 21:25:00,0.0446523167,244.6875978167,26.0,30.1281641
+2024-09-27 21:26:00,0.0499326333,268.6094167,26.0,30.1292318667
+2024-09-27 21:27:00,0.0587824833,312.1723801833,26.0,30.1169492
+2024-09-27 21:28:00,0.0564820333,302.2685031667,26.0,30.1185516
+2024-09-27 21:29:00,0.04404995,241.4995237667,26.0,30.12175555
+2024-09-27 21:30:00,0.05091,273.1464284167,26.0,30.1238914333
+2024-09-27 21:31:00,0.0537233,288.0871540667,26.0,30.12015355
+2024-09-27 21:32:00,0.069843,355.82185155,26.0,30.1132115667
+2024-09-27 21:33:00,0.07273005,370.9932673,26.0,30.1228239167
+2024-09-27 21:34:00,0.0615796333,323.9859014167,26.0,30.1094736833
+2024-09-27 21:35:00,0.0534313833,285.22909195,26.0,30.11214335
+2024-09-27 21:36:00,0.0532776,284.4056447833,26.0,30.1068033667
+2024-09-27 21:37:00,0.0532872833,284.7388662167,26.0,30.1068033667
+2024-09-27 21:38:00,0.05336425,284.4697239667,26.0,30.1121432
+2024-09-27 21:39:00,0.0532489667,283.2489564167,26.0,30.1110755833
+2024-09-27 21:40:00,0.0535052167,284.3735988167,26.0,30.1035995667
+2024-09-27 21:41:00,0.0536206667,285.0560715333,26.0,30.1073376
+2024-09-27 21:42:00,0.03040045,189.0515868333,33.1166666667,30.1121436
+2024-09-27 21:43:00,-0.12824735,0.1345722,45.0,30.1105416
+2024-09-27 21:44:00,-0.0453379667,87.1066977833,37.3333333333,30.1094734333
+2024-09-27 21:45:00,0.0533643667,301.3553379,26.0,30.10573555
+2024-09-27 21:46:00,0.0524958833,293.85133025,26.0,30.11214365
+2024-09-27 21:47:00,0.0526208667,293.7359850167,26.0,30.1132112667
+2024-09-27 21:48:00,0.0525791667,293.8000694333,26.0,30.1094732833
+2024-09-27 21:49:00,0.0526622667,293.5405337833,26.0,30.1089393
+2024-09-27 21:50:00,0.0527105,293.0022434333,26.0,30.1030654833
+2024-09-27 21:51:00,0.05279705,293.5116996333,26.0,30.11054135
+2024-09-27 21:52:00,0.0529606,293.7455955167,26.0,30.1068033667
+2024-09-27 21:53:00,0.0528805333,292.8708764333,26.0,30.1052014167
+2024-09-27 21:54:00,0.0529766,293.0406964333,26.0,30.10733745
+2024-09-27 21:55:00,0.0531079,293.3194517,26.0,30.10413345
+2024-09-27 21:56:00,0.0530503333,292.5184229167,26.0,30.1100074667
+2024-09-27 21:57:00,0.0526689333,291.5700085,26.0,30.1116099167
+2024-09-27 21:58:00,0.0529829333,291.9545005667,26.0,30.10733735
+2024-09-27 21:59:00,0.05897145,315.8378723667,26.0,30.1062694833
+2024-09-27 22:00:00,0.0748862667,390.7497546833,26.0,30.1089394
+2024-09-27 22:01:00,0.0750590833,391.0445384167,26.0,30.10413345
+2024-09-27 22:02:00,0.0606344333,327.3309819333,26.0,30.1052014167
+2024-09-27 22:03:00,0.05596605,306.6260782333,26.0,30.1100075667
+2024-09-27 22:04:00,0.05337375,293.4892698333,26.0,30.1019976167
+2024-09-27 22:05:00,0.0531431833,292.6914489333,26.0,30.0998615833
+2024-09-27 22:06:00,0.0533834667,292.6850396833,26.0,30.10092955
+2024-09-27 22:07:00,0.05346025,293.1111845333,26.0,30.10413345
+2024-09-27 22:08:00,0.05338985,292.3710343167,26.0,30.0982596333
+2024-09-27 22:09:00,0.0536495333,293.78724955,26.0,30.0966574833
+2024-09-27 22:10:00,0.0534732,293.0054518167,26.0,30.0977256
+2024-09-27 22:11:00,0.0537136333,292.9958377333,26.0,30.0971914167
+2024-09-27 22:12:00,0.0537424667,292.5825088167,26.0,30.0987936167
+2024-09-27 22:13:00,0.05367835,292.65620425,26.0,30.0971916667
+2024-09-27 22:14:00,0.0540050667,293.9570692167,26.0,30.0987936167
+2024-09-27 22:15:00,0.0539378,293.2970243833,26.0,30.10092945
+2024-09-27 22:16:00,0.0540082833,293.4091638333,26.0,30.0971915167
+2024-09-27 22:17:00,0.0541558,293.3707177,26.0,30.1014635333
+2024-09-27 22:18:00,0.0541974833,293.7071466333,26.0,30.0923858167
+2024-09-27 22:19:00,0.0541204333,293.0535078333,26.0,30.0971914167
+2024-09-27 22:20:00,0.0540786833,293.1464288167,26.0,30.0987935167
+2024-09-27 22:21:00,0.0540916167,293.4636381667,26.0,30.0987936167
+2024-09-27 22:22:00,0.0543030167,293.3162509667,26.0,30.0923855667
+2024-09-27 22:23:00,0.05411395,293.0791411833,26.0,30.0929196
+2024-09-27 22:24:00,0.0545979667,294.5498253667,26.0,30.0891815667
+2024-09-27 22:25:00,0.0544729667,293.3066357833,26.0,30.09291935
+2024-09-27 22:26:00,0.0545658667,294.2454379167,26.0,30.0886475833
+2024-09-27 22:27:00,0.05452095,293.0086522,26.0,30.09131755
+2024-09-27 22:28:00,0.05438005,292.8164076833,26.0,30.09131735
+2024-09-27 22:29:00,0.0544280333,293.0855512167,26.0,30.0881135
+2024-09-27 22:30:00,0.0545851333,293.2361428667,26.0,30.0918515333
+2024-09-27 22:31:00,0.05496635,294.2646595333,26.0,30.0939875167
+2024-09-27 22:32:00,0.0546973,293.1752714667,26.0,30.08971545
+2024-09-27 22:33:00,0.0546522667,292.6433837333,26.0,30.09932735
+2024-09-27 22:34:00,0.0547036,293.3418805333,26.0,30.0918513333
+2024-09-27 22:35:00,0.05481895,293.3162503833,26.0,30.0918512833
+2024-09-27 22:36:00,0.0549247333,293.5853915,26.0,30.0961236
+2024-09-27 22:37:00,0.0549022333,293.3354674667,26.0,30.0843751167
+2024-09-27 22:38:00,0.0546716,292.62416175,26.0,30.0811715667
+2024-09-27 22:39:00,0.05491825,293.09837125,26.0,30.0875794167
+2024-09-27 22:40:00,0.0550784,293.5950035167,26.0,30.0897154
+2024-09-27 22:41:00,0.0550785,293.0054492833,26.0,30.0955894167
+2024-09-27 22:42:00,0.0549502667,293.1880810667,26.0,30.0907829167
+2024-09-27 22:43:00,0.0550656167,292.9349588667,26.0,30.0971913667
+2024-09-27 22:44:00,0.05506895,292.74591865,26.0,30.0849093
+2024-09-27 22:45:00,0.05509445,292.6786350333,26.0,30.0891815167
+2024-09-27 22:46:00,0.05515235,293.0054467667,26.0,30.07689915
+2024-09-27 22:47:00,0.0553093667,292.8196114833,26.0,30.081705
+2024-09-27 22:48:00,0.0550208667,292.4319127167,26.0,30.0795688667
+2024-09-27 22:49:00,0.05500165,291.7622554667,26.0,30.0870454833
+2024-09-27 22:50:00,0.0554981833,293.2810016167,26.0,30.0790353333
+2024-09-27 22:51:00,0.0508041,272.0025624333,26.0,30.0790348833
+2024-09-27 22:52:00,0.0511245333,272.50560345,26.0,30.0822393333
+2024-09-27 22:53:00,0.0512431833,272.3133569333,26.0,30.0801033
+2024-09-27 22:54:00,0.00038765,123.6014089167,36.5833333333,30.0870452333
+2024-09-27 22:55:00,-0.1121082333,0.1089394,45.0,30.0795691167
+2024-09-27 22:56:00,-0.0336815167,0.1441845,18.3666666667,30.0817055
+2024-09-27 22:57:00,0.0006887,0.128164,0.0,30.0785012
+2024-09-27 22:58:00,0.0010602333,0.1313681,0.0,30.0817052
+2024-09-27 22:59:00,0.0009898667,0.1313681,0.0,30.0822390833
+2024-09-27 23:00:00,0.00081055,0.1249599,0.0,30.0833071
+2024-09-27 23:01:00,0.0008522,0.1345722,0.0,30.08010305
+2024-09-27 23:02:00,0.00098985,0.1441845,0.0,30.07689915
+2024-09-27 23:03:00,0.00093225,0.1217558,0.0,30.0774330333
+2024-09-27 23:04:00,0.000849,0.1441845,0.0,30.0843751167
+2024-09-27 23:05:00,0.0072764167,32.3229734,10.8666666667,30.0699569667
+2024-09-27 23:06:00,0.0406920333,236.4017961667,26.0,30.0833074
+2024-09-27 23:07:00,0.0406599667,235.02402935,26.0,30.0865111
+2024-09-27 23:08:00,0.0405830333,234.1300835333,26.0,30.0806371333
+2024-09-27 23:09:00,0.04067925,233.0567127833,26.0,30.0833073
+2024-09-27 23:10:00,0.0407497167,233.4636321333,26.0,30.0817053
+2024-09-27 23:11:00,0.0491508,269.3880143,26.0,30.0758308333
+2024-09-27 23:12:00,0.0698623,369.6859985,26.0,30.0672868
+2024-09-27 23:13:00,0.06842055,363.56296365,26.0,30.0795692167
+2024-09-27 23:14:00,0.0527619667,290.6600434667,26.0,30.0774326333
+2024-09-27 23:15:00,0.0498975,277.12591965,26.0,30.0763651167
+2024-09-27 23:16:00,0.0499583167,276.4049975,26.0,30.08651135
+2024-09-27 23:17:00,0.0499935,277.0265936667,26.0,30.0715590167
+2024-09-27 23:18:00,0.0498941667,276.5427736667,26.0,30.0694228333
+2024-09-27 23:19:00,0.0499871333,276.1807106167,26.0,30.0683548167
+2024-09-27 23:20:00,0.0500672,276.1166267167,26.0,30.0699569667
+2024-09-27 23:21:00,0.05015685,275.9179756167,26.0,30.0731610667
+2024-09-27 23:22:00,0.0502722167,276.4915107333,26.0,30.0694228333
+2024-09-27 23:23:00,0.0503011,276.3889767833,26.0,30.0736952
+2024-09-27 23:24:00,0.0501920833,276.3184863,26.0,30.073695
+2024-09-27 23:25:00,0.0503267,276.1038119,26.0,30.070491
+2024-09-27 23:26:00,0.0502242,275.4469733167,26.0,30.0694229333
+2024-09-27 23:27:00,0.05090025,277.5168158333,26.0,30.0688889
+2024-09-27 23:28:00,0.070468,366.4786920167,26.0,30.06087855
+2024-09-27 23:29:00,0.0711887667,371.5379659167,26.0,30.0646166333
+2024-09-27 23:30:00,0.0496731167,275.1874379,26.0,30.0640827167
+2024-09-27 23:31:00,0.0481544167,262.99583355,26.0,30.0640827
+2024-09-27 23:32:00,0.0507081167,276.4850989833,26.0,30.06301465
+2024-09-27 23:33:00,0.0507561167,276.5043210333,26.0,30.0576742833
+2024-09-27 23:34:00,0.0506472,276.0813809333,26.0,30.0624806333
+2024-09-27 23:35:00,0.0508778667,276.7766731333,26.0,30.0635486833
+2024-09-27 23:36:00,0.0509771833,276.4690776833,26.0,30.0608785667
+2024-09-27 23:37:00,0.0509484167,276.7862815833,26.0,30.0710250667
+2024-09-27 23:38:00,0.0510123833,276.8375485167,26.0,30.0630145333
+2024-09-27 23:39:00,0.0510379833,276.84716005,26.0,30.06034445
+2024-09-27 23:40:00,0.0511116833,276.5331590833,26.0,30.0571403167
+2024-09-27 23:41:00,0.0510380667,276.1006054833,26.0,30.0598104667
+2024-09-27 23:42:00,0.0512709153,276.384684678,26.0,30.0555563898
+2024-09-27 23:43:00,0.0512847833,277.1867987,26.0,30.0539362833
+2024-09-27 23:44:00,0.0512816167,276.6453009833,26.0,30.0571403833
+2024-09-27 23:45:00,0.0514129667,276.5748151333,26.0,30.05233405
+2024-09-27 23:46:00,0.0513874,276.4754865333,26.0,30.048596
+2024-09-27 23:47:00,0.05147385,276.5363680167,26.0,30.0534021667
+2024-09-27 23:48:00,0.0514353167,276.5780188667,26.0,30.05874245
+2024-09-27 23:49:00,0.0515635667,276.7606481833,26.0,30.0582083
+2024-09-27 23:50:00,0.0515027333,276.1710980333,26.0,30.04912995
+2024-09-27 23:51:00,0.0509067333,273.66869715,26.0,30.04966405
+2024-09-27 23:52:00,0.0502369833,270.45498305,26.0,30.0416536333
+2024-09-27 23:53:00,0.0492629667,265.3636688,26.0,30.0464598667
+2024-09-27 23:54:00,0.0482315,260.3043901,26.0,30.0512660833
+2024-09-27 23:55:00,0.0470875667,254.57545535,26.0,30.0528681833
+2024-09-27 23:56:00,0.04595005,249.65716195,26.0,30.0507321
+2024-09-27 23:57:00,0.0454436,247.4335165167,26.0,30.0485960333
+2024-09-27 23:58:00,0.04546295,246.8856169,26.0,30.0550042833
+2024-09-27 23:59:00,0.0456231667,247.0233928167,26.0,30.0485959833
+2024-09-28 00:00:00,0.0456969167,247.4911902333,26.0,30.0550041833
+2024-09-28 00:01:00,0.0451074333,244.7677049,26.0,30.0544702833
+2024-09-28 00:02:00,0.0352451167,196.02050805,26.0,30.0534022
+2024-09-28 00:03:00,0.0470074333,252.7939764167,26.0,30.0587424167
+2024-09-28 00:04:00,0.0470233167,253.18487565,26.0,30.04218765
+2024-09-28 00:05:00,0.0470522167,252.6305679333,26.0,30.0480618667
+2024-09-28 00:06:00,0.04696255,252.2588932,26.0,30.0400515833
+2024-09-28 00:07:00,0.04719655,253.2137124333,26.0,30.0416536667
+2024-09-28 00:08:00,0.0471901667,253.2105084667,26.0,30.0464598167
+2024-09-28 00:09:00,0.0472157167,252.8644665167,26.0,30.0395175333
+2024-09-28 00:10:00,0.04723495,253.0823447,26.0,30.0368474
+2024-09-28 00:11:00,0.0473150833,253.46683575,26.0,30.0384495667
+2024-09-28 00:12:00,0.0473599333,253.0246710833,26.0,30.0395175667
+2024-09-28 00:13:00,0.0472638167,253.2297329333,26.0,30.0315072333
+2024-09-28 00:14:00,0.0474209,253.5853869833,26.0,30.0405855667
+2024-09-28 00:15:00,0.0199488667,147.3726368167,33.75,30.0347113333
+2024-09-28 00:16:00,-0.1182121667,0.160205,45.0,30.03684745
+2024-09-28 00:17:00,-0.05457225,0.128164,25.0166666667,30.0432557333
+2024-09-28 00:18:00,0.0010091833,0.1345722,0.0,30.0437897667
+2024-09-28 00:19:00,0.0009994833,0.1345722,0.0,30.0331093
+2024-09-28 00:20:00,0.0009546833,0.128164,0.0,30.0315071833
+2024-09-28 00:21:00,0.00100265,0.1570009,0.0,30.0331093
+2024-09-28 00:22:00,0.0009418167,0.1121435,0.0,30.0315071833
+2024-09-28 00:23:00,0.00094185,0.1473886,0.0,30.0368475
+2024-09-28 00:24:00,0.0010379,0.1473886,0.0,30.0293711167
+2024-09-28 00:25:00,0.001041,0.1377763,0.0,30.0331092167
+2024-09-28 00:26:00,0.0009418,0.1345722,0.0,30.0331093333
+2024-09-28 00:27:00,0.0009194,0.1377763,0.0,30.0250989167
+2024-09-28 00:28:00,0.00093865,0.160205,0.0,30.0325752
+2024-09-28 00:29:00,0.0009194833,0.1666132,0.0,30.0352454
+2024-09-28 00:30:00,0.00095145,0.1409804,0.0,30.0331093
+2024-09-28 00:31:00,0.00101225,0.1537968,0.0,30.0250989
+2024-09-28 00:32:00,0.0009802333,0.1185517,0.0,30.0160204667
+2024-09-28 00:33:00,0.0009578333,0.1698173,0.0,30.0379154833
+2024-09-28 00:34:00,0.0009674,0.1409804,0.0,30.0325753
+2024-09-28 00:35:00,0.0009546333,0.192246,0.0,30.0373814667
+2024-09-28 00:36:00,0.0008906,0.160205,0.0,30.0363134333
+2024-09-28 00:37:00,0.0010667333,0.1473886,0.0,30.0363134333
+2024-09-28 00:38:00,0.0009834,0.1570009,0.0,30.02456485
+2024-09-28 00:39:00,0.00101225,0.1570009,0.0,30.03204125
+2024-09-28 00:40:00,0.0305574333,162.7363025,22.5,30.027235
+2024-09-28 00:41:00,0.0581416167,319.3880174,26.0,30.0283031
+2024-09-28 00:42:00,0.0583949,318.89779215,26.0,30.02403085
+2024-09-28 00:43:00,0.0585806333,318.51330115,26.0,30.0229628333
+2024-09-28 00:44:00,0.0586574333,318.5100968,26.0,30.02776905
+2024-09-28 00:45:00,0.0587248,318.5004847833,26.0,30.0229628667
+2024-09-28 00:46:00,0.0588401667,318.4876668667,26.0,30.02136075
+2024-09-28 00:47:00,0.0589043167,318.3402781333,26.0,30.0299051833
+2024-09-28 00:48:00,0.0589395667,317.84364315,26.0,30.0256329167
+2024-09-28 00:49:00,0.0590645167,318.3210561667,26.0,30.0272349833
+2024-09-28 00:50:00,0.0590805167,317.6065419167,26.0,30.02189475
+2024-09-28 00:51:00,0.05917355,317.7411086,26.0,30.0240308833
+2024-09-28 00:52:00,0.05927915,317.8340290667,26.0,30.0106802167
+2024-09-28 00:53:00,0.0592343,317.8276239833,26.0,30.0208267167
+2024-09-28 00:54:00,0.0594072333,317.66100915,26.0,30.0101462
+2024-09-28 00:55:00,0.05944575,317.5200296333,26.0,30.0229629
+2024-09-28 00:56:00,0.0595001667,317.5264336833,26.0,30.0250989333
+2024-09-28 00:57:00,0.0595450167,317.7379068333,26.0,30.0256328
+2024-09-28 00:58:00,0.05971495,317.64178305,26.0,30.02136075
+2024-09-28 00:59:00,0.05963465,317.4879923333,26.0,30.0213608667
+2024-09-28 01:00:00,0.0597983167,317.2861275833,26.0,30.0160205667
+2024-09-28 01:01:00,0.059731,317.2444731,26.0,30.0272350667
+2024-09-28 01:02:00,0.0599809167,317.4527351833,26.0,30.0085441
+2024-09-28 01:03:00,0.0494487667,270.0833104833,26.0,30.0213607
+2024-09-28 01:04:00,0.0406631167,226.1582836833,26.0,30.03524535
+2024-09-28 01:05:00,0.04100275,228.57417495,26.0,30.03150725
+2024-09-28 01:06:00,0.04087135,227.2989450667,26.0,30.03364335
+2024-09-28 01:07:00,0.0405415,226.3056748333,26.0,30.0379154833
+2024-09-28 01:08:00,0.0406888333,226.7190004667,26.0,30.0315072
+2024-09-28 01:09:00,0.0455237667,248.1928878,26.0,30.0299051667
+2024-09-28 01:10:00,0.05107325,275.3412401333,26.0,30.0240308
+2024-09-28 01:11:00,0.0512784333,275.98205435,26.0,30.0154864333
+2024-09-28 01:12:00,0.0511052167,274.97917805,26.0,30.0170884667
+2024-09-28 01:13:00,0.0511821,275.4629915,26.0,30.01869055
+2024-09-28 01:14:00,0.0513809833,275.5943594833,26.0,30.02616695
+2024-09-28 01:15:00,0.0514513667,275.69689045,26.0,30.0170885333
+2024-09-28 01:16:00,0.0513649,275.4373603167,26.0,30.0208267
+2024-09-28 01:17:00,0.05133615,275.0048071667,26.0,30.0213606833
+2024-09-28 01:18:00,0.0515506667,275.4405657,26.0,30.0176225167
+2024-09-28 01:19:00,0.0516019833,275.49503345,26.0,30.0133503167
+2024-09-28 01:20:00,0.0515893,275.6712572,26.0,30.0192247167
+2024-09-28 01:21:00,0.0517429833,275.5366874167,26.0,30.0208266667
+2024-09-28 01:22:00,0.05173965,275.11054245,26.0,30.0090781333
+2024-09-28 01:23:00,0.0517844833,275.15540025,26.0,30.006942
+2024-09-28 01:24:00,0.0518934833,276.00448325,26.0,30.01335035
+2024-09-28 01:25:00,0.0517620667,274.9118867,26.0,30.0202926667
+2024-09-28 01:26:00,0.0517685667,275.4565833167,26.0,30.02189465
+2024-09-28 01:27:00,0.0515283167,273.6046153667,26.0,30.0090781
+2024-09-28 01:28:00,0.0507303333,270.4517786667,26.0,30.0122823333
+2024-09-28 01:29:00,0.049939,265.9307937,26.0,30.0096121667
+2024-09-28 01:30:00,0.0486831167,260.26273655,26.0,30.0117483333
+2024-09-28 01:31:00,0.04753295,254.78692625,26.0,30.0106802333
+2024-09-28 01:32:00,0.0463825667,249.2502382,26.0,30.0128163167
+2024-09-28 01:33:00,0.0456424667,245.8442835167,26.0,30.0128163167
+2024-09-28 01:34:00,0.0456585167,245.5366876833,26.0,30.0128163333
+2024-09-28 01:35:00,0.04565845,245.4245450833,26.0,30.0101462
+2024-09-28 01:36:00,0.04572905,245.44376855,26.0,30.01815665
+2024-09-28 01:37:00,0.0458475667,245.8442848667,26.0,30.0181566167
+2024-09-28 01:38:00,0.0458091333,246.0429388667,26.0,30.0090781667
+2024-09-28 01:39:00,0.0457675667,245.6872800833,26.0,30.0133503167
+2024-09-28 01:40:00,0.0457835333,245.2226847667,26.0,30.0165544833
+2024-09-28 01:41:00,0.0458797333,245.9692432,26.0,30.0101462167
+2024-09-28 01:42:00,0.045838,245.2835622667,26.0,30.0128163
+2024-09-28 01:43:00,0.0458123833,245.1105410333,26.0,30.0117483333
+2024-09-28 01:44:00,0.0458892833,245.3925031167,26.0,30.0058739667
+2024-09-28 01:45:00,0.0460654,245.3027863333,26.0,30.0042718667
+2024-09-28 01:46:00,0.045963,245.6007711667,26.0,30.0128163667
+2024-09-28 01:47:00,0.0459661667,245.0720899,26.0,30.0085441
+2024-09-28 01:48:00,0.046075,245.5430958667,26.0,30.0085440833
+2024-09-28 01:49:00,0.0450176667,242.5248306667,26.4166666667,30.0064079667
+2024-09-28 01:50:00,-0.0645915,20.5446976,43.0166666667,30.0069420333
+2024-09-28 01:51:00,-0.1123134,0.1377763,45.2666666667,30.0117482833
+2024-09-28 01:52:00,5.755e-05,0.1025312,2.4,30.0021358333
+2024-09-28 01:53:00,0.0010858667,0.1089394,0.0,30.0106802667
+2024-09-28 01:54:00,0.0010058333,0.1025312,0.0,30.0069419833
+2024-09-28 01:55:00,0.00096745,0.1089394,0.0,30.0069420333
+2024-09-28 01:56:00,0.0009066,0.128164,0.0,30.0074760667
+2024-09-28 01:57:00,0.0009642333,0.1025312,0.0,30.0048059
+2024-09-28 01:58:00,0.0009066,0.1345722,0.0,30.0026697667
+2024-09-28 01:59:00,0.0009258,0.1730214,0.0,29.9999996
+2024-09-28 02:00:00,0.0009514,0.1345722,0.0,30.0026698333
+2024-09-28 02:01:00,0.000945,0.160205,0.0,30.0010676667
+2024-09-28 02:02:00,0.00089065,0.1730214,0.0,29.9967956333
+2024-09-28 02:03:00,0.0010058833,0.1217558,0.0,30.0053399667
+2024-09-28 02:04:00,0.0009354167,0.1121435,0.0,30.0005337333
+2024-09-28 02:05:00,0.00093545,0.1377763,0.0,30.0042718667
+2024-09-28 02:06:00,0.0008746,0.1666132,0.0,30.0026697667
+2024-09-28 02:07:00,0.0009162,0.1217558,0.0,30.0048059
+2024-09-28 02:08:00,0.000881,0.1313681,0.0,30.0042718667
+2024-09-28 02:09:00,0.0009707,0.1634091,0.0,30.0037377833
+2024-09-28 02:10:00,0.0009546,0.1698173,0.0,29.9978634667
+2024-09-28 02:11:00,0.0009226333,0.1185517,0.0,29.9999997
+2024-09-28 02:12:00,0.0009674333,0.1217558,2.8,29.9994655667
+2024-09-28 02:13:00,0.0360781833,199.9134902,25.7333333333,30.0101462333
+2024-09-28 02:14:00,0.04706505,264.1557222667,26.0,29.9935916
+2024-09-28 02:15:00,0.04939435,273.8096802167,26.0,29.9999997333
+2024-09-28 02:16:00,0.0515058833,282.8035873333,26.0,29.9925234667
+2024-09-28 02:17:00,0.0528452333,288.2281285833,26.0,29.9962614
+2024-09-28 02:18:00,0.0542422167,294.1749473833,26.0,30.0058739667
+2024-09-28 02:19:00,0.0514289667,282.0346058,26.0,29.9962615333
+2024-09-28 02:20:00,0.0539794833,293.83531315,26.0,29.9989317
+2024-09-28 02:21:00,0.0542293667,293.92823055,26.0,29.9941254667
+2024-09-28 02:22:00,0.0542133833,293.7487999667,26.0,29.9978636333
+2024-09-28 02:23:00,0.0543287833,294.5530322667,26.0,29.9877174
+2024-09-28 02:24:00,0.0543895833,293.9730909,26.0,29.9941254333
+2024-09-28 02:25:00,0.0545049167,294.4440906167,26.0,29.9903873667
+2024-09-28 02:26:00,0.0547740667,294.7260521833,26.0,29.9898535
+2024-09-28 02:27:00,0.05447625,293.7103508167,26.0,29.9877174
+2024-09-28 02:28:00,0.05457865,293.4956784333,26.0,29.9909214333
+2024-09-28 02:29:00,0.0546940167,293.7648221,26.0,29.9839791333
+2024-09-28 02:30:00,0.054694,293.3707185667,26.0,29.9861152333
+2024-09-28 02:31:00,0.05483165,293.4187797667,26.0,29.9877173667
+2024-09-28 02:32:00,0.0549021667,293.75200385,26.0,29.9845132667
+2024-09-28 02:33:00,0.0548990167,293.0535108333,26.0,29.9813091667
+2024-09-28 02:34:00,0.05480285,293.4988853333,26.0,29.9861151667
+2024-09-28 02:35:00,0.0549086167,293.1400204833,26.0,29.9882515667
+2024-09-28 02:36:00,0.0550785,294.2005786167,26.0,29.9823771667
+2024-09-28 02:37:00,0.0549663167,293.0919594333,26.0,29.9807752
+2024-09-28 02:38:00,0.0550592833,292.9221460167,26.0,29.9925236667
+2024-09-28 02:39:00,0.0550785333,293.0182666333,26.0,29.9882511
+2024-09-28 02:40:00,0.0548734333,291.9256673333,26.0,29.9813092667
+2024-09-28 02:41:00,0.05392505,287.3822489,26.0,29.9935915
+2024-09-28 02:42:00,0.0527908,281.9384817167,26.0,29.9983975333
+2024-09-28 02:43:00,0.0514899333,275.5879528,26.0,29.9850473333
+2024-09-28 02:44:00,0.0504614167,270.5863524667,26.0,29.9903876333
+2024-09-28 02:45:00,0.04932075,265.0112146667,26.0,29.9813091667
+2024-09-28 02:46:00,0.04879535,262.5889160333,26.0,29.9871835
+2024-09-28 02:47:00,0.0442197667,242.0698509,26.0,29.9855811333
+2024-09-28 02:48:00,0.0363602167,201.9000354167,26.0,29.9850473333
+2024-09-28 02:49:00,0.0365267667,202.4831811333,26.0,29.9935914
+2024-09-28 02:50:00,0.0364434333,202.3197724333,26.0,29.9951935333
+2024-09-28 02:51:00,0.0364435167,202.23646605,26.0,29.9941254
+2024-09-28 02:52:00,0.0363826167,201.76866645,26.0,29.9935913667
+2024-09-28 02:53:00,0.0399263333,217.99103015,26.0,29.9967955333
+2024-09-28 02:54:00,0.04676075,251.76546235,26.0,29.9807752667
+2024-09-28 02:55:00,0.04665185,250.6280054333,26.0,29.9845134
+2024-09-28 02:56:00,0.0464788,250.323614,26.0,29.9818432333
+2024-09-28 02:57:00,0.0465621833,250.3973096833,26.0,29.9893193
+2024-09-28 02:58:00,0.0466775,250.3428375833,26.0,29.9898535333
+2024-09-28 02:59:00,0.05422305,282.6882404,26.0,29.9861153333
+2024-09-28 03:00:00,0.0625824667,321.3393109333,26.0,29.9781050667
+2024-09-28 03:01:00,0.0612398833,314.94393325,26.0,29.9781051
+2024-09-28 03:02:00,0.0533097333,281.9256644167,26.0,29.978639
+2024-09-28 03:03:00,0.0499102,265.1746234667,26.0,29.9855812667
+2024-09-28 03:04:00,0.0507304167,268.3755221,26.0,29.973299
+2024-09-28 03:05:00,0.0508362,268.9298345833,26.0,29.9818430667
+2024-09-28 03:06:00,0.0509675333,269.11567245,26.0,29.9882517
+2024-09-28 03:07:00,0.0510668333,269.4072418333,26.0,29.9786392
+2024-09-28 03:08:00,0.05101565,269.1605292833,26.0,29.9823771
+2024-09-28 03:09:00,0.0510507833,268.7600134833,26.0,29.9770371
+2024-09-28 03:10:00,0.0511758167,269.5386095667,26.0,29.972765
+2024-09-28 03:11:00,0.0511630667,269.1925725667,26.0,29.974901
+2024-09-28 03:12:00,0.05120145,269.2822811833,26.0,29.9807752
+2024-09-28 03:13:00,0.0512207,269.2534470833,26.0,29.973833
+2024-09-28 03:14:00,0.05117255,268.5261165667,26.0,29.9765031
+2024-09-28 03:15:00,0.0503652333,264.6331286,26.0,29.976503
+2024-09-28 03:16:00,0.04932715,259.6443446833,26.0,29.975969
+2024-09-28 03:17:00,0.0482987167,254.7741078833,26.0,29.9781051
+2024-09-28 03:18:00,0.0472637833,249.8974707833,26.0,29.978639
+2024-09-28 03:19:00,0.0467607167,247.6257620833,26.0,29.9690269
+2024-09-28 03:20:00,0.0469528167,248.3627027833,26.0,29.96795895
+2024-09-28 03:21:00,0.0470617333,247.9365588333,26.0,29.97009495
+2024-09-28 03:22:00,0.0404004667,219.05478965,28.8,29.9749009667
+2024-09-28 03:23:00,-0.0933961833,0.2659403333,44.85,29.9797070667
+2024-09-28 03:24:00,-0.0927907333,0.7465555167,42.9333333333,29.972231
+2024-09-28 03:25:00,0.0616629833,320.1441862667,25.9833333333,29.973833
+2024-09-28 03:26:00,0.0698269667,362.4479324,26.0,29.9631529167
+2024-09-28 03:27:00,0.0600192333,318.5325226333,26.0,29.9732989667
+2024-09-28 03:28:00,0.05058315,274.380007,26.0,29.9674249
+2024-09-28 03:29:00,0.0506792333,274.37360085,26.0,29.9695609667
+2024-09-28 03:30:00,0.0507111667,273.9218226333,26.0,29.9738329167
+2024-09-28 03:31:00,0.0507561167,274.08843695,26.0,29.9727649833
+2024-09-28 03:32:00,0.0507721833,274.0307627833,26.0,29.971697
+2024-09-28 03:33:00,0.0509292,274.4665187333,26.0,29.970095
+2024-09-28 03:34:00,0.0507913667,273.2361398333,26.0,29.967959
+2024-09-28 03:35:00,0.0508266333,273.3675129667,26.0,29.970629
+2024-09-28 03:36:00,0.0510219,273.31944875,26.0,29.9674249
+2024-09-28 03:37:00,0.0510251167,273.8609430833,26.0,29.971697
+2024-09-28 03:38:00,0.0510122833,273.3226556833,26.0,29.966357
+2024-09-28 03:39:00,0.0511051833,273.5533528667,26.0,29.9636868833
+2024-09-28 03:40:00,0.0511662167,273.24255055,26.0,29.96795895
+2024-09-28 03:41:00,0.0512109667,273.3258600333,26.0,29.96795895
+2024-09-28 03:42:00,0.051086,272.8516488167,26.0,29.9615508167
+2024-09-28 03:43:00,0.0512206667,273.0791442,26.0,29.9578127667
+2024-09-28 03:44:00,0.0513425167,273.0438953167,26.0,29.9636868667
+2024-09-28 03:45:00,0.05128805,272.3389958,26.0,29.9615508833
+2024-09-28 03:46:00,0.0513586833,272.6786289667,26.0,29.9588807833
+2024-09-28 03:47:00,0.0513104167,272.5312392833,26.0,29.9626188833
+2024-09-28 03:48:00,0.05142265,272.78756905,26.0,29.9610169
+2024-09-28 03:49:00,0.0515572,272.9029143,26.0,29.9530066833
+2024-09-28 03:50:00,0.05151875,272.1948052833,26.0,29.96101685
+2024-09-28 03:51:00,0.0512302667,270.9355962167,26.0,29.95727875
+2024-09-28 03:52:00,0.0502371,265.7289346333,26.0,29.966357
+2024-09-28 03:53:00,0.0490452667,259.71804055,26.0,29.9594148333
+2024-09-28 03:54:00,0.0478983167,255.1650110167,26.0,29.9642209167
+2024-09-28 03:55:00,0.0473695833,252.1787909167,26.0,29.96742495
+2024-09-28 03:56:00,0.0474049,252.0089741833,26.0,29.9620847
+2024-09-28 03:57:00,0.0603204,307.6353747,26.0,29.9583467167
+2024-09-28 03:58:00,0.06714205,341.5764139167,26.0,29.9599488667
+2024-09-28 03:59:00,0.0650048667,330.6536385333,26.0,29.9508705833
+2024-09-28 04:00:00,0.0510156167,269.9006733167,26.0,29.9588808
+2024-09-28 04:01:00,0.0509739667,267.41429395,26.0,29.964221
+2024-09-28 04:02:00,0.05149625,269.6091033167,26.0,29.9631528833
+2024-09-28 04:03:00,0.0516148333,269.8013478333,26.0,29.9524727333
+2024-09-28 04:04:00,0.0514738667,269.3816124,26.0,29.9556767667
+2024-09-28 04:05:00,0.0513905333,269.1380978167,26.0,29.9519386167
+2024-09-28 04:06:00,0.0516500333,269.6603677667,26.0,29.9487345333
+2024-09-28 04:07:00,0.0516156154,269.7409651026,26.0,29.9567037692
+2024-09-28 04:09:00,0.0518263333,269.0804243333,26.0,29.9423264167
+2024-09-28 04:10:00,0.0517621167,268.8208941167,26.0,29.9460645667
+2024-09-28 04:11:00,0.0607562167,306.8503717167,26.0,29.9519387
+2024-09-28 04:12:00,0.0690933667,344.5626422833,26.0,29.9535406833
+2024-09-28 04:13:00,0.06612635,331.6661307833,26.0,29.9610169333
+2024-09-28 04:14:00,0.0545816102,282.5430768983,26.0,29.9561745254
+2024-09-28 04:15:00,0.0515250667,267.4463308667,26.0,29.9514046833
+2024-09-28 04:16:00,0.05161795,267.5841065,26.0,29.9508707167
+2024-09-28 04:17:00,0.0516595667,266.9272649167,26.0,29.9514046833
+2024-09-28 04:18:00,0.0516852,267.2636968667,26.0,29.9562107167
+2024-09-28 04:19:00,0.0515870508,267.1894572881,26.0,29.9496577797
+2024-09-28 04:20:00,0.05170445,266.94969485,26.0,29.9460645167
+2024-09-28 04:21:00,0.0518453833,267.3982707833,26.0,29.9492686167
+2024-09-28 04:22:00,0.0518357333,267.2476766667,26.0,29.94499655
+2024-09-28 04:23:00,0.0515091167,265.4277453667,26.0,29.9540746667
+2024-09-28 04:24:00,0.0519912034,266.8994574746,26.0,29.9431411017
+2024-09-28 04:25:00,0.0520602167,267.3502019167,26.0,29.9487346333
+2024-09-28 04:26:00,0.0520661017,267.228563322,26.0,29.945313322
+2024-09-28 04:27:00,0.05210495,266.5812214667,26.0,29.9492685833
+2024-09-28 04:28:00,0.0522172333,267.96860375,26.0,29.9471326167
+2024-09-28 04:29:00,0.0518478305,265.6384565763,26.0,29.9480286271
+2024-09-28 04:30:00,0.0509195167,260.8522909167,26.0,29.9492686667
+2024-09-28 04:31:00,0.04965715,254.90547765,26.0,29.9455306
+2024-09-28 04:32:00,0.0486159333,250.0801023167,26.0,29.9396564167
+2024-09-28 04:33:00,0.0479654833,246.76065655,26.0,29.9449965833
+2024-09-28 04:34:00,0.0391368475,204.4911733898,29.7796610169,29.9485717119
+2024-09-28 04:35:00,-0.1177540333,0.1185517,44.9166666667,29.9417924167
+2024-09-28 04:36:00,-0.0965269,8.3819291333,42.0333333333,29.9433945333
+2024-09-28 04:37:00,0.0661230333,339.990385,26.0,29.9460645833
+2024-09-28 04:38:00,0.0714291667,363.63024765,26.0,29.9332481167
+2024-09-28 04:39:00,0.0591955254,310.8781940169,26.0,29.9387965593
+2024-09-28 04:40:00,0.0512944333,272.9573856833,26.0,29.9417924833
+2024-09-28 04:41:00,0.0513264,272.9958352667,26.0,29.9444625667
+2024-09-28 04:42:00,0.0514642333,272.99262995,26.0,29.9433945333
+2024-09-28 04:43:00,0.0514418333,272.8740782667,26.0,29.9391224
+2024-09-28 04:44:00,0.0515577797,272.6635833559,26.0,29.9404257288
+2024-09-28 04:45:00,0.0515923667,272.6273619,26.0,29.9433945333
+2024-09-28 04:46:00,0.0516378621,272.6645948966,26.0,29.9401167759
+2024-09-28 04:47:00,0.0515699667,271.9480911,26.0,29.9455305167
+2024-09-28 04:48:00,0.05170115,272.3614185333,26.0,29.9385883833
+2024-09-28 04:49:00,0.0516652203,272.2139248644,26.0,29.9349949831
+2024-09-28 04:50:00,0.0518487,272.5600757167,26.0,29.9364522667
+2024-09-28 04:51:00,0.0518935,272.5857074833,26.0,29.9343161667
+2024-09-28 04:52:00,0.0510604667,268.3915384667,26.0,29.93752035
+2024-09-28 04:53:00,0.0450112167,239.0676054167,26.0,29.9401904
+2024-09-28 04:54:00,0.0561131186,290.0309004915,26.0,29.9404257458
+2024-09-28 04:55:00,0.0561744167,290.1922466833,26.0,29.9268399667
+2024-09-28 04:56:00,0.0560623167,289.5257951,26.0,29.9300440167
+2024-09-28 04:57:00,0.0561584,289.8558147333,26.0,29.9364523167
+2024-09-28 04:58:00,0.0562671833,289.7853267833,26.0,29.9385883167
+2024-09-28 04:59:00,0.0562987627,289.4932660847,26.0,29.9290212712
+2024-09-28 05:00:00,0.0565300667,289.5033656833,26.0,29.9359183
+2024-09-28 05:01:00,0.0562704333,288.0262766667,26.0,29.9327142
+2024-09-28 05:02:00,0.0554886167,284.5113774833,26.0,29.9348502
+2024-09-28 05:03:00,0.0542870833,279.6443455667,26.0,29.9279079
+2024-09-28 05:04:00,0.053020678,273.6085221186,26.0,29.9344518814
+2024-09-28 05:05:00,0.0518230333,267.9878239667,26.0,29.9343161667
+2024-09-28 05:06:00,0.0508017288,262.793868339,26.0,29.9339088475
+2024-09-28 05:07:00,0.0503586333,260.5639228167,26.0,29.93912235
+2024-09-28 05:08:00,0.0498909167,258.2249297,26.0,29.9348502667
+2024-09-28 05:09:00,0.0479749167,249.9391228,26.0,29.93004395
+2024-09-28 05:10:00,0.0503488136,260.4966897797,26.0,29.9404257119
+2024-09-28 05:11:00,0.0519544833,268.08715255,26.0,29.94019035
+2024-09-28 05:12:00,0.0542710167,277.8148051167,26.0,29.93431625
+2024-09-28 05:13:00,0.0556872333,283.9794932833,26.0,29.9396563833
+2024-09-28 05:14:00,0.0568120667,289.487346,26.0,29.9300440667
+2024-09-28 05:15:00,0.0572242373,290.2817984746,26.0,29.9344519661
+2024-09-28 05:16:00,0.0572059667,290.2819597,26.0,29.9257719
+2024-09-28 05:17:00,0.0573630667,290.68888005,26.0,29.91722735
+2024-09-28 05:18:00,0.0575425,290.8298575667,26.0,29.9166933167
+2024-09-28 05:19:00,0.0574079167,289.8782436333,26.0,29.9193635667
+2024-09-28 05:20:00,0.0574817119,290.3599984237,26.0,29.931736678
+2024-09-28 05:21:00,0.05722845,288.7343836833,26.0,29.928442
+2024-09-28 05:22:00,0.0575360667,289.6379365,26.0,29.9193635667
+2024-09-28 05:23:00,0.0515860833,263.4924733,26.0,29.92844175
+2024-09-28 05:24:00,0.0522428833,264.3671928833,26.0,29.9188294167
+2024-09-28 05:25:00,0.0537930678,271.4253927966,26.0,29.9083841695
+2024-09-28 05:26:00,0.0544281,274.1685400167,26.0,29.9198974833
+2024-09-28 05:27:00,0.0542550333,273.0599186167,26.0,29.9145572
+2024-09-28 05:28:00,0.0543543667,273.236145,26.0,29.9118871
+2024-09-28 05:29:00,0.0546108167,274.0852357,26.0,29.9145572167
+2024-09-28 05:30:00,0.0546499831,273.8431282203,26.0,29.913815
+2024-09-28 05:31:00,0.054662,274.0724177833,26.0,29.9225676833
+2024-09-28 05:32:00,0.05474845,273.7359837667,26.0,29.91562525
+2024-09-28 05:33:00,0.05474835,274.1268845,26.0,29.9161593667
+2024-09-28 05:34:00,0.0547036,273.8929860667,26.0,29.9209654833
+2024-09-28 05:35:00,0.0541775424,270.8291015932,26.0,29.9203319153
+2024-09-28 05:36:00,0.0534508983,267.554399339,26.0,29.9105565424
+2024-09-28 05:37:00,0.0524094,263.1624460667,26.0,29.91829545
+2024-09-28 05:38:00,0.0511855333,257.4847821,26.0,29.9156252667
+2024-09-28 05:39:00,0.05011835,252.5088105333,26.0,29.9118871
+2024-09-28 05:40:00,0.0489805424,247.0915987797,26.0,29.9121857627
+2024-09-28 05:41:00,0.0481193167,243.4700402167,26.0,29.9188294167
+2024-09-28 05:42:00,0.0480648167,242.4287104167,26.0,29.9124211
+2024-09-28 05:43:00,0.0480552167,242.8708750833,26.0,29.92043145
+2024-09-28 05:44:00,0.0481290167,242.5248332333,26.0,29.9092169333
+2024-09-28 05:45:00,0.0479606271,242.2267983898,26.0,29.9176165085
+2024-09-28 05:46:00,0.0338193667,174.9279074667,31.2666666667,29.9198974667
+2024-09-28 05:47:00,-0.1233484167,0.1409804,45.0,29.9150912667
+2024-09-28 05:48:00,-0.0789074,0.1345722,30.2333333333,29.9166931833
+2024-09-28 05:49:00,0.0009064833,0.1537968,0.0,29.9177613833
+2024-09-28 05:50:00,0.001084661,0.1401114915,0.0,29.9121857966
+2024-09-28 05:51:00,0.0010090333,0.1441845,0.0,29.9124211333
+2024-09-28 05:52:00,0.0010091167,0.1890419,0.0,29.9076148333
+2024-09-28 05:53:00,0.0010442333,0.1441845,0.0,29.9124211333
+2024-09-28 05:54:00,0.0009995167,0.1473886,0.0,29.9113530667
+2024-09-28 05:55:00,0.000990339,0.1596619322,0.0,29.9143580339
+2024-09-28 05:56:00,0.01291565,63.1880803,14.6833333333,29.9092169
+2024-09-28 05:57:00,0.0429638667,234.31271785,26.0,29.9150911833
+2024-09-28 05:58:00,0.0425536833,231.2784377167,26.0,29.9145571
+2024-09-28 05:59:00,0.0422459333,228.87856225,26.0,29.9113529167
+2024-09-28 06:00:00,0.0416651356,226.0846422203,26.0,29.9149010847
+2024-09-28 06:01:00,0.0408938167,221.9000328,26.0,29.9172273167
+2024-09-28 06:02:00,0.0434829667,232.3454025167,26.0,29.9198974333
+2024-09-28 06:03:00,0.0499069667,263.1304102667,26.0,29.9188293167
+2024-09-28 06:04:00,0.0520058167,269.3976273,26.0,29.9092169667
+2024-09-28 06:05:00,0.0673870169,339.346908678,26.0,29.9094705254
+2024-09-28 06:06:00,0.06831785,342.7266935667,26.0,29.9054789167
+2024-09-28 06:07:00,0.0536942667,280.25632725,26.0,29.9092169833
+2024-09-28 06:08:00,0.0513488167,267.2060267667,26.0,29.9134892
+2024-09-28 06:09:00,0.0522204167,271.0573511333,26.0,29.9161592
+2024-09-28 06:10:00,0.0524439831,272.3475180508,26.0,29.9089275085
+2024-09-28 06:11:00,0.0524126333,271.3905756167,26.0,29.9108191333
+2024-09-28 06:12:00,0.0524510833,271.5603932833,26.0,29.9065470167
+2024-09-28 06:13:00,0.05253745,271.32329005,26.0,29.9113530667
+2024-09-28 06:14:00,0.0525823833,271.422619,26.0,29.9044107833
+2024-09-28 06:15:00,0.0526721186,271.6762814407,26.0,29.9072980508
+2024-09-28 06:16:00,0.0526945167,271.4258244167,26.0,29.9028087833
+2024-09-28 06:17:00,0.05275545,271.6533131167,26.0,29.9049448167
+2024-09-28 06:18:00,0.0528099167,271.6565145,26.0,29.9081489667
+2024-09-28 06:19:00,0.0527971333,271.17589895,26.0,29.9092170333
+2024-09-28 06:20:00,0.0527568814,271.4449409661,26.0,29.9040398814
+2024-09-28 06:21:00,0.05219475,268.2794007167,26.0,29.9049448167
+2024-09-28 06:22:00,0.0510668667,262.9029206333,26.0,29.9081491667
+2024-09-28 06:23:00,0.0498813333,257.1739822333,26.0,29.9081488667
+2024-09-28 06:24:00,0.0487087167,251.69817595,26.0,29.89586665
+2024-09-28 06:25:00,0.0484362712,250.3760736949,26.0,29.8986095932
+2024-09-28 06:26:00,0.0484428,249.9647519167,26.0,29.9028089333
+2024-09-28 06:27:00,0.0484297797,249.949221,26.0,29.9056690339
+2024-09-28 06:28:00,0.04846205,249.8782429,26.0,29.9060127833
+2024-09-28 06:29:00,0.04858055,249.88785475,26.0,29.9017408667
+2024-09-28 06:30:00,0.0484981695,249.8514677966,26.0,29.9007813729
+2024-09-28 06:31:00,0.0485164833,249.63472885,26.0,29.9006728
+2024-09-28 06:32:00,0.0492279,251.0189021833,26.0,29.9017409667
+2024-09-28 06:33:00,0.0683596167,335.5238731833,26.0,29.8969350667
+2024-09-28 06:34:00,0.06909975,339.0676084667,26.0,29.9033428667
+2024-09-28 06:35:00,0.0602805763,302.272739661,26.0,29.8991522712
+2024-09-28 06:36:00,0.0507592833,258.7311743,26.0,29.8985367667
+2024-09-28 06:37:00,0.0514258667,262.0954830667,26.0,29.8953327667
+2024-09-28 06:38:00,0.0522748,265.1201516667,26.0,29.8883910333
+2024-09-28 06:39:00,0.0528131,267.55526895,26.0,29.90067265
+2024-09-28 06:40:00,0.0527991695,267.7010329153,26.0,29.8926357119
+2024-09-28 06:41:00,0.0529573667,267.45274215,26.0,29.8947988333
+2024-09-28 06:42:00,0.0529670833,267.9301509833,26.0,29.8969346667
+2024-09-28 06:43:00,0.052935,267.2925349833,26.0,29.8915947833
+2024-09-28 06:44:00,0.0530632,268.18007535,26.0,29.8889247667
+2024-09-28 06:45:00,0.0529132712,266.3585644068,26.0,29.8942647627
+2024-09-28 06:46:00,0.0523262,263.7936603,26.0,29.89586675
+2024-09-28 06:47:00,0.0515091333,259.7116344333,26.0,29.8937311167
+2024-09-28 06:48:00,0.0505285,255.6840720167,26.0,29.89586705
+2024-09-28 06:49:00,0.0497306833,252.2941382167,26.0,29.9028088333
+2024-09-28 06:50:00,0.0490031017,248.6197925932,26.0,29.8958940678
+2024-09-28 06:51:00,0.0486638667,247.03620925,26.0,29.8937311667
+2024-09-28 06:52:00,0.0488433833,247.3982715,26.0,29.8910609
+2024-09-28 06:53:00,0.0487310833,246.8824128,26.0,29.88625495
+2024-09-28 06:54:00,0.0487664833,246.6356978667,26.0,29.8894588
+2024-09-28 06:55:00,0.0487978475,246.8569967966,26.0,29.8920926271
+2024-09-28 06:56:00,0.06062165,295.2258932,26.0,29.8825167167
+2024-09-28 06:57:00,0.0658090333,319.1893632,26.0,29.8931963333
+2024-09-28 06:58:00,0.0647612,314.9535390667,26.0,29.8851869833
+2024-09-28 06:59:00,0.0520408667,261.2848481167,26.0,29.8867886833
+2024-09-28 07:00:00,0.0528578814,263.7974600339,26.0,29.8844898475
+2024-09-28 07:01:00,0.0533930833,265.3444400167,26.0,29.88144885
+2024-09-28 07:02:00,0.0534570667,265.7161156833,26.0,29.8883907333
+2024-09-28 07:03:00,0.0535212167,264.9086871667,26.0,29.8841186667
+2024-09-28 07:04:00,0.0534282833,265.5078479167,26.0,29.8803807333
+2024-09-28 07:05:00,0.0534214576,265.4168854576,26.0,29.8785161186
+2024-09-28 07:06:00,0.0010990167,130.8554956167,36.3666666667,29.87504075
+2024-09-28 07:07:00,-0.1327075167,0.1505927,45.0,29.8803807333
+2024-09-28 07:08:00,-0.0111375,146.7638575833,34.2166666667,29.8851869333
+2024-09-28 07:09:00,0.0534379667,279.5322067833,26.0,29.8846528
+2024-09-28 07:10:00,0.0526948983,273.7681905763,26.0,29.8796024407
+2024-09-28 07:11:00,0.0526432667,273.6494776833,26.0,29.8777105167
+2024-09-28 07:12:00,0.0528261167,273.3130441833,26.0,29.8761088167
+2024-09-28 07:13:00,0.0528933833,273.2105091167,26.0,29.8723707833
+2024-09-28 07:14:00,0.0529830667,273.2137139833,26.0,29.8755745833
+2024-09-28 07:15:00,0.0530371864,273.0774086102,26.0,29.8752579153
+2024-09-28 07:16:00,0.0532104,273.0663237833,26.0,29.87664255
+2024-09-28 07:17:00,0.0531688333,273.0567138333,26.0,29.8739726333
+2024-09-28 07:18:00,0.0631015667,312.8868972,26.0,29.8750406
+2024-09-28 07:19:00,0.0741076833,361.45466295,26.0,29.8803807333
+2024-09-28 07:20:00,0.0706944068,348.4704464237,26.0,29.8719994068
+2024-09-28 07:21:00,0.0528133,269.5354050667,26.0,29.87343865
+2024-09-28 07:22:00,0.0529733667,270.4357564667,26.0,29.8702346
+2024-09-28 07:23:00,0.0529990333,269.9455293167,26.0,29.8771767833
+2024-09-28 07:24:00,0.0530534833,270.0160202667,26.0,29.87664255
+2024-09-28 07:25:00,0.053014339,270.0731516949,26.0,29.8752578136
+2024-09-28 07:26:00,0.0531048,269.6443476833,26.0,29.8718367
+2024-09-28 07:27:00,0.0531591667,270.11855405,26.0,29.8771765333
+2024-09-28 07:28:00,0.0531079167,269.90067265,26.0,29.8777106667
+2024-09-28 07:29:00,0.0531912167,269.7404698667,26.0,29.8654288
+2024-09-28 07:30:00,0.0533172542,269.9949521356,26.0,29.8774299492
+2024-09-28 07:31:00,0.0533129833,269.32073425,26.0,29.8739723833
+2024-09-28 07:32:00,0.0532424333,268.5902005833,26.0,29.8654282
+2024-09-28 07:33:00,0.0534218833,269.1829530333,26.0,29.8697004167
+2024-09-28 07:34:00,0.0535116667,269.4008357833,26.0,29.87504045
+2024-09-28 07:35:00,0.0534963729,269.4507994746,26.0,29.8703700508
+2024-09-28 07:36:00,0.0523421833,264.42165735,26.0,29.8680983167
+2024-09-28 07:37:00,0.0470682667,238.99070895,26.0,29.8723703833
+2024-09-28 07:38:00,0.0574785167,285.7065026833,26.0,29.8691667833
+2024-09-28 07:39:00,0.0575425667,285.5559107167,26.0,29.865428
+2024-09-28 07:40:00,0.0570645593,283.2892540169,26.0,29.8692840847
+2024-09-28 07:41:00,0.0560429,278.5421359667,26.0,29.8697003167
+2024-09-28 07:42:00,0.05463955,272.4831772167,26.0,29.8659626833
+2024-09-28 07:43:00,0.0533450667,266.5299580667,26.0,29.8648940167
+2024-09-28 07:44:00,0.0520664667,260.6760636667,26.0,29.8670305
+2024-09-28 07:45:00,0.0480973051,243.1554392542,26.0,29.8654826441
+2024-09-28 07:46:00,0.0506406667,255.0528675667,26.0,29.8664967167
+2024-09-28 07:47:00,0.0531496167,265.8058303333,26.0,29.8723703833
+2024-09-28 07:48:00,0.0553252333,274.7356598833,26.0,29.8616904667
+2024-09-28 07:49:00,0.0567894,280.8651061333,26.0,29.86703045
+2024-09-28 07:50:00,0.0579019153,285.495195322,26.0,29.8567936949
+2024-09-28 07:51:00,0.0568793,282.9894275167,26.0,29.8638262
+2024-09-28 07:52:00,0.04382235,222.35821985,26.0,29.8627578833
+2024-09-28 07:53:00,0.0436303667,221.4802979,26.0,29.8632926667
+2024-09-28 07:54:00,0.0436207167,221.3841749333,26.0,29.8675643833
+2024-09-28 07:55:00,0.0438418475,221.8715242034,26.0,29.8719994576
+2024-09-28 07:56:00,0.0438288333,221.8103212833,26.0,29.8691662833
+2024-09-28 07:57:00,0.04427435,223.2585711333,26.0,29.8707680833
+2024-09-28 07:58:00,0.0531719833,262.2588879167,26.0,29.8638263
+2024-09-28 07:59:00,0.0536238167,265.0464593,26.0,29.8611558833
+2024-09-28 08:00:00,0.0525124068,259.8091622712,26.0,29.8475615085
+2024-09-28 08:01:00,0.0515026167,255.0240286333,26.0,29.8595543333
+2024-09-28 08:02:00,0.0503523667,249.8301830833,26.0,29.8675645333
+2024-09-28 08:03:00,0.0496251,246.75424885,26.0,29.8600881167
+2024-09-28 08:04:00,0.0494582333,245.1457843167,26.0,29.8579522333
+2024-09-28 08:05:00,0.0496677119,246.0619461017,26.0,29.855707322
+2024-09-28 08:06:00,0.0496666,246.23838935,26.0,29.8611560833
+2024-09-28 08:07:00,0.0497500833,245.6936889833,26.0,29.8584860167
+2024-09-28 08:08:00,0.0627779,298.8561352333,26.0,29.8622242
+2024-09-28 08:09:00,0.0635308833,305.0016036,26.0,29.8606218
+2024-09-28 08:10:00,0.0503618475,248.0235018983,26.0,29.8546213051
+2024-09-28 08:11:00,0.0553379833,269.3367487667,26.0,29.8563503833
+2024-09-28 08:12:00,0.0568409333,275.47580935,26.0,29.85581575
+2024-09-28 08:13:00,0.05808085,280.02883795,26.0,29.85261205
+2024-09-28 08:14:00,0.0583372667,281.5539896833,26.0,29.8515439333
+2024-09-28 08:15:00,0.0585439322,281.8848757797,26.0,29.8529924068
+2024-09-28 08:16:00,0.0584717667,281.74623415,26.0,29.8584857667
+2024-09-28 08:17:00,0.0587537,281.9096405,26.0,29.85261225
+2024-09-28 08:18:00,0.0162832167,149.3880148167,34.8666666667,29.8478058
+2024-09-28 08:19:00,-0.1219802667,0.1473886,45.0,29.8472718167
+2024-09-28 08:20:00,-0.0605802712,0.1368530847,27.8305084746,29.8584224915
+2024-09-28 08:21:00,-0.0029382833,0.1570009,18.1,29.8638261
+2024-09-28 08:22:00,-0.1082378167,0.1377763,55.9833333333,29.8413977
+2024-09-28 08:23:00,-0.0474592167,0.1409804,59.0,29.8435336833
+2024-09-28 08:24:00,-0.0256745167,0.1025312,61.65,29.8408636667
+2024-09-28 08:25:00,0.0005505085,0.1498867119,64.0,29.8443030508
+2024-09-28 08:26:00,0.0010923667,0.1345722,64.0,29.8446018
+2024-09-28 08:27:00,0.001038,0.1185517,64.0,29.8488738667
+2024-09-28 08:28:00,0.00098025,0.1730214,64.0,29.8547481333
+2024-09-28 08:29:00,0.0011341167,0.1634091,64.0,29.8483397833
+2024-09-28 08:30:00,0.0010228814,0.1433698983,64.0,29.8432170339

+ 0 - 0
algorithm/uf_rl/rl_model/DQN/uf_train/__init__.py


+ 170 - 0
algorithm/uf_rl/rl_model/DQN/uf_train/dqn_trainer.py

@@ -0,0 +1,170 @@
+import os
+import time
+from stable_baselines3 import DQN
+
+
+class DQNTrainer:
+    """
+    DQN训练器封装类
+
+    功能:
+    - 创建训练模型
+    - 训练智能体
+    - 保存与加载模型
+    - 在测试集环境上评估策略
+    """
+
+    def __init__(self, env, params, callback=None,PROJECT_ROOT=None,DIR_NAME=None):
+        """
+        初始化训练器
+
+        参数:
+            env: Gym环境实例(向量化环境)
+            params: DQNParams超参数对象
+            callback: 可选训练回调
+        """
+        self.env = env
+        self.params = params
+        self.callback = callback
+        self.PROJECT_ROOT = PROJECT_ROOT
+        self.dir_name = DIR_NAME
+        self.log_dir = self._create_log_dir()  # 创建TensorBoard日志目录
+        self.model = self._create_model()      # 创建DQN模型
+
+
+    # ------------------- 私有方法 -------------------
+    def _create_log_dir(self):
+        """
+        创建 TensorBoard 日志目录(固定在 PROJECT_ROOT/model_result/uf_dqn_tensorboard 下)
+        """
+        import os
+        import time
+
+        # 1️⃣ 时间戳,用于区分每次训练
+        timestamp = time.strftime("%Y%m%d-%H%M%S")
+
+        # 2️⃣ 将浮点参数转成整数便于命名
+        lr_int = int(self.params.learning_rate * 1e4)
+        gamma_int = int(self.params.gamma * 100)
+        exp_int = int(self.params.exploration_fraction * 100)
+
+        # 3️⃣ 构建日志目录名称
+        log_name = (
+            f"DQN_lr{lr_int}_buf{self.params.buffer_size}_bs{self.params.batch_size}"
+            f"_gamma{gamma_int}_exp{exp_int}_{self.params.remark}_{timestamp}"
+        )
+
+        # 4️⃣ 固定日志存放位置:PROJECT_ROOT/model_result/uf_dqn_tensorboard
+        # 假设在 run_dqn_train.py 中定义了 PROJECT_ROOT = "models/uf_rl"
+        base_dir = os.path.join(self.PROJECT_ROOT, "model_result", "uf_dqn_tensorboard",self.dir_name)
+        os.makedirs(base_dir, exist_ok=True)
+
+        # 5️⃣ 完整日志目录路径
+        log_dir = os.path.join(base_dir, log_name)
+        os.makedirs(log_dir, exist_ok=True)
+
+        return log_dir
+
+    def _create_model(self):
+        """
+        创建Stable-Baselines3 DQN模型
+        """
+        model = DQN(
+            policy="MlpPolicy",
+            env=self.env,
+            learning_rate=self.params.learning_rate,
+            buffer_size=self.params.buffer_size,
+            learning_starts=self.params.learning_starts,
+            batch_size=self.params.batch_size,
+            gamma=self.params.gamma,
+            train_freq=self.params.train_freq,
+            target_update_interval=self.params.target_update_interval,
+            tau=self.params.tau,
+            exploration_initial_eps=self.params.exploration_initial_eps,
+            exploration_fraction=self.params.exploration_fraction,
+            exploration_final_eps=self.params.exploration_final_eps,
+            verbose=1,
+            tensorboard_log=self.log_dir
+        )
+        return model
+
+    # ------------------- 公共方法 -------------------
+    def train(self, total_timesteps: int):
+        """
+        执行训练
+
+        参数:
+            total_timesteps: 总训练步数
+        """
+        if self.callback:
+            self.model.learn(total_timesteps=total_timesteps, callback=self.callback)
+        else:
+            self.model.learn(total_timesteps=total_timesteps)
+
+        print(f"✅ 模型训练完成!")
+        print(f"📊 日志保存在:{self.log_dir}")
+        print(f"💡 使用以下命令查看TensorBoard:")
+        print(f"   tensorboard --logdir={self.log_dir}")
+
+    def save(self, path=None):
+        """
+        保存模型
+
+        参数:
+            path: 可选路径,默认保存到日志目录下 dqn_model.zip
+        """
+        if path is None:
+            path = os.path.join(self.log_dir, "dqn_model.zip")
+        self.model.save(path)
+        print(f"💾 模型已保存到:{path}")
+
+    def load(self, path):
+        """
+        加载模型
+
+        参数:
+            path: 模型文件路径
+        """
+        self.model = DQN.load(path, env=self.env)
+        print(f"📥 模型已从 {path} 加载")
+
+    def evaluate(self, test_env, n_episodes=10, deterministic=True):
+        """
+        在测试环境上评估模型
+
+        参数:
+            test_env: 测试用Gym环境(非向量化)
+            n_episodes: 测试episode数量
+            deterministic: 是否使用确定性策略
+
+        返回:
+            list[dict]: 每个episode的统计信息,包括总奖励、步数、TMP序列等
+        """
+        results = []
+
+        for ep in range(n_episodes):
+            obs = test_env.reset()
+            done = False
+            total_reward = 0
+            steps = 0
+            tmp_after_ceb_list = []
+
+            while not done:
+                action, _ = self.model.predict(obs, deterministic=deterministic)
+                obs, reward, done, info = test_env.step(action)
+
+                total_reward += reward
+                steps += 1
+
+                # 可根据需要记录TMP、回收率等
+                tmp_after_ceb_list.append(info.get("tmp_after_ceb", 0))
+
+            ep_result = {
+                "episode": ep,
+                "total_reward": total_reward,
+                "steps": steps,
+                "tmp_after_ceb_sequence": tmp_after_ceb_list
+            }
+            results.append(ep_result)
+
+        return results

+ 303 - 0
algorithm/uf_rl/rl_model/DQN/uf_train/run_dqn_train.py

@@ -0,0 +1,303 @@
+"""
+DQN 超滤强化学习训练与测试主脚本(工程化优化版)
+"""
+
+import random
+from pathlib import Path
+import numpy as np
+import torch
+
+# ============================================================
+# 1. 导入模块
+# ============================================================
+CURRENT_DIR = Path(__file__).resolve().parent
+
+PROJECT_ROOT = CURRENT_DIR.parents[2]      # uf_rl
+
+
+# ---------- 数据 ----------
+from data_to_rl.data_splitter import ResetStatePoolLoader
+
+# ---------- 阻力模型 ----------
+from env.uf_resistance_models_load import load_resistance_models
+from env.uf_physics import UFPhysicsModel
+
+# ---------- 强化学习环境 ----------
+from env.env_params import (UFActionSpec, UFRewardParams, UFStateBounds)
+from env.env_config_loader import EnvConfigLoader, create_env_params_from_yaml
+from env.uf_env import UFSuperCycleEnv
+
+from env.env_visual import UFEpisodeRecorder, UFTrainingCallback
+
+from rl_model.DQN.dqn_model.dqn_config_loader import DQNConfigLoader
+from rl_model.DQN.uf_train.dqn_trainer import DQNTrainer
+
+
+# ---------- SB3 ----------
+from stable_baselines3.common.monitor import Monitor
+from stable_baselines3.common.vec_env import DummyVecEnv
+
+
+
+
+
+# ============================================================
+#  随机种子
+# ============================================================
+def set_global_seed(seed: int):
+    random.seed(seed)
+    np.random.seed(seed)
+    torch.manual_seed(seed)
+    torch.cuda.manual_seed_all(seed)
+
+    torch.backends.cudnn.deterministic = True
+    torch.backends.cudnn.benchmark = False
+
+    print(f"[Seed] Global random seed = {seed}")
+
+
+# ============================================================
+# 4. Reset State Pool 加载与划分
+# ============================================================
+def load_reset_state_pools():
+    loader = ResetStatePoolLoader(
+        csv_path=RESET_STATE_CSV,
+        train_ratio=0.8,
+        shuffle=True,
+        random_state=RANDOM_SEED,
+    )
+
+    train_pool, val_pool = loader.split()
+
+    print("[Data] Reset state pool loaded")
+    print(f"       Train pool size: {len(train_pool)}")
+    print(f"       Val   pool size: {len(val_pool)}")
+
+    return train_pool, val_pool
+
+
+# ============================================================
+# 5. 环境构造函数
+# ============================================================
+def make_env(
+    physics: UFPhysicsModel,
+    reward_params: UFRewardParams,
+    action_spec: UFActionSpec,
+    statebounds: UFStateBounds,
+    reset_state_pool,
+    seed: int,
+):
+    def _init():
+        env = UFSuperCycleEnv(
+            physics=physics,
+            reward_params=reward_params,
+            action_spec=action_spec,
+            statebounds=statebounds,
+            real_state_pool=reset_state_pool,
+            RANDOM_SEED=seed,
+        )
+        env.action_space.seed(seed)
+        env.observation_space.seed(seed)
+        return Monitor(env)
+    return _init
+
+
+
+# ============================================================
+# 6. 主流程
+# ============================================================
+def main():
+    # 创建配置加载器
+    config_loader = EnvConfigLoader(ENV_CONFIG_PATH)
+
+    # 验证配置
+    config_loader.validate_config()
+    config_loader.print_config_summary()
+
+    # 加载所有参数类
+    (
+        uf_state_default,  # UFState默认值(可用于reset)
+        phys_params,  # UFPhysicsParams
+        action_spec,  # UFActionSpec
+        reward_params,  # UFRewardParams
+        state_bounds  # UFStateBounds
+    ) = create_env_params_from_yaml(ENV_CONFIG_PATH)
+
+    # ---------- Seed ----------
+    set_global_seed(RANDOM_SEED)
+
+    # ---------- Reset states ----------
+    train_pool, val_pool = load_reset_state_pools()
+
+    # ---------- Resistance models ----------
+    res_fp, res_bw = load_resistance_models(phys_params)
+
+    # ---------- Physics ----------
+    physics_model = UFPhysicsModel(
+        phys_params=phys_params,
+        state_bounds=state_bounds,
+        resistance_model_fp=res_fp,
+        resistance_model_bw=res_bw,
+        IS_TIMES=IS_TIMES
+    )
+
+    # ---------- Environments ----------
+    train_env = DummyVecEnv([
+        make_env(
+            physics_model,
+            reward_params,
+            action_spec,
+            state_bounds,
+            train_pool,
+            RANDOM_SEED,
+        )
+    ])
+
+    val_env = DummyVecEnv([
+        make_env(
+            physics_model,
+            reward_params,
+            action_spec,
+            state_bounds,
+            val_pool,
+            RANDOM_SEED,
+        )
+    ])
+
+    # ---------- Callback ----------
+    recorder = UFEpisodeRecorder()
+    callback = UFTrainingCallback(recorder, verbose=1)
+
+    # ---------- Trainer ----------
+    # ========== 2. 加载DQN配置 ==========
+    dqn_loader = DQNConfigLoader(MODEL_CONFIG_PATH)
+    dqn_loader.validate_config()
+    dqn_params = dqn_loader.load_params()
+    dqn_loader.print_config_summary()
+    trainer = DQNTrainer(
+        env=train_env,
+        params=dqn_params,
+        callback=callback,
+        PROJECT_ROOT=PROJECT_ROOT,
+        DIR_NAME=DIR_NAME,
+    )
+
+
+    # ---------- Training ----------
+    print("\n Start training")
+    trainer.train(total_timesteps=TOTAL_TIMESTEPS)
+    trainer.save()
+
+    # ========================================================
+    # 验证
+    # ========================================================
+    print("\n[Eval] Start validation rollout")
+
+    TMP0_min = 0.01
+    TMP0_max = 0.08
+
+    rewards = []
+
+    # ---------- 用于可视化的容器(只记录第一个 episode) ----------
+    vis_tmp_series = []
+    vis_action_series = []
+
+    for ep_idx in range(len(val_pool)):
+        obs = val_env.reset()
+        episode_reward = 0.0
+
+        for step in range(10):
+            # ====================================================
+            # 可视化:只记录第一个 validation episode
+            # ====================================================
+            if ep_idx == 0:
+                TMP0_norm = obs[0]
+                TMP0 = (
+                        TMP0_norm * (TMP0_max - TMP0_min)
+                        + TMP0_min
+                )
+                vis_tmp_series.append(TMP0)
+
+            # ---------------- 策略决策 ----------------
+            action, _ = trainer.model.predict(
+                obs, deterministic=True
+            )
+
+            if ep_idx == 0:
+                vis_action_series.append(action[0])
+
+            # ---------------- 环境推进 ----------------
+            obs, reward, done, _ = val_env.step(action)
+            episode_reward += reward[0]
+
+            if done:
+                break
+
+        rewards.append(episode_reward)
+
+    # ========================================================
+    # 验证结果保存
+    # ========================================================
+    rewards = np.asarray(rewards)
+
+    save_path = Path(trainer.log_dir) / "val_rewards.npy"
+    np.save(save_path, rewards)
+
+    print(f"[Eval] Saved to {save_path}")
+    print(f"[Eval] Mean reward = {rewards.mean():.3f}")
+
+    # ========================================================
+    # 可视化(第一个 validation episode)
+    # ========================================================
+    import matplotlib.pyplot as plt
+
+    vis_tmp_series = np.asarray(vis_tmp_series)
+    vis_action_series = np.asarray(vis_action_series)
+    steps = np.arange(len(vis_tmp_series))
+
+    # ---------- TMP 曲线 ----------
+    plt.figure()
+    plt.plot(steps, vis_tmp_series, marker="o")
+    plt.axhline(
+        TMP0_max,
+        linestyle="--",
+        label="TMP Upper Limit"
+    )
+    plt.xlabel("Step")
+    plt.ylabel("TMP (MPa)")
+    plt.title("Validation Episode TMP Evolution")
+    plt.legend()
+    plt.grid(True)
+    plt.show()
+
+    # ---------- Action 曲线 ----------
+    plt.figure()
+    plt.plot(steps, vis_action_series, marker="o")
+    plt.xlabel("Step")
+    plt.ylabel("Action")
+    plt.title("Validation Episode Action Output")
+    plt.grid(True)
+    plt.show()
+
+
+# ============================================================
+# 入口
+# ============================================================
+if __name__ == "__main__":
+    # ============================================================
+    # 2. 全局配置
+    # ============================================================
+    RANDOM_SEED = 2025
+    TOTAL_TIMESTEPS = 300000
+    IS_TIMES = False
+
+    RESET_STATE_CSV = (
+            PROJECT_ROOT
+            / "datasets/UF_longting_data/rl_ready/output/reset_state_pool.csv"
+    )
+
+    ENV_CONFIG_PATH = PROJECT_ROOT / "longting" / "env_config.yaml"
+    MODEL_CONFIG_PATH = PROJECT_ROOT / "longting" / "dqn_config.yaml"
+    DIR_NAME = "longting48h"
+
+    main()

+ 0 - 0
algorithm/uf_rl/rl_model/__init__.py


BIN
algorithm/uf_rl/rl_model/__pycache__/__init__.cpython-314.pyc


+ 249 - 0
algorithm/uf_rl/uf_data_process/calculate.py

@@ -0,0 +1,249 @@
+from typing import List
+
+import numpy as np
+import pandas as pd
+
+class UFResistanceCalculator:
+    """针对单个段或段列表计算膜通量、TMP、粘度和膜阻力"""
+
+    def __init__(self, units: list, area_m2, scale_factor):
+        self.units = units
+
+        # 处理面积
+        self.A = area_m2
+        if isinstance(self.A, str):
+            try:
+                self.A = float(eval(self.A))  # 将 "128*40" -> 5120.0
+            except Exception as e:
+                print(f"无法解析面积 self.A: {self.A}, 错误: {e}")
+
+        # 处理 scale_factor
+        self.scale_factor = scale_factor
+        if isinstance(self.scale_factor, str):
+            try:
+                self.scale_factor = float(eval(self.scale_factor))
+            except Exception as e:
+                print(f"无法解析 scale_factor: {self.scale_factor}, 错误: {e}")
+
+    @staticmethod
+    def xishan_viscosity(T_celsius):
+        """锡山水厂水温校正粘度,单位 Pa·s"""
+        x = (T_celsius + 273.15) / 300
+        factor = 890 / (
+            280.68 * x ** -1.9 +
+            511.45 * x ** -7.7 +
+            61.131 * x ** -19.6 +
+            0.45903 * x ** -40
+        )
+        mu = 0.00089 / factor
+        return mu
+
+    def calculate_for_segment(self, seg_df: pd.DataFrame, temp_col="C.M.RO_TT_ZJS@out", flow_col=None, tmp_col=None):
+        """计算单个稳定进水段的 TMP、通量、粘度和膜阻力"""
+        seg_df = seg_df.copy()
+
+        for unit in self.units:
+            unit_flow_col = flow_col or f"C.M.{unit}_FT_JS@out"
+            tmp_col = tmp_col or f"C.M.{unit}_DB@press_PV"
+
+            if not all(col in seg_df.columns for col in [unit_flow_col, tmp_col, temp_col]):
+                continue
+
+            # 计算前检查
+            cols_to_check = [tmp_col, unit_flow_col, temp_col]
+            for col in cols_to_check:
+                # 检查非数值
+                if not pd.api.types.is_numeric_dtype(seg_df[col]):
+                    # 尝试强制转换为数值
+                    seg_df[col] = pd.to_numeric(seg_df[col], errors='coerce')
+
+                # 检查是否还有 NaN 或非有限值
+                if seg_df[col].isna().any() or not np.isfinite(seg_df[col]).all():
+                    print(f"⚠️ 列 {col} 包含 NaN 或非有限值")
+                    print(seg_df[col][~np.isfinite(seg_df[col])])
+
+            seg_df[f"{unit}_TMP_Pa"] = seg_df[tmp_col] * 1e6
+            seg_df[f"{unit}_J"] = seg_df[unit_flow_col] / self.A / 3600
+            seg_df[f"{unit}_mu"] = self.xishan_viscosity(seg_df[temp_col])
+            seg_df[f"{unit}_R"] = seg_df[f"{unit}_TMP_Pa"] / (seg_df[f"{unit}_J"] * seg_df[f"{unit}_mu"])
+            seg_df[f"{unit}_R_scaled"] = seg_df[f"{unit}_R"] / self.scale_factor
+
+        return seg_df
+
+    def calculate_for_segments(self, segments: list, temp_col="C.M.RO_TT_ZJS@out", flow_col=None, tmp_col=None):
+        result_segments = []
+        for seg in segments:
+            seg_res = self.calculate_for_segment(seg, temp_col=temp_col, flow_col=flow_col, tmp_col=tmp_col)
+            result_segments.append(seg_res)
+        return result_segments
+
+
+class PumpPowerCalculator:
+    """计算每个segment的供水泵和反洗泵平均功率"""
+
+    def __init__(self, event_col="event_type"):
+        self.event_col = event_col
+
+    def calculate_for_segment(
+        self,
+        seg_df: pd.DataFrame,
+        inlet_power_col=None,
+        bw_power_col=None
+    ):
+        """计算单个segment平均功率"""
+        seg_df = seg_df.copy()
+
+        required_cols = [self.event_col, inlet_power_col, bw_power_col]
+
+        if not all(col in seg_df.columns for col in required_cols):
+            print("⚠️ segment缺少必要列")
+            return seg_df
+
+        # 转为数值
+        seg_df[inlet_power_col] = pd.to_numeric(seg_df[inlet_power_col], errors="coerce")
+        seg_df[bw_power_col] = pd.to_numeric(seg_df[bw_power_col], errors="coerce")
+
+        # inlet平均功率
+        inlet_rows = seg_df[seg_df[self.event_col] == "inlet"]
+        inlet_mean = inlet_rows[inlet_power_col].mean()
+
+        # bw_phys平均功率
+        bw_rows = seg_df[seg_df[self.event_col] == "bw_phys"]
+        bw_mean = bw_rows[bw_power_col].mean()
+
+        seg_df["inlet_pump_power_mean"] = inlet_mean
+        seg_df["bw_pump_power_mean"] = bw_mean
+
+        return seg_df
+
+
+    def calculate_for_segments(
+        self,
+        segments: list,
+        inlet_power_col=None,
+        bw_power_col=None
+    ):
+        """批量计算"""
+        result_segments = []
+
+        for seg in segments:
+            seg_res = self.calculate_for_segment(
+                seg,
+                inlet_power_col=inlet_power_col,
+                bw_power_col=bw_power_col
+            )
+            result_segments.append(seg_res)
+
+        return result_segments
+
+
+# =============================
+#   单变量稳定性分析(例如 flow/TMP)
+# =============================
+class VariableStabilityAnalyzer:
+    """
+    单变量统计特征分析
+    为每段数据增加:均值 / 标准差 / CV 系数
+    """
+
+    def __init__(self):
+        pass
+
+    def analyze(self, seg: pd.DataFrame, col, prefix=None) -> pd.DataFrame:
+        """分析单段:计算均值、标准差、CV,并添加三列"""
+
+        seg = seg.copy()
+
+        prefix = prefix or col
+
+        mean_val = seg[col].mean()
+        std_val = seg[col].std()
+        cv_val = std_val / mean_val if mean_val != 0 else np.nan
+
+        # 添加三列(整段相同)
+        seg[f"{prefix}_mean"] = mean_val
+        seg[f"{prefix}_std"] = std_val
+        seg[f"{prefix}_cv"] = cv_val
+
+        return seg
+
+    def analyze_segments(self, segments: list, col, prefix=None) -> list:
+        """批量处理段列表"""
+        return [self.analyze(seg, col, prefix) for seg in segments]
+
+# =============================
+#       tmp_start / tmp_end 计算
+# =============================
+class UFPressureAnalyzer:
+    """
+    统计每段的压力起止平均值 tmp_start / tmp_end
+    """
+
+    def __init__(
+        self,
+        tmp_col,
+        head_n=20,
+        tail_n=20,
+        feature_start="tmp_start",
+        feature_end="tmp_end",
+    ):
+        self.tmp_col = tmp_col
+        self.head_n = head_n
+        self.tail_n = tail_n
+        self.feature_start = feature_start
+        self.feature_end = feature_end
+
+    def analyze(self, seg: pd.DataFrame) -> pd.DataFrame:
+        """分析单段,将 tmp_start / tmp_end 加入 DataFrame"""
+        seg = seg.sort_values("time").reset_index(drop=True).copy()
+
+        if len(seg) < self.head_n + self.tail_n:
+            seg[self.feature_start] = None
+            seg[self.feature_end] = None
+        else:
+            seg[self.feature_start] = (
+                seg[self.tmp_col].iloc[: self.head_n].mean()
+            )
+            seg[self.feature_end] = (
+                seg[self.tmp_col].iloc[-self.tail_n :].mean()
+            )
+
+        return seg
+
+    def analyze_segments(self, segments: list) -> list:
+        """批量处理段列表"""
+        return [self.analyze(seg) for seg in segments]
+
+# =============================
+#       R_scaled_start / R_scaled_end 计算
+# =============================
+class UFResistanceAnalyzer:
+    """
+    统计每段的放缩后膜阻力起止值 R_scaled_start / R_scaled_end
+    """
+
+    def __init__(self, resistance_col, head_n=20, tail_n=20, feature_start="R_scaled_start", feature_end="R_scaled_end"):
+        self.res_col = resistance_col
+        self.head_n = head_n
+        self.tail_n = tail_n
+        self.feature_start = feature_start
+        self.feature_end = feature_end
+
+    def analyze(self, seg: pd.DataFrame) -> pd.DataFrame:
+        """分析单段,将 R_scaled_start / R_scaled_end 加入 DataFrame"""
+        seg = seg.sort_values("time").reset_index(drop=True).copy()
+
+        if len(seg) < self.head_n + self.tail_n:
+            seg[self.feature_start] = None
+            seg[self.feature_end] = None
+        else:
+            seg[self.feature_start] = seg[self.res_col].iloc[:self.head_n].median()
+            seg[self.feature_end] = seg[self.res_col].iloc[-self.tail_n:].median()
+        return seg
+
+    def analyze_segments(self, segments: list) -> list:
+        """批量处理段列表"""
+        return [self.analyze(seg) for seg in segments]
+
+
+

+ 427 - 0
algorithm/uf_rl/uf_data_process/data_export.py

@@ -0,0 +1,427 @@
+import os, pandas as pd
+from datetime import datetime
+from sqlalchemy import create_engine, text
+from functools import reduce
+
+# ---------- 配置 ----------
+DB_USER = "whu"
+DB_PASS = "09093f4e6b33ddd"
+DB_HOST = "222.130.26.206"
+DB_PORT = 4000
+
+# 时间配置
+START_TIME = datetime(2026, 1, 22, 0, 0, 0)
+END_TIME = datetime(2026, 3, 15, 6, 0, 0)
+BOUNDARY = datetime(2025, 3, 25, 0, 0, 0)
+
+DELETE_PERIODS = [
+    ("2024-04-24 13:42:00", "2024-04-24 18:26:00"),
+    ("2024-11-09 12:34:00", "2024-11-11 10:46:00"),
+    ("2024-12-12 08:52:00", "2024-12-12 17:22:00"),
+    ("2024-12-15 16:00:00", "2024-12-16 09:34:00"),
+    ("2025-01-28 10:58:00", "2025-02-05 11:24:00"),
+]  # 来自张昊师兄的,需要被去除的黑名单区间
+DELETE_PERIODS = [(pd.to_datetime(s), pd.to_datetime(e)) for s, e in DELETE_PERIODS]
+
+# ---------- 传感器配置----------
+# 机组编号
+UNITS = [1, 2]
+
+BASE_VARIABLES = [
+    "ns=3;s={}#UF_JSFLOW_O",      # 进水流量
+    "ns=3;s={}#UF_JSPRESS_O",     # 进水压力
+    "ns=3;s=UF{}_SSD_KMYC",       # 跨膜压差
+    "ns=3;s=UF{}_STEP",           # 步序/控制字
+    "ns=3;s=ZZ_{}#UFBWB_POWER" # 反洗泵功率
+]
+SYSTEM_VARIABLES = [
+    "ns=3;s=ZJS_TEMP_O",          # 进水温度
+    "ns=3;s=RO_JSORP_O",          # 总产水ORP
+    "ns=3;s=RO_JSPH_O",           # 总产水PH
+    "ns=3;s=RO_JSDD_O",           # 总产水电导
+    "ns=3;s=CN_LEVEL_O", # 次钠液位
+    "ns=3;s=S_LEVEL_O", # 酸液位
+    "ns=3;s=J_LEVEL_O", # 碱液位
+    "ns=3;s=ZZ_UFGSB_POWER", # 超滤供水泵功率
+]
+
+# 输出目录
+BASE_OUTPUT_DIR = "../datasets/UF_yancheng_data"
+PROCESSED_OUTPUT_DIR = os.path.join(BASE_OUTPUT_DIR, "raw")
+
+# 创建目录
+os.makedirs(PROCESSED_OUTPUT_DIR, exist_ok=True)
+
+# 生成所有变量名称
+SENSOR_NAMES = []
+
+for unit in UNITS:
+    for var_template in BASE_VARIABLES:
+        SENSOR_NAMES.append(var_template.format(unit))
+
+SENSOR_NAMES.extend(SYSTEM_VARIABLES)
+
+print(f"总共查询 {len(SENSOR_NAMES)} 个变量")
+for i, var in enumerate(SENSOR_NAMES, 1):
+    print(f"{i:2d}. {var}")
+
+
+# ---------- 创建数据库引擎 ----------
+def create_db_engines():
+    """
+    创建数据库引擎,每个数据库只创建一次
+    """
+    try:
+        # 为两个数据库分别创建引擎
+        engine_test = create_engine(
+            f"mysql+pymysql://{DB_USER}:{DB_PASS}@{DB_HOST}:{DB_PORT}/ws_data_test?charset=utf8mb4"
+        )
+        engine_prod = create_engine(
+            f"mysql+pymysql://{DB_USER}:{DB_PASS}@{DB_HOST}:{DB_PORT}/ws_data?charset=utf8mb4"
+        )
+
+        # 测试连接
+        with engine_test.connect() as conn:
+            print("✅ 测试数据库连接成功!")
+        with engine_prod.connect() as conn:
+            print("✅ 生产数据库连接成功!")
+
+        return engine_test, engine_prod
+    except Exception as e:
+        print(f"❌ 数据库连接失败: {e}")
+        return None, None
+
+
+# ---------- 一分钟聚合查询函数(子查询方式)----------
+def fetch_valve_aggregated(name, start, end, engine, interval_minutes=1):
+    """
+    从数据库获取传感器数据并按分钟聚合,时间戳对齐到分钟开始(00秒)
+    """
+    interval_seconds = interval_minutes * 60
+
+    sql = text(f"""
+        SELECT 
+            FROM_UNIXTIME(FLOOR(UNIX_TIMESTAMP(MIN(h_time)) / {interval_seconds}) * {interval_seconds}) AS time,
+            AVG(val) AS val
+        FROM (
+            SELECT 
+                h_time,
+                val,
+                FLOOR(UNIX_TIMESTAMP(h_time) / {interval_seconds}) AS time_group
+            FROM dc_item_history_data_1497
+            WHERE item_name = :name
+              AND h_time BETWEEN :st AND :et
+              AND val IS NOT NULL
+        ) t
+        GROUP BY t.time_group
+        ORDER BY time
+    """)
+
+    try:
+        df = pd.read_sql(sql, engine, params={"name": name, "st": start, "et": end})
+        if not df.empty:
+            df['time'] = pd.to_datetime(df['time'])
+            # 确保时间戳是整分钟(去除秒和微秒)
+            df['time'] = df['time'].dt.floor('1min')
+            print(f"  ✓ {name}: {len(df)} 条记录")
+        return df
+    except Exception as e:
+        print(f"  ✗ {name} 查询失败: {str(e)}")
+        return pd.DataFrame()
+
+def fetch_special_data(sensor, start, end, boundary, engine_test, engine_prod):
+    """
+    专门获取步序数据,保持原始变化点
+    返回原始时间戳和值
+    """
+    sql = text("""
+               SELECT h_time AS time, val
+               FROM dc_item_history_data_1497
+               WHERE item_name = :name
+                 AND h_time BETWEEN :st
+                 AND :et
+                 AND val IS NOT NULL
+               ORDER BY h_time
+               """)
+
+    try:
+        # 根据边界时间选择合适的数据库引擎
+        if end <= boundary:
+            df = pd.read_sql(sql, engine_test, params={"name": sensor, "st": start, "et": end})
+        elif start >= boundary:
+            df = pd.read_sql(sql, engine_prod, params={"name": sensor, "st": start, "et": end})
+        else:
+            df1 = pd.read_sql(sql, engine_test, params={"name": sensor, "st": start, "et": boundary})
+            df2 = pd.read_sql(sql, engine_prod, params={"name": sensor, "st": boundary, "et": end})
+            df = pd.concat([df1, df2], ignore_index=True)
+
+        if not df.empty:
+            df['time'] = pd.to_datetime(df['time'])
+
+        return df
+    except Exception as e:
+        print(f"  ✗ {sensor} 查询失败: {str(e)}")
+        return pd.DataFrame()
+
+
+# ---------- 传感器数据查询函数(只获取聚合数据)----------
+# ---------- 传感器数据查询函数(只获取聚合数据)----------
+def fetch_sensor_data(sensor_names, start_time, end_time, boundary, engine_test, engine_prod):
+    """
+    获取多个传感器的分钟级聚合数据并合并为宽表
+    步序变量单独处理
+    """
+    # 识别步序变量和功率变量
+    step_vars = [v for v in sensor_names if 'STEP' in v]
+    power_vars = [v for v in sensor_names if 'POWER' in v]
+
+    # 需要特殊处理的变量
+    special_vars = step_vars + power_vars
+
+    # 其他连续变量
+    continuous_vars = [v for v in sensor_names if v not in special_vars]
+
+    print(f"\n识别到 {len(special_vars)} 个离散变量, {len(continuous_vars)} 个连续变量")
+
+    # 3. 创建完整的时间网格(整分钟)- 先创建
+    print(f"\n创建完整时间网格...")
+    time_grid = pd.date_range(
+        start=start_time.replace(second=0, microsecond=0),
+        end=end_time.replace(second=0, microsecond=0),
+        freq='1min'
+    )
+
+    # 创建以时间为索引的DataFrame
+    merged_df = pd.DataFrame(index=time_grid)
+    print(f"时间网格: {len(time_grid)} 个时间点")
+
+    # 1. 处理连续变量(按分钟聚合,时间对齐到整分钟)
+    if continuous_vars:
+        print("\n获取连续变量数据(分钟平均):")
+        for sensor in continuous_vars:
+            try:
+                # 根据边界时间选择合适的数据库引擎
+                if end_time <= boundary:
+                    df = fetch_valve_aggregated(sensor, start_time, end_time, engine_test)
+                elif start_time >= boundary:
+                    df = fetch_valve_aggregated(sensor, start_time, end_time, engine_prod)
+                else:
+                    df1 = fetch_valve_aggregated(sensor, start_time, boundary, engine_test)
+                    df2 = fetch_valve_aggregated(sensor, boundary, end_time, engine_prod)
+                    df = pd.concat([df1, df2], ignore_index=True)
+
+                if not df.empty:
+                    # 确保时间戳是整分钟并设为索引
+                    df['time'] = pd.to_datetime(df['time']).dt.floor('1min')
+                    df = df.drop_duplicates(subset=['time'], keep='first')
+                    df = df.set_index('time')
+
+                    # 添加到合并DataFrame
+                    merged_df[sensor] = df['val']
+                    print(f"  ✓ {sensor}: {len(df)} 条记录")
+                else:
+                    print(f"  ⚠ {sensor}: 无数据")
+
+            except Exception as e:
+                print(f"  ⚠ {sensor}: 处理失败 - {str(e)}")
+                continue
+
+    # 2. 处理离散变量(保持原始变化点)
+    if special_vars:
+        print("\n获取离散变量数据(原始变化点):")
+        for sensor in special_vars:
+            try:
+                # 获取原始数据(不聚合)
+                df = fetch_special_data(sensor, start_time, end_time, boundary, engine_test, engine_prod)
+
+                if not df.empty:
+                    # 创建分钟级的重采样
+                    df['time'] = pd.to_datetime(df['time'])
+                    df = df.set_index('time')
+
+                    # 重采样到分钟,对于离散变量使用前向填充
+                    df_resampled = df.resample('1min').ffill()
+
+                    # 添加到合并DataFrame
+                    merged_df[sensor] = df_resampled['val']
+                    print(f"  ✓ {sensor}: {len(df)} 个原始点 -> {len(df_resampled)} 个分钟点")
+                else:
+                    print(f"  ⚠ {sensor}: 无数据")
+
+            except Exception as e:
+                print(f"  ⚠ {sensor}: 处理失败 - {str(e)}")
+                continue
+
+    if merged_df.empty or len(merged_df.columns) == 0:
+        print("\n❌ 未获取到任何传感器数据")
+        return pd.DataFrame()
+
+    # 重置索引,将时间变为列
+    merged_df = merged_df.reset_index()
+    merged_df = merged_df.rename(columns={'index': 'time'})
+
+    print(f"\n合并完成,共 {len(merged_df)} 条时间记录 × {len(merged_df.columns) - 1} 个传感器")
+    print(f"数据框形状: {merged_df.shape}")
+
+    # 5. 删除黑名单时段
+    print("\n删除黑名单时段...")
+    original_len = len(merged_df)
+    for s, e in DELETE_PERIODS:
+        s = pd.to_datetime(s).floor('1min')
+        e = pd.to_datetime(e).floor('1min')
+        merged_df = merged_df[(merged_df['time'] < s) | (merged_df['time'] > e)]
+
+    deleted_count = original_len - len(merged_df)
+    if deleted_count > 0:
+        print(f"已删除 {deleted_count} 条黑名单时段数据")
+
+    return merged_df
+
+
+# ---------- 数据后处理函数(填充空值)----------
+def post_process_data(df, continuous_vars, step_vars):
+    """
+    对聚合后的数据进行后处理
+    连续变量:线性插值或前后填充
+    步序变量:已经前向填充,只需处理开头可能存在的空值
+    """
+    if df.empty:
+        print("警告:输入数据为空")
+        return df
+
+    df_processed = df.copy()
+    df_processed['time'] = pd.to_datetime(df_processed['time'])
+    df_processed = df_processed.set_index('time')
+
+    print(f"\n开始填充空值...")
+    print(f"数据时间范围: {df_processed.index.min()} 到 {df_processed.index.max()}")
+    print(f"数据行数: {len(df_processed)}")
+
+    missing_before = df_processed.isnull().sum().sum()
+    print(f"填充前空值数量: {missing_before}")
+
+    # 处理连续变量(使用线性插值更适合连续物理量)
+    for var in continuous_vars:
+        if var in df_processed.columns:
+            # 先线性插值,再前后填充边界
+            df_processed[var] = df_processed[var].interpolate(method='linear', limit_direction='both')
+
+    # 处理步序变量(可能开头有NaN,用后向填充)
+    for var in step_vars:
+        if var in df_processed.columns:
+            df_processed[var] = df_processed[var].bfill()  # 开头空值用第一个有效值填充
+
+    missing_after = df_processed.isnull().sum().sum()
+    print(f"填充后空值数量: {missing_after}")
+    print(f"填充了 {missing_before - missing_after} 个空值")
+
+    return df_processed.reset_index()
+
+
+# ---------- 数据分块保存函数 ----------
+def save_data_chunks(df, output_dir, prefix="sensor_data", chunk_size=200000):
+    """
+    将数据集分块保存,命名格式为 {prefix}_partX_of_Y.csv
+    """
+    total_rows = len(df)
+    num_chunks = (total_rows + chunk_size - 1) // chunk_size  # 向上取整
+
+    print(f"\n开始分块保存数据(共 {num_chunks} 个文件)...")
+
+    for i in range(num_chunks):
+        start_idx = i * chunk_size
+        end_idx = min((i + 1) * chunk_size, total_rows)
+        chunk_df = df.iloc[start_idx:end_idx]
+
+        if not chunk_df.empty:
+            file_path = os.path.join(
+                output_dir,
+                f"{prefix}_part{i + 1}_of_{num_chunks}.csv"
+            )
+            chunk_df.to_csv(file_path, index=False, encoding='utf_8_sig')
+            print(f"  ✓ 已保存第 {i + 1}/{num_chunks} 个文件: {os.path.basename(file_path)} ({len(chunk_df)} 行)")
+
+    return num_chunks
+
+
+# ---------- 主程序 ----------
+if __name__ == "__main__":
+    print("=" * 70)
+    print("传感器数据采集程序 - 分钟级聚合版本")
+    print("=" * 70)
+
+    # 1. 创建数据库引擎
+    print("\n[1/4] 创建数据库连接...")
+    engine_test, engine_prod = create_db_engines()
+    if engine_test is None or engine_prod is None:
+        print("❌ 数据库连接失败,程序退出")
+        exit(1)
+
+    # 2. 获取一分钟聚合数据
+    print(f"\n[2/4] 获取一分钟聚合数据...")
+    print(f"时间范围: {START_TIME} 到 {END_TIME}")
+
+    agg_df = fetch_sensor_data(
+        SENSOR_NAMES,
+        START_TIME,
+        END_TIME,
+        BOUNDARY,
+        engine_test,
+        engine_prod
+    )
+
+    if agg_df.empty:
+        print("\n❌ 未获取到任何聚合数据,程序退出")
+        exit(1)
+
+    print(f"\n✅ 聚合数据获取完成!")
+    print(f"总数据量: {len(agg_df)} 条记录")
+    print(f"时间范围: {agg_df['time'].min()} 到 {agg_df['time'].max()}")
+    print(f"时间间隔: 1分钟(整点)")
+    print(f"传感器数量: {len(agg_df.columns) - 1} 个")
+
+    # 识别步序变量
+    step_vars = [col for col in agg_df.columns if 'STEP' in col]
+    continuous_vars = [col for col in agg_df.columns if col != 'time' and col not in step_vars]
+
+    # 3. 后处理数据
+    print(f"\n[3/4] 后处理聚合数据...")
+    processed_df = post_process_data(agg_df, continuous_vars, step_vars)
+
+    print(f"\n✅ 后处理完成!")
+    print(f"处理后数据行数: {len(processed_df)}")
+
+    # 4. 保存数据(分块保存)
+    print(f"\n[4/4] 保存数据...")
+
+    # 直接使用分块保存,不再保存单个大文件
+    chunk_size = 200000  # 每块20万行
+    num_chunks = save_data_chunks(
+        processed_df,
+        PROCESSED_OUTPUT_DIR,
+        "uf_all_units_processed_1min",
+        chunk_size
+    )
+
+    print("\n" + "=" * 70)
+    print("✅ 所有任务完成!")
+    print("=" * 70)
+
+    # 显示文件信息
+    print(f"\n文件信息:")
+    print(f"输出目录: {PROCESSED_OUTPUT_DIR}")
+    print(f"文件数量: {num_chunks} 个")
+
+    # 计算总大小
+    total_size = 0
+    for i in range(num_chunks):
+        file_path = os.path.join(
+            PROCESSED_OUTPUT_DIR,
+            f"uf_all_units_processed_1min_part{i + 1}_of_{num_chunks}.csv"
+        )
+        if os.path.exists(file_path):
+            file_size = os.path.getsize(file_path) / (1024 * 1024)
+            total_size += file_size
+            print(f"  {os.path.basename(file_path)}: {file_size:.2f} MB")
+
+    print(f"总文件大小: {total_size:.2f} MB")

+ 160 - 0
algorithm/uf_rl/uf_data_process/filter.py

@@ -0,0 +1,160 @@
+import os
+from typing import List
+import numpy as np
+import pandas as pd
+import yaml
+
+# =============================
+#       过滤网络错误上传重复数据
+# =============================
+class ConstantFlowFilter:
+    """
+    过滤掉因网络异常导致的“连续若干行流量重复”的无效段。
+    """
+
+    def __init__(self, flow_col, repeat_len=20):
+        self.flow_col = flow_col
+        self.repeat_len = repeat_len
+
+    def filter(self, seg_df):
+        valid_segments = []
+
+        for sid, g in seg_df.groupby("segment_id"):
+            g = g.sort_values("time").reset_index(drop=True)
+
+            # 若小于 repeat_len,无需检查
+            if len(g) < self.repeat_len:
+                valid_segments.append(g)
+                continue
+
+            # 检查是否存在连续 repeat_len 行流量完全相同
+            has_constant_flow = False
+            for i in range(len(g) - self.repeat_len + 1):
+                window = g[self.flow_col].iloc[i:i + self.repeat_len]
+                if window.nunique() == 1:     # 流量全程不变
+                    has_constant_flow = True
+                    break
+
+            if not has_constant_flow:
+                valid_segments.append(g)
+
+        return valid_segments
+
+
+# =============================
+#       过滤过短事件
+# =============================
+class EventQualityFilter:
+    """
+    根据段落最基本质量进行过滤:
+    例如点数是否足够、事件类型是否单一等。
+    """
+
+    def __init__(self, min_points=40):
+        self.min_points = min_points
+
+    def filter(self, seg_df):
+        """
+        seg_df: pd.DataFrame 或者 list of pd.DataFrame
+        返回 list of pd.DataFrame,每个元素都是通过质量过滤的段
+        """
+        # 如果传入的是单个 DataFrame,就包装成 list
+        if isinstance(seg_df, pd.DataFrame):
+            seg_list = [seg_df]
+        elif isinstance(seg_df, list):
+            seg_list = seg_df
+        else:
+            raise ValueError(f"Unsupported type for seg_df: {type(seg_df)}")
+
+        valid = []
+        for df in seg_list:
+            for sid, g in df.groupby("segment_id"):
+                if len(g) >= self.min_points:
+                    valid.append(g)
+
+        return valid
+
+
+
+# =============================
+#       稳定进水数据提取
+# =============================
+class InletSegmentFilter:
+    def __init__(self, control_col, stable_codes, min_points=40):
+        self.control_col = control_col
+        self.stable_min, self.stable_max = stable_codes
+        self.min_points = min_points
+
+    def extract(self, seg_df):
+        """
+        seg_df: pd.DataFrame 或 list of pd.DataFrame
+        返回 list of pd.DataFrame,每个元素都是稳定段
+        """
+        # 如果传入的是单个 DataFrame,就包装成 list
+        if isinstance(seg_df, pd.DataFrame):
+            seg_list = [seg_df]
+        elif isinstance(seg_df, list):
+            seg_list = seg_df
+        else:
+            raise ValueError(f"Unsupported type for seg_df: {type(seg_df)}")
+
+        stable_segments = []
+        for df in seg_list:
+            for sid, group in df.groupby("segment_id"):
+                # 筛选出在稳定范围内的数据点
+                stable = group[
+                    (group[self.control_col] >= self.stable_min) &
+                    (group[self.control_col] <= self.stable_max)
+                    ]
+                # 如果满足最小点数要求,则保留
+                if len(stable) >= self.min_points:
+                    stable_segments.append(stable)
+        return stable_segments
+
+
+import pandas as pd
+import numpy as np
+
+
+class FlowOutlierFilter:
+    """
+    对稳定段进行异常值剔除
+    规则:flow 不在 mean ± n*std 范围内的行删除
+    """
+
+    def __init__(self, n_sigma=3):
+        self.n_sigma = n_sigma
+
+    def filter_segment(self, seg: pd.DataFrame, flow_col, prefix="flow"):
+
+        seg = seg.copy()
+
+        mean_col = f"{prefix}_mean"
+        std_col = f"{prefix}_std"
+
+        # 如果统计列不存在,直接返回
+        if mean_col not in seg.columns or std_col not in seg.columns:
+            return seg
+
+        mean_val = seg[mean_col].iloc[0]
+        std_val = seg[std_col].iloc[0]
+
+        lower = mean_val - self.n_sigma * std_val
+        upper = mean_val + self.n_sigma * std_val
+
+        seg = seg[(seg[flow_col] >= lower) & (seg[flow_col] <= upper)]
+
+        return seg
+
+    def filter_segments(self, segments, flow_col, prefix="flow"):
+
+        result = []
+
+        for seg in segments:
+            filtered = self.filter_segment(seg, flow_col, prefix)
+            result.append(filtered)
+
+        return result
+
+
+

+ 427 - 0
algorithm/uf_rl/uf_data_process/fit.py

@@ -0,0 +1,427 @@
+# cycle_analysis.py
+import numpy as np
+import pandas as pd
+from scipy.optimize import minimize, curve_fit
+from sklearn.metrics import r2_score
+from datetime import timedelta
+
+class ChemicalCycleSegmenter:
+    """
+    根据 event_type == 'bw_chem' 划分化学周期。
+    每两个化学反冲洗之间所有 inlet 稳定段属于一个化学周期。
+    若化学周期时间长度 > max_hours,则标记为无效周期。
+    """
+
+    def __init__(self, max_hours=60):
+        self.max_hours = max_hours
+
+    def assign_cycles(self, df_unit, stable_segments):
+        """
+        df_unit:含 time, event_type
+        stable_segments:列表,每段有 segment_id, time 等
+
+        返回 cycles: dict(cycle_id -> { segments:[], valid:True/False, start_time/end_time })
+        """
+
+        # 找出所有化学反冲洗的位置
+        ceb_times = df_unit.loc[df_unit["event_type"] == "bw_chem", "time"].sort_values().to_list()
+
+        cycles = {}
+        if len(ceb_times) < 2:
+            return cycles
+
+        for i in range(len(ceb_times) - 1):
+            cycle_id = i + 1
+            t_start = ceb_times[i]
+            t_end = ceb_times[i + 1]
+
+            # 该周期内的稳定 segment
+            segs = []
+            for seg in stable_segments:
+                if seg["time"].iloc[0] >= t_start and seg["time"].iloc[-1] <= t_end:
+                    segs.append(seg)
+
+            if len(segs) == 0:
+                continue
+
+            # 判断周期有效性(长度 <= max_hours)
+            hours = (t_end - t_start).total_seconds() / 3600
+            valid = hours <= self.max_hours
+
+            cycles[cycle_id] = {
+                "segments": segs,
+                "start": t_start,
+                "end": t_end,
+                "hours": hours,
+                "valid": valid
+            }
+
+            # 回写到段中
+            for seg in segs:
+                seg["chem_cycle_id"] = cycle_id
+                seg["chem_cycle_valid"] = valid
+
+        return cycles
+
+class ShortTermCycleFoulingFitter:
+    """
+    在一个化学周期内:
+    - 每个 segment 有独立的 R0 和 t
+    - 所有 segment 共享同一个短期污染速率 νK
+    """
+
+    def __init__(self, unit):
+        self.unit = unit
+
+    @staticmethod
+    def _is_invalid(x):
+        return (
+            x is None
+            or np.any(pd.isna(x))
+            or np.any(np.isinf(x))
+            or np.any(np.abs(x) > 1e20)
+        )
+
+    def fit_cycle(self, segments):
+        # ============================
+        # 1. 初步检查
+        # ============================
+        if not segments:
+            return np.nan, np.nan
+
+        R_col = f"{self.unit}_R_scaled"
+        J_col = f"{self.unit}_J"
+
+        # ============================
+        # 2. 预处理每个 segment
+        # ============================
+        seg_data = []
+
+        for seg in segments:
+            if not {"time", R_col, J_col}.issubset(seg.columns):
+                continue
+
+            seg = seg.sort_values("time").drop_duplicates(subset=["time"])
+            if len(seg) < 2:
+                continue
+
+            if not pd.api.types.is_datetime64_any_dtype(seg["time"]):
+                seg["time"] = pd.to_datetime(seg["time"], errors="coerce")
+            seg = seg.dropna(subset=["time"])
+            if len(seg) == 0:
+                # 该段完全无效,直接跳过
+                continue
+
+            # 局部时间(秒)
+            try:
+                t = (seg["time"] - seg["time"].iloc[0]).dt.total_seconds().astype(float)
+            except Exception:
+                continue
+
+            # 丢弃 t <= 0
+            mask = t > 0
+            if mask.sum() < 1:
+                continue
+
+            R = seg.loc[mask, R_col].astype(float).values
+            J = seg.loc[mask, J_col].astype(float).values
+            t = t[mask].values
+            R0 = float(seg[R_col].iloc[0])
+
+            # 数值与物理检查
+            if any(self._is_invalid(x) for x in [R, J, t, R0]):
+                continue
+            if np.all(J == 0):
+                continue
+
+            seg_data.append((R, J, t, R0))
+
+        if len(seg_data) == 0:
+            return np.nan, np.nan
+
+        # ============================
+        # 3. 定义 Huber 损失
+        # ============================
+        delta = 1.0
+
+        def huber_loss(res):
+            abs_r = np.abs(res)
+            quad = np.minimum(abs_r, delta)
+            lin = abs_r - quad
+            return 0.5 * quad**2 + delta * lin
+
+        # ============================
+        # 4. 定义 cycle 级 loss(共享 νK)
+        # ============================
+        def loss(nuk):
+            nuk = float(nuk[0])
+            total = 0.0
+
+            for R, J, t, R0 in seg_data:
+                pred = R0 + nuk * J * t
+                res = R - pred
+                total += np.sum(huber_loss(res))
+
+            return total
+
+        # ============================
+        # 5. 优化求解 νK
+        # ============================
+        try:
+            res = minimize(
+                loss,
+                x0=[1e-6],
+                bounds=[(0.0, None)],
+                options={"maxiter": 500, "disp": False}
+            )
+        except Exception:
+            return np.nan, np.nan
+
+        if not res.success:
+            return np.nan, np.nan
+
+        nuk_opt = float(res.x[0])
+
+        # ============================
+        # 6. 计算整体 R²(跨 segment)
+        # ============================
+        R_all = []
+        pred_all = []
+
+        for R, J, t, R0 in seg_data:
+            pred = R0 + nuk_opt * J * t
+            if self._is_invalid(pred):
+                continue
+            R_all.append(R)
+            pred_all.append(pred)
+
+        if not R_all:
+            return nuk_opt, np.nan
+
+        R_all = np.concatenate(R_all)
+        pred_all = np.concatenate(pred_all)
+
+        try:
+            r2 = r2_score(R_all, pred_all)
+        except Exception:
+            r2 = np.nan
+
+        return nuk_opt, r2
+
+
+
+class LongTermFoulingFitter:
+    """
+    长期 fouling 拟合:
+    - 使用每个 segment 起点的“稳健不可逆阻力估计”
+    - 拟合不可逆阻力随 cycle 内累计时间的增长
+    """
+
+    def __init__(self, unit, max_hours=60.0):
+        self.unit = unit
+        self.max_hours = max_hours
+
+    @staticmethod
+    def _power_law(t, a, b, R0):
+        return R0 + a * np.power(t, b)
+
+    @staticmethod
+    def _is_invalid(x):
+        return (
+            x is None
+            or np.any(pd.isna(x))
+            or np.any(np.isinf(x))
+            or np.any(np.abs(x) > 1e20)
+        )
+
+    def _robust_segment_R0(self, seg):
+        """
+        计算单个 segment 的稳健不可逆阻力:
+        1. 优先使用 post_bw_inlet == True 的行
+        2. 否则使用前 10 行
+        """
+        R_col = "R_scaled_start"
+
+        if "post_bw_inlet" in seg.columns:
+            mask = seg["post_bw_inlet"] == True
+            if mask.any():
+                R_vals = seg.loc[mask, R_col].astype(float).values
+                if not self._is_invalid(R_vals):
+                    return float(np.mean(R_vals))
+
+        # fallback:前 10 行
+        R_vals = seg[R_col].iloc[:10].astype(float).values
+        if not self._is_invalid(R_vals):
+            return float(np.mean(R_vals))
+
+        return None
+
+    def fit_cycle(self, segments):
+        # ============================
+        # 1. 空检查
+        # ============================
+        if not segments:
+            return np.nan, np.nan, np.nan
+
+        T_list = []
+        R_list = []
+
+        # cycle 起点
+        try:
+            cycle_start = min(seg["time"].iloc[0] for seg in segments)
+        except Exception:
+            return np.nan, np.nan, np.nan
+
+        R0_cycle = None
+
+        for seg in segments:
+            if "time" not in seg.columns or "R_scaled_start" not in seg.columns:
+                continue
+
+            seg = seg.sort_values("time")
+            if len(seg) < 1:
+                continue
+
+            t0 = seg["time"].iloc[0]
+
+            # ===== 稳健 R0 =====
+            R0 = self._robust_segment_R0(seg)
+            if R0 is None:
+                continue
+
+            # cycle 起点 R0
+            if t0 == cycle_start:
+                R0_cycle = R0
+
+            # 累计时间(小时)
+            T = (t0 - cycle_start).total_seconds() / 3600.0
+            if T < 0 or T > self.max_hours:
+                continue
+
+            if self._is_invalid(T) or self._is_invalid(R0):
+                continue
+
+            T_list.append(T)
+            R_list.append(R0)
+
+        # 至少 3 个点
+        if len(T_list) < 3 or R0_cycle is None:
+            return np.nan, np.nan, np.nan
+
+        T = np.asarray(T_list, dtype=float)
+        R = np.asarray(R_list, dtype=float)
+
+        if self._is_invalid(T) or self._is_invalid(R):
+            return np.nan, np.nan, np.nan
+
+        # ============================
+        # 2. 幂律拟合(b ∈ [0, 3])
+        # ============================
+        try:
+            popt, _ = curve_fit(
+                lambda tt, a, b: self._power_law(tt, a, b, R0_cycle),
+                T,
+                R,
+                p0=(0.1, 1.0),
+                bounds=([0.0, 0.0], [np.inf, 3.0]),
+                maxfev=8000
+            )
+            a, b = popt
+        except Exception:
+            return np.nan, np.nan, np.nan
+
+        if self._is_invalid(a) or self._is_invalid(b):
+            return np.nan, np.nan, np.nan
+
+        # ============================
+        # 3. 拟合质量
+        # ============================
+        pred = self._power_law(T, a, b, R0_cycle)
+        if self._is_invalid(pred):
+            return np.nan, np.nan, np.nan
+
+        try:
+            r2 = r2_score(R, pred)
+        except Exception:
+            r2 = np.nan
+
+        return a, b, r2
+
+
+class ChemicalBackwashCleaner:
+    """
+    根据有效化学周期计算 CEB 去除膜阻力:
+    ΔR = 上周期末 R_end - 下周期初 R_start
+    """
+
+    def __init__(self, unit):
+        self.unit = unit
+
+    def compute_removal(self, cycles):
+        ids = sorted(cycles.keys())
+
+        for i in range(len(ids) - 1):
+            cid1 = ids[i]
+            cid2 = ids[i + 1]
+
+            c1 = cycles[cid1]
+            c2 = cycles[cid2]
+
+            if not (c1["valid"] and c2["valid"]):
+                c1["R_removed"] = None
+                continue
+
+            # 上周期末:最后段的 R_end
+            seg_last = c1["segments"][-1]
+            R_end = seg_last[f"R_scaled_end"]
+
+            # 下周期初:第一段的 R_start
+            seg_first = c2["segments"][0]
+            R_start = seg_first[f"R_scaled_start"]
+
+            c1["R_removed"] = R_end - R_start
+
+        # 最后一个周期无 CEB 去除值
+        if ids:
+            cycles[ids[-1]]["R_removed"] = None
+
+        return cycles
+
+    def compute_dose(self, cycles, NaClO_col, HCl_col, NaOH_col):
+        ids = sorted(cycles.keys())
+
+        for i in range(len(ids) - 1):
+            cid1 = ids[i]
+            cid2 = ids[i + 1]
+
+            c1 = cycles[cid1]
+            c2 = cycles[cid2]
+
+            if not (c1["valid"] and c2["valid"]):
+                c1["R_removed"] = None
+                continue
+
+            # 上周期末:最后段的 R_end
+            seg_last = c1["segments"][-1]
+            NaClO_dose_end = seg_last[NaClO_col]
+            HCl_dose_end = seg_last[HCl_col]
+            NaOH_dose_end = seg_last[NaOH_col]
+
+            # 下周期初:第一段的 R_start
+            seg_first = c2["segments"][0]
+            NaClO_dose_start = seg_first[NaClO_col]
+            HCl_dose_start = seg_last[HCl_col]
+            NaOH_dose_start = seg_last[NaOH_col]
+
+            c1["NaClO_dose_removed"] = NaClO_dose_end - NaClO_dose_start
+            c1["HCl_dose_removed"] = HCl_dose_end - HCl_dose_start
+            c1["NaOH_dose_removed"] = NaOH_dose_end - NaOH_dose_start
+
+
+        # 最后一个周期无 CEB 去除值
+        if ids:
+            cycles[ids[-1]]["NaClO_dose_removed"] = None
+            cycles[ids[-1]]["HCl_dose_removed"] = None
+            cycles[ids[-1]]["NaOH_dose_removed"] = None
+
+        return cycles

+ 89 - 0
algorithm/uf_rl/uf_data_process/label.py

@@ -0,0 +1,89 @@
+import numpy as np
+import pandas as pd
+
+# =============================
+#     事件识别和划分
+# =============================
+class UFEventClassifier:
+    def __init__(self, unit_name, inlet_codes, physical_codes, chemical_codes, ctrl_col):
+        self.unit = unit_name
+        self.inlet_min, self.inlet_max = inlet_codes
+        self.physical_min, self.physical_max = physical_codes
+        self.chemical_min, self.chemical_max = chemical_codes
+        self.ctrl_col = ctrl_col
+
+    def classify(self, df):
+        df = df.copy()
+        df["event_type"] = "other"
+
+        df.loc[(df[self.ctrl_col] >= self.inlet_min) & (df[self.ctrl_col] <= self.inlet_max), "event_type"] = "inlet"
+        df.loc[(df[self.ctrl_col] >= self.physical_min) & (df[self.ctrl_col] <= self.physical_max), "event_type"] = "bw_phys"
+        df.loc[(df[self.ctrl_col] >= self.chemical_min) & (df[self.ctrl_col] <= self.chemical_max), "event_type"] = "bw_chem"
+
+        return df
+
+    def segment(self, df, inlet_types=None):
+        """
+        将连续的事件划分为段
+
+        Args:
+            df: 输入数据框
+            inlet_types: 被视为进水的事件类型列表,默认为 ["inlet", "other"]
+        """
+        if inlet_types is None:
+            inlet_types = ["inlet", "other"]  # 默认包括inlet和other
+
+        df = df.copy()
+        df["segment_id"] = np.nan
+        seg_id = 0
+        in_inlet = False
+
+        for i, evt in enumerate(df["event_type"]):
+            if evt in inlet_types:  # 使用可配置的事件类型列表
+                if not in_inlet:
+                    seg_id += 1
+                    in_inlet = True
+                df.loc[i, "segment_id"] = seg_id
+            else:
+                in_inlet = False
+
+        df = df[df["segment_id"].notna()].copy()
+        df["segment_id"] = df["segment_id"].astype(int)
+        return df
+
+
+class PostBackwashInletMarker:
+    """
+    标记反冲洗事件后的前 N 个进水点
+    """
+
+    def __init__(self, n_points=10):
+        self.n_points = n_points
+        self.label_col = "post_bw_inlet"  # 新标记列
+
+    def mark(self, df: pd.DataFrame) -> pd.DataFrame:
+        df = df.copy()
+
+        # 确保 event_type 清洗干净,避免 object array 卡死
+        df['event_type'] = (
+            df['event_type']
+            .astype(str)
+            .str.strip()
+            .fillna('')
+        )
+
+        df[self.label_col] = False
+
+        # 找出所有反冲洗事件索引
+        bw_idx = df.index[df['event_type'].isin(['bw_phys', 'bw_chem'])]
+
+        # 预先计算 inlet mask,避免多次 object-level 比较
+        inlet_mask = (df['event_type'] == 'inlet')
+
+        for idx in bw_idx:
+            # 只看 idx 之后的 inlet
+            candidate_idx = df.index[(df.index > idx) & inlet_mask]
+            post_idx = candidate_idx[: self.n_points]
+            df.loc[post_idx, self.label_col] = True
+
+        return df

+ 41 - 0
algorithm/uf_rl/uf_data_process/load.py

@@ -0,0 +1,41 @@
+import os
+import pandas as pd
+import yaml
+
+
+# =============================
+#        配置加载器
+# =============================
+class UFConfigLoader:
+    def __init__(self, config_path="config/uf_analyze_config.yaml"):
+        with open(config_path, "r", encoding="utf-8") as f:
+            self.cfg = yaml.safe_load(f)
+
+    @property
+    def uf(self):
+        return self.cfg["UF"]
+
+    @property
+    def params(self):
+        return self.cfg["Params"]
+
+    @property
+    def paths(self):
+        return self.cfg["Paths"]
+
+# =============================
+#        数据加载器
+# =============================
+class UFDataLoader:
+    def __init__(self, data_path: str):
+        self.data_path = data_path
+
+    def load_all_csv(self) -> pd.DataFrame:
+        """读取目录下所有 CSV 并合并成一个 DataFrame"""
+        files = [f for f in os.listdir(self.data_path) if f.endswith(".csv")]
+        dfs = [pd.read_csv(os.path.join(self.data_path, f), parse_dates=["time"]) for f in files]
+        return pd.concat(dfs, ignore_index=True)
+
+    def load_single_csv(self, file_name: str) -> pd.DataFrame:
+        """读取单个 CSV"""
+        return pd.read_csv(os.path.join(self.data_path, file_name), parse_dates=["time"])

+ 404 - 0
algorithm/uf_rl/uf_data_process/pipeline.py

@@ -0,0 +1,404 @@
+import os
+from typing import List, Dict
+
+import numpy as np
+import pandas as pd
+from pathlib import Path
+from load import UFConfigLoader, UFDataLoader
+from label import UFEventClassifier, PostBackwashInletMarker
+from filter import ConstantFlowFilter, EventQualityFilter, InletSegmentFilter,FlowOutlierFilter
+from calculate import UFResistanceCalculator, PumpPowerCalculator, VariableStabilityAnalyzer, UFResistanceAnalyzer, UFPressureAnalyzer
+from fit import ChemicalCycleSegmenter, ChemicalBackwashCleaner, ShortTermCycleFoulingFitter,LongTermFoulingFitter
+
+class UFAnalysisPipeline:
+    """
+    Pipeline:
+    - 支持四个机组逐机组标记事件并分段
+    - 过滤与稳定段提取
+    - 每段逐行计算膜阻力,并对多变量做稳定性分析
+    - 仅在 CEB 前后最近段均为有效稳定进水段时计算去除阻力
+    - 生成每段汇总表并保存
+    """
+
+    def __init__(self, cfg: 'UFConfigLoader'):
+        self.cfg = cfg
+        uf_cfg = cfg.uf
+        params = cfg.params
+        paths = cfg.paths
+
+        # 机组列表(优先从配置读取)
+        self.units = uf_cfg.get("units", ["UF1", "UF2", "UF3", "UF4"])
+        self.stable_codes = uf_cfg.get("stable_codes", "[24.0, 26.0]")
+
+        project_root = Path(paths["project_root"])
+        raw_data_path = project_root / paths["raw_data_path"]
+        output_path = project_root / paths["output_path"]
+        filter_output_path = project_root / paths["filter_output_path"]
+        self.loader = UFDataLoader(raw_data_path)
+        self.output_dir = output_path
+        self.filter_output_dir = filter_output_path
+        output_path.mkdir(parents=True, exist_ok=True)
+        filter_output_path.mkdir(parents=True, exist_ok=True)
+
+        # 列名配置
+        column_formats = uf_cfg.get("column_formats", {})
+        self.ctrl_format = column_formats.get("ctrl_col", "C.M.{unit}_DB@word_control")
+        self.flow_format = column_formats.get("flow_col", "C.M.{unit}_FT_JS@out")
+        self.tmp_format = column_formats.get("tmp_col", "C.M.{unit}_DB@press_PV")
+        self.BWB_POWER_format = column_formats.get("BWB_POWER_col", "ns=3;s=ZZ_{unit}#UFBWB_POWER")
+        self.GSB_POWER_col = column_formats.get("GSB_POWER_col", "ns=3;s=ZZ_UFGSB_POWER")
+        self.temp_col = column_formats.get("temp_col", "C.M.RO_TT_ZJS@out")
+        self.orp_col = column_formats.get("orp_col", "C.M.UF_ORP_ZCS@out")
+        self.NaClO_col = column_formats.get("NaClO_col", "ns=3;s=CN_LEVEL_O")
+        self.HCl_col = column_formats.get("HCl_col", "ns=3;s=S_LEVEL_O")
+        self.NaOH_col = column_formats.get("NaOH_col", "ns=3;s=J_LEVEL_O")
+
+        # 过滤器
+        self.min_points = params.get("min_points", 20)
+        self.initial_points = params.get("initial_points", 10)
+
+        self.quality_filter = EventQualityFilter(min_points=self.min_points)
+        self.flow_filter = FlowOutlierFilter(n_sigma=3)
+        self.initial_label = PostBackwashInletMarker(n_points=self.initial_points)
+
+        # 阻力计算器
+        self.res_calc = UFResistanceCalculator(self.units, area_m2=uf_cfg["area_m2"], scale_factor=params.get("scale_factor", 1e10))
+        self.segment_head_n = params.get("segment_head_n", 10)
+        self.segment_tail_n = params.get("segment_tail_n", 10)
+
+        # 功率计算器
+        self.power_calc = PumpPowerCalculator(event_col=column_formats.get("event_col", "event_type"))
+
+
+    # ----------------------------
+    # 加载所有 CSV 并合并
+    # ----------------------------
+
+    def load_all(self) -> pd.DataFrame:
+        print(f"[INFO] Loading raw data from: {self.loader.data_path}")
+        df = self.loader.load_all_csv()
+
+        if df.empty:
+            print("[WARN] No CSV files found!")
+        return df
+
+    # ----------------------------
+    # 多变量稳定性检查(针对单个 segment)
+    # 返回每个变量的稳定性 bool 与总体稳定 bool(所有变量均稳定)
+    # ----------------------------
+    def analyze_variables_stability(self, seg: pd.DataFrame, flow_col: str) -> Dict[str, bool]:
+        cols = {
+            "flow": flow_col,
+            "temp": self.temp_col,
+            "orp": self.orp_col,
+        }
+        results = {}
+        for name, col in cols.items():
+            if col not in seg.columns:
+                results[name] = False
+                continue
+            series = seg[col].dropna()
+            if len(series) < 2:
+                results[name] = False
+                continue
+            std = float(series.std())
+            x = np.arange(len(series))
+            try:
+                slope = float(np.polyfit(x, series.values, 1)[0])
+            except Exception:
+                slope = np.inf
+            results[name] = (std <= self.var_max_std) and (abs(slope) <= self.var_max_slope)
+        results["all_stable"] = all(results[k] for k in ["flow", "temp", "orp", "cond"])
+        return results
+
+    # ----------------------------
+    # 从 stable_segments 列表计算每段的 R_start/R_end(按机组)
+    # 返回 seg_summary DataFrame(每行对应一个 stable inlet segment)
+    # ----------------------------
+    def summarize_R_for_unit(self, stable_segments: List[pd.DataFrame], unit: str) -> pd.DataFrame:
+        rows = []
+        if unit in ["1", "2", "3", "4"] or unit in [1, 2, 3, 4]:  # 处理字符串和数字
+            res_col = f"UF{unit}_R"
+        else:
+            res_col = f"{unit}_R"
+        for seg in stable_segments:
+            seg = seg.sort_values("time").reset_index(drop=True)
+            if len(seg) < (self.segment_head_n + self.segment_tail_n):
+                # 跳过过短的
+                continue
+            R_start = seg[res_col].iloc[:self.segment_head_n].median()
+            R_end = seg[res_col].iloc[-self.segment_tail_n:].median()
+            rows.append({
+                "unit": unit,
+                "segment_id": int(seg["segment_id"].iloc[0]),
+                "start_time": seg["time"].iloc[0],
+                "end_time": seg["time"].iloc[-1],
+                "R_start": float(R_start),
+                "R_end": float(R_end),
+                "n_points": len(seg)
+            })
+        return pd.DataFrame(rows)
+
+    # ----------------------------
+    # 主运行逻辑
+    # ----------------------------
+    def run(self):
+        df = self.load_all()
+        if df.empty:
+            raise ValueError("未找到原始数据或数据为空")
+
+        df = df.sort_values("time").reset_index(drop=True)
+        df = df.ffill().bfill()
+        cols_to_convert = df.columns.difference(["time"])
+        df[cols_to_convert] = df[cols_to_convert].apply(pd.to_numeric, errors='coerce')
+        df[cols_to_convert] = df[cols_to_convert].ffill().bfill()
+
+        # 为安全:确保 time 列为 datetime
+        if not np.issubdtype(df["time"].dtype, np.datetime64):
+            df["time"] = pd.to_datetime(df["time"])
+
+        # 只保留 2025-06-11 及之后的数据
+        start_date = pd.Timestamp("2025-06-10")
+        df = df[df["time"] >= start_date].reset_index(drop=True)
+
+        # 逐机组处理
+        for unit in self.units:
+            print(f"Processing {unit} ...")
+            ctrl_col = self.ctrl_format.format(unit=unit)
+            flow_col = self.flow_format.format(unit=unit)
+            tmp_col = self.tmp_format.format(unit=unit)
+            BWB_POWER_col = self.BWB_POWER_format.format(unit=unit)
+
+            # 去除无关列
+            other_units = [u for u in self.units if u != unit]
+            cols_to_drop = [col for col in df.columns if any(ou in col for ou in other_units)]
+            df_unit = df.drop(columns=cols_to_drop)
+
+            # 逐机组事件识别:使用 UFEventClassifier(实例化单机组)
+            event_clf = UFEventClassifier(unit, self.cfg.uf["inlet_codes"],
+                                          self.cfg.uf["physical_bw_code"], self.cfg.uf["chemical_bw_code"],
+                                          ctrl_col)
+            df_unit = event_clf.classify(df_unit)  # 产生 event_type 列
+            df_unit_mark = self.initial_label.mark(df_unit) # 标记反冲洗事件后的前 N 个进水点
+            seg_df = event_clf.segment(df_unit_mark) # 根据 event_type 列编号事件段落
+
+            # 对 seg_df 进行按 segment 分组后逐段过滤:
+            const_flow_filter = ConstantFlowFilter(flow_col=flow_col, repeat_len=20)
+            segments = const_flow_filter.filter(seg_df) # 去除出现网络错误的进水段
+            segments = self.quality_filter.filter(segments) # 去除时间过短的进水段
+
+            # -----------------------------
+            # 逐段计算本段的进水泵平均功率与反洗泵功率
+            # -----------------------------
+            segments = self.power_calc.calculate_for_segments(segments,inlet_power_col=self.GSB_POWER_col,bw_power_col=BWB_POWER_col)
+
+            # 提取稳定进水段
+            stable_extractor = InletSegmentFilter(ctrl_col,stable_codes=self.stable_codes,min_points=self.min_points)
+            stable_segments = stable_extractor.extract(segments)  # 提取稳定进水数据
+
+            # 若无稳定进水段,则记录不稳定段并跳过该机组
+            if len(stable_segments) == 0:
+                print(f"  No stable segments found for {unit}")
+                continue
+
+            # -----------------------------
+            # 逐段计算膜阻力
+            # -----------------------------
+            stable_segments = self.res_calc.calculate_for_segments(
+                stable_segments,
+                temp_col=self.temp_col,
+                flow_col=flow_col,
+                tmp_col=tmp_col,
+            )
+
+
+            # -----------------------------
+            # 逐段计算 起止跨膜压差tmp_start/tmp_end 放缩后的R_start/R_end 进水变量稳定性
+            # -----------------------------
+
+            # 变量稳定性分析
+            vsa = VariableStabilityAnalyzer()
+            stable_segments = vsa.analyze_segments(stable_segments, col=flow_col, prefix="flow")
+            stable_segments = vsa.analyze_segments(stable_segments, col=self.temp_col, prefix="temp")
+            stable_segments = self.flow_filter.filter_segments(stable_segments,flow_col=flow_col,prefix="flow" )
+
+            # 跨膜压差统计
+            upa = UFPressureAnalyzer(
+                tmp_col=tmp_col,
+                head_n=self.segment_head_n,
+                tail_n=self.segment_tail_n,
+            )
+
+            stable_segments = upa.analyze_segments(stable_segments)
+
+            # 膜阻力统计
+            res_scaled_col = f"{unit}_R_scaled"
+            ura = UFResistanceAnalyzer(
+                resistance_col=res_scaled_col,
+                head_n=self.segment_head_n,
+                tail_n=self.segment_tail_n
+            )
+            stable_segments = ura.analyze_segments(stable_segments)
+
+            # ----------------------------------
+            # 1. 化学周期划分
+            # ----------------------------------
+            cycle_segmenter = ChemicalCycleSegmenter(max_hours = 100)
+            cycles = cycle_segmenter.assign_cycles(df_unit, stable_segments)
+
+            # ----------------------------------
+            # 2. 每个周期拟合短期污染 nuK
+            # ----------------------------------
+            stf = ShortTermCycleFoulingFitter(unit)
+            for cid, cycle in cycles.items():
+                if not cycle["valid"]:
+                    cycle["nuk"] = None
+                    cycle["nuk_r2"] = None
+                    continue
+
+                nuk, r2 = stf.fit_cycle(cycle["segments"])
+                cycle["nuk"] = nuk
+                cycle["nuk_r2"] = r2
+
+                for seg in cycle["segments"]:
+                    seg["cycle_nuK"] = nuk
+                    seg["cycle_nuK_R2"] = r2
+
+            # ----------------------------------
+            # 3. 长期污染模型幂律拟合
+            # ----------------------------------
+            ltf = LongTermFoulingFitter(unit)
+            for cid, cycle in cycles.items():
+                if not cycle["valid"]:
+                    cycle["a"] = None
+                    cycle["b"] = None
+                    cycle["lt_r2"] = None
+                    continue
+
+                a, b, r2 = ltf.fit_cycle(cycle["segments"])
+                cycle["a"] = a
+                cycle["b"] = b
+                cycle["lt_r2"] = r2
+
+                for seg in cycle["segments"]:
+                    seg["cycle_long_a"] = a
+                    seg["cycle_long_b"] = b
+                    seg["cycle_long_r2"] = r2
+
+            # ----------------------------------
+            # 4. 计算化学反冲洗去除膜阻力及耗药量
+            # ----------------------------------
+            cbc = ChemicalBackwashCleaner(unit)
+            cycles = cbc.compute_removal(cycles)
+            cycles = cbc.compute_dose(cycles, self.NaClO_col, self.HCl_col, self.NaOH_col)
+
+            # 回写到稳定段
+            for cid, cycle in cycles.items():
+                R_removed = cycle["R_removed"]
+                for seg in cycle["segments"]:
+                    seg["cycle_R_removed"] = R_removed
+
+            # -----------------------------
+            # 记录所有稳定进水段信息(stable_segments 仅包含稳定段)
+            # -----------------------------
+            all_inlet_segment_rows = []
+
+            for seg in stable_segments:  # seg 是一个 DataFrame
+                seg_id = int(seg["segment_id"].iloc[0])
+
+                row = {
+                    "seg_id": seg_id,
+                    "start_time": seg["time"].iloc[0],
+                    "end_time": seg["time"].iloc[-1],
+                }
+
+                # 添加跨膜压差、膜阻力与变量波动性分析列
+                row.update({
+                    "tmp_start": seg["tmp_start"].iloc[0] if "tmp_start" in seg.columns else None,
+                    "tmp_end": seg["tmp_end"].iloc[-1] if "tmp_end" in seg.columns else None,
+
+                    "R_scaled_start": seg["R_scaled_start"].iloc[0] if "R_scaled_start" in seg.columns else None,
+                    "R_scaled_end": seg["R_scaled_end"].iloc[-1] if "R_scaled_end" in seg.columns else None,
+
+                    "flow_mean": seg["flow_mean"].iloc[0] if "flow_mean" in seg.columns else None,
+                    "flow_std": seg["flow_std"].iloc[0] if "flow_std" in seg.columns else None,
+                    "flow_cv": seg["flow_cv"].iloc[0] if "flow_cv" in seg.columns else None,
+
+                    "temp_mean": seg["temp_mean"].iloc[0] if "temp_mean" in seg.columns else None,
+                    "temp_std": seg["temp_std"].iloc[0] if "temp_std" in seg.columns else None,
+                    "temp_cv": seg["temp_cv"].iloc[0] if "temp_cv" in seg.columns else None,
+                })
+
+                # === 将化学周期字段写入 row ===
+                chem_cols = [col for col in seg.columns if col.startswith("chem_cycle_")]
+                for col in chem_cols:
+                    row[col] = seg[col].iloc[0]
+
+                # === 将 nuK、长期污染参数、CEB 去除阻力写入 row ===
+                cycle_cols = [col for col in seg.columns if col.startswith("cycle_")]
+                for col in cycle_cols:
+                    row[col] = seg[col].iloc[0]
+
+                all_inlet_segment_rows.append(row)
+
+            # -----------------------------
+            # 最终构建 DataFrame
+            # -----------------------------
+            df_segments = pd.DataFrame(all_inlet_segment_rows)
+
+            # 输出文件名:unit_xxx_segments.csv
+            filename = f"{unit}_segments.csv"
+            output_path = os.path.join(self.output_dir, filename)
+
+            df_segments.to_csv(output_path, index=False)
+            print(f"Saved inlet segment data to: {output_path}")
+
+            # ======================================================
+            # 扩展功能:化学周期过滤与导出(基于 chem_cycle_id)
+            # ======================================================
+
+            print(f">>> Post-filtering chemical cycles for unit {unit} ...")
+
+            # 1. 仅保留有化学周期标识的段
+            if "chem_cycle_id" not in df_segments.columns:
+                print(f"No chem_cycle_id found for unit {unit}, skipping cycle filtering.")
+            else:
+                df_valid = df_segments.dropna(subset=["chem_cycle_id", "cycle_nuK_R2", "cycle_long_r2"]).copy()
+                df_valid["chem_cycle_id"] = df_valid["chem_cycle_id"].astype(int)
+
+                # 2. 根据 R2 过滤化学周期
+                grouped = df_valid.groupby("chem_cycle_id")
+
+                valid_cycle_ids = []
+                for cid, g in grouped:
+                    # 同一化学周期的 R2 相同(周期级拟合),用第一行即可
+                    nuk_r2 = g["cycle_nuK_R2"].iloc[0] if "cycle_nuK_R2" in g.columns else None
+                    long_r2 = g["cycle_long_r2"].iloc[0] if "cycle_long_r2" in g.columns else None
+
+                    if nuk_r2 is not None and long_r2 is not None:
+                        if nuk_r2 > 0.4 and long_r2 > 0.4:
+                            valid_cycle_ids.append(cid)
+
+                # 3. 过滤后的化学周期数据
+                df_cycles_kept = df_valid[df_valid["chem_cycle_id"].isin(valid_cycle_ids)]
+
+                # 4. 保存过滤结果
+                cycle_filename = f"{unit}_filtered_cycles.csv"
+                cycle_output_path = os.path.join(self.filter_output_dir, cycle_filename)
+
+                df_cycles_kept.to_csv(cycle_output_path, index=False)
+                print(f"Saved filtered chemical cycles to: {cycle_output_path}")
+
+                # 5. 统计保留行占比
+                total_rows = len(df_valid)
+                kept_rows = len(df_cycles_kept)
+                pct = kept_rows / total_rows * 100 if total_rows > 0 else 0
+
+                print(
+                    f"Unit {unit}: kept {kept_rows}/{total_rows} rows "
+                    f"({pct:.2f} percent) after cycle R2 filtering."
+                )
+
+
+
+

+ 93 - 0
algorithm/uf_rl/uf_data_process/plot.py

@@ -0,0 +1,93 @@
+import os
+import glob
+import pandas as pd
+import numpy as np
+import matplotlib.pyplot as plt
+from matplotlib.font_manager import FontProperties
+
+# ===================== 配置 =====================
+data_dir = r"/datasets/UF_longting_data/processed\segments"
+target_col = "cycle_long_r2"
+
+# ===================== 中文字体设置 =====================
+# 注意:这里使用 SimHei 字体,可显示中文
+font = FontProperties(fname=r"C:\Windows\Fonts\simhei.ttf", size=12)
+
+# ===================== 读取所有 CSV =====================
+all_files = glob.glob(os.path.join(data_dir, "*.csv"))
+
+values = []
+
+for file in all_files:
+    try:
+        df = pd.read_csv(file)
+        if target_col in df.columns:
+            vals = df[target_col].dropna().values
+            values.append(vals)
+    except Exception as e:
+        print(f"读取失败: {file}, 错误: {e}")
+
+# 合并所有数据
+if len(values) == 0:
+    raise ValueError("未在任何 CSV 中找到有效的 cycle_long_R2 数据")
+
+data = np.concatenate(values)
+total_count = len(data)
+
+# ===================== 定义区间 =====================
+bins = [
+    -np.inf,
+    0.0,
+    0.5,
+    0.6,
+    0.7,
+    0.8,
+    0.9,
+    1.0
+]
+
+labels = [
+    "<0",
+    "0 – 0.5",
+    "0.5 – 0.6",
+    "0.6 – 0.7",
+    "0.7 – 0.8",
+    "0.8 – 0.9",
+    "0.9 – 1.0"
+]
+
+# ===================== 统计分布 =====================
+counts = pd.cut(
+    data,
+    bins=bins,
+    labels=labels,
+    right=True,
+    include_lowest=True
+).value_counts().sort_index()
+
+ratios = counts / total_count * 100
+
+# ===================== 输出结果 =====================
+result = pd.DataFrame({
+    "样本数": counts,
+    "占比 (%)": ratios.round(2)
+})
+
+print(f"\n总样本数: {total_count}\n")
+print(result)
+
+# ===================== 绘制柱状图 =====================
+plt.figure(figsize=(10, 6))
+plt.bar(labels, ratios, color='skyblue', edgecolor='black')
+plt.title("cycle_long_R2 数据分布柱状图", fontproperties=font)
+plt.xlabel("区间", fontproperties=font)
+plt.ylabel("占比 (%)", fontproperties=font)
+plt.ylim(0, 100)
+plt.grid(axis='y', linestyle='--', alpha=0.7)
+
+# 在柱子上显示百分比
+for i, v in enumerate(ratios):
+    plt.text(i, v + 1, f"{v:.1f}%", ha='center', va='bottom', fontsize=10, fontproperties=font)
+
+plt.tight_layout()
+plt.show()

+ 34 - 0
algorithm/uf_rl/uf_data_process/run_ufdata_pipeline.py

@@ -0,0 +1,34 @@
+import os
+import sys
+from pathlib import Path
+
+THIS_FILE = Path(__file__).resolve()
+UF_RL_ROOT = THIS_FILE.parents[1]
+
+from load import UFConfigLoader
+from pipeline import UFAnalysisPipeline
+
+
+def main():
+    print("=====================================")
+    print("    UF Multi-Unit Analysis Pipeline")
+    print("=====================================")
+
+    # 1. 加载配置文件
+    cfg = UFConfigLoader(CONFIG_PATH)
+
+    # 2. 创建 pipeline
+    pipeline = UFAnalysisPipeline(cfg)
+
+    # 3. 执行 pipeline
+    print(">>> Running pipeline ...")
+    results = pipeline.run()
+
+    print("=========================================")
+    print("Pipeline finished. Results saved.")
+    print("=========================================")
+
+
+if __name__ == "__main__":
+    CONFIG_PATH = UF_RL_ROOT / "yancheng" / "uf_analyze_config.yaml"
+    main()

+ 168 - 0
algorithm/uf_rl/uf_data_process/水厂变量名称记录.md

@@ -0,0 +1,168 @@
+# 各水厂OPC UA变量配置总览
+
+## 当前缺失变量:
+> 龙亭等新水岛供水泵功率命名均为总功率,需确认是否为所有超滤机组的总用电,且需要确认功率单位
+> 
+> 盐城:ns=3;s=ZZ_UFGSB_POWER命名为UF供水泵相电压总功率,需确认是否表示超滤供水泵功率
+> 
+> 
+## 龙亭新水岛
+> **机组数量**:2台
+> 
+> **数据库id**:1450
+> 
+> **可用数据开始时间**:2025-6-11
+
+### UF机组变量
+```python
+UNITS = [1, 2]
+
+BASE_VARIABLES = [
+    "ns=3;s={}#UF_JSFLOW_O",      # 进水流量
+    "ns=3;s={}#UF_JSPRESS_O",     # 进水压力
+    "ns=3;s=UF{}_SSD_KMYC",       # 跨膜压差
+    "ns=3;s=UF{}_STEP",           # 步序/控制字
+    "ns=3;s=ZZ_{}#UFBWB_POWER", # 反洗泵功率
+]
+SYSTEM_VARIABLES = [
+    "ns=3;s=ZJS_TEMP_O",          # 进水温度
+    "ns=3;s=RO_JSORP_O",          # 总产水ORP
+    "ns=3;s=RO_JSPH_O",           # 总产水PH
+    "ns=3;s=RO_JSDD_O",           # 总产水电导
+    "ns=3;s=CN_LEVEL_O", # 次钠液位
+    "ns=3;s=S_LEVEL_O", # 酸液位
+    "ns=3;s=J_LEVEL_O", # 碱液位
+    "ns=3;s=ZZ_UFGSB_POWER", # 超滤供水泵功率
+    
+]
+```
+
+## 兰考新水岛
+> **机组数量**:2台
+> 
+> **数据库id**:1451
+> 
+> **数据开始时间**:2025-09-04
+
+### UF机组变量
+```python
+UNITS = [1, 2]
+
+BASE_VARIABLES = [
+    "ns=3;s={}#UF_JSFLOW_O",      # 进水流量
+    "ns=3;s={}#UF_JSPRESS_O",     # 进水压力
+    "ns=3;s=UF{}_SSD_KMYC",       # 跨膜压差
+    "ns=3;s=UF{}_STEP",           # 步序/控制字
+    "ns=3;s=ZZ_{}#UFBWB_POWER", # 反洗泵功率
+]
+SYSTEM_VARIABLES = [
+    "ns=3;s=ZJS_TEMP_O",          # 进水温度
+    "ns=3;s=RO_JSORP_O",          # 总产水ORP
+    "ns=3;s=RO_JSPH_O",           # 总产水PH
+    "ns=3;s=RO_JSDD_O",           # 总产水电导
+    "ns=3;s=CN_LEVEL_O", # 次钠液位
+    "ns=3;s=S_LEVEL_O", # 酸液位
+    "ns=3;s=J_LEVEL_O", # 碱液位
+    "ns=3;s=ZZ_UFGSB_POWER", # 超滤供水泵功率
+    
+]
+```
+
+
+
+## 安镇
+> **机组数量**:2台
+> 
+> **数据库id**:1181
+> 
+> **数据开始时间**:2025-06-10
+
+### UF机组变量
+```python
+UNITS = [1, 2]
+
+BASE_VARIABLES = [
+    "AR.{}#UF_JSFLOW_O",      # 进水流量
+    "AR.{}#UF_JSPRESS_O",     # 进水压力
+    "AR.UF{}_SSD_KMYC",       # 跨膜压差
+    "AR.UF{}_STEP",           # 步序/控制字
+    "AR.ZZ_{}#UFBWB_POWER" # 反洗泵功率
+]
+SYSTEM_VARIABLES = [
+    "AR.ZJS_TEMP_O",          # 进水温度
+    "AR.RO_JSORP_O",          # 总产水ORP
+    "AR.RO_JSPH_O",           # 总产水PH
+    "AR.RO_JSDD_O",           # 总产水电导
+    "AR.CN_LEVEL_O",       # 次钠液位
+    "AR.S_LEVEL_O",           # 酸液位
+    "AR.J_LEVEL_O",           # 碱液位
+    "AR.ZZ_UFGSB_POWER", # 超滤供水泵功率
+]
+```
+
+
+## 盐城
+> **机组数量**:2台
+> 
+> **数据库id**:1497
+> 
+> **数据开始时间**:2026-01-22
+
+### UF机组变量
+```python
+UNITS = [1, 2]
+
+BASE_VARIABLES = [
+    "ns=3;s={}#UF_JSFLOW_O",      # 进水流量
+    "ns=3;s={}#UF_JSPRESS_O",     # 进水压力
+    "ns=3;s=UF{}_SSD_KMYC",       # 跨膜压差
+    "ns=3;s=UF{}_STEP",           # 步序/控制字
+    "ns=3;s=ZZ_{}#UFBWB_POWER" # 反洗泵功率
+]
+SYSTEM_VARIABLES = [
+    "ns=3;s=ZJS_TEMP_O",          # 进水温度
+    "ns=3;s=RO_JSORP_O",          # 总产水ORP
+    "ns=3;s=RO_JSPH_O",           # 总产水PH
+    "ns=3;s=RO_JSDD_O",           # 总产水电导
+    "ns=3;s=CN_LEVEL_O", # 次钠液位
+    "ns=3;s=S_LEVEL_O", # 酸液位
+    "ns=3;s=J_LEVEL_O", # 碱液位
+    "ns=3;s=ZZ_UFGSB_POWER", # 超滤供水泵功率
+]
+```
+
+
+## 锡山中荷
+> **系统类型**:超滤+反渗透系统
+> 
+> **机组数量**:4台
+> 
+> **数据库id**:92
+> 
+> **数据开始时间**:2024-03-01
+
+### UF机组变量
+```python
+UNITS = [1, 2, 3, 4]
+
+BASE_VARIABLES = [
+    "C.M.UF{}_FT_JS@out",  # 进水流量
+    "C.M.UF{}_PT_JS@out",  # 进水压力(如有)
+    "C.M.UF{}_DB@press_PV",  # 跨膜压差(如有)
+    "C.M.UF{}_DB@word_control" # 步序/控制字
+]
+
+SYSTEM_VARIABLES = [
+    "C.M.RO_TT_ZJS@out", # 进水温度
+    "C.M.UF_ORP_ZCS@out",  # 总产水ORP
+    "C.M.UF_PH_ZCS@out",  # 总产水PH
+    "C.M.RO_Cond_ZJS@out" # 总产水电导
+    "C.M.LT_NaClO@out",
+    "C.M.LT_HCl@out",  # 酸液位
+    "C.M.LT_NaOH@out"
+    "WG_A.1AA1-1.A13"  # 超滤供水泵A功率
+    "WG_A.1AA1-2.A13"  # 超滤供水泵B功率
+    "WG_A.1AA2-1.A13"  # 超滤反洗泵A功率
+    "WG_A.1AA1-2.A13"  # 超滤反洗泵B功率
+]
+```

+ 161 - 0
algorithm/uf_rl/进水动作版超滤训练源码/UF_resistance_models.py

@@ -0,0 +1,161 @@
+"""
+超滤膜阻力模型模块
+====================
+本模块定义了超滤膜阻力的动态变化模型,包括:
+1. ResistanceIncreaseModel: 过滤阶段膜阻力上升模型
+2. ResistanceDecreaseModel: 反洗阶段膜阻力下降模型
+
+这些模型用于模拟超滤膜在运行过程中的阻力变化,是强化学习环境的核心组件。
+"""
+
+import torch
+import numpy as np
+
+
+# ==================== 膜阻力上升模型 ====================
+class ResistanceIncreaseModel(torch.nn.Module):
+    """
+    过滤阶段膜阻力上升模型
+    
+    功能说明:
+    - 计算在过滤阶段膜阻力的增长量 ΔR
+    - 膜阻力上升主要由污染物在膜表面的累积引起
+    - 阻力增长速率与膜通量(J)和过滤时长(L_s)相关
+    
+    模型公式:
+        ΔR = nuK × J × L_s
+        其中:
+        - nuK: 膜阻力增长系数(反映水质污染特性)
+        - J: 膜通量 = q_UF / A / 3600 [m/s]
+        - L_s: 过滤时长 [秒]
+    """
+    
+    def __init__(self):
+        """初始化膜阻力上升模型(无需训练参数)"""
+        super().__init__()
+
+    def forward(self, p, L_s):
+        """
+        前向传播:计算膜阻力上升量
+        
+        参数:
+            p (UFParams): 超滤运行参数对象,包含:
+                - q_UF: 过滤进水流量 [m³/h]
+                - nuK: 膜阻力增长系数 [m⁻¹/s]
+            L_s (float): 过滤时长 [秒]
+        
+        返回:
+            float: 膜阻力上升量 ΔR(已缩放1e10)
+        
+        注意:
+            - 实际膜阻力量级为1e12,为便于数值计算已缩放至1e2量级
+            - 膜面积 A = 128组 × 40 m²/组 = 5120 m²
+        """
+        # 计算膜有效面积(锡山水厂配置:128组膜,每组40m²)
+        A = 128 * 40.0  # [m²]
+        
+        # 计算膜通量 J = 流量 / 面积 / 时间单位转换
+        # q_UF [m³/h] → J [m³/(m²·s)]
+        J = p.q_UF / A / 3600  # [m/s]
+        
+        # 膜阻力上升模型(线性模型,已缩放)
+        # nuK: 阻力增长速率,反映水质污染特性
+        # J: 膜通量,通量越大污染速率越快
+        # L_s: 过滤时间,时间越长累积污染越多
+        dR = p.nuK * J * L_s  # [缩放后的阻力单位]
+        
+        return float(dR)
+
+
+# ==================== 膜阻力下降模型 ====================
+class ResistanceDecreaseModel(torch.nn.Module):
+    """
+    反洗阶段膜阻力下降模型
+    
+    功能说明:
+    - 计算物理反冲洗能够去除的膜阻力量
+    - 区分可逆污染和不可逆污染
+    - 反洗时长影响去除效率
+    
+    模型原理:
+    1. 膜污染分为两类:
+       - 可逆污染:可通过物理反洗去除(如表面颗粒物)
+       - 不可逆污染:无法通过物理反洗去除(如孔内吸附污染)
+    
+    2. 不可逆污染累积模型:
+       R_irr = R0 + slope × t^power
+       其中 t 为累积运行时间
+    
+    3. 反洗效率模型:
+       time_gain = 1 - exp(-t_bw / τ)
+       反洗时间越长,去除效率越高,但存在上限
+    """
+    
+    def __init__(self):
+        """初始化膜阻力下降模型(无需训练参数)"""
+        super().__init__()
+
+    def forward(self, p, R0, R_end, L_h_next_start, t_bw_s):
+        """
+        前向传播:计算物理反洗能够去除的膜阻力
+        
+        参数:
+            p (UFParams): 超滤运行参数对象,包含:
+                - slope: 不可逆污染增长斜率
+                - power: 不可逆污染增长幂次
+                - tau_bw_s: 反洗时长影响的时间尺度
+            R0 (float): 本超级周期初始膜阻力
+            R_end (float): 过滤结束时的膜阻力(峰值)
+            L_h_next_start (float): 下一小周期起始时的累积运行时间 [小时]
+            t_bw_s (float): 物理反洗时长 [秒]
+        
+        返回:
+            float: 物理反洗实际去除的膜阻力量
+        
+        计算步骤:
+            1. 基于长期污染模型计算本周期的不可逆污染增量
+            2. 计算可逆污染量 = 当前总污染 - 不可逆污染
+            3. 应用时间因子(反洗时长的影响)
+            4. 返回实际去除的阻力(不超过可逆污染量)
+        """
+        # ========== 步骤1:计算不可逆污染累积 ==========
+        # 使用幂律模型描述长期不可逆污染的累积
+        # R_irr(t) = R0 + slope × t^power
+
+        
+        # 下一小周期开始时的理论膜阻力
+        delta_R = p.slope * (L_h_next_start ** p.power)
+        R_next_start = R0 + delta_R
+
+        # 这部分污染可以通过物理反洗去除
+        reversible_R = max(R_end - R_next_start, 0.0)
+        
+        # ========== 步骤3:计算反洗时间效率因子 ==========
+        # 使用指数衰减模型:time_gain = 1 - exp(-t_bw / τ)
+        # τ (tau_bw_s): 时间尺度参数
+        # - 反洗时间 t_bw = 0 时,time_gain = 0(无去除效果)
+        # - 反洗时间 t_bw → ∞ 时,time_gain → 1(达到最大效率)
+        # - 反洗时间 t_bw = τ 时,time_gain ≈ 0.632(去除63.2%)
+        # τ参考计算: 以60s去除效率高于95%的要求推算得τ取20s
+        # 该取值下:
+        # - 反洗时间 t_bw = 40s 时,time_gain ≈ 0.865(去除86.5%)
+        # - 反洗时间 t_bw = 60s 时,time_gain ≈ 0.950(去除95.0%)
+        time_gain = 1.0 - np.exp(- (t_bw_s / p.tau_bw_s))
+        
+        # ========== 步骤4:计算实际去除的膜阻力 ==========
+        # 实际去除量 = 可逆污染量 × 时间效率因子
+        dR_bw = reversible_R * time_gain
+        
+        # 确保去除量不超过可逆污染总量(物理约束)
+        return float(np.clip(dR_bw, 0.0, reversible_R))
+
+
+# ===== 主程序 =====
+if __name__ == "__main__":
+    model_fp = ResistanceIncreaseModel()
+    model_bw = ResistanceDecreaseModel()
+
+    torch.save(model_fp.state_dict(), "resistance_model_fp.pth")
+    torch.save(model_bw.state_dict(), "resistance_model_bw.pth")
+
+    print("模型已安全保存为 resistance_model_fp.pth、resistance_model_bw.pth")

+ 233 - 0
algorithm/uf_rl/进水动作版超滤训练源码/fixed_DQN_decide.py

@@ -0,0 +1,233 @@
+import numpy as np
+from stable_baselines3 import DQN
+from fixed_DQN_env import UFSuperCycleEnv, UFParams
+from fixed_DQN_env import simulate_one_supercycle
+
+# 模型路径
+MODEL_PATH = "model/dqn_model.zip"
+
+# 加载模型(只加载一次,提高效率)
+model = DQN.load(MODEL_PATH)
+
+def run_uf_DQN_decide(uf_params, TMP0_value: float):
+    """
+    单步决策函数(新版):
+    当前模型只输出进水时间 L_s,不输出反洗时间。
+    """
+
+    # 1. 初始化环境
+    env = UFSuperCycleEnv(uf_params)
+
+    # 2. 设置 TMP0
+    env.current_params.TMP0 = TMP0_value
+
+    # 3. 获取观察(归一化)
+    obs = env._get_obs().reshape(1, -1)
+
+    # 4. 模型预测动作
+    action, _ = model.predict(obs, deterministic=True)
+
+    # 5. 新模型动作只返回 L_s
+    L_s = env._get_action_values(action[0])   # 单值
+
+    # 6. 在环境中执行动作
+    next_obs, reward, terminated, truncated, info = env.step(action[0])
+
+    # 7. 返回结构化结果
+    return {
+        "action": int(action[0]),
+        "L_s": float(L_s),
+        "t_bw_s": None,       # 保留字段但固定为 None
+        "next_obs": next_obs,
+        "reward": reward,
+        "terminated": terminated,
+        "truncated": truncated,
+        "info": info
+    }
+
+def generate_plc_instructions(current_L_s, model_prev_L_s, model_L_s):
+    """
+    根据工厂当前值、模型上一轮决策值和模型当前轮决策值,生成PLC指令。
+
+    新增功能:
+    1. 处理None值情况:如果模型上一轮值为None,则使用工厂当前值;
+       如果工厂当前值也为None,则返回None并提示错误。
+    """
+    # 参数配置保持不变
+    params = UFParams(
+        L_min_s=3600.0, L_max_s=4800.0, L_step_s=60.0,
+    )
+
+    # 参数解包
+    L_step_s = params.L_step_s
+    L_min_s = params.L_min_s
+    L_max_s = params.L_max_s
+    adjustment_threshold = 1.0
+
+    # 处理None值情况
+    if model_prev_L_s is None:
+        if current_L_s is None:
+            print("错误: 过滤时长的工厂当前值和模型上一轮值均为None")
+            return None, None
+        else:
+            # 使用工厂当前值作为基准
+            effective_current_L = current_L_s
+            source_L = "工厂当前值(模型上一轮值为None)"
+    else:
+        # 模型上一轮值不为None,继续检查工厂当前值
+        if current_L_s is None:
+            effective_current_L = model_prev_L_s
+            source_L = "模型上一轮值(工厂当前值为None)"
+        else:
+            effective_current_L = model_prev_L_s
+            source_L = "模型上一轮值"
+
+
+    # 检测所有输入值是否在规定范围内(只对非None值进行检查)
+    # 工厂当前值检查(警告)
+    if current_L_s is not None and not (L_min_s <= current_L_s <= L_max_s):
+        print(f"警告: 当前过滤时长 {current_L_s} 秒不在允许范围内 [{L_min_s}, {L_max_s}]")
+
+    # 模型上一轮决策值检查(警告)
+    if model_prev_L_s is not None and not (L_min_s <= model_prev_L_s <= L_max_s):
+        print(f"警告: 模型上一轮过滤时长 {model_prev_L_s} 秒不在允许范围内 [{L_min_s}, {L_max_s}]")
+
+    # 模型当前轮决策值检查(错误)
+    if model_L_s is None:
+        raise ValueError("错误: 决策模型建议的过滤时长不能为None")
+    elif not (L_min_s <= model_L_s <= L_max_s):
+        raise ValueError(f"错误: 决策模型建议的过滤时长 {model_L_s} 秒不在允许范围内 [{L_min_s}, {L_max_s}]")
+
+    print(f"过滤时长基准: {source_L}, 值: {effective_current_L}")
+
+    # 使用选定的基准值进行计算调整
+    L_diff = model_L_s - effective_current_L
+    L_adjustment = 0
+    if abs(L_diff) >= adjustment_threshold * L_step_s:
+        if L_diff >= 0:
+            L_adjustment = L_step_s
+        else:
+            L_adjustment = -L_step_s
+    next_L_s = effective_current_L + L_adjustment
+
+    return next_L_s
+
+
+def calc_uf_cycle_metrics(p, TMP0, max_tmp_during_filtration, min_tmp_during_filtration, L_s: float, t_bw_s):
+    """
+    计算 UF 超滤系统的核心性能指标
+
+    参数:
+        p (UFParams): UF 系统参数
+        L_s (float): 单次过滤时间(秒)
+        t_bw_s (float): 单次反洗时间(秒)
+
+    返回:
+        dict: {
+            "k_bw_per_ceb": 小周期次数,
+            "ton_water_energy_kWh_per_m3": 吨水电耗,
+            "recovery": 回收率,
+            "net_delivery_rate_m3ph": 净供水率 (m³/h),
+            "daily_prod_time_h": 日均产水时间 (小时/天)
+            "max_permeability": 全周期最高渗透率(lmh/bar)
+        }
+    """
+    # 将跨膜压差写入参数
+    p.TMP0 = TMP0
+
+    # 模拟该参数下的超级周期
+    info, next_params = simulate_one_supercycle(p, L_s, t_bw_s)
+
+    # 获得模型模拟周期信息
+    k_bw_per_ceb = info["k_bw_per_ceb"]
+    ton_water_energy_kWh_per_m3 = info["ton_water_energy_kWh_per_m3"]
+    recovery = info["recovery"]
+    daily_prod_time_h = info["daily_prod_time_h"]
+
+    # 获得模型模拟周期内最高跨膜压差/最低跨膜压差
+    if max_tmp_during_filtration is None:
+        max_tmp_during_filtration = info["max_TMP_during_filtration"]
+    if min_tmp_during_filtration is None:
+        min_tmp_during_filtration = info["min_TMP_during_filtration"]
+
+    # 计算最高渗透率
+    max_permeability = 100 * p.q_UF / (128*40) / min_tmp_during_filtration
+
+
+    return {
+        "k_bw_per_ceb": k_bw_per_ceb,
+        "ton_water_energy_kWh_per_m3": ton_water_energy_kWh_per_m3,
+        "recovery": recovery,
+        "daily_prod_time_h": daily_prod_time_h,
+        "max_permeability": max_permeability
+    }
+
+
+# ==============================
+# 示例调用
+# ==============================
+if __name__ == "__main__":
+    # -------------------------
+    # 1. 初始化参数
+    # -------------------------
+    uf_params = UFParams()
+    TMP0 = 0.01   # 原始跨膜压差
+
+    # -------------------------
+    # 2. 调用模型做一次决策(只输出 L_s)
+    # -------------------------
+    model_decide_result = run_uf_DQN_decide(uf_params, TMP0)
+    model_L_s = model_decide_result["L_s"]     # 只输出 L_s
+    print(f"模型决策进水时长 L_s = {model_L_s}")
+
+    # -------------------------
+    # 3. 工厂当前值 + 模型上一轮值(示例值)
+    # -------------------------
+    current_L_s = 3800
+    model_prev_L_s = 4040
+
+    # -------------------------
+    # 4. 生成 PLC 指令(新版仅 L_s)
+    # -------------------------
+    plc_L_s = generate_plc_instructions(current_L_s,model_prev_L_s,model_L_s)
+
+    print(f"PLC 指令 L_s = {plc_L_s}")
+
+    # -------------------------
+    # 5. 反洗时长由工厂参数决定/固定值
+    #    (新模型不输出 t_bw_s)
+    # -------------------------
+    plc_t_bw_s = uf_params.fixed_t_bw_s
+
+    # -------------------------
+    # 6. 工厂 TMP 最大/最小(可为空 None)
+    # -------------------------
+    max_tmp_during_filtration = 0.050176
+    min_tmp_during_filtration = 0.012496
+
+    # -------------------------
+    # 7. 计算周期指标(模型动作实际效果)
+    # -------------------------
+    execution_result = calc_uf_cycle_metrics(
+        p=uf_params,
+        TMP0=TMP0,
+        max_tmp_during_filtration=max_tmp_during_filtration,
+        min_tmp_during_filtration=min_tmp_during_filtration,
+        L_s=plc_L_s,
+        t_bw_s=plc_t_bw_s   # 仍需要反洗时长参数
+    )
+
+    # -------------------------
+    # 8. 打印结果
+    # -------------------------
+    print("\n===== 单步决策结果 =====")
+    print(f"模型动作编号: {model_decide_result['action']}")
+    print(f"模型选择的 L_s: {model_L_s} 秒")
+    print(f"PLC 下发 L_s: {plc_L_s} 秒")
+    print(f"PLC 下发反洗时长(固定) t_bw_s: {plc_t_bw_s} 秒")
+
+    print(f"周期对应的反洗次数: {execution_result['k_bw_per_ceb']}")
+    print(f"吨水电耗: {execution_result['ton_water_energy_kWh_per_m3']}")
+    print(f"回收率: {execution_result['recovery']}")
+    print(f"日均产水时间: {execution_result['daily_prod_time_h']}")
+    print(f"最高渗透率: {execution_result['max_permeability']}")

+ 1052 - 0
algorithm/uf_rl/进水动作版超滤训练源码/fixed_DQN_env.py

@@ -0,0 +1,1052 @@
+"""
+超滤强化学习环境模块
+========================
+本模块定义了超滤系统的强化学习环境,包括:
+1. UFParams: 超滤系统参数配置类
+2. 膜阻力与跨膜压差转换函数
+3. simulate_one_supercycle: 超级周期模拟函数
+4. calculate_reward: 奖励函数
+5. is_dead_cycle: 失败判定函数
+6. UFSuperCycleEnv: Gymnasium环境类
+
+模块设计说明:
+- 基于 Gymnasium (原OpenAI Gym) 标准接口
+- 模拟超滤膜的"超级周期"运行(多次物理反洗 + 一次化学反洗)
+- 强化学习智能体通过优化过滤时长和反洗时长来最大化回收率并控制污染累积
+"""
+
+import os
+import torch
+from pathlib import Path
+import numpy as np
+import gymnasium as gym
+from gymnasium import spaces
+from typing import Dict, Tuple, Optional
+import torch
+import torch.nn as nn
+from dataclasses import dataclass, asdict
+from UF_resistance_models import ResistanceIncreaseModel, ResistanceDecreaseModel  # 导入膜阻力模型
+import copy
+
+
+# ==================== 超滤系统参数配置类 ====================
+@dataclass
+class UFParams:
+    """
+    超滤系统参数配置类
+    
+    功能:统一管理超滤系统的所有运行参数,包括:
+    - 膜动态运行参数(流量、温度、压差等)
+    - 膜阻力模型参数(污染增长速率、去除效率等)
+    - 膜运行约束参数(各参数的上下限)
+    - 反洗参数(物理反洗、化学反洗)
+    - 动作搜索范围(过滤时长、反洗时长的取值范围)
+    - 奖励函数参数
+    
+    设计思想:
+    - 使用 dataclass 装饰器,自动生成 __init__、__repr__ 等方法
+    - 所有参数带有类型注解和默认值
+    - 参数值基于锡山水厂的实际运行数据和经验
+    """
+    # ========== 膜动态运行参数 ==========
+    # 这些参数描述超滤膜的实时运行状态,在环境模拟中会动态变化
+    
+    q_UF: float = 360.0  
+    # 过滤进水流量(m³/h)
+    # 说明:影响膜通量,进而影响污染速率
+    # 典型范围:250-400 m³/h
+    
+    TMP0: float = 0.03 
+    # 初始跨膜压差(MPa,兆帕)
+    # 说明:反映膜阻力状态,TMP 越高表示膜污染越严重
+    # 正常范围:0.01-0.035 MPa,超过 0.08 MPa 需停机检修
+    
+    temp: float = 25.0  
+    # 水温(摄氏度)
+    # 说明:影响水的粘度,进而影响跨膜压差
+    # 典型范围:10-40℃,25℃为标准温度
+
+    # ========== 膜阻力模型参数 ==========
+    # 这些参数描述膜污染的物理化学特性,基于历史数据拟合得到
+    
+    nuK: float = 6.92e+01
+    # 过滤阶段膜阻力增长系数(缩放后单位)
+    # 说明:反映水质污染特性,nuK 越大表示水质越差、膜污染越快
+    # 物理意义:单位膜通量、单位时间的阻力增长速率
+    
+    slope: float = 3.44e-01 
+    # 全周期不可逆污染增长斜率
+    # 说明:描述长期不可逆污染的累积速率(幂律模型的系数)
+    
+    power: float = 1.032 
+    # 全周期不可逆污染增长幂次
+    # 说明:描述长期污染的非线性特性(幂律模型的指数)
+    # power > 1 表示污染加速累积,power < 1 表示污染增速放缓
+    
+    tau_bw_s: float = 30.0  
+    # 物理反洗时长影响的时间尺度(秒)
+    # 说明:反洗效率的特征时间,当反洗时长 = tau 时,达到约 63% 效率
+    
+    gamma_t: float = 1.0  
+    # 物理反洗时长作用指数(保留参数,当前未使用)
+    
+    ceb_removal: float = 150  
+    # 化学增强反洗(CEB)可去除的膜阻力(缩放后单位)
+    # 说明:CEB 比物理反洗更彻底,可去除部分不可逆污染
+
+    # ========== 膜运行约束参数 ==========
+    # 定义各运行参数的物理约束和安全限制
+    
+    global_TMP_hard_limit: float = 0.08
+    # TMP 硬上限(MPa)
+    # 说明:超过此值将导致episode失败,需立即停机
+
+    global_TMP_soft_limit: float = 0.06
+    # TMP 软上限 (MPa)
+    # 说明:此上限用于指导奖励函数中膜阻力允许上升值,越接近该上限,系统对膜阻力上升控制的更严格
+
+    # --- 初始TMP约束 ---
+    TMP0_max: float = 0.035  # 初始TMP上限(MPa)
+    TMP0_min: float = 0.01   # 初始TMP下限(MPa)
+    
+    # --- 流量约束 ---
+    q_UF_max: float = 400.0  # 进水流量上限(m³/h)
+    q_UF_min: float = 250.0  # 进水流量下限(m³/h)
+    
+    # --- 温度约束 ---
+    temp_max: float = 40.0   # 温度上限(℃)
+    temp_min: float = 10.0   # 温度下限(℃)
+    
+    # --- 短期污染模型参数约束 ---
+    nuK_max: float = 1e+02   # 阻力增长系数上限
+    nuK_min: float = 6e+01   # 阻力增长系数下限
+    
+    # --- 长期污染模型参数约束 ---
+    slope_max: float = 10    # 不可逆污染斜率上限
+    slope_min: float = 0.5   # 不可逆污染斜率下限
+    power_max: float = 1.5   # 不可逆污染幂次上限
+    power_min: float = 0.5   # 不可逆污染幂次下限
+    
+    # --- CEB去除能力约束 ---
+    ceb_removal_max: float = 200  # CEB去除阻力上限(缩放后)
+    ceb_removal_min: float = 100  # CEB去除阻力下限(缩放后)
+
+    # ========== 反洗参数(固定配置) ==========
+    q_bw_m3ph: float = 1000.0
+    # 物理反洗流量(m³/h)
+    # 说明:反洗流量通常为正常过滤流量的 2-3 倍
+    fixed_t_bw_s = 60 # 固定物理反洗时间
+
+    # ========== CEB 化学反洗参数 ==========
+    T_ceb_interval_h: float = 48.0
+    # CEB 间隔时间(小时)
+    # 说明:每运行约 60 小时执行一次化学增强反洗
+
+    v_ceb_m3: float = 20.0
+    # CEB 用水体积(m³)
+
+    t_ceb_s: float = 40 * 60.0
+    # CEB 时长(秒,这里为 40 分钟)
+
+    # ========== 强化学习动作空间搜索范围 ==========
+    # 定义智能体可选择的动作范围(离散化)
+
+    L_min_s: float = 3800.0  # 过滤时长下限(秒,约 63 分钟)
+    L_max_s: float = 4800.0  # 过滤时长上限,改为 4800s
+    t_bw_min_s: float = 40.0  # 物理反洗时长下限(秒)
+    t_bw_max_s: float = 60.0  # 物理反洗时长上限(秒)
+
+    # ========== 动作离散化网格 ==========
+    L_step_s: float = 60.0  # 过滤时长步长(秒)
+    t_bw_step_s: float = 5.0  # 物理反洗时长步长(秒)
+
+    # ========== 奖励函数参数 ==========
+    k_rec = 5.0  # 回收率敏感度系数(控制回收率奖励的陡峭程度)
+    k_res = 10.0  # 残余污染敏感度系数(控制污染惩罚的陡峭程度)
+    rec_low, rec_high = 0.92, 0.99  # 回收率的正常范围
+    rr0 = 0.08  # 残余污染比例的参考值
+
+
+# ==================== 辅助函数:膜阻力与跨膜压差转换 ====================
+
+def xishan_viscosity(temp):
+    """
+    锡山水厂水温粘度修正公式
+    
+    功能:根据水温计算水的动力粘度(考虑温度影响)
+    
+    参数:
+        temp (float): 水温(摄氏度)
+    
+    返回:
+        float: 水的动力粘度 μ (Pa·s)
+    
+    原理:
+    - 水的粘度随温度升高而降低
+    - 25℃时纯水粘度约为 0.00089 Pa·s
+    - 本公式基于锡山水厂PLC系统的经验修正因子
+    
+    注意:
+    - 本公式基于纯水粘度修正
+    - 实际水厂水质与纯水有差异,对粘度有一定影响
+    - 未来可根据实际水质进一步校准
+    """
+    # 温度归一化(相对于300K)
+    x = (temp + 273.15) / 300  # 摄氏度转开尔文
+    
+    # 温度修正因子(经验公式,基于锡山水厂PLC)
+    factor = 890 / (
+        280.68 * x ** -1.9 + 
+        511.45 * x ** -7.7 + 
+        61.131 * x ** -19.6 + 
+        0.45903 * x ** -40
+    )
+    
+    # 计算修正后的粘度(25℃标准粘度 / 修正因子)
+    mu = 0.00089 / factor  # [Pa·s]
+    
+    return mu
+
+
+def _calculate_resistance(tmp, q_UF, temp):
+    """
+    由跨膜压差计算膜阻力
+    
+    功能:根据 Darcy 定律,由跨膜压差反推膜阻力
+    
+    参数:
+        tmp (float): 跨膜压差 TMP (MPa)
+        q_UF (float): 过滤流量 (m³/h)
+        temp (float): 水温 (℃)
+    
+    返回:
+        float: 膜阻力 R(已缩放 1e10)
+    
+    原理:
+        Darcy 定律:J = TMP / (μ × R)
+        其中:
+        - J: 膜通量 [m/s]
+        - TMP: 跨膜压差 [Pa]
+        - μ: 水的动力粘度 [Pa·s]
+        - R: 膜阻力 [m⁻¹]
+        
+        反解得:R = TMP / (J × μ)
+    
+    注意:
+        - 超滤膜阻力实际量级为 1e12 m⁻¹
+        - 为便于数值计算,已缩放 1e10 倍至 1e2 量级
+    """
+    # 膜有效面积(锡山水厂配置:128组 × 40 m²/组)
+    A = 128 * 40  # [m²]
+    
+    # 温度修正后的水粘度
+    mu = xishan_viscosity(temp)  # [Pa·s]
+    
+    # 跨膜压差单位转换:MPa → Pa
+    TMP_Pa = tmp * 1e6  # [Pa]
+    
+    # 计算膜通量:流量 / 面积
+    J = q_UF / A / 3600  # [m³/h] → [m³/(m²·s)] = [m/s]
+    
+    # 物理约束检查:通量和粘度必须为正
+    if J <= 0 or mu <= 0:
+        return np.nan
+    
+    # 根据 Darcy 定律计算膜阻力并缩放
+    R = TMP_Pa / (J * mu) / 1e10  # [m⁻¹] → [缩放单位]
+
+    return float(R)
+
+
+def _calculate_tmp(R, q_UF, temp):
+    """
+    由膜阻力计算跨膜压差
+    
+    功能:根据 Darcy 定律,由膜阻力计算跨膜压差(_calculate_resistance 的逆运算)
+    
+    参数:
+        R (float): 膜阻力(已缩放 1e10)
+        q_UF (float): 过滤流量 (m³/h)
+        temp (float): 水温 (℃)
+    
+    返回:
+        float: 跨膜压差 TMP (MPa)
+    
+    原理:
+        Darcy 定律:TMP = J × μ × R
+        其中:
+        - J: 膜通量 [m/s]
+        - μ: 水的动力粘度 [Pa·s]
+        - R: 膜阻力 [m⁻¹]
+    """
+    # 膜有效面积
+    A = 128 * 40  # [m²]
+    
+    # 温度修正后的水粘度
+    mu = xishan_viscosity(temp)  # [Pa·s]
+    
+    # 计算膜通量
+    J = q_UF / A / 3600  # [m/s]
+    
+    # 根据 Darcy 定律计算跨膜压差(还原缩放)
+    TMP_Pa = R * J * mu * 1e10  # [缩放单位] → [Pa]
+    
+    # 单位转换:Pa → MPa
+    tmp = TMP_Pa / 1e6  # [MPa]
+
+    return float(tmp)
+
+
+# ==================== 膜阻力模型加载函数 ====================
+
+def load_resistance_models():
+    """
+    加载膜阻力预测模型(单例模式)
+    
+    功能:
+    - 加载预训练的膜阻力上升模型和下降模型
+    - 使用全局变量实现单例模式,避免重复加载
+    - 仅在首次调用时执行加载操作
+    
+    返回:
+        tuple: (resistance_model_fp, resistance_model_bw)
+            - resistance_model_fp: 过滤阶段阻力上升模型
+            - resistance_model_bw: 反洗阶段阻力下降模型
+    
+    注意:
+    - 模型文件必须与本脚本位于同一目录
+    - 模型已设置为推理模式(eval),不会更新参数
+    """
+    # 声明全局变量(实现单例模式)
+    global resistance_model_fp, resistance_model_bw
+
+    # 检查模型是否已加载(避免重复加载)
+    if "resistance_model_fp" in globals() and resistance_model_fp is not None:
+        return resistance_model_fp, resistance_model_bw
+
+    print("🔄 正在加载膜阻力模型...")
+
+    # 初始化模型对象
+    resistance_model_fp = ResistanceIncreaseModel()
+    resistance_model_bw = ResistanceDecreaseModel()
+
+    # 获取当前脚本所在目录
+    base_dir = Path(__file__).resolve().parent
+
+    # 构造模型文件路径
+    fp_path = base_dir / "resistance_model_fp.pth"   # 过滤阶段模型
+    bw_path = base_dir / "resistance_model_bw.pth"   # 反洗阶段模型
+
+    # 检查模型文件是否存在
+    assert fp_path.exists(), f"缺少膜阻力上升模型文件: {fp_path.name}"
+    assert bw_path.exists(), f"缺少膜阻力下降模型文件: {bw_path.name}"
+
+    # 加载模型权重(map_location="cpu" 确保在没有GPU的环境也能运行)
+    resistance_model_fp.load_state_dict(torch.load(fp_path, map_location="cpu"))
+    resistance_model_bw.load_state_dict(torch.load(bw_path, map_location="cpu"))
+
+    # 设置为推理模式(禁用 dropout、batchnorm 等训练特性)
+    resistance_model_fp.eval()
+    resistance_model_bw.eval()
+
+    print("✅ 膜阻力模型加载成功!")
+    return resistance_model_fp, resistance_model_bw
+
+
+# ==================== 膜阻力模型调用函数 ====================
+
+def _delta_resistance(p, L_s: float) -> float:
+    """
+    计算过滤阶段膜阻力上升量
+    
+    功能:调用预训练的膜阻力上升模型
+    
+    参数:
+        p (UFParams): 超滤运行参数
+        L_s (float): 过滤时长(秒)
+    
+    返回:
+        float: 膜阻力上升量 ΔR
+    """
+    return resistance_model_fp(p, L_s)
+
+
+def phi_bw_of(p, R0: float, R_end: float, L_h_next_start: float, t_bw_s: float) -> float:
+    """
+    计算物理反洗可去除的膜阻力
+    
+    功能:调用预训练的膜阻力下降模型
+    
+    参数:
+        p (UFParams): 超滤运行参数
+        R0 (float): 本超级周期初始膜阻力
+        R_end (float): 过滤结束时的膜阻力
+        L_h_start (float): 本小周期起始累积运行时间(小时)
+        L_h_next_start (float): 下一小周期起始累积运行时间(小时)
+        t_bw_s (float): 物理反洗时长(秒)
+    
+    返回:
+        float: 物理反洗去除的膜阻力量
+    """
+    return resistance_model_bw(p, R0, R_end, L_h_next_start, t_bw_s)
+
+
+def _v_bw_m3(p, t_bw_s: float) -> float:
+    """
+    计算物理反洗水耗
+    
+    参数:
+        p (UFParams): 超滤运行参数(包含反洗流量 q_bw_m3ph)
+        t_bw_s (float): 物理反洗时长(秒)
+    
+    返回:
+        float: 反洗水耗(立方米)
+    
+    公式:
+        V = Q × t
+        其中 Q 为反洗流量 [m³/h],t 为反洗时长 [h]
+    """
+    return float(p.q_bw_m3ph * (float(t_bw_s) / 3600.0))
+
+def simulate_one_supercycle(p: UFParams, L_s: float, t_bw_s: float):
+    """
+    模拟一个完整的超级周期(Super Cycle)
+    
+    功能:
+    - 模拟超滤膜的完整运行周期:多次"过滤+物理反洗"小周期 + 一次化学增强反洗(CEB)
+    - 计算周期内的各项性能指标(回收率、TMP变化、污染累积等)
+    - 返回周期结束后的状态参数,用于下一周期的模拟
+    
+    参数:
+        p (UFParams): 当前超滤系统参数
+        L_s (float): 过滤时长(秒)
+        t_bw_s (float): 物理反洗时长(秒)
+    
+    返回:
+        tuple: (info, next_params)
+            - info (dict): 本周期的性能指标字典
+            - next_params (UFParams): 更新后的系统参数(用于下一周期)
+    
+    超级周期结构:
+        超级周期 = k 次小周期 + 1 次 CEB
+        小周期 = 过滤 + 物理反洗
+        
+        时间轴:
+        |-- 小周期1 --|-- 小周期2 --|-- ... --|-- 小周期k --|-- CEB --|
+        | 滤 | 物洗 | 滤 | 物洗 |  ...  | 滤 | 物洗 |   化洗  |
+    """
+    
+    # ========== 初始化周期参数 ==========
+    L_h = float(L_s) / 3600.0  # 过滤时长转换:秒 → 小时
+
+    # 初始状态
+    tmp = p.TMP0  # 当前跨膜压差
+    R0 = _calculate_resistance(p.TMP0, p.q_UF, p.temp)  # 初始膜阻力
+    
+    # 跟踪变量(用于记录周期内的极值)
+    max_tmp_during_filtration = tmp  # 周期内最大TMP
+    min_tmp_during_filtration = tmp  # 周期内最小TMP
+    max_residual_increase = 0.0      # 周期内最大残余污染增量
+
+    # ========== 计算小周期数量 ==========
+    # 小周期时长 = 过滤时长 + 物理反洗时长
+    t_small_cycle_h = (L_s + t_bw_s) / 3600.0  # [小时]
+    
+    # 计算一个超级周期内包含多少个小周期
+    # k = floor(CEB间隔时间 / 小周期时长)
+    k_bw_per_ceb = int(np.floor(p.T_ceb_interval_h / t_small_cycle_h))
+    if k_bw_per_ceb < 1:
+        k_bw_per_ceb = 1  # 至少包含1个小周期
+
+    # ========== 吨水电耗查找表 ==========
+    # 键:过滤时长(秒),值:吨水电耗(kWh/m³)
+    energy_lookup = {
+        3600: 0.1034, 3660: 0.1031, 3720: 0.1029, 3780: 0.1026,
+        3840: 0.1023, 3900: 0.1021, 3960: 0.1019, 4020: 0.1017,
+        4080: 0.1015, 4140: 0.1012, 4200: 0.1011, 4260: 0.1008,
+        4320: 0.1007, 4380: 0.1005, 4440: 0.1003, 4500: 0.1001,
+        4560: 0.0999, 4620: 0.0998, 4680: 0.0996, 4740: 0.0995,
+        4800: 0.0993,
+    }
+
+    # ========== 循环模拟每个小周期(过滤 + 物理反洗) ==========
+    for idx in range(k_bw_per_ceb):
+        # --- 小周期开始状态 ---
+        tmp_run_start = tmp      # 本次过滤开始时的TMP
+        q_UF = p.q_UF            # 过滤流量
+        temp = p.temp            # 水温
+
+        # --- 过滤阶段:膜阻力上升 ---
+        R_run_start = _calculate_resistance(tmp_run_start, q_UF, temp)  # 过滤开始时的膜阻力
+        d_R = _delta_resistance(p, L_s)  # 过滤阶段膜阻力增量
+        R_peak = R_run_start + d_R       # 过滤结束时的膜阻力(峰值)
+        tmp_peak = _calculate_tmp(R_peak, q_UF, temp)  # 过滤结束时的TMP(峰值)
+
+        # 更新TMP极值记录
+        max_tmp_during_filtration = max(max_tmp_during_filtration, tmp_peak)
+        min_tmp_during_filtration = min(min_tmp_during_filtration, tmp_run_start)
+
+        # --- 物理反洗阶段:膜阻力下降 ---
+        # 计算累积运行时间(用于长期污染模型)
+        L_h_next_start = (L_s + t_bw_s) / 3600.0 * (idx + 1)  # 下一小周期起始时间
+        
+        # 调用膜阻力下降模型,计算物理反洗可去除的阻力
+        reversible_R = phi_bw_of(p, R0, R_peak, L_h_next_start, t_bw_s)
+        
+        # 物理反洗后的膜阻力
+        R_after_bw = R_peak - reversible_R
+        tmp_after_bw = _calculate_tmp(R_after_bw, q_UF, temp)
+
+        # 计算残余污染增量(反洗后的TMP相对本次开始的增加)
+        residual_inc = tmp_after_bw - tmp_run_start
+        max_residual_increase = max(max_residual_increase, residual_inc)
+
+        # 更新TMP(作为下一小周期的起始TMP)
+        tmp = tmp_after_bw
+
+    # ========== 化学增强反洗 (CEB) ==========
+    # CEB比物理反洗更彻底,可去除部分不可逆污染
+    R_after_ceb = R_peak - p.ceb_removal  # CEB后的膜阻力
+    tmp_after_ceb = _calculate_tmp(R_after_ceb, q_UF, temp)  # CEB后的TMP
+
+    # ============================================================
+    # 计算周期性能指标
+    # ============================================================
+
+    # ========== 水量平衡计算 ==========
+    # 进水总量(所有小周期的过滤进水之和)
+    V_feed_super = k_bw_per_ceb * p.q_UF * L_h  # [m³]
+    
+    # 损失水量(物理反洗 + 化学反洗)
+    V_loss_super = k_bw_per_ceb * _v_bw_m3(p, t_bw_s) + p.v_ceb_m3  # [m³]
+    
+    # 净产水量
+    V_net = max(0.0, V_feed_super - V_loss_super)  # [m³]
+    
+    # 回收率(净产水 / 进水总量)
+    # 加1e-12避免除零,max确保非负
+    recovery = max(0.0, V_net / max(V_feed_super, 1e-12))  # [无量纲,0-1之间]
+
+    # ========== 时间与能耗计算 ==========
+    # 超级周期总时长
+    T_super_h = k_bw_per_ceb * (L_s + t_bw_s) / 3600.0 + p.t_ceb_s / 3600.0  # [小时]
+    
+    # 日均产水时间(24小时内实际产水的时间)
+    daily_prod_time_h = k_bw_per_ceb * L_h / T_super_h * 24.0  # [小时]
+
+    # 吨水电耗(从查找表获取最接近的值)
+    closest_L = min(energy_lookup.keys(), key=lambda x: abs(x - L_s))
+    ton_water_energy = energy_lookup[closest_L]  # [kWh/m³]
+
+    # ===== 新指标:膜阻力允许上升空间 =====
+    R_max = _calculate_resistance(max_tmp_during_filtration, p.q_UF, p.temp)
+    R_soft_limit = _calculate_resistance(p.global_TMP_soft_limit, p.q_UF, p.temp)
+    delta_R_allow = max(R_soft_limit - R_max, 1e-6)  # 供奖励函数使用的“污染允许增长空间”
+
+    # ========== 构建性能指标字典 ==========
+    info = {
+        # 运行参数
+        "q_UF": p.q_UF,                                        # 过滤流量
+        "temp": p.temp,                                        # 水温
+        
+        # 水量指标
+        "recovery": recovery,                                  # 回收率
+        "V_feed_super_m3": V_feed_super,                      # 进水总量
+        "V_loss_super_m3": V_loss_super,                      # 损失水量
+        "V_net_super_m3": V_net,                              # 净产水量
+        
+        # 时间指标
+        "supercycle_time_h": T_super_h,                       # 超级周期时长
+        "daily_prod_time_h": daily_prod_time_h,               # 日均产水时间
+        "k_bw_per_ceb": k_bw_per_ceb,                         # 小周期数量
+        
+        # TMP指标
+        "max_TMP_during_filtration": max_tmp_during_filtration,  # 周期内最大TMP
+        "min_TMP_during_filtration": min_tmp_during_filtration,  # 周期内最小TMP
+        "global_TMP_limit": p.global_TMP_hard_limit,                  # TMP限制
+        "TMP0": p.TMP0,                                          # 周期初始TMP
+        "TMP_after_ceb": tmp_after_ceb,                          # CEB后TMP
+        
+        # 膜阻力指标
+        "R0": R0,                                              # 周期初始膜阻力
+        "R_after_ceb": R_after_ceb,                            # CEB后膜阻力
+        "max_residual_increase_per_run": max_residual_increase,  # 最大残余污染增量
+        "delta_R_allow": delta_R_allow,                                    # 污染允许增长空间
+        
+        # 能耗指标
+        "ton_water_energy_kWh_per_m3": ton_water_energy,      # 吨水电耗
+    }
+
+    # ============================================================
+    # 状态更新:生成下一周期的初始参数
+    # ============================================================
+
+    # 深拷贝当前参数(避免修改原对象)
+    next_params = copy.deepcopy(p)
+
+    # ========== 必须更新的参数 ==========
+    # 更新TMP为CEB后的值(作为下一超级周期的起始TMP)
+    next_params.TMP0 = tmp_after_ceb
+
+    # ========== 可选更新的参数(当前保持不变) ==========
+    # 这些参数可根据实际情况动态调整,预留扩展接口
+    next_params.slope = p.slope                # 长期污染斜率
+    next_params.power = p.power                # 长期污染幂次
+    next_params.ceb_removal = p.ceb_removal    # CEB去除能力
+    next_params.nuK = p.nuK                    # 短期污染系数
+    next_params.q_UF = p.q_UF                  # 过滤流量
+    next_params.temp = p.temp                  # 水温
+
+    return info, next_params
+
+def calculate_reward(p: UFParams, info: dict) -> float:
+    """
+    计算强化学习奖励函数
+    
+    功能:
+    - 平衡回收率和残余污染两个目标
+    - TMP不直接参与奖励计算(通过失败判定间接影响)
+    - 使用 tanh 函数实现平滑的非线性奖励
+    
+    参数:
+        p (UFParams): 超滤参数(包含奖励函数超参数)
+        info (dict): 周期性能指标字典
+    
+    返回:
+        float: 奖励值(通常在 -2 到 +2 之间)
+    
+    设计思想:
+    - 高回收率 → 水资源利用率高 → 正奖励
+    - 低残余污染 → 膜长期稳定运行 → 正奖励
+    - 两者需要权衡:过短的过滤时间提高回收率但污染去除不彻底;
+                      过长的过滤时间污染控制好但回收率下降
+    
+    参考点设计:
+    - (recovery=0.97, residual_ratio=0.1) → reward ≈ 0(高回收但污染高)
+    - (recovery=0.90, residual_ratio=0.0) → reward ≈ 0(低污染但回收率低)
+    - (recovery≈0.94, residual_ratio≈0.05) → reward > 0(平衡点)
+    """
+    # ========== 提取性能指标 ==========
+    recovery = info["recovery"]  # 回收率 [0-1]
+    
+    # 污染比例:实际上升的阻力 / 允许上升的阻力
+    # 允许上升的阻力值 = 当前阻力值软上限 - 当前阻力
+    residual_ratio = (info["R_after_ceb"] - info["R0"]) / info["delta_R_allow"]
+
+    # ========== 回收率奖励项 ==========
+    # 将回收率归一化到 [0, 1] 区间(基于预期范围)
+    rec_norm = (recovery - p.rec_low) / (p.rec_high - p.rec_low)
+    
+    # 使用 tanh 函数构建平滑的 S 型奖励曲线
+    # - rec_norm = 0.5 时(回收率处于中间值),rec_reward = 0
+    # - rec_norm > 0.5 时,rec_reward > 0(鼓励高回收率)
+    # - rec_norm < 0.5 时,rec_reward < 0(惩罚低回收率)
+    # - k_rec 控制曲线陡峭程度,越大变化越陡
+    rec_reward = np.clip(np.tanh(p.k_rec * (rec_norm - 0.5)), -1, 1)
+
+    # ========== 污染惩罚项 ==========
+    # 使用 tanh 函数构建惩罚曲线
+    # - residual_ratio < rr0 时,res_penalty > 0(奖励低污染)
+    # - residual_ratio > rr0 时,res_penalty < 0(惩罚高污染)
+    # - k_res 控制曲线陡峭程度
+    res_penalty = -np.tanh(p.k_res * (residual_ratio / p.rr0 - 1))
+
+    # ========== 组合奖励 ==========
+    # 简单线性组合两项(也可以加权)
+    total_reward = rec_reward + res_penalty
+
+    # 可选:添加平移项使特定点的奖励为零(当前未使用)
+    # total_reward -= offset
+
+    return total_reward
+
+
+def is_dead_cycle(info: dict) -> bool:
+    """
+    判断当前超级周期是否成功(可行)
+    
+    功能:
+    - 检查超级周期是否违反运行约束
+    - 用于强化学习的失败判定(terminated条件)
+    - True表示成功,False表示失败
+    
+    参数:
+        info (dict): simulate_one_supercycle() 返回的性能指标字典
+    
+    返回:
+        bool: True表示成功周期,False表示失败周期
+    
+    失败条件(任一满足即失败):
+    1. TMP超限:max_TMP > global_TMP_limit
+       - 原因:TMP过高会损坏膜或影响产水质量
+       - 阈值:0.08 MPa(可配置)
+    
+    2. 回收率过低:recovery < 0.75
+       - 原因:回收率太低说明反洗水耗过大,经济性差
+       - 阈值:75%(可调整)
+    
+    3. 残余污染累积过快:(R_after_ceb - R0) / R0 > 0.1
+       - 原因:单个超级周期污染增长超过10%,长期运行不可持续
+       - 阈值:10%(可调整)
+    """
+    # ========== 获取关键指标 ==========
+    TMP_limit = info.get("global_TMP_limit", 0.08)  # TMP硬约束上限
+    max_tmp = info.get("max_TMP_during_filtration", 0)  # 周期内最大TMP
+    recovery = info.get("recovery", 1.0)  # 回收率
+    R_after_ceb = info.get("R_after_ceb", 0)  # CEB后膜阻力
+    R0 = info.get("R0", 1e-6)  # 初始膜阻力
+    delta_R_allow = info.get("delta_R_allow", 1e-6) # 允许上升的膜阻力(加小值避免除零)
+
+    # ========== 失败条件检查 ==========
+    # 条件1:TMP超限
+    if max_tmp > TMP_limit:
+        return False  # 失败
+    
+    # 条件2:回收率过低
+    if recovery < 0.75:
+        return False  # 失败
+    
+    # 条件3:污染增长量超过容许范围
+    residual_increase = (R_after_ceb - R0) / delta_R_allow
+    if residual_increase > 1/15:
+        return False  # 失败
+
+    # 所有条件通过
+    return True  # 成功
+
+
+class UFSuperCycleEnv(gym.Env):
+    """
+    超滤系统强化学习环境(Gymnasium标准接口)
+    
+    功能:
+    - 模拟超滤膜的超级周期运行
+    - 智能体在每个超级周期选择过滤时长和反洗时长
+    - 目标:最大化回收率同时控制污染累积
+    
+    状态空间 (8维,归一化到 [0,1]):
+        1. TMP0: 初始跨膜压差
+        2. q_UF: 过滤流量
+        3. temp: 水温
+        4. R0: 初始膜阻力
+        5. nuK: 短期污染系数
+        6. slope: 长期污染斜率
+        7. power: 长期污染幂次
+        8. ceb_removal: CEB去除能力
+    
+    动作空间 (离散):
+        - 二维离散动作组合:(过滤时长, 反洗时长)
+        - 过滤时长: L_min_s ~ L_max_s,步长 L_step_s
+        - 反洗时长: t_bw_min_s ~ t_bw_max_s,步长 t_bw_step_s
+        - 总动作数 = len(L_values) × len(t_bw_values)
+    
+    奖励机制:
+        - 基于回收率和残余污染的平衡
+        - 失败 (TMP超限、回收率过低、污染过快) 时给予大负奖励 (-10)
+    
+    终止条件:
+        - terminated: 违反运行约束(失败)
+        - truncated: 达到最大步数 (max_episode_steps)
+    """
+
+    metadata = {"render_modes": ["human"]}
+
+    def __init__(self, base_params, resistance_models=None, max_episode_steps: int = 15):
+        """
+        初始化超滤强化学习环境
+        
+        参数:
+            base_params (UFParams): 基础超滤参数配置
+            resistance_models (tuple, optional): 预加载的膜阻力模型,默认None(自动加载)
+            max_episode_steps (int): 每个episode的最大步数,默认15
+                注:每步代表一个超级周期(约2-3天),15步约一个月
+        """
+        super(UFSuperCycleEnv, self).__init__()
+
+        # ========== 参数初始化 ==========
+        self.base_params = base_params  # 基础参数(不变)
+        self.current_params = copy.deepcopy(base_params)  # 当前参数(动态更新)
+        self.max_episode_steps = max_episode_steps  # 最大步数
+        self.current_step = 0  # 当前步数计数器
+
+        # ========== 加载膜阻力模型 ==========
+        if resistance_models is None:
+            # 自动加载模型
+            self.resistance_model_fp, self.resistance_model_bw = load_resistance_models()
+        else:
+            # 使用预加载的模型(用于并行环境避免重复加载)
+            self.resistance_model_fp, self.resistance_model_bw = resistance_models
+
+        # ========== 构建进水时间离散动作空间 ==========
+        # 过滤时长候选值(例:3800, 3860, 3920, ..., 5940, 6000秒)
+        self.L_values = np.arange( self.base_params.L_min_s,
+                                   self.base_params.L_max_s,
+                                   self.base_params.L_step_s )
+        self.action_space = spaces.Discrete(len(self.L_values))
+
+        # ========== 定义状态空间 ==========
+        # 8维连续状态,归一化到 [0, 1]
+        self.observation_space = spaces.Box(
+            low=np.zeros(8, dtype=np.float32),
+            high=np.ones(8, dtype=np.float32),
+            dtype=np.float32
+        )
+
+        # ========== 初始化环境(调用reset) ==========
+        self.reset(seed=None)
+
+    def generate_initial_state(self):
+        """
+        随机生成一个初始状态,并判断其污染增长速率约束是否满足。
+        若满足返回 True,不满足返回 False。
+        (不负责重复采样,由 reset() 控制)
+        """
+
+        # 基础常数
+        A = 128 * 40.0  # 有效膜面积
+
+        # ---- 1. 随机生成 TMP、q_UF、温度 ----
+        self.current_params.TMP0 = np.random.uniform(
+            self.current_params.TMP0_min, self.current_params.TMP0_max
+        )
+        self.current_params.q_UF = np.random.uniform(
+            self.current_params.q_UF_min, self.current_params.q_UF_max
+        )
+        self.current_params.temp = np.random.uniform(
+            self.current_params.temp_min, self.current_params.temp_max
+        )
+
+        q_UF = self.current_params.q_UF
+
+        # ---- 2. 随机 slope、power ----
+        slope = np.random.uniform(self.current_params.slope_min, self.current_params.slope_max)
+        power = np.random.uniform(self.current_params.power_min, self.current_params.power_max)
+
+        # ---- 3. 计算满足条件所需的最小 nuK ----
+        # 计算 t_max
+        t_max = 60 if power >= 1 else 1
+
+        required_nuK_min = (
+                slope * power * (t_max ** (power - 1)) * (A / q_UF)
+        )
+
+        # 若 required_nuK_min 超过可选范围 → 初始状态非法
+        if required_nuK_min > self.current_params.nuK_max:
+            return False
+
+        # ---- 4. 在可行范围中采样 nuK ----
+        nuK_low = max(required_nuK_min, self.current_params.nuK_min)
+        nuK_high = self.current_params.nuK_max
+        self.current_params.nuK = np.random.uniform(nuK_low, nuK_high)
+
+        # ---- 5. 生成 CEB 去除率 ----
+        self.current_params.ceb_removal = np.random.uniform(
+            self.current_params.ceb_removal_min,
+            self.current_params.ceb_removal_max
+        )
+
+        # ---- 6. 计算初始膜阻力 ----
+        self.current_params.R0 = _calculate_resistance(
+            self.current_params.TMP0,
+            self.current_params.q_UF,
+            self.current_params.temp
+        )
+
+        # ---- 7. slope/power 写入 ----
+        self.current_params.slope = slope
+        self.current_params.power = power
+
+        return True
+
+    def reset(self, seed=None, options=None, max_attempts: int = 1000):
+        super().reset(seed=seed)
+
+        attempts = 0
+        while attempts < max_attempts:
+            attempts += 1
+
+            # ==== Step 0: 生成初始状态(含污染速率约束) ====
+            ok_init = self.generate_initial_state()
+            if not ok_init:
+                continue
+
+            # 运行稳定性检测
+            ok_run = self.check_dead_initial_state(
+                max_steps=getattr(self, "max_episode_steps", 15),
+                L_s=3800, t_bw_s=60
+            )
+
+            # 满足 → 成功生成初始状态
+            if ok_run:
+                break
+
+        else:
+            raise RuntimeError(f"在 {max_attempts} 次尝试后仍无法生成可行初始状态。")
+
+        # 初始化环境状态
+        self.current_step = 0
+        self.last_action = (self.base_params.L_min_s, self.base_params.t_bw_min_s)
+        self.max_TMP_during_filtration = self.current_params.TMP0
+        self.TMP0 = self.current_params.TMP0
+
+        return self._get_obs(), {}
+
+    def check_dead_initial_state(self, max_steps: int = None,
+                                 L_s: int = 3800, t_bw_s: int = 60) -> bool:
+        """
+        判断当前环境生成的初始状态是否为可行(non-dead)。
+        使用最保守策略连续模拟 max_steps 次:
+            若任意一次 is_dead_cycle(info) 返回 False 或 next_params[0] < 0,则视为必死状态。
+
+        参数:
+            max_steps: 模拟步数,默认使用 self.max_episode_steps
+            L_s: 过滤时长(s),默认 3800
+            t_bw_s: 物理反洗时长(s),默认 60
+
+        返回:
+            bool: True 表示可行状态(non-dead),False 表示必死状态
+        """
+        if max_steps is None:
+            max_steps = getattr(self, "max_episode_steps", 15)
+
+        if not hasattr(self, "current_params"):
+            raise AttributeError("generate_initial_state() 未设置 current_params。")
+
+        import copy
+        curr_p = copy.deepcopy(self.current_params)
+
+        # 逐步模拟
+        for step in range(max_steps):
+            try:
+                info, next_params = simulate_one_supercycle(curr_p, L_s, t_bw_s)
+            except Exception:
+                # 异常即视为不可行
+                return False
+
+            # 任意一次不可行即为必死状态
+            if not is_dead_cycle(info):
+                return False
+
+            # 新增判断:下一步跨膜压差 TMP 不可能为负
+            if next_params.TMP0 < 0:
+                return False
+
+            # 更新参数,进入下一步模拟
+            curr_p = next_params
+
+        return True
+
+    def _get_state_copy(self):
+        return copy.deepcopy(self.current_params)
+
+    def _get_obs(self):
+        """
+        构建当前环境归一化状态向量
+        """
+        # === 1. 从 current_params 读取动态参数 ===
+        TMP0 = self.current_params.TMP0
+        q_UF = self.current_params.q_UF
+        temp = self.current_params.temp
+
+        # === 2. 计算本周期初始膜阻力 ===
+        R0 = _calculate_resistance(TMP0, q_UF, temp)
+
+        # === 3. 从 current_params 读取膜阻力增长模型参数 ===
+        nuk = self.current_params.nuK
+        slope = self.current_params.slope
+        power = self.current_params.power
+        ceb_removal = self.current_params.ceb_removal
+
+        # === 4. 从 current_params 动态读取上下限 ===
+        TMP0_min, TMP0_max = self.current_params.TMP0_min, self.current_params.global_TMP_hard_limit
+        q_UF_min, q_UF_max = self.current_params.q_UF_min, self.current_params.q_UF_max
+        temp_min, temp_max = self.current_params.temp_min, self.current_params.temp_max
+        nuK_min, nuK_max = self.current_params.nuK_min, self.current_params.nuK_max
+        slope_min, slope_max = self.current_params.slope_min, self.current_params.slope_max
+        power_min, power_max = self.current_params.power_min, self.current_params.power_max
+        ceb_min, ceb_max = self.current_params.ceb_removal_min, self.current_params.ceb_removal_max
+
+        # === 5. 归一化计算(clip防止越界) ===
+        TMP0_norm = np.clip((TMP0 - TMP0_min) / (TMP0_max - TMP0_min), 0, 1)
+        q_UF_norm = np.clip((q_UF - q_UF_min) / (q_UF_max - q_UF_min), 0, 1)
+        temp_norm = np.clip((temp - temp_min) / (temp_max - temp_min), 0, 1)
+
+        # R0 不在 current_params 中定义上下限,设定经验范围
+        R0_norm = np.clip((R0 - 100.0) / (600.0 - 100.0), 0, 1)
+
+        short_term_norm = np.clip((nuk - nuK_min) / (nuK_max - nuK_min), 0, 1)
+        long_term_slope_norm = np.clip((slope - slope_min) / (slope_max - slope_min), 0, 1)
+        long_term_power_norm = np.clip((power - power_min) / (power_max - power_min), 0, 1)
+        ceb_removal_norm = np.clip((ceb_removal - ceb_min) / (ceb_max - ceb_min), 0, 1)
+
+        # === 6. 构建观测向量 ===
+        obs = np.array([
+            TMP0_norm,
+            q_UF_norm,
+            temp_norm,
+            R0_norm,
+            short_term_norm,
+            long_term_slope_norm,
+            long_term_power_norm,
+            ceb_removal_norm
+        ], dtype=np.float32)
+
+        return obs
+
+    def _get_action_values(self, action: int):
+        """ 新版动作解释函数: action 只对应一个 L_s,不再包含 t_bw_s。 """
+
+        L_s = self.L_values[action]
+        return L_s
+
+    def step(self, action):
+        self.current_step += 1
+        L_s= self._get_action_values(action)
+        L_s = np.clip(L_s, self.base_params.L_min_s, self.base_params.L_max_s)
+        t_bw_s = self.current_params.fixed_t_bw_s
+
+        # 模拟超级周期
+        info, next_params = simulate_one_supercycle(self.current_params, L_s, t_bw_s)
+        # 根据 info 判断是否成功
+        feasible = is_dead_cycle(info)  # True 表示成功循环,False 表示失败
+
+        if feasible:
+            # 每步奖励
+            reward = calculate_reward(self.current_params, info)
+            self.current_params = next_params
+            terminated = False
+        else:
+            # 中途失败惩罚
+            reward = -20
+            terminated = True
+
+        # 判断是否到达最大步数
+        truncated = self.current_step >= self.max_episode_steps
+
+        self.last_action = (L_s, t_bw_s)
+        next_obs = self._get_obs()
+
+        info["feasible"] = feasible
+        info["step"] = self.current_step
+
+        # ===================== 测试终末奖励:鼓励 TMP 接近初始状态 =====================
+        # 仅在 episode 自然结束(满步但未提前失败)时触发
+        if truncated and not terminated:
+            TMP_initial = self.TMP0  # reset 时记录的初始 TMP
+            TMP_final = next_obs[0]  # next_obs 提供的最终 TMP
+
+            delta_ratio = abs((TMP_final - TMP_initial) / TMP_initial)
+
+            alpha = 4.0  # TMP 偏差敏感度
+            gamma = 5.0  # 奖励幅度
+            stability_reward = gamma * (np.exp(-alpha * delta_ratio) - 1) # 量级在0到-5之间
+
+            reward += stability_reward
+            terminated = True  # episode 正式结束
+
+
+        return next_obs, reward, terminated, truncated, info
+
+
+
+

+ 536 - 0
algorithm/uf_rl/进水动作版超滤训练源码/fixed_DQN_train.py

@@ -0,0 +1,536 @@
+"""
+DQN 强化学习训练模块
+======================
+本模块实现基于 Stable-Baselines3 的 DQN 强化学习训练流程,包括:
+1. DQNParams: DQN超参数配置类
+2. UFEpisodeRecorder: Episode数据记录器
+3. UFTrainingCallback: 训练回调器
+4. DQNTrainer: DQN训练器封装
+5. train_uf_rl_agent: 主训练函数
+
+DQN算法简介:
+- Deep Q-Network(深度Q网络)
+- 基于价值的强化学习算法
+- 使用经验回放和目标网络稳定训练
+- 适用于离散动作空间
+
+训练流程:
+1. 初始化环境和DQN智能体
+2. 收集经验(exploration)
+3. 从经验池采样训练(exploitation)
+4. 周期性更新目标网络
+5. 记录训练指标到TensorBoard
+"""
+
+import os
+import time
+import random
+import numpy as np
+import torch
+from stable_baselines3 import DQN
+from stable_baselines3.common.monitor import Monitor
+from stable_baselines3.common.vec_env import DummyVecEnv
+from stable_baselines3.common.callbacks import BaseCallback
+
+from fixed_DQN_env import UFParams, UFSuperCycleEnv
+
+
+# ==================== DQN超参数配置类 ====================
+class DQNParams:
+    """
+    DQN 超参数配置类
+    
+    功能:统一管理DQN算法的所有超参数
+    
+    超参数说明:
+    - learning_rate: 神经网络学习率,控制梯度下降的步长
+    - buffer_size: 经验回放缓冲区大小,存储历史经验
+    - learning_starts: 开始训练前先收集的经验数量(warm-up)
+    - batch_size: 每次训练采样的batch大小
+    - gamma: 折扣因子,权衡即时奖励和长期奖励
+    - train_freq: 训练频率,每隔多少步训练一次
+    - target_update_interval: 目标网络更新频率
+    - tau: 软更新系数(soft update)
+    - exploration_*: ε-贪心策略的探索率参数
+    """
+    # ========== 神经网络参数 ==========
+    learning_rate: float = 1e-4  
+    # 学习率,控制神经网络权重更新的步长
+    # 典型范围:1e-5 ~ 1e-3
+    # 过大:训练不稳定;过小:收敛慢
+
+    # ========== 经验回放参数 ==========
+    buffer_size: int = 100000  
+    # 经验回放缓冲区大小(可存储的transition数量)
+    # 作用:打破样本间的时间相关性,提高训练稳定性
+    # 建议:至少存储几个完整episode的经验
+
+    learning_starts: int = 10000  
+    # 开始训练前先收集的步数(预填充缓冲区)
+    # 作用:确保缓冲区有足够的多样性样本再开始训练
+    # 建议:设为buffer_size的10%-20%
+
+    batch_size: int = 32  
+    # 每次训练从缓冲区采样的样本数量
+    # 典型值:32, 64, 128, 256
+    # 过大:显存占用高,训练慢;过小:梯度估计不准确
+
+    # ========== 强化学习参数 ==========
+    gamma: float = 0.95  
+    # 折扣因子(discount factor),γ ∈ [0, 1]
+    # 作用:权衡即时奖励和长期奖励
+    # γ=0:只考虑当前奖励(短视)
+    # γ=1:完全考虑未来奖励(长视)
+    # 通常设为0.9-0.99
+
+    train_freq: int = 4  
+    # 训练频率:每收集多少步执行一次训练
+    # 作用:平衡数据收集和网络更新
+    # 典型值:1(每步训练)或4-16(批量训练)
+
+    # ========== 目标网络参数 ==========
+    target_update_interval: int = 1  
+    # 目标网络更新间隔(硬更新)
+    # 作用:目标网络每隔多少次训练更新一次
+    # 注:使用软更新(tau)时此参数通常设为1
+
+    tau: float = 0.005  
+    # 软更新系数(soft update)
+    # θ_target = τ×θ + (1-τ)×θ_target
+    # τ=1:硬更新(完全复制)
+    # τ<<1:软更新(平滑过渡,更稳定)
+    # 典型值:0.001 - 0.01
+
+    # ========== 探索策略参数(ε-greedy) ==========
+    exploration_initial_eps: float = 1.0  
+    # 初始探索率 ε_0
+    # ε=1:完全随机探索
+    # ε=0:完全利用已学知识
+
+    exploration_fraction: float = 0.3  
+    # 探索率衰减比例
+    # 表示训练总步数的前30%进行ε衰减
+    # 例:总共10万步,前3万步ε从1.0衰减到0.02
+
+    exploration_final_eps: float = 0.02  
+    # 最终探索率 ε_final
+    # 衰减结束后保持此值(保留小概率探索)
+    # 典型值:0.01 - 0.05
+
+    # ========== 日志参数 ==========
+    remark: str = "default"  
+    # 实验备注,用于区分不同训练实验
+    # 会自动添加到TensorBoard日志目录名中
+
+# ==================== Episode数据记录器 ====================
+class UFEpisodeRecorder:
+    """
+    Episode数据记录器
+    
+    功能:
+    - 记录训练过程中每个episode的详细数据
+    - 存储每步的状态、动作、奖励、info等信息
+    - 计算episode级别的统计指标
+    
+    用途:
+    - 训练监控:实时查看智能体表现
+    - 调试分析:定位问题episode
+    - 数据分析:评估策略改进效果
+    """
+
+    def __init__(self):
+        """初始化记录器"""
+        self.episode_data = []      # 存储所有完成的episode数据
+        self.current_episode = []   # 当前正在进行的episode数据
+
+    def record_step(self, obs, action, reward, done, info):
+        """
+        记录单步交互数据
+        
+        参数:
+            obs: 当前状态观测
+            action: 执行的动作
+            reward: 获得的奖励
+            done: 是否结束
+            info: 额外信息字典
+        """
+        # 构建单步数据字典
+        step_data = {
+            "obs": obs.copy(),                        # 状态(深拷贝避免引用问题)
+            "action": action.copy(),                  # 动作
+            "reward": reward,                         # 奖励
+            "done": done,                             # 是否终止
+            "info": info.copy() if info else {}      # 环境信息
+        }
+        
+        # 添加到当前episode
+        self.current_episode.append(step_data)
+
+        # 如果episode结束,保存并重置
+        if done:
+            self.episode_data.append(self.current_episode)
+            self.current_episode = []
+
+    def get_episode_stats(self, episode_idx=-1):
+        """
+        获取指定episode的统计信息
+        
+        参数:
+            episode_idx (int): episode索引,默认-1(最后一个)
+        
+        返回:
+            dict: 包含以下统计指标的字典
+                - total_reward: 总奖励
+                - avg_recovery: 平均回收率
+                - feasible_steps: 可行步数
+                - total_steps: 总步数
+        """
+        if not self.episode_data:
+            return {}
+
+        episode = self.episode_data[episode_idx]
+        
+        # 计算总奖励
+        total_reward = sum(step["reward"] for step in episode)
+        
+        # 计算平均回收率(从info中提取)
+        recovery_values = [
+            step["info"].get("recovery", 0) 
+            for step in episode 
+            if "recovery" in step["info"]
+        ]
+        avg_recovery = np.mean(recovery_values) if recovery_values else 0.0
+        
+        # 计算可行步数(成功的超级周期数)
+        feasible_steps = sum(
+            1 for step in episode 
+            if step["info"].get("feasible", False)
+        )
+
+        return {
+            "total_reward": total_reward,
+            "avg_recovery": avg_recovery,
+            "feasible_steps": feasible_steps,
+            "total_steps": len(episode)
+        }
+
+
+# ==================== 训练回调器 ====================
+class UFTrainingCallback(BaseCallback):
+    """
+    自定义训练回调器
+    
+    功能:
+    - 在每个训练步骤调用,记录数据到recorder
+    - 兼容Stable-Baselines3的回调机制
+    - 不依赖环境内部属性,使用标准接口获取数据
+    
+    回调时机:
+    - _on_step(): 每执行一步环境交互后调用
+    
+    设计特点:
+    1. 从self.locals获取当前步的数据(SB3提供的接口)
+    2. 处理向量化环境(DummyVecEnv)的数据格式
+    3. 自动检测episode结束并触发记录
+    """
+
+    def __init__(self, recorder, verbose=0):
+        """
+        初始化回调器
+        
+        参数:
+            recorder (UFEpisodeRecorder): 数据记录器实例
+            verbose (int): 日志详细程度,0=关闭,1=打印每步信息
+        """
+        super(UFTrainingCallback, self).__init__(verbose)
+        self.recorder = recorder
+
+    def _on_step(self) -> bool:
+        """
+        每步回调函数(Stable-Baselines3标准接口)
+        
+        返回:
+            bool: True表示继续训练,False表示提前终止
+        """
+        try:
+            # 从SB3的self.locals获取当前步数据
+            new_obs = self.locals.get("new_obs")      # 新状态
+            actions = self.locals.get("actions")      # 执行的动作
+            rewards = self.locals.get("rewards")      # 获得的奖励
+            dones = self.locals.get("dones")          # 是否结束
+            infos = self.locals.get("infos")          # 环境信息
+
+            # 处理向量化环境(取第一个环境的数据)
+            if len(new_obs) > 0:
+                step_obs = new_obs[0]
+                step_action = actions[0] if actions is not None else None
+                step_reward = rewards[0] if rewards is not None else 0.0
+                step_done = dones[0] if dones is not None else False
+                step_info = infos[0] if infos is not None else {}
+
+                # 可选:打印当前步信息(用于调试)
+                if self.verbose:
+                    print(f"[Step {self.num_timesteps}] "
+                          f"动作={step_action}, "
+                          f"奖励={step_reward:.3f}, "
+                          f"Done={step_done}")
+
+                # 记录数据到recorder
+                self.recorder.record_step(
+                    obs=step_obs,
+                    action=step_action,
+                    reward=step_reward,
+                    done=step_done,
+                    info=step_info,
+                )
+
+        except Exception as e:
+            # 异常处理:避免回调错误中断训练
+            if self.verbose:
+                print(f"[Callback Error] {e}")
+
+        # 返回True继续训练
+        return True
+
+
+
+
+# ==================== DQN训练器封装类 ====================
+class DQNTrainer:
+    def __init__(self, env, params, callback=None):
+        """
+        初始化训练器
+
+        参数:
+            env: Gymnasium环境
+            params: DQN 超参数配置
+            callback: 可选训练回调
+        """
+        self.env = env
+        self.params = params
+        self.callback = callback
+        self.log_dir = self._create_log_dir()  # 创建日志目录
+        self.model = self._create_model()  # 创建 DQN 模型
+
+    def _create_log_dir(self):
+        """
+        创建 TensorBoard 日志目录,保证 Windows 下路径安全
+
+        返回:
+            str: 可用的日志目录路径
+        """
+        timestamp = time.strftime("%Y%m%d-%H%M%S")
+
+        # 用整数代替浮点数,避免路径中包含小数点
+        lr_int = int(self.params.learning_rate * 1e4)
+        gamma_int = int(self.params.gamma * 100)
+        exp_int = int(self.params.exploration_fraction * 100)
+
+        # 生成目录名
+        log_name = f"DQN_lr{lr_int}_buf{self.params.buffer_size}_bs{self.params.batch_size}_gamma{gamma_int}_exp{exp_int}_{self.params.remark}_{timestamp}"
+
+        # 使用短路径,避免 Windows 路径过长
+        base_dir = r"E:\Greentech\models\uf-rl\uf_dqn_tensorboard"
+        os.makedirs(base_dir, exist_ok=True)
+        log_dir = os.path.join(base_dir, log_name)
+
+        # 尝试创建目录,防止偶发锁或占用
+        attempt = 0
+        while attempt < 5:
+            try:
+                os.makedirs(log_dir, exist_ok=True)
+                if not os.path.isdir(log_dir):
+                    raise RuntimeError(f"{log_dir} 已存在但不是目录!")
+                break
+            except Exception as e:
+                attempt += 1
+                time.sleep(0.1)
+                log_dir += f"_{attempt}"
+        else:
+            raise RuntimeError(f"无法创建日志目录: {log_dir}")
+
+        return log_dir
+
+    def _create_model(self):
+        """
+        创建 Stable-Baselines3 DQN 模型
+        """
+        model = DQN(
+            policy="MlpPolicy",
+            env=self.env,
+            learning_rate=self.params.learning_rate,
+            buffer_size=self.params.buffer_size,
+            learning_starts=self.params.learning_starts,
+            batch_size=self.params.batch_size,
+            gamma=self.params.gamma,
+            train_freq=self.params.train_freq,
+            target_update_interval=1,
+            tau=0.005,
+            exploration_initial_eps=self.params.exploration_initial_eps,
+            exploration_fraction=self.params.exploration_fraction,
+            exploration_final_eps=self.params.exploration_final_eps,
+            verbose=1,
+            tensorboard_log=self.log_dir
+        )
+        return model
+
+    def train(self, total_timesteps: int):
+        """
+        执行训练
+        
+        参数:
+            total_timesteps (int): 总训练步数
+                注:对于超滤环境,每步代表一个超级周期(约2-3天)
+                    150000步 ≈ 10000个episode ≈ 10000个超级周期 ≈ 约54年
+        """
+        if self.callback:
+            # 使用回调器训练
+            self.model.learn(total_timesteps=total_timesteps, callback=self.callback)
+        else:
+            # 不使用回调器训练
+            self.model.learn(total_timesteps=total_timesteps)
+        
+        print(f"✅ 模型训练完成!")
+        print(f"📊 日志保存在:{self.log_dir}")
+        print(f"💡 使用以下命令查看TensorBoard:")
+        print(f"   tensorboard --logdir={self.log_dir}")
+
+    def save(self, path=None):
+        """
+        保存模型
+        
+        参数:
+            path (str, optional): 保存路径,默认保存到日志目录下的dqn_model.zip
+        """
+        if path is None:
+            path = os.path.join(self.log_dir, "dqn_model.zip")
+        self.model.save(path)
+        print(f"💾 模型已保存到:{path}")
+
+    def load(self, path):
+        """
+        加载模型
+        
+        参数:
+            path (str): 模型文件路径(.zip文件)
+        """
+        self.model = DQN.load(path, env=self.env)
+        print(f"📥 模型已从 {path} 加载")
+
+
+# ==================== 辅助函数:随机种子设置 ====================
+def set_global_seed(seed: int):
+    """
+    固定全局随机种子,保证训练可复现
+    
+    参数:
+        seed (int): 随机种子
+    
+    作用:
+    - 固定Python、NumPy、PyTorch的随机数生成器
+    - 确保相同种子产生相同的训练结果
+    - 便于实验对比和问题复现
+    
+    注意:
+    - 即使固定种子,多线程/多进程仍可能产生微小差异
+    - GPU运算的非确定性也可能影响复现性
+    """
+    random.seed(seed)           # Python随机数
+    np.random.seed(seed)        # NumPy随机数
+    torch.manual_seed(seed)     # PyTorch CPU随机数
+    torch.cuda.manual_seed_all(seed)  # PyTorch GPU随机数
+    
+    # 设置PyTorch为确定性模式(可能影响性能)
+    torch.backends.cudnn.deterministic = True
+    torch.backends.cudnn.benchmark = False
+
+
+# ==================== 主训练函数 ====================
+def train_uf_rl_agent(params: UFParams, total_timesteps: int = 10000, seed: int = 2025):
+    """
+    超滤强化学习智能体训练主函数
+    
+    参数:
+        params (UFParams): 超滤环境参数
+        total_timesteps (int): 总训练步数,默认10000
+        seed (int): 随机种子,默认2025
+    
+    返回:
+        DQN: 训练好的DQN模型
+    
+    训练流程:
+    1. 固定随机种子(确保可复现)
+    2. 创建记录器和回调器
+    3. 创建并包装环境(Monitor + DummyVecEnv)
+    4. 初始化DQN训练器
+    5. 执行训练
+    6. 保存模型
+    7. 输出统计信息
+    """
+    # 步骤1:固定随机种子
+    set_global_seed(seed)
+    print(f"🎲 随机种子已设置为: {seed}")
+    
+    # 步骤2:创建数据记录器和回调器
+    recorder = UFEpisodeRecorder()
+    callback = UFTrainingCallback(recorder, verbose=1)
+    
+    # 步骤3:创建环境(使用闭包和向量化)
+    def make_env():
+        """环境工厂函数"""
+        env = UFSuperCycleEnv(params)  # 创建超滤环境
+        env = Monitor(env)              # 包装Monitor(记录episode统计)
+        return env
+    
+    # 向量化环境(即使只有一个环境,也需要向量化以兼容SB3)
+    env = DummyVecEnv([make_env])
+    
+    # 步骤4:创建DQN训练器
+    dqn_params = DQNParams()
+    trainer = DQNTrainer(env, dqn_params, callback=callback)
+    
+    # 步骤5:执行训练
+    trainer.train(total_timesteps)
+    
+    # 步骤6:保存模型
+    trainer.save()
+    
+    # 步骤7:输出最终统计信息
+    stats = callback.recorder.get_episode_stats()
+    print("\n" + "="*60)
+    print("📈 训练统计")
+    print("="*60)
+    print(f"总奖励: {stats.get('total_reward', 0):.2f}")
+    print(f"平均回收率: {stats.get('avg_recovery', 0):.3f}")
+    print(f"可行步数: {stats.get('feasible_steps', 0)}")
+    print(f"总步数: {stats.get('total_steps', 0)}")
+    print("="*60)
+    
+    return trainer.model
+
+
+# ==================== 主程序入口 ====================
+if __name__ == "__main__":
+    """
+    训练脚本入口
+    
+    使用方法:
+        python fixed_DQN_train.py
+    
+    训练参数:
+        - total_timesteps=150000: 总训练步数
+        - 约10000个episode(每个episode最多15步)
+        - 约需训练数小时至数天(取决于硬件)
+    """
+    print("="*60)
+    print("🚀 开始训练超滤强化学习智能体")
+    print("="*60)
+    
+    # 初始化超滤参数
+    params = UFParams()
+    
+    # 执行训练
+    train_uf_rl_agent(params, total_timesteps=200000)
+    
+    print("\n🎉 训练流程全部完成!")
+

+ 0 - 0
algorithm/uf_rl/进水动作版超滤训练源码/model/dqn_model.zip


+ 0 - 0
algorithm/uf_rl/进水动作版超滤训练源码/resistance_model_bw.pth


+ 0 - 0
algorithm/uf_rl/进水动作版超滤训练源码/resistance_model_fp.pth


+ 10 - 0
env.example

@@ -1 +1,11 @@
+DEFAULT_API_BASE_URL="xxx"
+DEFAULT_LOGIN_USER="xxx"
+DEFAULT_LOGIN_PASSWORD="xxx"
+DEFAULT_LOGIN_DEP_ID="xxx"
+DEFAULT_SCADA_SECRET="xxx"
 
+# DQN 轻量模型
+IS_TIMES=0
+PLANT="anzhen"
+
+# DQN 完整模型

+ 9 - 0
main.py

@@ -0,0 +1,9 @@
+from algorithm import uf_rl
+
+uf_rl.dqn_decide_model({
+        'units_to_run': ["UF1"], # 新增输入:本次调用的机组对象名
+        'TMP0': 0.07,            # 原始 TMP0
+        'q_UF': 300,             # 进水流量
+        'temp': 20.0             # 进水温度
+    })
+pass

Some files were not shown because too many files changed in this diff