DQN_env.py 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068
  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 = 4.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 = 6e+01 # 阻力增长系数上限
  99. nuK_min: float = 3e+01 # 阻力增长系数下限
  100. # --- 长期污染模型参数约束 ---
  101. slope_max: float = 10 # 不可逆污染斜率上限
  102. slope_min: float = 0.1 # 不可逆污染斜率下限
  103. power_max: float = 1.3 # 不可逆污染幂次上限
  104. power_min: float = 0.8 # 不可逆污染幂次下限
  105. # --- CEB去除能力约束 ---
  106. ceb_removal_max: float = 150 # 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 = 60.0
  114. # CEB 间隔时间(小时)
  115. # 说明:每运行约 60 小时执行一次化学增强反洗
  116. v_ceb_m3: float = 30.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_start: 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_start, 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_start = (L_s + t_bw_s) / 3600.0 * idx # 本小周期起始时间
  375. L_h_next_start = (L_s + t_bw_s) / 3600.0 * (idx + 1) # 下一小周期起始时间
  376. # 调用膜阻力下降模型,计算物理反洗可去除的阻力
  377. reversible_R = phi_bw_of(p, R_run_start, R_peak, L_h_start, L_h_next_start, t_bw_s)
  378. # 物理反洗后的膜阻力
  379. R_after_bw = R_peak - reversible_R
  380. tmp_after_bw = _calculate_tmp(R_after_bw, q_UF, temp)
  381. # 计算残余污染增量(反洗后的TMP相对本次开始的增加)
  382. residual_inc = tmp_after_bw - tmp_run_start
  383. max_residual_increase = max(max_residual_increase, residual_inc)
  384. # 更新TMP(作为下一小周期的起始TMP)
  385. tmp = tmp_after_bw
  386. # ========== 化学增强反洗 (CEB) ==========
  387. # CEB比物理反洗更彻底,可去除部分不可逆污染
  388. R_after_ceb = R_peak - p.ceb_removal # CEB后的膜阻力
  389. tmp_after_ceb = _calculate_tmp(R_after_ceb, q_UF, temp) # CEB后的TMP
  390. # ============================================================
  391. # 计算周期性能指标
  392. # ============================================================
  393. # ========== 水量平衡计算 ==========
  394. # 进水总量(所有小周期的过滤进水之和)
  395. V_feed_super = k_bw_per_ceb * p.q_UF * L_h # [m³]
  396. # 损失水量(物理反洗 + 化学反洗)
  397. V_loss_super = k_bw_per_ceb * _v_bw_m3(p, t_bw_s) + p.v_ceb_m3 # [m³]
  398. # 净产水量
  399. V_net = max(0.0, V_feed_super - V_loss_super) # [m³]
  400. # 回收率(净产水 / 进水总量)
  401. # 加1e-12避免除零,max确保非负
  402. recovery = max(0.0, V_net / max(V_feed_super, 1e-12)) # [无量纲,0-1之间]
  403. # ========== 时间与能耗计算 ==========
  404. # 超级周期总时长
  405. T_super_h = k_bw_per_ceb * (L_s + t_bw_s) / 3600.0 + p.t_ceb_s / 3600.0 # [小时]
  406. # 日均产水时间(24小时内实际产水的时间)
  407. daily_prod_time_h = k_bw_per_ceb * L_h / T_super_h * 24.0 # [小时]
  408. # 吨水电耗(从查找表获取最接近的值)
  409. closest_L = min(energy_lookup.keys(), key=lambda x: abs(x - L_s))
  410. ton_water_energy = energy_lookup[closest_L] # [kWh/m³]
  411. # ===== 新指标:膜阻力允许上升空间 =====
  412. R_max = _calculate_resistance(max_tmp_during_filtration, p.q_UF, p.temp)
  413. R_soft_limit = _calculate_resistance(p.global_TMP_soft_limit, p.q_UF, p.temp)
  414. delta_R_allow = max(R_soft_limit - R_max, 1e-6) # 供奖励函数使用的“污染允许增长空间”
  415. # ========== 构建性能指标字典 ==========
  416. info = {
  417. # 运行参数
  418. "q_UF": p.q_UF, # 过滤流量
  419. "temp": p.temp, # 水温
  420. # 水量指标
  421. "recovery": recovery, # 回收率
  422. "V_feed_super_m3": V_feed_super, # 进水总量
  423. "V_loss_super_m3": V_loss_super, # 损失水量
  424. "V_net_super_m3": V_net, # 净产水量
  425. # 时间指标
  426. "supercycle_time_h": T_super_h, # 超级周期时长
  427. "daily_prod_time_h": daily_prod_time_h, # 日均产水时间
  428. "k_bw_per_ceb": k_bw_per_ceb, # 小周期数量
  429. # TMP指标
  430. "max_TMP_during_filtration": max_tmp_during_filtration, # 周期内最大TMP
  431. "min_TMP_during_filtration": min_tmp_during_filtration, # 周期内最小TMP
  432. "global_TMP_limit": p.global_TMP_hard_limit, # TMP限制
  433. "TMP0": p.TMP0, # 周期初始TMP
  434. "TMP_after_ceb": tmp_after_ceb, # CEB后TMP
  435. # 膜阻力指标
  436. "R0": R0, # 周期初始膜阻力
  437. "R_after_ceb": R_after_ceb, # CEB后膜阻力
  438. "max_residual_increase_per_run": max_residual_increase, # 最大残余污染增量
  439. "delta_R_allow": delta_R_allow, # 污染允许增长空间
  440. # 能耗指标
  441. "ton_water_energy_kWh_per_m3": ton_water_energy, # 吨水电耗
  442. }
  443. # ============================================================
  444. # 状态更新:生成下一周期的初始参数
  445. # ============================================================
  446. # 深拷贝当前参数(避免修改原对象)
  447. next_params = copy.deepcopy(p)
  448. # ========== 必须更新的参数 ==========
  449. # 更新TMP为CEB后的值(作为下一超级周期的起始TMP)
  450. next_params.TMP0 = tmp_after_ceb
  451. # ========== 可选更新的参数(当前保持不变) ==========
  452. # 这些参数可根据实际情况动态调整,预留扩展接口
  453. next_params.slope = p.slope # 长期污染斜率
  454. next_params.power = p.power # 长期污染幂次
  455. next_params.ceb_removal = p.ceb_removal # CEB去除能力
  456. next_params.nuK = p.nuK # 短期污染系数
  457. next_params.q_UF = p.q_UF # 过滤流量
  458. next_params.temp = p.temp # 水温
  459. return info, next_params
  460. def calculate_reward(p: UFParams, info: dict) -> float:
  461. """
  462. 计算强化学习奖励函数
  463. 功能:
  464. - 平衡回收率和残余污染两个目标
  465. - TMP不直接参与奖励计算(通过失败判定间接影响)
  466. - 使用 tanh 函数实现平滑的非线性奖励
  467. 参数:
  468. p (UFParams): 超滤参数(包含奖励函数超参数)
  469. info (dict): 周期性能指标字典
  470. 返回:
  471. float: 奖励值(通常在 -2 到 +2 之间)
  472. 设计思想:
  473. - 高回收率 → 水资源利用率高 → 正奖励
  474. - 低残余污染 → 膜长期稳定运行 → 正奖励
  475. - 两者需要权衡:过短的过滤时间提高回收率但污染去除不彻底;
  476. 过长的过滤时间污染控制好但回收率下降
  477. 参考点设计:
  478. - (recovery=0.97, residual_ratio=0.1) → reward ≈ 0(高回收但污染高)
  479. - (recovery=0.90, residual_ratio=0.0) → reward ≈ 0(低污染但回收率低)
  480. - (recovery≈0.94, residual_ratio≈0.05) → reward > 0(平衡点)
  481. """
  482. # ========== 提取性能指标 ==========
  483. recovery = info["recovery"] # 回收率 [0-1]
  484. # 污染比例:实际上升的阻力 / 允许上升的阻力
  485. # 允许上升的阻力值 = 当前阻力值软上限 - 当前阻力
  486. residual_ratio = (info["R_after_ceb"] - info["R0"]) / info["delta_R_allow"]
  487. # ========== 回收率奖励项 ==========
  488. # 将回收率归一化到 [0, 1] 区间(基于预期范围)
  489. rec_norm = (recovery - p.rec_low) / (p.rec_high - p.rec_low)
  490. # 使用 tanh 函数构建平滑的 S 型奖励曲线
  491. # - rec_norm = 0.5 时(回收率处于中间值),rec_reward = 0
  492. # - rec_norm > 0.5 时,rec_reward > 0(鼓励高回收率)
  493. # - rec_norm < 0.5 时,rec_reward < 0(惩罚低回收率)
  494. # - k_rec 控制曲线陡峭程度,越大变化越陡
  495. rec_reward = np.clip(np.tanh(p.k_rec * (rec_norm - 0.5)), -1, 1)
  496. # ========== 污染惩罚项 ==========
  497. # 使用 tanh 函数构建惩罚曲线
  498. # - residual_ratio < rr0 时,res_penalty > 0(奖励低污染)
  499. # - residual_ratio > rr0 时,res_penalty < 0(惩罚高污染)
  500. # - k_res 控制曲线陡峭程度
  501. res_penalty = -np.tanh(p.k_res * (residual_ratio / p.rr0 - 1))
  502. # ========== 组合奖励 ==========
  503. # 简单线性组合两项(也可以加权)
  504. total_reward = rec_reward + res_penalty
  505. # 可选:添加平移项使特定点的奖励为零(当前未使用)
  506. # total_reward -= offset
  507. return total_reward
  508. def is_dead_cycle(info: dict) -> bool:
  509. """
  510. 判断当前超级周期是否成功(可行)
  511. 功能:
  512. - 检查超级周期是否违反运行约束
  513. - 用于强化学习的失败判定(terminated条件)
  514. - True表示成功,False表示失败
  515. 参数:
  516. info (dict): simulate_one_supercycle() 返回的性能指标字典
  517. 返回:
  518. bool: True表示成功周期,False表示失败周期
  519. 失败条件(任一满足即失败):
  520. 1. TMP超限:max_TMP > global_TMP_limit
  521. - 原因:TMP过高会损坏膜或影响产水质量
  522. - 阈值:0.08 MPa(可配置)
  523. 2. 回收率过低:recovery < 0.75
  524. - 原因:回收率太低说明反洗水耗过大,经济性差
  525. - 阈值:75%(可调整)
  526. 3. 残余污染累积过快:(R_after_ceb - R0) / R0 > 0.1
  527. - 原因:单个超级周期污染增长超过10%,长期运行不可持续
  528. - 阈值:10%(可调整)
  529. """
  530. # ========== 获取关键指标 ==========
  531. TMP_limit = info.get("global_TMP_limit", 0.08) # TMP硬约束上限
  532. max_tmp = info.get("max_TMP_during_filtration", 0) # 周期内最大TMP
  533. recovery = info.get("recovery", 1.0) # 回收率
  534. R_after_ceb = info.get("R_after_ceb", 0) # CEB后膜阻力
  535. R0 = info.get("R0", 1e-6) # 初始膜阻力
  536. delta_R_allow = info.get("delta_R_allow", 1e-6) # 允许上升的膜阻力(加小值避免除零)
  537. # ========== 失败条件检查 ==========
  538. # 条件1:TMP超限
  539. if max_tmp > TMP_limit:
  540. return False # 失败
  541. # 条件2:回收率过低
  542. if recovery < 0.75:
  543. return False # 失败
  544. # 条件3:污染增长量超过容许范围
  545. residual_increase = (R_after_ceb - R0) / delta_R_allow
  546. if residual_increase > 1/15:
  547. return False # 失败
  548. # 所有条件通过
  549. return True # 成功
  550. def check_pollution_dead_initial_state(p: UFParams, L_s: float = 4900, t_bw_s: float = 50) -> bool:
  551. """
  552. 检查污染动力学是否物理可行(非即死状态)
  553. 若任意一次 reversible_R = 0.0,则返回 False(即死状态)
  554. """
  555. try:
  556. d_R = _delta_resistance(p, L_s)
  557. R0 = p.R0
  558. for idx in [0, 30]:
  559. L_h_start = (L_s + t_bw_s) / 3600.0 * idx
  560. L_h_next_start = (L_s + t_bw_s) / 3600.0 * (idx + 1)
  561. reversible_R = phi_bw_of(p, R0, d_R, L_h_start, L_h_next_start, t_bw_s)
  562. if reversible_R == 0.0: # 说明原始选择的阻力组合中长期污染增长曲线上升比短期的总污染曲线还要快,该组合不符合物理常识
  563. return False # 即死状态
  564. return True
  565. except Exception:
  566. # 若出现数值溢出或参数异常,也视为不可行
  567. return False
  568. class UFSuperCycleEnv(gym.Env):
  569. """
  570. 超滤系统强化学习环境(Gymnasium标准接口)
  571. 功能:
  572. - 模拟超滤膜的超级周期运行
  573. - 智能体在每个超级周期选择过滤时长和反洗时长
  574. - 目标:最大化回收率同时控制污染累积
  575. 状态空间 (8维,归一化到 [0,1]):
  576. 1. TMP0: 初始跨膜压差
  577. 2. q_UF: 过滤流量
  578. 3. temp: 水温
  579. 4. R0: 初始膜阻力
  580. 5. nuK: 短期污染系数
  581. 6. slope: 长期污染斜率
  582. 7. power: 长期污染幂次
  583. 8. ceb_removal: CEB去除能力
  584. 动作空间 (离散):
  585. - 二维离散动作组合:(过滤时长, 反洗时长)
  586. - 过滤时长: L_min_s ~ L_max_s,步长 L_step_s
  587. - 反洗时长: t_bw_min_s ~ t_bw_max_s,步长 t_bw_step_s
  588. - 总动作数 = len(L_values) × len(t_bw_values)
  589. 奖励机制:
  590. - 基于回收率和残余污染的平衡
  591. - 失败 (TMP超限、回收率过低、污染过快) 时给予大负奖励 (-10)
  592. 终止条件:
  593. - terminated: 违反运行约束(失败)
  594. - truncated: 达到最大步数 (max_episode_steps)
  595. """
  596. metadata = {"render_modes": ["human"]}
  597. def __init__(self, base_params, resistance_models=None, max_episode_steps: int = 15):
  598. """
  599. 初始化超滤强化学习环境
  600. 参数:
  601. base_params (UFParams): 基础超滤参数配置
  602. resistance_models (tuple, optional): 预加载的膜阻力模型,默认None(自动加载)
  603. max_episode_steps (int): 每个episode的最大步数,默认15
  604. 注:每步代表一个超级周期(约2-3天),15步约一个月
  605. """
  606. super(UFSuperCycleEnv, self).__init__()
  607. # ========== 参数初始化 ==========
  608. self.base_params = base_params # 基础参数(不变)
  609. self.current_params = copy.deepcopy(base_params) # 当前参数(动态更新)
  610. self.max_episode_steps = max_episode_steps # 最大步数
  611. self.current_step = 0 # 当前步数计数器
  612. # ========== 加载膜阻力模型 ==========
  613. if resistance_models is None:
  614. # 自动加载模型
  615. self.resistance_model_fp, self.resistance_model_bw = load_resistance_models()
  616. else:
  617. # 使用预加载的模型(用于并行环境避免重复加载)
  618. self.resistance_model_fp, self.resistance_model_bw = resistance_models
  619. # ========== 构建离散动作空间 ==========
  620. # 过滤时长候选值(例:3800, 3860, 3920, ..., 5940, 6000秒)
  621. self.L_values = np.arange(
  622. self.base_params.L_min_s,
  623. self.base_params.L_max_s,
  624. self.base_params.L_step_s
  625. )
  626. # 反洗时长候选值(例:40, 45, 50, 55, 60秒)
  627. self.t_bw_values = np.arange(
  628. self.base_params.t_bw_min_s,
  629. self.base_params.t_bw_max_s + self.base_params.t_bw_step_s, # +step确保包含上限
  630. self.base_params.t_bw_step_s
  631. )
  632. self.num_L = len(self.L_values) # 过滤时长选项数
  633. self.num_bw = len(self.t_bw_values) # 反洗时长选项数
  634. # 定义单一离散动作空间(笛卡尔积编码)
  635. # 动作编号 action = L_idx × num_bw + t_bw_idx
  636. # 例:num_L=37, num_bw=5 → 总动作数=185
  637. self.action_space = spaces.Discrete(self.num_L * self.num_bw)
  638. # ========== 定义状态空间 ==========
  639. # 8维连续状态,归一化到 [0, 1]
  640. self.observation_space = spaces.Box(
  641. low=np.zeros(8, dtype=np.float32),
  642. high=np.ones(8, dtype=np.float32),
  643. dtype=np.float32
  644. )
  645. # ========== 初始化环境(调用reset) ==========
  646. self.reset(seed=None)
  647. def generate_initial_state(self):
  648. """
  649. 随机生成一个初始状态,不进行死状态判断
  650. """
  651. self.current_params.TMP0 = np.random.uniform(
  652. self.current_params.TMP0_min, self.current_params.TMP0_max
  653. )
  654. self.current_params.q_UF = np.random.uniform(
  655. self.current_params.q_UF_min, self.current_params.q_UF_max
  656. )
  657. self.current_params.temp = np.random.uniform(
  658. self.current_params.temp_min, self.current_params.temp_max
  659. )
  660. self.current_params.R0 = _calculate_resistance(
  661. self.current_params.TMP0,
  662. self.current_params.q_UF,
  663. self.current_params.temp
  664. )
  665. self.current_params.nuK = np.random.uniform(
  666. self.current_params.nuK_min, self.current_params.nuK_max
  667. )
  668. self.current_params.slope = np.random.uniform(
  669. self.current_params.slope_min, self.current_params.slope_max
  670. )
  671. self.current_params.power = np.random.uniform(
  672. self.current_params.power_min, self.current_params.power_max
  673. )
  674. self.current_params.ceb_removal = np.random.uniform(
  675. self.current_params.ceb_removal_min, self.current_params.ceb_removal_max
  676. )
  677. return self._get_state_copy()
  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. self.generate_initial_state()
  684. p = self.current_params
  685. # Step 1: 运行稳定性检测
  686. ok_run = self.check_dead_initial_state(
  687. max_steps=getattr(self, "max_episode_steps", 15),
  688. L_s=3800, t_bw_s=60
  689. )
  690. # Step 2: 污染动力学检测
  691. ok_pollution = check_pollution_dead_initial_state(self.current_params, L_s=3800, t_bw_s=60)
  692. if ok_run and ok_pollution:
  693. break
  694. else:
  695. # 超过最大尝试次数仍未生成可行状态
  696. raise RuntimeError(f"在 {max_attempts} 次尝试后仍无法生成可行初始状态。")
  697. # 初始化步数、动作、最大 TMP
  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. # 记录本episode初始TMP,用于终末奖励
  702. self.TMP0 = self.current_params.TMP0
  703. return self._get_obs(), {}
  704. def check_dead_initial_state(self, max_steps: int = None,
  705. L_s: int = 3800, t_bw_s: int = 60) -> bool:
  706. """
  707. 判断当前环境生成的初始状态是否为可行(non-dead)。
  708. 使用最保守策略连续模拟 max_steps 次:
  709. 若任意一次 is_dead_cycle(info) 返回 False 或 next_params[0] < 0,则视为必死状态。
  710. 参数:
  711. max_steps: 模拟步数,默认使用 self.max_episode_steps
  712. L_s: 过滤时长(s),默认 3800
  713. t_bw_s: 物理反洗时长(s),默认 60
  714. 返回:
  715. bool: True 表示可行状态(non-dead),False 表示必死状态
  716. """
  717. if max_steps is None:
  718. max_steps = getattr(self, "max_episode_steps", 15)
  719. # 生成初始状态
  720. self.generate_initial_state()
  721. if not hasattr(self, "current_params"):
  722. raise AttributeError("generate_initial_state() 未设置 current_params。")
  723. import copy
  724. curr_p = copy.deepcopy(self.current_params)
  725. # 逐步模拟
  726. for step in range(max_steps):
  727. try:
  728. info, next_params = simulate_one_supercycle(curr_p, L_s, t_bw_s)
  729. except Exception:
  730. # 异常即视为不可行
  731. return False
  732. # 任意一次不可行即为必死状态
  733. if not is_dead_cycle(info):
  734. return False
  735. # 新增判断:下一步跨膜压差 TMP 不可能为负
  736. if next_params.TMP0 < 0:
  737. return False
  738. # 更新参数,进入下一步模拟
  739. curr_p = next_params
  740. return True
  741. def _get_state_copy(self):
  742. return copy.deepcopy(self.current_params)
  743. def _get_obs(self):
  744. """
  745. 构建当前环境归一化状态向量
  746. """
  747. # === 1. 从 current_params 读取动态参数 ===
  748. TMP0 = self.current_params.TMP0
  749. q_UF = self.current_params.q_UF
  750. temp = self.current_params.temp
  751. # === 2. 计算本周期初始膜阻力 ===
  752. R0 = _calculate_resistance(TMP0, q_UF, temp)
  753. # === 3. 从 current_params 读取膜阻力增长模型参数 ===
  754. nuk = self.current_params.nuK
  755. slope = self.current_params.slope
  756. power = self.current_params.power
  757. ceb_removal = self.current_params.ceb_removal
  758. # === 4. 从 current_params 动态读取上下限 ===
  759. TMP0_min, TMP0_max = self.current_params.TMP0_min, self.current_params.global_TMP_hard_limit
  760. q_UF_min, q_UF_max = self.current_params.q_UF_min, self.current_params.q_UF_max
  761. temp_min, temp_max = self.current_params.temp_min, self.current_params.temp_max
  762. nuK_min, nuK_max = self.current_params.nuK_min, self.current_params.nuK_max
  763. slope_min, slope_max = self.current_params.slope_min, self.current_params.slope_max
  764. power_min, power_max = self.current_params.power_min, self.current_params.power_max
  765. ceb_min, ceb_max = self.current_params.ceb_removal_min, self.current_params.ceb_removal_max
  766. # === 5. 归一化计算(clip防止越界) ===
  767. TMP0_norm = np.clip((TMP0 - TMP0_min) / (TMP0_max - TMP0_min), 0, 1)
  768. q_UF_norm = np.clip((q_UF - q_UF_min) / (q_UF_max - q_UF_min), 0, 1)
  769. temp_norm = np.clip((temp - temp_min) / (temp_max - temp_min), 0, 1)
  770. # R0 不在 current_params 中定义上下限,设定经验范围
  771. R0_norm = np.clip((R0 - 100.0) / (800.0 - 100.0), 0, 1)
  772. short_term_norm = np.clip((nuk - nuK_min) / (nuK_max - nuK_min), 0, 1)
  773. long_term_slope_norm = np.clip((slope - slope_min) / (slope_max - slope_min), 0, 1)
  774. long_term_power_norm = np.clip((power - power_min) / (power_max - power_min), 0, 1)
  775. ceb_removal_norm = np.clip((ceb_removal - ceb_min) / (ceb_max - ceb_min), 0, 1)
  776. # === 6. 构建观测向量 ===
  777. obs = np.array([
  778. TMP0_norm,
  779. q_UF_norm,
  780. temp_norm,
  781. R0_norm,
  782. short_term_norm,
  783. long_term_slope_norm,
  784. long_term_power_norm,
  785. ceb_removal_norm
  786. ], dtype=np.float32)
  787. return obs
  788. def _get_action_values(self, action):
  789. """
  790. 将动作还原为实际时长
  791. """
  792. L_idx = action // self.num_bw
  793. t_bw_idx = action % self.num_bw
  794. return self.L_values[L_idx], self.t_bw_values[t_bw_idx]
  795. def step(self, action):
  796. self.current_step += 1
  797. L_s, t_bw_s = self._get_action_values(action)
  798. L_s = np.clip(L_s, self.base_params.L_min_s, self.base_params.L_max_s)
  799. t_bw_s = np.clip(t_bw_s, self.base_params.t_bw_min_s, self.base_params.t_bw_max_s)
  800. # 模拟超级周期
  801. info, next_params = simulate_one_supercycle(self.current_params, L_s, t_bw_s)
  802. # 根据 info 判断是否成功
  803. feasible = is_dead_cycle(info) # True 表示成功循环,False 表示失败
  804. if feasible:
  805. # 每步奖励
  806. reward = calculate_reward(self.current_params, info)
  807. self.current_params = next_params
  808. terminated = False
  809. else:
  810. # 中途失败惩罚
  811. reward = -20
  812. terminated = True
  813. # 判断是否到达最大步数
  814. truncated = self.current_step >= self.max_episode_steps
  815. self.last_action = (L_s, t_bw_s)
  816. next_obs = self._get_obs()
  817. info["feasible"] = feasible
  818. info["step"] = self.current_step
  819. # ===================== 终末奖励:鼓励 TMP 接近初始状态 =====================
  820. # 仅在 episode 自然结束(满步但未提前失败)时触发
  821. if truncated and not terminated:
  822. TMP_initial = self.TMP0 # reset 时记录的初始 TMP
  823. TMP_final = next_obs[self.obs_index["TMP0"]] # next_obs 提供的最终 TMP
  824. delta_ratio = abs((TMP_final - TMP_initial) / TMP_initial)
  825. alpha = 4.0 # TMP 偏差敏感度
  826. gamma = 2.0 # 奖励幅度
  827. stability_reward = gamma * (np.exp(-alpha * delta_ratio) - 1)
  828. reward += stability_reward
  829. terminated = True # episode 正式结束
  830. return next_obs, reward, terminated, truncated, info