"""控制性能报告:在达到稳态后输出 PID 参数、调节时间、超调量和控制范围。 本模块只负责观测每个控制周期产出的 ControlStepResult,用滑动时间窗口判断 稳态,并在首次进入稳态时打印一份一次性报告;不参与任何控制计算。 """ from collections import deque import math import statistics class PerformanceReporter: """逐周期观测流量,判定稳态并输出控制性能报告。 稳态判定采用两个条件同时满足(AND): 1. 滑窗均值落在目标流量 ±band_pct% 误差带内; 2. 滑窗标准差低于 std_pct% 目标流量阈值。 报告只在首次判定稳态时输出一次。 """ def __init__( self, pid, *, window_s, sample_period_s, band_pct, std_pct, flow_range_min_slm, flow_range_max_slm, initial_flow_slm=None, logger=None, ): self.pid = pid self.window_s = float(window_s) self.sample_period_s = float(sample_period_s) self.band_pct = float(band_pct) self.std_pct = float(std_pct) self.flow_range_min_slm = float(flow_range_min_slm) self.flow_range_max_slm = float(flow_range_max_slm) self.initial_flow_slm = ( None if initial_flow_slm is None else float(initial_flow_slm) ) self.logger = logger self.target_flow_slm = None self.settling_time_s = None self.steady_mean = None self.steady_std = None self.steady_error_slm = None self._first_timestamp = None self._peak_flow_slm = None self._min_flow_slm = None self._window = deque() # 元素为 (elapsed_s, flow_slm) self._reached = False def observe(self, result): """喂入一个 ControlStepResult;达到稳态时自动输出一次报告。""" if self._reached: return if result.status != "OK" or result.measured_flow_slm is None: return flow = float(result.measured_flow_slm) if not math.isfinite(flow): return target = float(result.target_flow_slm) if target <= 0.0: return self.target_flow_slm = target if self._first_timestamp is None: self._first_timestamp = float(result.timestamp) elapsed_s = max(0.0, float(result.timestamp) - self._first_timestamp) if self.initial_flow_slm is None: self.initial_flow_slm = flow if self._peak_flow_slm is None or flow > self._peak_flow_slm: self._peak_flow_slm = flow if self._min_flow_slm is None or flow < self._min_flow_slm: self._min_flow_slm = flow self._window.append((elapsed_s, flow)) cutoff = elapsed_s - self.window_s while self._window and self._window[0][0] < cutoff: self._window.popleft() if len(self._window) < 2: return # 采样时间戳会有轻微抖动。旧样本按 window_s 剔除后,窗口跨度通常会 # 比 window_s 少一个采样周期;若仍严格要求完整 window_s,判稳可能 # 只能依赖浮点时间戳碰巧相等。允许一个采样周期的覆盖余量。 min_span = max(0.0, self.window_s - self.sample_period_s) if self._window[-1][0] - self._window[0][0] < min_span: return flows = [item[1] for item in self._window] mean = statistics.mean(flows) std = statistics.pstdev(flows) band = self.band_pct / 100.0 * target std_threshold = self.std_pct / 100.0 * target if abs(mean - target) <= band and std <= std_threshold: self._reached = True self.settling_time_s = elapsed_s self.steady_mean = mean self.steady_std = std self.steady_error_slm = target - mean self._report() def finalize(self): """运行结束调用;若始终未达到稳态则补一条提示日志。""" if not self._reached and self.target_flow_slm is not None: self._log("info", "本次运行未达到稳态,未生成控制性能报告") def reset(self): """清空判稳状态,使切换目标后能重新判定稳态并再次输出报告。 ``initial_flow_slm`` 一并置空,观察器会在下一个有效样本上重新把它 初始化为切换目标时刻的实测流量,从而保证新目标的超调方向判断正确。 """ self._reached = False self.target_flow_slm = None self.settling_time_s = None self.steady_mean = None self.steady_std = None self.steady_error_slm = None self._first_timestamp = None self._peak_flow_slm = None self._min_flow_slm = None self.initial_flow_slm = None self._window.clear() @property def steady_reached(self): """是否已判定达到稳态(供主循环决定是否自动停止)。""" return self._reached def _report(self): target = self.target_flow_slm initial = self.initial_flow_slm peak = self._peak_flow_slm minimum = self._min_flow_slm if initial < target: overshoot_abs = max(0.0, peak - target) extremum_text = f"峰值 {peak:.3f} SLM" direction = "超出目标" elif initial > target: overshoot_abs = max(0.0, target - minimum) extremum_text = f"谷值 {minimum:.3f} SLM" direction = "低于目标" else: overshoot_abs = 0.0 extremum_text = f"峰值 {peak:.3f} SLM" direction = "无" overshoot_pct = overshoot_abs / target * 100.0 lines = [ "===== 控制性能报告 =====", ( f"PID 参数: Kp={self.pid.kp:.3f} Ki={self.pid.ki:.3f} " f"Kd={self.pid.kd:.3f}" ), f"目标流量: {target:.3f} SLM", ( f"控制范围: {self.flow_range_min_slm:.3f} ~ " f"{self.flow_range_max_slm:.3f} SLM" ), f"调节时间(至稳态): {self.settling_time_s:.3f} s", f"稳态流量(窗口均值): {self.steady_mean:.3f} SLM", f"稳态标准差(窗口): {self.steady_std:.3f} SLM", ( f"稳态误差: {self.steady_error_slm:+.3f} SLM " f"(目标 - 稳态均值)" ), f"初始流量: {initial:.3f} SLM", ( f"超调量: {extremum_text},{direction} " f"{overshoot_abs:+.3f} SLM ({overshoot_pct:+.2f}%)" ), "========================", ] self._log("info", "\n".join(lines)) def _log(self, level, message, *args): if self.logger is not None: getattr(self.logger, level)(message, *args) else: print(message)