logging_system.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646
  1. # -*- coding: utf-8 -*-
  2. """
  3. RO膜污染监控与CIP预测系统 - 日志记录模块
  4. 功能:
  5. 1. 记录每次分析的输入参数
  6. 2. 保存预测数据和分析结果
  7. 3. 生成可视化图表
  8. 4. 导出HTML格式分析报告
  9. """
  10. import os
  11. import json
  12. import logging
  13. import pandas as pd
  14. import numpy as np
  15. from datetime import datetime, timedelta
  16. import matplotlib.pyplot as plt
  17. import seaborn as sns
  18. from pathlib import Path
  19. # 设置中文字体
  20. plt.rcParams['font.sans-serif'] = ['SimHei', 'Arial Unicode MS', 'DejaVu Sans']
  21. plt.rcParams['axes.unicode_minus'] = False
  22. class CIPAnalysisLogger:
  23. """
  24. CIP分析日志记录器
  25. 功能:
  26. - 记录分析会话信息
  27. - 保存输入输出数据
  28. - 生成分析图表和报告
  29. """
  30. def __init__(self, log_dir="analysis_logs", unit_filter=None):
  31. """
  32. 初始化日志记录器
  33. Args:
  34. log_dir: str,日志目录路径,默认"analysis_logs"
  35. unit_filter: str,机组过滤器,如'RO1',用于目录命名
  36. 目录结构(优化后):
  37. analysis_logs/
  38. CIP_RO1_20251105_155542/ # 类别_机组_时间
  39. CIP_Analysis_20251105_155542.log
  40. data/
  41. plots/
  42. reports/
  43. CIP_ALL_20251105_160000/ # 全机组分析
  44. """
  45. # 生成会话ID和时间戳
  46. self.timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
  47. self.session_id = f"CIP_Analysis_{self.timestamp}"
  48. # 创建按类别和时间分组的会话目录
  49. base_log_dir = Path(log_dir)
  50. base_log_dir.mkdir(exist_ok=True)
  51. # 目录命名:CIP_机组_时间
  52. unit_name = unit_filter if unit_filter else "ALL"
  53. dir_name = f"CIP_{unit_name}_{self.timestamp}"
  54. self.log_dir = base_log_dir / dir_name
  55. self.log_dir.mkdir(exist_ok=True)
  56. # 创建子目录结构(放在会话目录下)
  57. self.data_dir = self.log_dir / "data"
  58. self.plots_dir = self.log_dir / "plots"
  59. self.reports_dir = self.log_dir / "reports"
  60. for dir_path in [self.data_dir, self.plots_dir, self.reports_dir]:
  61. dir_path.mkdir(exist_ok=True)
  62. # 初始化日志
  63. self.setup_logging()
  64. # 初始化数据存储
  65. self.analysis_data = {
  66. "session_info": {
  67. "session_id": self.session_id,
  68. "start_time": datetime.now().isoformat(),
  69. "analysis_type": "CIP时机预测",
  70. "system_version": "v4_wuhan"
  71. },
  72. "input_parameters": {},
  73. "prediction_data": {},
  74. "unit_analysis": {},
  75. "final_results": {},
  76. "performance_metrics": {}
  77. }
  78. self.logger.info(f"CIP分析会话: {self.session_id}, 目录: {self.log_dir}")
  79. def setup_logging(self):
  80. """设置日志配置"""
  81. log_file = self.log_dir / f"{self.session_id}.log"
  82. # 创建logger
  83. self.logger = logging.getLogger(self.session_id)
  84. self.logger.setLevel(logging.INFO)
  85. # 禁止日志传播到父logger,避免重复输出
  86. self.logger.propagate = False
  87. # 清除已有的所有处理器(防止重复添加)
  88. if self.logger.handlers:
  89. for handler in self.logger.handlers[:]:
  90. handler.close()
  91. self.logger.removeHandler(handler)
  92. # 创建文件处理器
  93. file_handler = logging.FileHandler(log_file, encoding='utf-8')
  94. file_handler.setLevel(logging.INFO)
  95. # 创建控制台处理器
  96. console_handler = logging.StreamHandler()
  97. console_handler.setLevel(logging.INFO)
  98. # 创建格式器
  99. formatter = logging.Formatter(
  100. '%(asctime)s - %(levelname)s - %(message)s',
  101. datefmt='%Y-%m-%d %H:%M:%S'
  102. )
  103. file_handler.setFormatter(formatter)
  104. console_handler.setFormatter(formatter)
  105. # 添加处理器
  106. self.logger.addHandler(file_handler)
  107. self.logger.addHandler(console_handler)
  108. def log_input_parameters(self, strategy, start_date, prediction_start_date=None):
  109. """
  110. 记录输入参数
  111. Args:
  112. strategy: int,策略编号
  113. start_date: str,起始时间
  114. prediction_start_date: datetime,预测起始时间
  115. """
  116. params = {
  117. "strategy": strategy,
  118. "start_date": start_date,
  119. "prediction_start_date": prediction_start_date.isoformat() if prediction_start_date else None,
  120. "analysis_timestamp": datetime.now().isoformat()
  121. }
  122. self.analysis_data["input_parameters"] = params
  123. pred_start = f", 预测起始: {prediction_start_date}" if prediction_start_date else ""
  124. self.logger.info(f"参数 - 策略: {strategy}, 起始: {start_date}{pred_start}")
  125. def log_prediction_data(self, all_data):
  126. """
  127. 记录预测数据概况
  128. Args:
  129. all_data: pd.DataFrame,预测数据
  130. """
  131. try:
  132. data_info = {
  133. "shape": list(all_data.shape),
  134. "time_range": {
  135. "start": all_data.index.min().isoformat(),
  136. "end": all_data.index.max().isoformat()
  137. },
  138. "columns": list(all_data.columns),
  139. "data_points": len(all_data),
  140. "missing_values": all_data.isnull().sum().to_dict()
  141. }
  142. self.analysis_data["prediction_data"] = data_info
  143. # 保存数据到文件
  144. data_file = self.data_dir / f"{self.session_id}_prediction_data.csv"
  145. all_data.to_csv(data_file)
  146. self.logger.info(f"预测数据 - 形状: {data_info['shape']}, 点数: {data_info['data_points']}, 已保存: {data_file.name}")
  147. except Exception as e:
  148. self.logger.error(f"记录预测数据失败: {e}")
  149. def log_unit_days(self, unit_days_dict):
  150. """
  151. 记录各机组预测天数
  152. Args:
  153. unit_days_dict: dict,机组ID到预测天数的映射
  154. """
  155. self.analysis_data["unit_days"] = unit_days_dict
  156. days_str = ", ".join([f"RO{uid}:{days}天" for uid, days in unit_days_dict.items()])
  157. self.logger.info(f"预测天数 - {days_str}")
  158. def log_unit_analysis_start(self, unit_id, predict_days):
  159. """
  160. 记录机组分析开始
  161. Args:
  162. unit_id: int,机组ID
  163. predict_days: int,预测天数
  164. """
  165. self.logger.info(f"[RO{unit_id}] 开始分析, 预测周期: {predict_days}天")
  166. if unit_id not in self.analysis_data["unit_analysis"]:
  167. self.analysis_data["unit_analysis"][unit_id] = {}
  168. self.analysis_data["unit_analysis"][unit_id]["predict_days"] = predict_days
  169. self.analysis_data["unit_analysis"][unit_id]["analysis_start"] = datetime.now().isoformat()
  170. def log_unit_pressure_data(self, unit_id, truncated_data, pressure_columns):
  171. """
  172. 记录机组压差数据
  173. Args:
  174. unit_id: int,机组ID
  175. truncated_data: pd.DataFrame,截取后的数据
  176. pressure_columns: list,压差列名列表
  177. """
  178. try:
  179. unit_data = {
  180. "pressure_columns": pressure_columns,
  181. "data_shape": list(truncated_data.shape),
  182. "data_range": {
  183. "start": truncated_data.index.min().isoformat(),
  184. "end": truncated_data.index.max().isoformat()
  185. }
  186. }
  187. self.analysis_data["unit_analysis"][unit_id]["pressure_data"] = unit_data
  188. cols_str = ", ".join(pressure_columns)
  189. self.logger.info(f"[RO{unit_id}] 压差列: {len(pressure_columns)}个 ({cols_str})")
  190. # 保存数据文件
  191. unit_data_file = self.data_dir / f"{self.session_id}_RO{unit_id}_pressure_data.csv"
  192. truncated_data[pressure_columns].to_csv(unit_data_file)
  193. except Exception as e:
  194. self.logger.error(f"记录RO{unit_id}压差数据失败: {e}")
  195. def log_cip_analysis_result(self, unit_id, column, optimal_time, analysis):
  196. """
  197. 记录CIP分析结果(增强版,包含诊断信息)
  198. Args:
  199. unit_id: int,机组ID
  200. column: str,压差列名
  201. optimal_time: pd.Timestamp,最优CIP时间
  202. analysis: dict,分析结果(可能包含诊断信息)
  203. """
  204. try:
  205. if "cip_results" not in self.analysis_data["unit_analysis"][unit_id]:
  206. self.analysis_data["unit_analysis"][unit_id]["cip_results"] = []
  207. result = {
  208. "column": column,
  209. "optimal_time": optimal_time.isoformat() if optimal_time else None,
  210. "delay_days": analysis.get("delay_days"),
  211. "k_value": analysis.get("best_k"),
  212. "analysis_details": analysis
  213. }
  214. self.analysis_data["unit_analysis"][unit_id]["cip_results"].append(result)
  215. if optimal_time:
  216. self.logger.info(f" {column}: {optimal_time} (第{analysis['delay_days']}天, k={analysis['best_k']:.6f})")
  217. else:
  218. error_msg = analysis.get('error', '未知原因')
  219. self.logger.info(f" {column}: 未找到CIP时机 - {error_msg}")
  220. # 记录详细诊断信息
  221. if 'hint' in analysis:
  222. self.logger.info(f" 提示: {analysis['hint']}")
  223. if 'valid_k_count' in analysis:
  224. self.logger.info(f" 有效k值数量: {analysis['valid_k_count']}")
  225. if 'rising_periods_count' in analysis:
  226. self.logger.info(f" 上升趋势段数: {analysis['rising_periods_count']}")
  227. if 'data_days' in analysis:
  228. self.logger.info(f" 数据覆盖天数: {analysis['data_days']}天")
  229. except Exception as e:
  230. self.logger.error(f"记录CIP分析结果失败: {e}")
  231. def log_unit_strategy_result(self, unit_id, optimal_time, strategy_desc):
  232. """
  233. 记录机组策略选择结果
  234. Args:
  235. unit_id: int,机组ID
  236. optimal_time: pd.Timestamp,最优CIP时间
  237. strategy_desc: str,策略描述
  238. """
  239. try:
  240. strategy_result = {
  241. "optimal_time": optimal_time.isoformat() if optimal_time else None,
  242. "strategy_description": strategy_desc,
  243. "selection_timestamp": datetime.now().isoformat()
  244. }
  245. self.analysis_data["unit_analysis"][unit_id]["strategy_result"] = strategy_result
  246. self.logger.info(f"[RO{unit_id}] 最优CIP时机: {optimal_time}, 策略: {strategy_desc}")
  247. except Exception as e:
  248. self.logger.error(f"记录RO{unit_id}策略结果失败: {e}")
  249. def log_final_results(self, result_df):
  250. """
  251. 记录最终分析结果
  252. Args:
  253. result_df: pd.DataFrame,最终结果表
  254. """
  255. try:
  256. # 转换为可序列化格式
  257. final_results = []
  258. for _, row in result_df.iterrows():
  259. result = {
  260. "unit": row["机组类型"],
  261. "cip_time": row["CIP时机"].isoformat() if pd.notna(row["CIP时机"]) else None,
  262. "strategy_description": row["策略说明"]
  263. }
  264. final_results.append(result)
  265. self.analysis_data["final_results"] = final_results
  266. # 保存结果文件
  267. result_file = self.data_dir / f"{self.session_id}_final_results.csv"
  268. result_df.to_csv(result_file, index=False, encoding='utf-8')
  269. results_summary = ", ".join([f"{r['unit']}: {r['cip_time'] or 'N/A'}" for r in final_results])
  270. self.logger.info(f"最终结果 - {results_summary}, 已保存: {result_file.name}")
  271. except Exception as e:
  272. self.logger.error(f"记录最终结果失败: {e}")
  273. def create_analysis_plots(self, all_data, unit_days_dict):
  274. """
  275. 创建分析图表
  276. 功能:生成压差趋势图和机组对比图
  277. Args:
  278. all_data: pd.DataFrame,完整预测数据
  279. unit_days_dict: dict,各机组预测天数
  280. """
  281. try:
  282. # 创建压差趋势总览图
  283. fig, axes = plt.subplots(2, 2, figsize=(20, 12))
  284. fig.suptitle(f'RO膜污染分析总览 - {self.session_id}', fontsize=16, fontweight='bold')
  285. # 为每个机组绘制压差趋势
  286. for idx, unit_id in enumerate([1, 2, 3, 4]):
  287. ax = axes[idx//2, idx%2]
  288. # 筛选该机组的压差列
  289. unit_columns = [col for col in all_data.columns if f'RO{unit_id}' in col and 'DPT' in col]
  290. if unit_columns:
  291. # 截取预测天数范围内的数据
  292. predict_days = unit_days_dict.get(unit_id, 90)
  293. end_time = all_data.index[0] + timedelta(days=predict_days)
  294. truncated_data = all_data.loc[all_data.index <= end_time]
  295. # 绘制各段压差曲线
  296. for col in unit_columns:
  297. if col in truncated_data.columns:
  298. stage = "一段" if "DPT_1" in col else "二段"
  299. ax.plot(truncated_data.index, truncated_data[col],
  300. label=f'{stage}压差', linewidth=2, alpha=0.8)
  301. ax.set_title(f'RO{unit_id} 压差趋势 (预测{predict_days}天)', fontweight='bold')
  302. ax.set_xlabel('时间')
  303. ax.set_ylabel('压差 (MPa)')
  304. ax.legend()
  305. ax.grid(True, alpha=0.3)
  306. ax.tick_params(axis='x', rotation=45)
  307. else:
  308. # 无数据时显示提示
  309. ax.text(0.5, 0.5, f'RO{unit_id}\n无压差数据',
  310. ha='center', va='center', transform=ax.transAxes,
  311. fontsize=14, bbox=dict(boxstyle="round,pad=0.3", facecolor="lightgray"))
  312. ax.set_title(f'RO{unit_id} - 无数据')
  313. plt.tight_layout()
  314. # 保存图表文件
  315. plot_file = self.plots_dir / f"{self.session_id}_pressure_trends.png"
  316. plt.savefig(plot_file, dpi=300, bbox_inches='tight')
  317. plt.close()
  318. # 创建机组对比图
  319. comparison_plot = self._create_unit_comparison_plot(unit_days_dict)
  320. self.logger.info(f"分析图表 - 已保存: {plot_file.name}, {comparison_plot}")
  321. except Exception as e:
  322. self.logger.error(f"创建分析图表失败: {e}")
  323. def _create_unit_comparison_plot(self, unit_days_dict):
  324. """创建机组对比图"""
  325. try:
  326. fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 6))
  327. # 预测天数对比
  328. units = list(unit_days_dict.keys())
  329. days = list(unit_days_dict.values())
  330. bars = ax1.bar([f'RO{u}' for u in units], days,
  331. color=['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728'])
  332. ax1.set_title('各机组预测周期对比', fontweight='bold')
  333. ax1.set_ylabel('预测天数')
  334. ax1.grid(True, alpha=0.3)
  335. # 在柱子上显示数值
  336. for bar, day in zip(bars, days):
  337. height = bar.get_height()
  338. ax1.text(bar.get_x() + bar.get_width()/2., height + 1,
  339. f'{day}天', ha='center', va='bottom', fontweight='bold')
  340. # CIP时机分布(如果有结果的话)
  341. if self.analysis_data.get("final_results"):
  342. cip_times = []
  343. units_with_cip = []
  344. for result in self.analysis_data["final_results"]:
  345. if result["cip_time"]:
  346. cip_times.append(pd.to_datetime(result["cip_time"]))
  347. units_with_cip.append(result["unit"])
  348. if cip_times:
  349. # 计算从预测开始的天数
  350. start_time = min(cip_times)
  351. days_from_start = [(t - start_time).days for t in cip_times]
  352. bars2 = ax2.bar(units_with_cip, days_from_start,
  353. color=['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728'][:len(units_with_cip)])
  354. ax2.set_title('各机组CIP建议时机', fontweight='bold')
  355. ax2.set_ylabel('距离预测开始天数')
  356. ax2.grid(True, alpha=0.3)
  357. # 在柱子上显示数值
  358. for bar, day in zip(bars2, days_from_start):
  359. height = bar.get_height()
  360. ax2.text(bar.get_x() + bar.get_width()/2., height + 0.5,
  361. f'{day}天', ha='center', va='bottom', fontweight='bold')
  362. else:
  363. ax2.text(0.5, 0.5, '无有效CIP时机',
  364. ha='center', va='center', transform=ax2.transAxes,
  365. fontsize=14, bbox=dict(boxstyle="round,pad=0.3", facecolor="lightcoral"))
  366. else:
  367. ax2.text(0.5, 0.5, '分析未完成',
  368. ha='center', va='center', transform=ax2.transAxes,
  369. fontsize=14, bbox=dict(boxstyle="round,pad=0.3", facecolor="lightgray"))
  370. plt.tight_layout()
  371. # 保存图表文件
  372. comparison_file = self.plots_dir / f"{self.session_id}_unit_comparison.png"
  373. plt.savefig(comparison_file, dpi=300, bbox_inches='tight')
  374. plt.close()
  375. return comparison_file.name
  376. except Exception as e:
  377. self.logger.error(f"创建机组对比图失败: {e}")
  378. return None
  379. def generate_analysis_report(self):
  380. """
  381. 生成分析报告
  382. 功能:
  383. 1. 保存完整分析数据(JSON格式)
  384. 2. 生成HTML格式报告
  385. 3. 输出会话总结
  386. """
  387. try:
  388. # 记录结束时间
  389. self.analysis_data["session_info"]["end_time"] = datetime.now().isoformat()
  390. # 保存JSON数据
  391. json_file = self.reports_dir / f"{self.session_id}_analysis_data.json"
  392. with open(json_file, 'w', encoding='utf-8') as f:
  393. json.dump(self.analysis_data, f, ensure_ascii=False, indent=2)
  394. # 生成HTML报告
  395. html_report = self._generate_html_report()
  396. html_file = self.reports_dir / f"{self.session_id}_report.html"
  397. with open(html_file, 'w', encoding='utf-8') as f:
  398. f.write(html_report)
  399. self.logger.info(f"分析报告 - 已保存: {json_file.name}, {html_file.name}")
  400. # 生成会话总结
  401. self._log_session_summary()
  402. except Exception as e:
  403. self.logger.error(f"生成分析报告失败: {e}")
  404. def _generate_html_report(self):
  405. """生成HTML格式的分析报告"""
  406. session_info = self.analysis_data["session_info"]
  407. input_params = self.analysis_data["input_parameters"]
  408. final_results = self.analysis_data.get("final_results", [])
  409. html = f"""
  410. <!DOCTYPE html>
  411. <html lang="zh-CN">
  412. <head>
  413. <meta charset="UTF-8">
  414. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  415. <title>CIP分析报告 - {session_info['session_id']}</title>
  416. <style>
  417. body {{ font-family: Arial, sans-serif; margin: 20px; background-color: #f5f5f5; }}
  418. .container {{ max-width: 1200px; margin: 0 auto; background-color: white; padding: 20px; border-radius: 10px; box-shadow: 0 0 10px rgba(0,0,0,0.1); }}
  419. h1 {{ color: #2c3e50; text-align: center; border-bottom: 3px solid #3498db; padding-bottom: 10px; }}
  420. h2 {{ color: #34495e; border-left: 4px solid #3498db; padding-left: 10px; }}
  421. .info-grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; margin: 20px 0; }}
  422. .info-card {{ background-color: #ecf0f1; padding: 15px; border-radius: 8px; border-left: 4px solid #3498db; }}
  423. .result-table {{ width: 100%; border-collapse: collapse; margin: 20px 0; }}
  424. .result-table th, .result-table td {{ border: 1px solid #ddd; padding: 12px; text-align: left; }}
  425. .result-table th {{ background-color: #3498db; color: white; }}
  426. .result-table tr:nth-child(even) {{ background-color: #f2f2f2; }}
  427. .status-success {{ color: #27ae60; font-weight: bold; }}
  428. .status-warning {{ color: #f39c12; font-weight: bold; }}
  429. .status-error {{ color: #e74c3c; font-weight: bold; }}
  430. .timestamp {{ color: #7f8c8d; font-size: 0.9em; }}
  431. </style>
  432. </head>
  433. <body>
  434. <div class="container">
  435. <h1>RO膜污染监控与CIP预测分析报告</h1>
  436. <div class="info-grid">
  437. <div class="info-card">
  438. <h3>📋 会话信息</h3>
  439. <p><strong>会话ID:</strong> {session_info['session_id']}</p>
  440. <p><strong>开始时间:</strong> <span class="timestamp">{session_info['start_time']}</span></p>
  441. <p><strong>结束时间:</strong> <span class="timestamp">{session_info.get('end_time', '进行中')}</span></p>
  442. <p><strong>系统版本:</strong> {session_info['system_version']}</p>
  443. </div>
  444. <div class="info-card">
  445. <h3>⚙️ 输入参数</h3>
  446. <p><strong>策略:</strong> {input_params.get('strategy', 'N/A')}</p>
  447. <p><strong>起始时间:</strong> {input_params.get('start_date', 'N/A')}</p>
  448. <p><strong>预测起始:</strong> {input_params.get('prediction_start_date', 'N/A')}</p>
  449. </div>
  450. </div>
  451. <h2>🎯 分析结果</h2>
  452. <table class="result-table">
  453. <thead>
  454. <tr>
  455. <th>机组</th>
  456. <th>CIP建议时机</th>
  457. <th>策略说明</th>
  458. <th>状态</th>
  459. </tr>
  460. </thead>
  461. <tbody>
  462. """
  463. for result in final_results:
  464. status_class = "status-success" if result["cip_time"] else "status-warning"
  465. status_text = "有建议" if result["cip_time"] else "无建议"
  466. cip_time_display = result["cip_time"] or "N/A"
  467. html += f"""
  468. <tr>
  469. <td><strong>{result['unit']}</strong></td>
  470. <td>{cip_time_display}</td>
  471. <td>{result['strategy_description']}</td>
  472. <td class="{status_class}">{status_text}</td>
  473. </tr>
  474. """
  475. html += """
  476. </tbody>
  477. </table>
  478. <h2>📊 数据文件</h2>
  479. <div class="info-card">
  480. <p>本次分析生成的所有数据文件和图表已保存在以下目录:</p>
  481. <ul>
  482. <li><strong>数据文件:</strong> analysis_logs/data/</li>
  483. <li><strong>图表文件:</strong> analysis_logs/plots/</li>
  484. <li><strong>报告文件:</strong> analysis_logs/reports/</li>
  485. </ul>
  486. </div>
  487. <h2>ℹ️ 说明</h2>
  488. <div class="info-card">
  489. <p>本报告基于RO膜污染监控与CIP预测系统生成,包含了完整的分析过程记录。工艺人员可以根据以下内容进行合理性排查:</p>
  490. <ul>
  491. <li>查看各机组的压差趋势图,判断污染发展情况</li>
  492. <li>对比预测周期与历史经验,评估合理性</li>
  493. <li>分析k值变化趋势,了解膜污染速率</li>
  494. <li>检查CIP建议时机是否符合实际运行需求</li>
  495. </ul>
  496. </div>
  497. <div class="timestamp" style="text-align: center; margin-top: 30px; border-top: 1px solid #ddd; padding-top: 15px;">
  498. 报告生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
  499. </div>
  500. </div>
  501. </body>
  502. </html>
  503. """
  504. return html
  505. def _log_session_summary(self):
  506. """
  507. 记录会话总结
  508. 功能:输出分析耗时、成功率、文件统计等信息
  509. """
  510. session_info = self.analysis_data["session_info"]
  511. start_time = datetime.fromisoformat(session_info["start_time"])
  512. end_time = datetime.fromisoformat(session_info["end_time"])
  513. duration = end_time - start_time
  514. # 统计分析结果
  515. final_results = self.analysis_data.get("final_results", [])
  516. success_count = sum(1 for r in final_results if r["cip_time"])
  517. total_count = len(final_results)
  518. success_rate = f"{success_count/total_count*100:.1f}%" if total_count > 0 else "N/A"
  519. # 统计生成的文件
  520. data_files = len(list(self.data_dir.glob(f"{self.session_id}_*.csv")))
  521. plot_files = len(list(self.plots_dir.glob(f"{self.session_id}_*.png")))
  522. self.logger.info(f"会话结束 - ID: {session_info['session_id']}, 耗时: {duration.total_seconds():.2f}秒, 机组: {total_count}个, 成功: {success_count}个({success_rate}), 文件: {data_files}数据+{plot_files}图表")
  523. def close(self):
  524. """关闭日志记录器"""
  525. self.generate_analysis_report()
  526. # 关闭所有处理器
  527. for handler in self.logger.handlers[:]:
  528. handler.close()
  529. self.logger.removeHandler(handler)