完善流量控制前馈、稳态判定与阀门标定流程
- 前馈冻结改用可配置的 3 秒流量绝对误差滑窗,修复采样抖动造成的重复解冻,并保留下游扰动后的自动更新 - 支持非单调阀特性反解、最小可测面积以下直接全关,以及闭阀端精细扫点与辨识开关 - 调整双压力判稳、最长等待时间、在线入口全开收尾和闭环四联图 - 更新配置、README、阀模型及离线测试,归档本轮实验数据与诊断产物
This commit is contained in:
+115
-18
@@ -4,6 +4,7 @@
|
||||
安全处理;底层 Modbus 通讯仍由 PcControl.Easy521ModbusClient 完成。
|
||||
"""
|
||||
|
||||
from collections import deque
|
||||
from dataclasses import asdict, dataclass
|
||||
import math
|
||||
import time
|
||||
@@ -101,6 +102,8 @@ class FlowControlLoop:
|
||||
valve_model=None,
|
||||
feedforward_enabled=False,
|
||||
feedforward_correction_band_pct=20.0,
|
||||
feedforward_update_error_threshold_slm=5.0,
|
||||
feedforward_update_window_s=3.0,
|
||||
gain_schedule_enabled=False,
|
||||
gain_schedule_ref_abs_kpa=501.325,
|
||||
gain_schedule_floor_abs_kpa=60.0,
|
||||
@@ -109,9 +112,11 @@ class FlowControlLoop:
|
||||
pid_near_gains=(0.05, 0.01, 0.0),
|
||||
opening_rate_far_pct_s=20.0,
|
||||
opening_rate_near_pct_s=2.0,
|
||||
pid_near_error_min_slm=3.0,
|
||||
pid_near_error_min_slm=1.0,
|
||||
pid_near_error_max_slm=3.0,
|
||||
pid_near_error_target_ratio=0.05,
|
||||
pid_far_error_min_slm=12.0,
|
||||
pid_far_error_min_slm=4.0,
|
||||
pid_far_error_max_slm=12.0,
|
||||
pid_far_error_target_ratio=0.20,
|
||||
pid_mode_switch_confirm_cycles=10,
|
||||
logger=None,
|
||||
@@ -144,21 +149,29 @@ class FlowControlLoop:
|
||||
):
|
||||
raise ValueError("FAR/NEAR 最大开度变化速度必须大于 0")
|
||||
near_error_min = float(pid_near_error_min_slm)
|
||||
near_error_max = float(pid_near_error_max_slm)
|
||||
far_error_min = float(pid_far_error_min_slm)
|
||||
far_error_max = float(pid_far_error_max_slm)
|
||||
near_error_ratio = float(pid_near_error_target_ratio)
|
||||
far_error_ratio = float(pid_far_error_target_ratio)
|
||||
if any(
|
||||
not math.isfinite(value) or value < 0
|
||||
for value in (
|
||||
near_error_min,
|
||||
near_error_max,
|
||||
far_error_min,
|
||||
far_error_max,
|
||||
near_error_ratio,
|
||||
far_error_ratio,
|
||||
)
|
||||
):
|
||||
raise ValueError("PID 模式切换阈值必须是非负有限数值")
|
||||
if near_error_min >= far_error_min:
|
||||
raise ValueError("PID NEAR 绝对误差阈值必须小于 FAR 阈值")
|
||||
if near_error_min > near_error_max:
|
||||
raise ValueError("PID NEAR 误差下限不得大于上限")
|
||||
if far_error_min > far_error_max:
|
||||
raise ValueError("PID FAR 误差下限不得大于上限")
|
||||
if near_error_max >= far_error_min:
|
||||
raise ValueError("PID NEAR 阈值范围必须整体小于 FAR 阈值范围")
|
||||
if near_error_ratio >= far_error_ratio:
|
||||
raise ValueError("PID NEAR 相对误差阈值必须小于 FAR 阈值")
|
||||
confirm_cycles = int(pid_mode_switch_confirm_cycles)
|
||||
@@ -202,6 +215,23 @@ class FlowControlLoop:
|
||||
if not math.isfinite(feedforward_band) or not 0.0 < feedforward_band <= 100.0:
|
||||
raise ValueError("feedforward_correction_band_pct 必须位于 0~100")
|
||||
self.feedforward_correction_band_pct = feedforward_band
|
||||
feedforward_update_threshold = float(
|
||||
feedforward_update_error_threshold_slm
|
||||
)
|
||||
if (
|
||||
not math.isfinite(feedforward_update_threshold)
|
||||
or feedforward_update_threshold < 0.0
|
||||
):
|
||||
raise ValueError(
|
||||
"feedforward_update_error_threshold_slm 必须是非负有限数值"
|
||||
)
|
||||
self.feedforward_update_error_threshold_slm = (
|
||||
feedforward_update_threshold
|
||||
)
|
||||
feedforward_window = float(feedforward_update_window_s)
|
||||
if not math.isfinite(feedforward_window) or feedforward_window <= 0.0:
|
||||
raise ValueError("feedforward_update_window_s 必须是正的有限数值")
|
||||
self.feedforward_update_window_s = feedforward_window
|
||||
|
||||
self.gain_schedule_enabled = bool(gain_schedule_enabled)
|
||||
self.gain_schedule_ref_abs_kpa = float(gain_schedule_ref_abs_kpa)
|
||||
@@ -236,14 +266,18 @@ class FlowControlLoop:
|
||||
self._current_scale = 1.0
|
||||
self._current_base_gains = far_gains
|
||||
self._last_feedforward_opening = None
|
||||
self._feedforward_error_window = deque()
|
||||
self._feedforward_window_ready = False
|
||||
|
||||
self.pid_far_gains = far_gains
|
||||
self.pid_near_gains = near_gains
|
||||
self.opening_rate_far_pct_s = far_rate
|
||||
self.opening_rate_near_pct_s = near_rate
|
||||
self.pid_near_error_min_slm = near_error_min
|
||||
self.pid_near_error_max_slm = near_error_max
|
||||
self.pid_near_error_target_ratio = near_error_ratio
|
||||
self.pid_far_error_min_slm = far_error_min
|
||||
self.pid_far_error_max_slm = far_error_max
|
||||
self.pid_far_error_target_ratio = far_error_ratio
|
||||
self.pid_mode_switch_confirm_cycles = confirm_cycles
|
||||
self.logger = logger
|
||||
@@ -277,6 +311,7 @@ class FlowControlLoop:
|
||||
target_changed = target != self.target_flow_slm
|
||||
self.target_flow_slm = target
|
||||
if target_changed:
|
||||
self._reset_feedforward_error_window()
|
||||
self._pid_mode_confirm_count = 0
|
||||
if self.last_flow_slm is None:
|
||||
self._apply_pid_mode(
|
||||
@@ -312,6 +347,7 @@ class FlowControlLoop:
|
||||
# 前馈打底,PID 从零修正量开始。
|
||||
self.pid.reset(initial_output=0.0)
|
||||
self._last_feedforward_opening = None
|
||||
self._reset_feedforward_error_window()
|
||||
else:
|
||||
self.pid.reset(initial_output=initial_opening)
|
||||
self.last_opening_pct = initial_opening
|
||||
@@ -433,6 +469,8 @@ class FlowControlLoop:
|
||||
|
||||
if self.target_flow_slm <= self.zero_flow_threshold_slm:
|
||||
self.pid.reset(initial_output=0.0)
|
||||
self._last_feedforward_opening = None
|
||||
self._reset_feedforward_error_window()
|
||||
opening = 0.0
|
||||
opening_ff = None
|
||||
correction = None
|
||||
@@ -445,16 +483,26 @@ class FlowControlLoop:
|
||||
opening_ff = None
|
||||
correction = None
|
||||
if self._feedforward_active:
|
||||
opening_ff = self._compute_feedforward_opening(
|
||||
self.target_flow_slm, pressure_before, pressure
|
||||
mean_error_abs = self._feedforward_window_mean_abs_error(
|
||||
current_time,
|
||||
flow,
|
||||
)
|
||||
if opening_ff is not None:
|
||||
self._last_feedforward_opening = opening_ff
|
||||
base = (
|
||||
opening_ff
|
||||
if opening_ff is not None
|
||||
else self._last_feedforward_opening
|
||||
should_update_feedforward = (
|
||||
self._last_feedforward_opening is None
|
||||
or mean_error_abs is None
|
||||
or mean_error_abs
|
||||
> self.feedforward_update_error_threshold_slm
|
||||
)
|
||||
if should_update_feedforward:
|
||||
candidate = self._compute_feedforward_opening(
|
||||
self.target_flow_slm, pressure_before, pressure
|
||||
)
|
||||
if candidate is not None:
|
||||
self._last_feedforward_opening = candidate
|
||||
|
||||
# 窗口内复用缓存前馈;PID correction 每个周期照常更新。
|
||||
opening_ff = self._last_feedforward_opening
|
||||
base = opening_ff
|
||||
if base is None:
|
||||
base = 0.0
|
||||
correction = self.pid.update(
|
||||
@@ -510,6 +558,49 @@ class FlowControlLoop:
|
||||
gain_scale=scale,
|
||||
)
|
||||
|
||||
def _feedforward_window_mean_abs_error(self, timestamp, flow):
|
||||
"""返回完整滑窗的流量绝对误差均值;窗口尚未积满时返回 None。"""
|
||||
timestamp = float(timestamp)
|
||||
error_abs = abs(self.target_flow_slm - float(flow))
|
||||
|
||||
# perf_counter 正常单调递增;若测试或外部调用传入了回退时间戳,
|
||||
# 将其视为一个新窗口,避免沿用错误的“已积满”状态。
|
||||
if (
|
||||
self._feedforward_error_window
|
||||
and timestamp < self._feedforward_error_window[-1][0]
|
||||
):
|
||||
self._reset_feedforward_error_window()
|
||||
|
||||
self._feedforward_error_window.append((timestamp, error_abs))
|
||||
|
||||
# “窗口已积满”只描述数据是否经过了一个完整窗口。首次满足后保持
|
||||
# ready,直至目标改变/控制器重启/零流量复位,避免队首淘汰和采样
|
||||
# 抖动让窗口在 ready/not-ready 之间反复切换。
|
||||
if (
|
||||
not self._feedforward_window_ready
|
||||
and timestamp - self._feedforward_error_window[0][0]
|
||||
>= self.feedforward_update_window_s
|
||||
):
|
||||
self._feedforward_window_ready = True
|
||||
|
||||
cutoff = timestamp - self.feedforward_update_window_s
|
||||
while (
|
||||
self._feedforward_error_window
|
||||
and self._feedforward_error_window[0][0] < cutoff
|
||||
):
|
||||
self._feedforward_error_window.popleft()
|
||||
|
||||
if not self._feedforward_window_ready:
|
||||
return None
|
||||
return sum(item[1] for item in self._feedforward_error_window) / len(
|
||||
self._feedforward_error_window
|
||||
)
|
||||
|
||||
def _reset_feedforward_error_window(self):
|
||||
"""清空前馈误差滑窗,并要求重新累计一个完整窗口。"""
|
||||
self._feedforward_error_window.clear()
|
||||
self._feedforward_window_ready = False
|
||||
|
||||
def _update_pid_mode(self, flow):
|
||||
"""按误差滞回切换 FAR/NEAR;参数只在模式变化时更新一次。"""
|
||||
error_abs = abs(self.target_flow_slm - flow)
|
||||
@@ -545,13 +636,19 @@ class FlowControlLoop:
|
||||
)
|
||||
|
||||
def _pid_error_thresholds(self, target):
|
||||
near_threshold = max(
|
||||
self.pid_near_error_min_slm,
|
||||
self.pid_near_error_target_ratio * target,
|
||||
near_threshold = min(
|
||||
self.pid_near_error_max_slm,
|
||||
max(
|
||||
self.pid_near_error_min_slm,
|
||||
self.pid_near_error_target_ratio * target,
|
||||
),
|
||||
)
|
||||
far_threshold = max(
|
||||
self.pid_far_error_min_slm,
|
||||
self.pid_far_error_target_ratio * target,
|
||||
far_threshold = min(
|
||||
self.pid_far_error_max_slm,
|
||||
max(
|
||||
self.pid_far_error_min_slm,
|
||||
self.pid_far_error_target_ratio * target,
|
||||
),
|
||||
)
|
||||
return near_threshold, far_threshold
|
||||
|
||||
|
||||
Reference in New Issue
Block a user