完善流量控制前馈、稳态判定与阀门标定流程

- 前馈冻结改用可配置的 3 秒流量绝对误差滑窗,修复采样抖动造成的重复解冻,并保留下游扰动后的自动更新
- 支持非单调阀特性反解、最小可测面积以下直接全关,以及闭阀端精细扫点与辨识开关
- 调整双压力判稳、最长等待时间、在线入口全开收尾和闭环四联图
- 更新配置、README、阀模型及离线测试,归档本轮实验数据与诊断产物
This commit is contained in:
2026-09-02 16:14:42 +08:00
parent 5888b4ccce
commit cfeedcf887
80 changed files with 40473 additions and 171 deletions
+115 -18
View File
@@ -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