Files
flow_control/performance_reporter.py
T
louis 1d065d81d3 新增输出稳态时滑动窗口流量均值和标准差,以及稳态误差(判稳成功时目标流量减去当时流量)。
目前逻辑是「一直控制 + 每秒状态照常输出 + 稳态时额外多打印一次性能报告」。
新增一个可选的自动停止功能--config开关,「达到稳态后自动停止控制」,让 reporter 通知主循环退出。
2026-08-20 09:24:58 +08:00

168 lines
5.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""控制性能报告:在达到稳态后输出 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,
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.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
if self._window[-1][0] - self._window[0][0] < self.window_s:
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", "本次运行未达到稳态,未生成控制性能报告")
@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)