fit.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381
  1. # cycle_analysis.py
  2. import numpy as np
  3. import pandas as pd
  4. from scipy.optimize import minimize, curve_fit
  5. from sklearn.metrics import r2_score
  6. from datetime import timedelta
  7. class ChemicalCycleSegmenter:
  8. """
  9. 根据 event_type == 'bw_chem' 划分化学周期。
  10. 每两个化学反冲洗之间所有 inlet 稳定段属于一个化学周期。
  11. 若化学周期时间长度 > max_hours,则标记为无效周期。
  12. """
  13. def __init__(self, max_hours=60):
  14. self.max_hours = max_hours
  15. def assign_cycles(self, df_unit, stable_segments):
  16. """
  17. df_unit:含 time, event_type
  18. stable_segments:列表,每段有 segment_id, time 等
  19. 返回 cycles: dict(cycle_id -> { segments:[], valid:True/False, start_time/end_time })
  20. """
  21. # 找出所有化学反冲洗的位置
  22. ceb_times = df_unit.loc[df_unit["event_type"] == "bw_chem", "time"].sort_values().to_list()
  23. cycles = {}
  24. if len(ceb_times) < 2:
  25. return cycles
  26. for i in range(len(ceb_times) - 1):
  27. cycle_id = i + 1
  28. t_start = ceb_times[i]
  29. t_end = ceb_times[i + 1]
  30. # 该周期内的稳定 segment
  31. segs = []
  32. for seg in stable_segments:
  33. if seg["time"].iloc[0] >= t_start and seg["time"].iloc[-1] <= t_end:
  34. segs.append(seg)
  35. if len(segs) == 0:
  36. continue
  37. # 判断周期有效性(长度 <= max_hours)
  38. hours = (t_end - t_start).total_seconds() / 3600
  39. valid = hours <= self.max_hours
  40. cycles[cycle_id] = {
  41. "segments": segs,
  42. "start": t_start,
  43. "end": t_end,
  44. "hours": hours,
  45. "valid": valid
  46. }
  47. # 回写到段中
  48. for seg in segs:
  49. seg["chem_cycle_id"] = cycle_id
  50. seg["chem_cycle_valid"] = valid
  51. return cycles
  52. class ShortTermCycleFoulingFitter:
  53. """
  54. 在一个化学周期内:
  55. - 每个 segment 有独立的 R0 和 t
  56. - 所有 segment 共享同一个短期污染速率 νK
  57. """
  58. def __init__(self, unit):
  59. self.unit = unit
  60. @staticmethod
  61. def _is_invalid(x):
  62. return (
  63. x is None
  64. or np.any(pd.isna(x))
  65. or np.any(np.isinf(x))
  66. or np.any(np.abs(x) > 1e20)
  67. )
  68. def fit_cycle(self, segments):
  69. # ============================
  70. # 1. 初步检查
  71. # ============================
  72. if not segments:
  73. return np.nan, np.nan
  74. R_col = f"{self.unit}_R_scaled"
  75. J_col = f"{self.unit}_J"
  76. # ============================
  77. # 2. 预处理每个 segment
  78. # ============================
  79. seg_data = []
  80. for seg in segments:
  81. if not {"time", R_col, J_col}.issubset(seg.columns):
  82. continue
  83. seg = seg.sort_values("time").drop_duplicates(subset=["time"])
  84. if len(seg) < 2:
  85. continue
  86. # 局部时间(秒)
  87. try:
  88. t = (seg["time"] - seg["time"].iloc[0]).dt.total_seconds().astype(float)
  89. except Exception:
  90. continue
  91. # 丢弃 t <= 0
  92. mask = t > 0
  93. if mask.sum() < 1:
  94. continue
  95. R = seg.loc[mask, R_col].astype(float).values
  96. J = seg.loc[mask, J_col].astype(float).values
  97. t = t[mask].values
  98. R0 = float(seg[R_col].iloc[0])
  99. # 数值与物理检查
  100. if any(self._is_invalid(x) for x in [R, J, t, R0]):
  101. continue
  102. if np.all(J == 0):
  103. continue
  104. seg_data.append((R, J, t, R0))
  105. if len(seg_data) == 0:
  106. return np.nan, np.nan
  107. # ============================
  108. # 3. 定义 Huber 损失
  109. # ============================
  110. delta = 1.0
  111. def huber_loss(res):
  112. abs_r = np.abs(res)
  113. quad = np.minimum(abs_r, delta)
  114. lin = abs_r - quad
  115. return 0.5 * quad**2 + delta * lin
  116. # ============================
  117. # 4. 定义 cycle 级 loss(共享 νK)
  118. # ============================
  119. def loss(nuk):
  120. nuk = float(nuk[0])
  121. total = 0.0
  122. for R, J, t, R0 in seg_data:
  123. pred = R0 + nuk * J * t
  124. res = R - pred
  125. total += np.sum(huber_loss(res))
  126. return total
  127. # ============================
  128. # 5. 优化求解 νK
  129. # ============================
  130. try:
  131. res = minimize(
  132. loss,
  133. x0=[1e-6],
  134. bounds=[(0.0, None)],
  135. options={"maxiter": 500, "disp": False}
  136. )
  137. except Exception:
  138. return np.nan, np.nan
  139. if not res.success:
  140. return np.nan, np.nan
  141. nuk_opt = float(res.x[0])
  142. # ============================
  143. # 6. 计算整体 R²(跨 segment)
  144. # ============================
  145. R_all = []
  146. pred_all = []
  147. for R, J, t, R0 in seg_data:
  148. pred = R0 + nuk_opt * J * t
  149. if self._is_invalid(pred):
  150. continue
  151. R_all.append(R)
  152. pred_all.append(pred)
  153. if not R_all:
  154. return nuk_opt, np.nan
  155. R_all = np.concatenate(R_all)
  156. pred_all = np.concatenate(pred_all)
  157. try:
  158. r2 = r2_score(R_all, pred_all)
  159. except Exception:
  160. r2 = np.nan
  161. return nuk_opt, r2
  162. class LongTermFoulingFitter:
  163. """
  164. 长期 fouling 拟合:
  165. - 使用每个 segment 起点的“稳健不可逆阻力估计”
  166. - 拟合不可逆阻力随 cycle 内累计时间的增长
  167. """
  168. def __init__(self, unit, max_hours=60.0):
  169. self.unit = unit
  170. self.max_hours = max_hours
  171. @staticmethod
  172. def _power_law(t, a, b, R0):
  173. return R0 + a * np.power(t, b)
  174. @staticmethod
  175. def _is_invalid(x):
  176. return (
  177. x is None
  178. or np.any(pd.isna(x))
  179. or np.any(np.isinf(x))
  180. or np.any(np.abs(x) > 1e20)
  181. )
  182. def _robust_segment_R0(self, seg):
  183. """
  184. 计算单个 segment 的稳健不可逆阻力:
  185. 1. 优先使用 post_bw_inlet == True 的行
  186. 2. 否则使用前 10 行
  187. """
  188. R_col = "R_scaled_start"
  189. if "post_bw_inlet" in seg.columns:
  190. mask = seg["post_bw_inlet"] == True
  191. if mask.any():
  192. R_vals = seg.loc[mask, R_col].astype(float).values
  193. if not self._is_invalid(R_vals):
  194. return float(np.mean(R_vals))
  195. # fallback:前 10 行
  196. R_vals = seg[R_col].iloc[:10].astype(float).values
  197. if not self._is_invalid(R_vals):
  198. return float(np.mean(R_vals))
  199. return None
  200. def fit_cycle(self, segments):
  201. # ============================
  202. # 1. 空检查
  203. # ============================
  204. if not segments:
  205. return np.nan, np.nan, np.nan
  206. T_list = []
  207. R_list = []
  208. # cycle 起点
  209. try:
  210. cycle_start = min(seg["time"].iloc[0] for seg in segments)
  211. except Exception:
  212. return np.nan, np.nan, np.nan
  213. R0_cycle = None
  214. for seg in segments:
  215. if "time" not in seg.columns or "R_scaled_start" not in seg.columns:
  216. continue
  217. seg = seg.sort_values("time")
  218. if len(seg) < 1:
  219. continue
  220. t0 = seg["time"].iloc[0]
  221. # ===== 稳健 R0 =====
  222. R0 = self._robust_segment_R0(seg)
  223. if R0 is None:
  224. continue
  225. # cycle 起点 R0
  226. if t0 == cycle_start:
  227. R0_cycle = R0
  228. # 累计时间(小时)
  229. T = (t0 - cycle_start).total_seconds() / 3600.0
  230. if T < 0 or T > self.max_hours:
  231. continue
  232. if self._is_invalid(T) or self._is_invalid(R0):
  233. continue
  234. T_list.append(T)
  235. R_list.append(R0)
  236. # 至少 3 个点
  237. if len(T_list) < 3 or R0_cycle is None:
  238. return np.nan, np.nan, np.nan
  239. T = np.asarray(T_list, dtype=float)
  240. R = np.asarray(R_list, dtype=float)
  241. if self._is_invalid(T) or self._is_invalid(R):
  242. return np.nan, np.nan, np.nan
  243. # ============================
  244. # 2. 幂律拟合(b ∈ [0, 3])
  245. # ============================
  246. try:
  247. popt, _ = curve_fit(
  248. lambda tt, a, b: self._power_law(tt, a, b, R0_cycle),
  249. T,
  250. R,
  251. p0=(0.1, 1.0),
  252. bounds=([0.0, 0.0], [np.inf, 3.0]),
  253. maxfev=8000
  254. )
  255. a, b = popt
  256. except Exception:
  257. return np.nan, np.nan, np.nan
  258. if self._is_invalid(a) or self._is_invalid(b):
  259. return np.nan, np.nan, np.nan
  260. # ============================
  261. # 3. 拟合质量
  262. # ============================
  263. pred = self._power_law(T, a, b, R0_cycle)
  264. if self._is_invalid(pred):
  265. return np.nan, np.nan, np.nan
  266. try:
  267. r2 = r2_score(R, pred)
  268. except Exception:
  269. r2 = np.nan
  270. return a, b, r2
  271. class ChemicalBackwashCleaner:
  272. """
  273. 根据有效化学周期计算 CEB 去除膜阻力:
  274. ΔR = 上周期末 R_end - 下周期初 R_start
  275. """
  276. def __init__(self, unit):
  277. self.unit = unit
  278. def compute_removal(self, cycles):
  279. ids = sorted(cycles.keys())
  280. for i in range(len(ids) - 1):
  281. cid1 = ids[i]
  282. cid2 = ids[i + 1]
  283. c1 = cycles[cid1]
  284. c2 = cycles[cid2]
  285. if not (c1["valid"] and c2["valid"]):
  286. c1["R_removed"] = None
  287. continue
  288. # 上周期末:最后段的 R_end
  289. seg_last = c1["segments"][-1]
  290. R_end = seg_last[f"R_scaled_end"]
  291. # 下周期初:第一段的 R_start
  292. seg_first = c2["segments"][0]
  293. R_start = seg_first[f"R_scaled_start"]
  294. c1["R_removed"] = R_end - R_start
  295. # 最后一个周期无 CEB 去除值
  296. if ids:
  297. cycles[ids[-1]]["R_removed"] = None
  298. return cycles