DQN_env.py 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074
  1. """
  2. 超滤强化学习环境模块
  3. ========================
  4. 本模块定义了超滤系统的强化学习环境,包括:
  5. 1. UFParams: 超滤系统参数配置类
  6. 2. 膜阻力与跨膜压差转换函数
  7. 3. simulate_one_supercycle: 超级周期模拟函数
  8. 4. calculate_reward: 奖励函数
  9. 5. is_dead_cycle: 失败判定函数
  10. 6. UFSuperCycleEnv: Gymnasium环境类
  11. 模块设计说明:
  12. - 基于 Gymnasium (原OpenAI Gym) 标准接口
  13. - 模拟超滤膜的"超级周期"运行(多次物理反洗 + 一次化学反洗)
  14. - 强化学习智能体通过优化过滤时长和反洗时长来最大化回收率并控制污染累积
  15. """
  16. import os
  17. import torch
  18. from pathlib import Path
  19. import numpy as np
  20. import gymnasium as gym
  21. from gymnasium import spaces
  22. from typing import Dict, Tuple, Optional
  23. import torch
  24. import torch.nn as nn
  25. from dataclasses import dataclass, asdict
  26. from UF_resistance_models import ResistanceIncreaseModel, ResistanceDecreaseModel # 导入膜阻力模型
  27. import copy
  28. # ==================== 超滤系统参数配置类 ====================
  29. @dataclass
  30. class UFParams:
  31. """
  32. 超滤系统参数配置类
  33. 功能:统一管理超滤系统的所有运行参数,包括:
  34. - 膜动态运行参数(流量、温度、压差等)
  35. - 膜阻力模型参数(污染增长速率、去除效率等)
  36. - 膜运行约束参数(各参数的上下限)
  37. - 反洗参数(物理反洗、化学反洗)
  38. - 动作搜索范围(过滤时长、反洗时长的取值范围)
  39. - 奖励函数参数
  40. 设计思想:
  41. - 使用 dataclass 装饰器,自动生成 __init__、__repr__ 等方法
  42. - 所有参数带有类型注解和默认值
  43. - 参数值基于锡山水厂的实际运行数据和经验
  44. """
  45. # ========== 膜动态运行参数 ==========
  46. # 这些参数描述超滤膜的实时运行状态,在环境模拟中会动态变化
  47. q_UF: float = 360.0
  48. # 过滤进水流量(m³/h)
  49. # 说明:影响膜通量,进而影响污染速率
  50. # 典型范围:250-400 m³/h
  51. TMP0: float = 0.03
  52. # 初始跨膜压差(MPa,兆帕)
  53. # 说明:反映膜阻力状态,TMP 越高表示膜污染越严重
  54. # 正常范围:0.01-0.035 MPa,超过 0.08 MPa 需停机检修
  55. temp: float = 25.0
  56. # 水温(摄氏度)
  57. # 说明:影响水的粘度,进而影响跨膜压差
  58. # 典型范围:10-40℃,25℃为标准温度
  59. # ========== 膜阻力模型参数 ==========
  60. # 这些参数描述膜污染的物理化学特性,基于历史数据拟合得到
  61. nuK: float = 6.92e+01
  62. # 过滤阶段膜阻力增长系数(缩放后单位)
  63. # 说明:反映水质污染特性,nuK 越大表示水质越差、膜污染越快
  64. # 物理意义:单位膜通量、单位时间的阻力增长速率
  65. slope: float = 3.44e-01
  66. # 全周期不可逆污染增长斜率
  67. # 说明:描述长期不可逆污染的累积速率(幂律模型的系数)
  68. power: float = 1.032
  69. # 全周期不可逆污染增长幂次
  70. # 说明:描述长期污染的非线性特性(幂律模型的指数)
  71. # power > 1 表示污染加速累积,power < 1 表示污染增速放缓
  72. tau_bw_s: float = 30.0
  73. # 物理反洗时长影响的时间尺度(秒)
  74. # 说明:反洗效率的特征时间,当反洗时长 = tau 时,达到约 63% 效率
  75. gamma_t: float = 1.0
  76. # 物理反洗时长作用指数(保留参数,当前未使用)
  77. ceb_removal: float = 150
  78. # 化学增强反洗(CEB)可去除的膜阻力(缩放后单位)
  79. # 说明:CEB 比物理反洗更彻底,可去除部分不可逆污染
  80. # ========== 膜运行约束参数 ==========
  81. # 定义各运行参数的物理约束和安全限制
  82. global_TMP_hard_limit: float = 0.08
  83. # TMP 硬上限(MPa)
  84. # 说明:超过此值将导致episode失败,需立即停机
  85. global_TMP_soft_limit: float = 0.06
  86. # TMP 软上限 (MPa)
  87. # 说明:此上限用于指导奖励函数中膜阻力允许上升值,越接近该上限,系统对膜阻力上升控制的更严格
  88. # --- 初始TMP约束 ---
  89. TMP0_max: float = 0.035 # 初始TMP上限(MPa)
  90. TMP0_min: float = 0.01 # 初始TMP下限(MPa)
  91. # --- 流量约束 ---
  92. q_UF_max: float = 400.0 # 进水流量上限(m³/h)
  93. q_UF_min: float = 250.0 # 进水流量下限(m³/h)
  94. # --- 温度约束 ---
  95. temp_max: float = 40.0 # 温度上限(℃)
  96. temp_min: float = 10.0 # 温度下限(℃)
  97. # --- 短期污染模型参数约束 ---
  98. nuK_max: float = 1e+02 # 阻力增长系数上限
  99. nuK_min: float = 6e+01 # 阻力增长系数下限
  100. # --- 长期污染模型参数约束 ---
  101. slope_max: float = 10 # 不可逆污染斜率上限
  102. slope_min: float = 0.5 # 不可逆污染斜率下限
  103. power_max: float = 1.5 # 不可逆污染幂次上限
  104. power_min: float = 0.5 # 不可逆污染幂次下限
  105. # --- CEB去除能力约束 ---
  106. ceb_removal_max: float = 200 # CEB去除阻力上限(缩放后)
  107. ceb_removal_min: float = 100 # CEB去除阻力下限(缩放后)
  108. # ========== 反洗参数(固定配置) ==========
  109. q_bw_m3ph: float = 1000.0
  110. # 物理反洗流量(m³/h)
  111. # 说明:反洗流量通常为正常过滤流量的 2-3 倍
  112. # ========== CEB 化学反洗参数 ==========
  113. T_ceb_interval_h: float = 48.0
  114. # CEB 间隔时间(小时)
  115. # 说明:每运行约 60 小时执行一次化学增强反洗
  116. v_ceb_m3: float = 20.0
  117. # CEB 用水体积(m³)
  118. t_ceb_s: float = 40 * 60.0
  119. # CEB 时长(秒,这里为 40 分钟)
  120. # ========== 强化学习动作空间搜索范围 ==========
  121. # 定义智能体可选择的动作范围(离散化)
  122. L_min_s: float = 3800.0 # 过滤时长下限(秒,约 63 分钟)
  123. L_max_s: float = 4800.0 # 过滤时长上限,改为 4800s
  124. t_bw_min_s: float = 40.0 # 物理反洗时长下限(秒)
  125. t_bw_max_s: float = 60.0 # 物理反洗时长上限(秒)
  126. # ========== 动作离散化网格 ==========
  127. L_step_s: float = 60.0 # 过滤时长步长(秒)
  128. t_bw_step_s: float = 5.0 # 物理反洗时长步长(秒)
  129. # ========== 奖励函数参数 ==========
  130. k_rec = 5.0 # 回收率敏感度系数(控制回收率奖励的陡峭程度)
  131. k_res = 10.0 # 残余污染敏感度系数(控制污染惩罚的陡峭程度)
  132. rec_low, rec_high = 0.92, 0.99 # 回收率的正常范围
  133. rr0 = 0.08 # 残余污染比例的参考值
  134. # ==================== 辅助函数:膜阻力与跨膜压差转换 ====================
  135. def xishan_viscosity(temp):
  136. """
  137. 锡山水厂水温粘度修正公式
  138. 功能:根据水温计算水的动力粘度(考虑温度影响)
  139. 参数:
  140. temp (float): 水温(摄氏度)
  141. 返回:
  142. float: 水的动力粘度 μ (Pa·s)
  143. 原理:
  144. - 水的粘度随温度升高而降低
  145. - 25℃时纯水粘度约为 0.00089 Pa·s
  146. - 本公式基于锡山水厂PLC系统的经验修正因子
  147. 注意:
  148. - 本公式基于纯水粘度修正
  149. - 实际水厂水质与纯水有差异,对粘度有一定影响
  150. - 未来可根据实际水质进一步校准
  151. """
  152. # 温度归一化(相对于300K)
  153. x = (temp + 273.15) / 300 # 摄氏度转开尔文
  154. # 温度修正因子(经验公式,基于锡山水厂PLC)
  155. factor = 890 / (
  156. 280.68 * x ** -1.9 +
  157. 511.45 * x ** -7.7 +
  158. 61.131 * x ** -19.6 +
  159. 0.45903 * x ** -40
  160. )
  161. # 计算修正后的粘度(25℃标准粘度 / 修正因子)
  162. mu = 0.00089 / factor # [Pa·s]
  163. return mu
  164. def _calculate_resistance(tmp, q_UF, temp):
  165. """
  166. 由跨膜压差计算膜阻力
  167. 功能:根据 Darcy 定律,由跨膜压差反推膜阻力
  168. 参数:
  169. tmp (float): 跨膜压差 TMP (MPa)
  170. q_UF (float): 过滤流量 (m³/h)
  171. temp (float): 水温 (℃)
  172. 返回:
  173. float: 膜阻力 R(已缩放 1e10)
  174. 原理:
  175. Darcy 定律:J = TMP / (μ × R)
  176. 其中:
  177. - J: 膜通量 [m/s]
  178. - TMP: 跨膜压差 [Pa]
  179. - μ: 水的动力粘度 [Pa·s]
  180. - R: 膜阻力 [m⁻¹]
  181. 反解得:R = TMP / (J × μ)
  182. 注意:
  183. - 超滤膜阻力实际量级为 1e12 m⁻¹
  184. - 为便于数值计算,已缩放 1e10 倍至 1e2 量级
  185. """
  186. # 膜有效面积(锡山水厂配置:128组 × 40 m²/组)
  187. A = 128 * 40 # [m²]
  188. # 温度修正后的水粘度
  189. mu = xishan_viscosity(temp) # [Pa·s]
  190. # 跨膜压差单位转换:MPa → Pa
  191. TMP_Pa = tmp * 1e6 # [Pa]
  192. # 计算膜通量:流量 / 面积
  193. J = q_UF / A / 3600 # [m³/h] → [m³/(m²·s)] = [m/s]
  194. # 物理约束检查:通量和粘度必须为正
  195. if J <= 0 or mu <= 0:
  196. return np.nan
  197. # 根据 Darcy 定律计算膜阻力并缩放
  198. R = TMP_Pa / (J * mu) / 1e10 # [m⁻¹] → [缩放单位]
  199. return float(R)
  200. def _calculate_tmp(R, q_UF, temp):
  201. """
  202. 由膜阻力计算跨膜压差
  203. 功能:根据 Darcy 定律,由膜阻力计算跨膜压差(_calculate_resistance 的逆运算)
  204. 参数:
  205. R (float): 膜阻力(已缩放 1e10)
  206. q_UF (float): 过滤流量 (m³/h)
  207. temp (float): 水温 (℃)
  208. 返回:
  209. float: 跨膜压差 TMP (MPa)
  210. 原理:
  211. Darcy 定律:TMP = J × μ × R
  212. 其中:
  213. - J: 膜通量 [m/s]
  214. - μ: 水的动力粘度 [Pa·s]
  215. - R: 膜阻力 [m⁻¹]
  216. """
  217. # 膜有效面积
  218. A = 128 * 40 # [m²]
  219. # 温度修正后的水粘度
  220. mu = xishan_viscosity(temp) # [Pa·s]
  221. # 计算膜通量
  222. J = q_UF / A / 3600 # [m/s]
  223. # 根据 Darcy 定律计算跨膜压差(还原缩放)
  224. TMP_Pa = R * J * mu * 1e10 # [缩放单位] → [Pa]
  225. # 单位转换:Pa → MPa
  226. tmp = TMP_Pa / 1e6 # [MPa]
  227. return float(tmp)
  228. # ==================== 膜阻力模型加载函数 ====================
  229. def load_resistance_models():
  230. """
  231. 加载膜阻力预测模型(单例模式)
  232. 功能:
  233. - 加载预训练的膜阻力上升模型和下降模型
  234. - 使用全局变量实现单例模式,避免重复加载
  235. - 仅在首次调用时执行加载操作
  236. 返回:
  237. tuple: (resistance_model_fp, resistance_model_bw)
  238. - resistance_model_fp: 过滤阶段阻力上升模型
  239. - resistance_model_bw: 反洗阶段阻力下降模型
  240. 注意:
  241. - 模型文件必须与本脚本位于同一目录
  242. - 模型已设置为推理模式(eval),不会更新参数
  243. """
  244. # 声明全局变量(实现单例模式)
  245. global resistance_model_fp, resistance_model_bw
  246. # 检查模型是否已加载(避免重复加载)
  247. if "resistance_model_fp" in globals() and resistance_model_fp is not None:
  248. return resistance_model_fp, resistance_model_bw
  249. print("🔄 正在加载膜阻力模型...")
  250. # 初始化模型对象
  251. resistance_model_fp = ResistanceIncreaseModel()
  252. resistance_model_bw = ResistanceDecreaseModel()
  253. # 获取当前脚本所在目录
  254. base_dir = Path(__file__).resolve().parent
  255. # 构造模型文件路径
  256. fp_path = base_dir / "resistance_model_fp.pth" # 过滤阶段模型
  257. bw_path = base_dir / "resistance_model_bw.pth" # 反洗阶段模型
  258. # 检查模型文件是否存在
  259. assert fp_path.exists(), f"缺少膜阻力上升模型文件: {fp_path.name}"
  260. assert bw_path.exists(), f"缺少膜阻力下降模型文件: {bw_path.name}"
  261. # 加载模型权重(map_location="cpu" 确保在没有GPU的环境也能运行)
  262. resistance_model_fp.load_state_dict(torch.load(fp_path, map_location="cpu"))
  263. resistance_model_bw.load_state_dict(torch.load(bw_path, map_location="cpu"))
  264. # 设置为推理模式(禁用 dropout、batchnorm 等训练特性)
  265. resistance_model_fp.eval()
  266. resistance_model_bw.eval()
  267. print("✅ 膜阻力模型加载成功!")
  268. return resistance_model_fp, resistance_model_bw
  269. # ==================== 膜阻力模型调用函数 ====================
  270. def _delta_resistance(p, L_s: float) -> float:
  271. """
  272. 计算过滤阶段膜阻力上升量
  273. 功能:调用预训练的膜阻力上升模型
  274. 参数:
  275. p (UFParams): 超滤运行参数
  276. L_s (float): 过滤时长(秒)
  277. 返回:
  278. float: 膜阻力上升量 ΔR
  279. """
  280. return resistance_model_fp(p, L_s)
  281. def phi_bw_of(p, R0: float, R_end: float, L_h_next_start: float, t_bw_s: float) -> float:
  282. """
  283. 计算物理反洗可去除的膜阻力
  284. 功能:调用预训练的膜阻力下降模型
  285. 参数:
  286. p (UFParams): 超滤运行参数
  287. R0 (float): 本超级周期初始膜阻力
  288. R_end (float): 过滤结束时的膜阻力
  289. L_h_start (float): 本小周期起始累积运行时间(小时)
  290. L_h_next_start (float): 下一小周期起始累积运行时间(小时)
  291. t_bw_s (float): 物理反洗时长(秒)
  292. 返回:
  293. float: 物理反洗去除的膜阻力量
  294. """
  295. return resistance_model_bw(p, R0, R_end, L_h_next_start, t_bw_s)
  296. def _v_bw_m3(p, t_bw_s: float) -> float:
  297. """
  298. 计算物理反洗水耗
  299. 参数:
  300. p (UFParams): 超滤运行参数(包含反洗流量 q_bw_m3ph)
  301. t_bw_s (float): 物理反洗时长(秒)
  302. 返回:
  303. float: 反洗水耗(立方米)
  304. 公式:
  305. V = Q × t
  306. 其中 Q 为反洗流量 [m³/h],t 为反洗时长 [h]
  307. """
  308. return float(p.q_bw_m3ph * (float(t_bw_s) / 3600.0))
  309. def simulate_one_supercycle(p: UFParams, L_s: float, t_bw_s: float):
  310. """
  311. 模拟一个完整的超级周期(Super Cycle)
  312. 功能:
  313. - 模拟超滤膜的完整运行周期:多次"过滤+物理反洗"小周期 + 一次化学增强反洗(CEB)
  314. - 计算周期内的各项性能指标(回收率、TMP变化、污染累积等)
  315. - 返回周期结束后的状态参数,用于下一周期的模拟
  316. 参数:
  317. p (UFParams): 当前超滤系统参数
  318. L_s (float): 过滤时长(秒)
  319. t_bw_s (float): 物理反洗时长(秒)
  320. 返回:
  321. tuple: (info, next_params)
  322. - info (dict): 本周期的性能指标字典
  323. - next_params (UFParams): 更新后的系统参数(用于下一周期)
  324. 超级周期结构:
  325. 超级周期 = k 次小周期 + 1 次 CEB
  326. 小周期 = 过滤 + 物理反洗
  327. 时间轴:
  328. |-- 小周期1 --|-- 小周期2 --|-- ... --|-- 小周期k --|-- CEB --|
  329. | 滤 | 物洗 | 滤 | 物洗 | ... | 滤 | 物洗 | 化洗 |
  330. """
  331. # ========== 初始化周期参数 ==========
  332. L_h = float(L_s) / 3600.0 # 过滤时长转换:秒 → 小时
  333. # 初始状态
  334. tmp = p.TMP0 # 当前跨膜压差
  335. R0 = _calculate_resistance(p.TMP0, p.q_UF, p.temp) # 初始膜阻力
  336. # 跟踪变量(用于记录周期内的极值)
  337. max_tmp_during_filtration = tmp # 周期内最大TMP
  338. min_tmp_during_filtration = tmp # 周期内最小TMP
  339. max_residual_increase = 0.0 # 周期内最大残余污染增量
  340. # ========== 计算小周期数量 ==========
  341. # 小周期时长 = 过滤时长 + 物理反洗时长
  342. t_small_cycle_h = (L_s + t_bw_s) / 3600.0 # [小时]
  343. # 计算一个超级周期内包含多少个小周期
  344. # k = floor(CEB间隔时间 / 小周期时长)
  345. k_bw_per_ceb = int(np.floor(p.T_ceb_interval_h / t_small_cycle_h))
  346. if k_bw_per_ceb < 1:
  347. k_bw_per_ceb = 1 # 至少包含1个小周期
  348. # ========== 吨水电耗查找表 ==========
  349. # 键:过滤时长(秒),值:吨水电耗(kWh/m³)
  350. energy_lookup = {
  351. 3600: 0.1034, 3660: 0.1031, 3720: 0.1029, 3780: 0.1026,
  352. 3840: 0.1023, 3900: 0.1021, 3960: 0.1019, 4020: 0.1017,
  353. 4080: 0.1015, 4140: 0.1012, 4200: 0.1011, 4260: 0.1008,
  354. 4320: 0.1007, 4380: 0.1005, 4440: 0.1003, 4500: 0.1001,
  355. 4560: 0.0999, 4620: 0.0998, 4680: 0.0996, 4740: 0.0995,
  356. 4800: 0.0993,
  357. }
  358. # ========== 循环模拟每个小周期(过滤 + 物理反洗) ==========
  359. for idx in range(k_bw_per_ceb):
  360. # --- 小周期开始状态 ---
  361. tmp_run_start = tmp # 本次过滤开始时的TMP
  362. q_UF = p.q_UF # 过滤流量
  363. temp = p.temp # 水温
  364. # --- 过滤阶段:膜阻力上升 ---
  365. R_run_start = _calculate_resistance(tmp_run_start, q_UF, temp) # 过滤开始时的膜阻力
  366. d_R = _delta_resistance(p, L_s) # 过滤阶段膜阻力增量
  367. R_peak = R_run_start + d_R # 过滤结束时的膜阻力(峰值)
  368. tmp_peak = _calculate_tmp(R_peak, q_UF, temp) # 过滤结束时的TMP(峰值)
  369. # 更新TMP极值记录
  370. max_tmp_during_filtration = max(max_tmp_during_filtration, tmp_peak)
  371. min_tmp_during_filtration = min(min_tmp_during_filtration, tmp_run_start)
  372. # --- 物理反洗阶段:膜阻力下降 ---
  373. # 计算累积运行时间(用于长期污染模型)
  374. L_h_next_start = (L_s + t_bw_s) / 3600.0 * (idx + 1) # 下一小周期起始时间
  375. # 调用膜阻力下降模型,计算物理反洗可去除的阻力
  376. reversible_R = phi_bw_of(p, R0, R_peak, L_h_next_start, t_bw_s)
  377. # 物理反洗后的膜阻力
  378. R_after_bw = R_peak - reversible_R
  379. tmp_after_bw = _calculate_tmp(R_after_bw, q_UF, temp)
  380. # 计算残余污染增量(反洗后的TMP相对本次开始的增加)
  381. residual_inc = tmp_after_bw - tmp_run_start
  382. max_residual_increase = max(max_residual_increase, residual_inc)
  383. # 更新TMP(作为下一小周期的起始TMP)
  384. tmp = tmp_after_bw
  385. # ========== 化学增强反洗 (CEB) ==========
  386. # CEB比物理反洗更彻底,可去除部分不可逆污染
  387. R_after_ceb = R_peak - p.ceb_removal # CEB后的膜阻力
  388. tmp_after_ceb = _calculate_tmp(R_after_ceb, q_UF, temp) # CEB后的TMP
  389. # ============================================================
  390. # 计算周期性能指标
  391. # ============================================================
  392. # ========== 水量平衡计算 ==========
  393. # 进水总量(所有小周期的过滤进水之和)
  394. V_feed_super = k_bw_per_ceb * p.q_UF * L_h # [m³]
  395. # 损失水量(物理反洗 + 化学反洗)
  396. V_loss_super = k_bw_per_ceb * _v_bw_m3(p, t_bw_s) + p.v_ceb_m3 # [m³]
  397. # 净产水量
  398. V_net = max(0.0, V_feed_super - V_loss_super) # [m³]
  399. # 回收率(净产水 / 进水总量)
  400. # 加1e-12避免除零,max确保非负
  401. recovery = max(0.0, V_net / max(V_feed_super, 1e-12)) # [无量纲,0-1之间]
  402. # ========== 时间与能耗计算 ==========
  403. # 超级周期总时长
  404. T_super_h = k_bw_per_ceb * (L_s + t_bw_s) / 3600.0 + p.t_ceb_s / 3600.0 # [小时]
  405. # 日均产水时间(24小时内实际产水的时间)
  406. daily_prod_time_h = k_bw_per_ceb * L_h / T_super_h * 24.0 # [小时]
  407. # 吨水电耗(从查找表获取最接近的值)
  408. closest_L = min(energy_lookup.keys(), key=lambda x: abs(x - L_s))
  409. ton_water_energy = energy_lookup[closest_L] # [kWh/m³]
  410. # ===== 新指标:膜阻力允许上升空间 =====
  411. R_max = _calculate_resistance(max_tmp_during_filtration, p.q_UF, p.temp)
  412. R_soft_limit = _calculate_resistance(p.global_TMP_soft_limit, p.q_UF, p.temp)
  413. delta_R_allow = max(R_soft_limit - R_max, 1e-6) # 供奖励函数使用的“污染允许增长空间”
  414. # ========== 构建性能指标字典 ==========
  415. info = {
  416. # 运行参数
  417. "q_UF": p.q_UF, # 过滤流量
  418. "temp": p.temp, # 水温
  419. # 水量指标
  420. "recovery": recovery, # 回收率
  421. "V_feed_super_m3": V_feed_super, # 进水总量
  422. "V_loss_super_m3": V_loss_super, # 损失水量
  423. "V_net_super_m3": V_net, # 净产水量
  424. # 时间指标
  425. "supercycle_time_h": T_super_h, # 超级周期时长
  426. "daily_prod_time_h": daily_prod_time_h, # 日均产水时间
  427. "k_bw_per_ceb": k_bw_per_ceb, # 小周期数量
  428. # TMP指标
  429. "max_TMP_during_filtration": max_tmp_during_filtration, # 周期内最大TMP
  430. "min_TMP_during_filtration": min_tmp_during_filtration, # 周期内最小TMP
  431. "global_TMP_limit": p.global_TMP_hard_limit, # TMP限制
  432. "TMP0": p.TMP0, # 周期初始TMP
  433. "TMP_after_ceb": tmp_after_ceb, # CEB后TMP
  434. # 膜阻力指标
  435. "R0": R0, # 周期初始膜阻力
  436. "R_after_ceb": R_after_ceb, # CEB后膜阻力
  437. "max_residual_increase_per_run": max_residual_increase, # 最大残余污染增量
  438. "delta_R_allow": delta_R_allow, # 污染允许增长空间
  439. # 能耗指标
  440. "ton_water_energy_kWh_per_m3": ton_water_energy, # 吨水电耗
  441. }
  442. # ============================================================
  443. # 状态更新:生成下一周期的初始参数
  444. # ============================================================
  445. # 深拷贝当前参数(避免修改原对象)
  446. next_params = copy.deepcopy(p)
  447. # ========== 必须更新的参数 ==========
  448. # 更新TMP为CEB后的值(作为下一超级周期的起始TMP)
  449. next_params.TMP0 = tmp_after_ceb
  450. # ========== 可选更新的参数(当前保持不变) ==========
  451. # 这些参数可根据实际情况动态调整,预留扩展接口
  452. next_params.slope = p.slope # 长期污染斜率
  453. next_params.power = p.power # 长期污染幂次
  454. next_params.ceb_removal = p.ceb_removal # CEB去除能力
  455. next_params.nuK = p.nuK # 短期污染系数
  456. next_params.q_UF = p.q_UF # 过滤流量
  457. next_params.temp = p.temp # 水温
  458. return info, next_params
  459. def calculate_reward(p: UFParams, info: dict) -> float:
  460. """
  461. 计算强化学习奖励函数
  462. 功能:
  463. - 平衡回收率和残余污染两个目标
  464. - TMP不直接参与奖励计算(通过失败判定间接影响)
  465. - 使用 tanh 函数实现平滑的非线性奖励
  466. 参数:
  467. p (UFParams): 超滤参数(包含奖励函数超参数)
  468. info (dict): 周期性能指标字典
  469. 返回:
  470. float: 奖励值(通常在 -2 到 +2 之间)
  471. 设计思想:
  472. - 高回收率 → 水资源利用率高 → 正奖励
  473. - 低残余污染 → 膜长期稳定运行 → 正奖励
  474. - 两者需要权衡:过短的过滤时间提高回收率但污染去除不彻底;
  475. 过长的过滤时间污染控制好但回收率下降
  476. 参考点设计:
  477. - (recovery=0.97, residual_ratio=0.1) → reward ≈ 0(高回收但污染高)
  478. - (recovery=0.90, residual_ratio=0.0) → reward ≈ 0(低污染但回收率低)
  479. - (recovery≈0.94, residual_ratio≈0.05) → reward > 0(平衡点)
  480. """
  481. # ========== 提取性能指标 ==========
  482. recovery = info["recovery"] # 回收率 [0-1]
  483. # 污染比例:实际上升的阻力 / 允许上升的阻力
  484. # 允许上升的阻力值 = 当前阻力值软上限 - 当前阻力
  485. residual_ratio = (info["R_after_ceb"] - info["R0"]) / info["delta_R_allow"]
  486. # ========== 回收率奖励项 ==========
  487. # 将回收率归一化到 [0, 1] 区间(基于预期范围)
  488. rec_norm = (recovery - p.rec_low) / (p.rec_high - p.rec_low)
  489. # 使用 tanh 函数构建平滑的 S 型奖励曲线
  490. # - rec_norm = 0.5 时(回收率处于中间值),rec_reward = 0
  491. # - rec_norm > 0.5 时,rec_reward > 0(鼓励高回收率)
  492. # - rec_norm < 0.5 时,rec_reward < 0(惩罚低回收率)
  493. # - k_rec 控制曲线陡峭程度,越大变化越陡
  494. rec_reward = np.clip(np.tanh(p.k_rec * (rec_norm - 0.5)), -1, 1)
  495. # ========== 污染惩罚项 ==========
  496. # 使用 tanh 函数构建惩罚曲线
  497. # - residual_ratio < rr0 时,res_penalty > 0(奖励低污染)
  498. # - residual_ratio > rr0 时,res_penalty < 0(惩罚高污染)
  499. # - k_res 控制曲线陡峭程度
  500. res_penalty = -np.tanh(p.k_res * (residual_ratio / p.rr0 - 1))
  501. # ========== 组合奖励 ==========
  502. # 简单线性组合两项(也可以加权)
  503. total_reward = rec_reward + res_penalty
  504. # 可选:添加平移项使特定点的奖励为零(当前未使用)
  505. # total_reward -= offset
  506. return total_reward
  507. def is_dead_cycle(info: dict) -> bool:
  508. """
  509. 判断当前超级周期是否成功(可行)
  510. 功能:
  511. - 检查超级周期是否违反运行约束
  512. - 用于强化学习的失败判定(terminated条件)
  513. - True表示成功,False表示失败
  514. 参数:
  515. info (dict): simulate_one_supercycle() 返回的性能指标字典
  516. 返回:
  517. bool: True表示成功周期,False表示失败周期
  518. 失败条件(任一满足即失败):
  519. 1. TMP超限:max_TMP > global_TMP_limit
  520. - 原因:TMP过高会损坏膜或影响产水质量
  521. - 阈值:0.08 MPa(可配置)
  522. 2. 回收率过低:recovery < 0.75
  523. - 原因:回收率太低说明反洗水耗过大,经济性差
  524. - 阈值:75%(可调整)
  525. 3. 残余污染累积过快:(R_after_ceb - R0) / R0 > 0.1
  526. - 原因:单个超级周期污染增长超过10%,长期运行不可持续
  527. - 阈值:10%(可调整)
  528. """
  529. # ========== 获取关键指标 ==========
  530. TMP_limit = info.get("global_TMP_limit", 0.08) # TMP硬约束上限
  531. max_tmp = info.get("max_TMP_during_filtration", 0) # 周期内最大TMP
  532. recovery = info.get("recovery", 1.0) # 回收率
  533. R_after_ceb = info.get("R_after_ceb", 0) # CEB后膜阻力
  534. R0 = info.get("R0", 1e-6) # 初始膜阻力
  535. delta_R_allow = info.get("delta_R_allow", 1e-6) # 允许上升的膜阻力(加小值避免除零)
  536. # ========== 失败条件检查 ==========
  537. # 条件1:TMP超限
  538. if max_tmp > TMP_limit:
  539. return False # 失败
  540. # 条件2:回收率过低
  541. if recovery < 0.75:
  542. return False # 失败
  543. # 条件3:污染增长量超过容许范围
  544. residual_increase = (R_after_ceb - R0) / delta_R_allow
  545. if residual_increase > 1/15:
  546. return False # 失败
  547. # 所有条件通过
  548. return True # 成功
  549. class UFSuperCycleEnv(gym.Env):
  550. """
  551. 超滤系统强化学习环境(Gymnasium标准接口)
  552. 功能:
  553. - 模拟超滤膜的超级周期运行
  554. - 智能体在每个超级周期选择过滤时长和反洗时长
  555. - 目标:最大化回收率同时控制污染累积
  556. 状态空间 (8维,归一化到 [0,1]):
  557. 1. TMP0: 初始跨膜压差
  558. 2. q_UF: 过滤流量
  559. 3. temp: 水温
  560. 4. R0: 初始膜阻力
  561. 5. nuK: 短期污染系数
  562. 6. slope: 长期污染斜率
  563. 7. power: 长期污染幂次
  564. 8. ceb_removal: CEB去除能力
  565. 动作空间 (离散):
  566. - 二维离散动作组合:(过滤时长, 反洗时长)
  567. - 过滤时长: L_min_s ~ L_max_s,步长 L_step_s
  568. - 反洗时长: t_bw_min_s ~ t_bw_max_s,步长 t_bw_step_s
  569. - 总动作数 = len(L_values) × len(t_bw_values)
  570. 奖励机制:
  571. - 基于回收率和残余污染的平衡
  572. - 失败 (TMP超限、回收率过低、污染过快) 时给予大负奖励 (-10)
  573. 终止条件:
  574. - terminated: 违反运行约束(失败)
  575. - truncated: 达到最大步数 (max_episode_steps)
  576. """
  577. metadata = {"render_modes": ["human"]}
  578. def __init__(self, base_params, resistance_models=None, max_episode_steps: int = 15):
  579. """
  580. 初始化超滤强化学习环境
  581. 参数:
  582. base_params (UFParams): 基础超滤参数配置
  583. resistance_models (tuple, optional): 预加载的膜阻力模型,默认None(自动加载)
  584. max_episode_steps (int): 每个episode的最大步数,默认15
  585. 注:每步代表一个超级周期(约2-3天),15步约一个月
  586. """
  587. super(UFSuperCycleEnv, self).__init__()
  588. # ========== 参数初始化 ==========
  589. self.base_params = base_params # 基础参数(不变)
  590. self.current_params = copy.deepcopy(base_params) # 当前参数(动态更新)
  591. self.max_episode_steps = max_episode_steps # 最大步数
  592. self.current_step = 0 # 当前步数计数器
  593. # ========== 加载膜阻力模型 ==========
  594. if resistance_models is None:
  595. # 自动加载模型
  596. self.resistance_model_fp, self.resistance_model_bw = load_resistance_models()
  597. else:
  598. # 使用预加载的模型(用于并行环境避免重复加载)
  599. self.resistance_model_fp, self.resistance_model_bw = resistance_models
  600. # ========== 构建离散动作空间 ==========
  601. # 过滤时长候选值(例:3800, 3860, 3920, ..., 5940, 6000秒)
  602. self.L_values = np.arange(
  603. self.base_params.L_min_s,
  604. self.base_params.L_max_s,
  605. self.base_params.L_step_s
  606. )
  607. # 反洗时长候选值(例:40, 45, 50, 55, 60秒)
  608. self.t_bw_values = np.arange(
  609. self.base_params.t_bw_min_s,
  610. self.base_params.t_bw_max_s + self.base_params.t_bw_step_s, # +step确保包含上限
  611. self.base_params.t_bw_step_s
  612. )
  613. self.num_L = len(self.L_values) # 过滤时长选项数
  614. self.num_bw = len(self.t_bw_values) # 反洗时长选项数
  615. # 定义单一离散动作空间(笛卡尔积编码)
  616. # 动作编号 action = L_idx × num_bw + t_bw_idx
  617. # 例:num_L=37, num_bw=5 → 总动作数=185
  618. self.action_space = spaces.Discrete(self.num_L * self.num_bw)
  619. # ========== 定义状态空间 ==========
  620. # 8维连续状态,归一化到 [0, 1]
  621. self.observation_space = spaces.Box(
  622. low=np.zeros(8, dtype=np.float32),
  623. high=np.ones(8, dtype=np.float32),
  624. dtype=np.float32
  625. )
  626. # ========== 初始化环境(调用reset) ==========
  627. self.reset(seed=None)
  628. def generate_initial_state(self):
  629. """
  630. 随机生成一个初始状态,并判断其污染增长速率约束是否满足。
  631. 若满足返回 True,不满足返回 False。
  632. (不负责重复采样,由 reset() 控制)
  633. """
  634. # 基础常数
  635. A = 128 * 40.0 # 有效膜面积
  636. # ---- 1. 随机生成 TMP、q_UF、温度 ----
  637. self.current_params.TMP0 = np.random.uniform(
  638. self.current_params.TMP0_min, self.current_params.TMP0_max
  639. )
  640. self.current_params.q_UF = np.random.uniform(
  641. self.current_params.q_UF_min, self.current_params.q_UF_max
  642. )
  643. self.current_params.temp = np.random.uniform(
  644. self.current_params.temp_min, self.current_params.temp_max
  645. )
  646. q_UF = self.current_params.q_UF
  647. # ---- 2. 随机 slope、power ----
  648. slope = np.random.uniform(self.current_params.slope_min, self.current_params.slope_max)
  649. power = np.random.uniform(self.current_params.power_min, self.current_params.power_max)
  650. # ---- 3. 计算满足条件所需的最小 nuK ----
  651. # 计算 t_max
  652. t_max = 60 if power >= 1 else 1
  653. required_nuK_min = (
  654. slope * power * (t_max ** (power - 1)) * (A / q_UF)
  655. )
  656. # 若 required_nuK_min 超过可选范围 → 初始状态非法
  657. if required_nuK_min > self.current_params.nuK_max:
  658. return False
  659. # ---- 4. 在可行范围中采样 nuK ----
  660. nuK_low = max(required_nuK_min, self.current_params.nuK_min)
  661. nuK_high = self.current_params.nuK_max
  662. self.current_params.nuK = np.random.uniform(nuK_low, nuK_high)
  663. # ---- 5. 生成 CEB 去除率 ----
  664. self.current_params.ceb_removal = np.random.uniform(
  665. self.current_params.ceb_removal_min,
  666. self.current_params.ceb_removal_max
  667. )
  668. # ---- 6. 计算初始膜阻力 ----
  669. self.current_params.R0 = _calculate_resistance(
  670. self.current_params.TMP0,
  671. self.current_params.q_UF,
  672. self.current_params.temp
  673. )
  674. # ---- 7. slope/power 写入 ----
  675. self.current_params.slope = slope
  676. self.current_params.power = power
  677. return True
  678. def reset(self, seed=None, options=None, max_attempts: int = 1000):
  679. super().reset(seed=seed)
  680. attempts = 0
  681. while attempts < max_attempts:
  682. attempts += 1
  683. # ==== Step 0: 生成初始状态(含污染速率约束) ====
  684. ok_init = self.generate_initial_state()
  685. if not ok_init:
  686. continue
  687. # 运行稳定性检测
  688. ok_run = self.check_dead_initial_state(
  689. max_steps=getattr(self, "max_episode_steps", 15),
  690. L_s=3800, t_bw_s=60
  691. )
  692. # 满足 → 成功生成初始状态
  693. if ok_run:
  694. break
  695. else:
  696. raise RuntimeError(f"在 {max_attempts} 次尝试后仍无法生成可行初始状态。")
  697. # 初始化环境状态
  698. self.current_step = 0
  699. self.last_action = (self.base_params.L_min_s, self.base_params.t_bw_min_s)
  700. self.max_TMP_during_filtration = self.current_params.TMP0
  701. self.TMP0 = self.current_params.TMP0
  702. return self._get_obs(), {}
  703. def check_dead_initial_state(self, max_steps: int = None,
  704. L_s: int = 3800, t_bw_s: int = 60) -> bool:
  705. """
  706. 判断当前环境生成的初始状态是否为可行(non-dead)。
  707. 使用最保守策略连续模拟 max_steps 次:
  708. 若任意一次 is_dead_cycle(info) 返回 False 或 next_params[0] < 0,则视为必死状态。
  709. 参数:
  710. max_steps: 模拟步数,默认使用 self.max_episode_steps
  711. L_s: 过滤时长(s),默认 3800
  712. t_bw_s: 物理反洗时长(s),默认 60
  713. 返回:
  714. bool: True 表示可行状态(non-dead),False 表示必死状态
  715. """
  716. if max_steps is None:
  717. max_steps = getattr(self, "max_episode_steps", 15)
  718. if not hasattr(self, "current_params"):
  719. raise AttributeError("generate_initial_state() 未设置 current_params。")
  720. import copy
  721. curr_p = copy.deepcopy(self.current_params)
  722. # 逐步模拟
  723. for step in range(max_steps):
  724. try:
  725. info, next_params = simulate_one_supercycle(curr_p, L_s, t_bw_s)
  726. except Exception:
  727. # 异常即视为不可行
  728. return False
  729. # 任意一次不可行即为必死状态
  730. if not is_dead_cycle(info):
  731. return False
  732. # 新增判断:下一步跨膜压差 TMP 不可能为负
  733. if next_params.TMP0 < 0:
  734. return False
  735. # 更新参数,进入下一步模拟
  736. curr_p = next_params
  737. return True
  738. def _get_state_copy(self):
  739. return copy.deepcopy(self.current_params)
  740. def _get_obs(self):
  741. """
  742. 构建当前环境归一化状态向量
  743. """
  744. # === 1. 从 current_params 读取动态参数 ===
  745. TMP0 = self.current_params.TMP0
  746. q_UF = self.current_params.q_UF
  747. temp = self.current_params.temp
  748. # === 2. 计算本周期初始膜阻力 ===
  749. R0 = _calculate_resistance(TMP0, q_UF, temp)
  750. # === 3. 从 current_params 读取膜阻力增长模型参数 ===
  751. nuk = self.current_params.nuK
  752. slope = self.current_params.slope
  753. power = self.current_params.power
  754. ceb_removal = self.current_params.ceb_removal
  755. # === 4. 从 current_params 动态读取上下限 ===
  756. TMP0_min, TMP0_max = self.current_params.TMP0_min, self.current_params.global_TMP_hard_limit
  757. q_UF_min, q_UF_max = self.current_params.q_UF_min, self.current_params.q_UF_max
  758. temp_min, temp_max = self.current_params.temp_min, self.current_params.temp_max
  759. nuK_min, nuK_max = self.current_params.nuK_min, self.current_params.nuK_max
  760. slope_min, slope_max = self.current_params.slope_min, self.current_params.slope_max
  761. power_min, power_max = self.current_params.power_min, self.current_params.power_max
  762. ceb_min, ceb_max = self.current_params.ceb_removal_min, self.current_params.ceb_removal_max
  763. # === 5. 归一化计算(clip防止越界) ===
  764. TMP0_norm = np.clip((TMP0 - TMP0_min) / (TMP0_max - TMP0_min), 0, 1)
  765. q_UF_norm = np.clip((q_UF - q_UF_min) / (q_UF_max - q_UF_min), 0, 1)
  766. temp_norm = np.clip((temp - temp_min) / (temp_max - temp_min), 0, 1)
  767. # R0 不在 current_params 中定义上下限,设定经验范围
  768. R0_norm = np.clip((R0 - 100.0) / (600.0 - 100.0), 0, 1)
  769. short_term_norm = np.clip((nuk - nuK_min) / (nuK_max - nuK_min), 0, 1)
  770. long_term_slope_norm = np.clip((slope - slope_min) / (slope_max - slope_min), 0, 1)
  771. long_term_power_norm = np.clip((power - power_min) / (power_max - power_min), 0, 1)
  772. ceb_removal_norm = np.clip((ceb_removal - ceb_min) / (ceb_max - ceb_min), 0, 1)
  773. # === 6. 构建观测向量 ===
  774. obs = np.array([
  775. TMP0_norm,
  776. q_UF_norm,
  777. temp_norm,
  778. R0_norm,
  779. short_term_norm,
  780. long_term_slope_norm,
  781. long_term_power_norm,
  782. ceb_removal_norm
  783. ], dtype=np.float32)
  784. return obs
  785. def _get_action_values(self, action):
  786. """
  787. 将动作还原为实际时长
  788. """
  789. L_idx = action // self.num_bw
  790. t_bw_idx = action % self.num_bw
  791. return self.L_values[L_idx], self.t_bw_values[t_bw_idx]
  792. def step(self, action):
  793. self.current_step += 1
  794. L_s, t_bw_s = self._get_action_values(action)
  795. L_s = np.clip(L_s, self.base_params.L_min_s, self.base_params.L_max_s)
  796. t_bw_s = np.clip(t_bw_s, self.base_params.t_bw_min_s, self.base_params.t_bw_max_s)
  797. # 模拟超级周期
  798. info, next_params = simulate_one_supercycle(self.current_params, L_s, t_bw_s)
  799. # 根据 info 判断是否成功
  800. feasible = is_dead_cycle(info) # True 表示成功循环,False 表示失败
  801. if feasible:
  802. # 每步奖励
  803. reward = calculate_reward(self.current_params, info)
  804. self.current_params = next_params
  805. terminated = False
  806. else:
  807. # 中途失败惩罚
  808. reward = -10
  809. terminated = True
  810. # 判断是否到达最大步数
  811. truncated = self.current_step >= self.max_episode_steps
  812. self.last_action = (L_s, t_bw_s)
  813. next_obs = self._get_obs()
  814. info["feasible"] = feasible
  815. info["step"] = self.current_step
  816. # # ===================== 测试终末奖励:鼓励 TMP 接近初始状态 =====================
  817. # # 仅在 episode 自然结束(满步但未提前失败)时触发
  818. # if truncated and not terminated:
  819. # TMP_initial = self.TMP0 # reset 时记录的初始 TMP
  820. # TMP_final = next_obs[0] # next_obs 提供的最终 TMP
  821. #
  822. # delta_ratio = abs((TMP_final - TMP_initial) / TMP_initial)
  823. #
  824. # alpha = 4.0 # TMP 偏差敏感度
  825. # gamma = 5.0 # 奖励幅度
  826. # stability_reward = gamma * (np.exp(-alpha * delta_ratio) - 1) # 量级在0到-5之间
  827. #
  828. # reward += stability_reward
  829. # terminated = True # episode 正式结束
  830. # # ===================== 测试结果 =====================
  831. # 增加该奖励后强化学习依然能保证奖励收敛,但是损失函数在2-3之间反复震荡,无法降低,见reward_test&loss_test
  832. # 原设想是只能听在大额偏移发生前能通过该奖励学习到提前减小偏移步伐,但是实际训练时该惩罚反复被触发
  833. # 推测是终末的大额奖惩无法有效传递回过往时间步引导智能体学习,可能由于状态中缺少预测值,智能体会将其观测为不可控事件,暂时不添加该奖励,TODO:等待优化
  834. return next_obs, reward, terminated, truncated, info