增加两档PID控制,针对流量误差大和小的情况分别控制,在误差大时快速调节,误差小时慢慢调节以减少震荡和稳态误差。

This commit is contained in:
2026-08-21 17:25:30 +08:00
parent c56858ab0f
commit 26d4cbc316
21 changed files with 14662 additions and 14 deletions
+147
View File
@@ -89,6 +89,15 @@ class FlowControlLoop:
max_control_dt_s=None,
max_consecutive_flow_failures=3,
max_consecutive_pressure_failures=3,
pid_far_gains=(0.5, 0.5, 0.0),
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_target_ratio=0.05,
pid_far_error_min_slm=12.0,
pid_far_error_target_ratio=0.20,
pid_mode_switch_confirm_cycles=3,
logger=None,
):
if period_s <= 0:
@@ -100,6 +109,46 @@ class FlowControlLoop:
if pressure_channel is not None and max_pressure_kpa is None:
raise ValueError("启用压力监测时必须设置 max_pressure_kpa")
far_gains = tuple(float(value) for value in pid_far_gains)
near_gains = tuple(float(value) for value in pid_near_gains)
if len(far_gains) != 3 or len(near_gains) != 3:
raise ValueError("FAR/NEAR PID 参数必须各包含 Kp、Ki、Kd")
if any(
not math.isfinite(value) or value < 0
for value in far_gains + near_gains
):
raise ValueError("FAR/NEAR PID 参数必须是非负有限数值")
far_rate = float(opening_rate_far_pct_s)
near_rate = float(opening_rate_near_pct_s)
if (
not math.isfinite(far_rate)
or not math.isfinite(near_rate)
or far_rate <= 0
or near_rate <= 0
):
raise ValueError("FAR/NEAR 最大开度变化速度必须大于 0")
near_error_min = float(pid_near_error_min_slm)
far_error_min = float(pid_far_error_min_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,
far_error_min,
near_error_ratio,
far_error_ratio,
)
):
raise ValueError("PID 模式切换阈值必须是非负有限数值")
if near_error_min >= 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)
if confirm_cycles != pid_mode_switch_confirm_cycles or confirm_cycles < 1:
raise ValueError("PID 模式切换确认周期数必须至少为 1")
self.hardware = hardware
self.pid = pid
self.period_s = float(period_s)
@@ -125,6 +174,15 @@ class FlowControlLoop:
self.max_consecutive_pressure_failures = int(
max_consecutive_pressure_failures
)
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_target_ratio = near_error_ratio
self.pid_far_error_min_slm = far_error_min
self.pid_far_error_target_ratio = far_error_ratio
self.pid_mode_switch_confirm_cycles = confirm_cycles
self.logger = logger
self.target_flow_slm = 0.0
@@ -137,6 +195,9 @@ class FlowControlLoop:
self.pressure_failure_count = 0
self.running = False
self.faulted = False
self.pid_mode = "FAR"
self._pid_mode_confirm_count = 0
self._apply_pid_mode("FAR", force=True, log_change=False)
def set_target_flow(self, target_flow_slm):
"""设置目标流量;超出允许范围时拒绝,而不是静默截断。"""
@@ -148,11 +209,15 @@ class FlowControlLoop:
f"目标流量 {target} SLM 超出 "
f"{self.target_min_slm}~{self.target_max_slm} SLM"
)
target_changed = target != self.target_flow_slm
self.target_flow_slm = target
if target_changed:
self._apply_pid_mode("FAR", reason="目标流量切换")
def start(self, initial_opening=100.0):
"""复位状态并启动控制;默认从全开开度开始。"""
initial_opening = self._bounded_opening(initial_opening)
self._apply_pid_mode("FAR", reason="控制启动")
self.pid.reset(initial_output=initial_opening)
self.last_opening_pct = initial_opening
self.last_motor_position = opening_to_motor_position(
@@ -252,6 +317,7 @@ class FlowControlLoop:
self.pid.reset(initial_output=0.0)
opening = 0.0
else:
self._update_pid_mode(flow)
opening = self.pid.update(
measurement=flow,
setpoint=self.target_flow_slm,
@@ -287,6 +353,86 @@ class FlowControlLoop:
actual_dt_s=actual_dt,
)
def _update_pid_mode(self, flow):
"""按误差滞回切换 FAR/NEAR;参数只在模式变化时更新一次。"""
error_abs = abs(self.target_flow_slm - flow)
near_threshold = max(
self.pid_near_error_min_slm,
self.pid_near_error_target_ratio * self.target_flow_slm,
)
far_threshold = max(
self.pid_far_error_min_slm,
self.pid_far_error_target_ratio * self.target_flow_slm,
)
if self.pid_mode == "FAR":
condition_met = error_abs <= near_threshold
next_mode = "NEAR"
threshold = near_threshold
else:
condition_met = error_abs >= far_threshold
next_mode = "FAR"
threshold = far_threshold
if not condition_met:
self._pid_mode_confirm_count = 0
return
self._pid_mode_confirm_count += 1
if self._pid_mode_confirm_count < self.pid_mode_switch_confirm_cycles:
return
comparison = "<=" if next_mode == "NEAR" else ">="
self._apply_pid_mode(
next_mode,
reason=(
f"|误差|={error_abs:.3f} SLM {comparison} "
f"阈值={threshold:.3f} SLM,连续 "
f"{self.pid_mode_switch_confirm_cycles} 个周期"
),
)
def _apply_pid_mode(
self,
mode,
*,
reason=None,
force=False,
log_change=True,
):
if mode not in ("FAR", "NEAR"):
raise ValueError(f"未知 PID 模式: {mode!r}")
self._pid_mode_confirm_count = 0
if not force and mode == self.pid_mode:
return False
previous_mode = self.pid_mode
if mode == "FAR":
gains = self.pid_far_gains
opening_rate = self.opening_rate_far_pct_s
else:
gains = self.pid_near_gains
opening_rate = self.opening_rate_near_pct_s
self.pid.update_parameters(*gains)
self.pid.set_output_rate_limit(opening_rate)
self.pid_mode = mode
if log_change:
reason_text = "" if reason is None else f";原因={reason}"
self._log(
"info",
"PID 模式 %s -> %sKp=%.3f Ki=%.3f Kd=%.3f "
"最大开度速度=%.3f%%/s%s",
previous_mode,
mode,
gains[0],
gains[1],
gains[2],
opening_rate,
reason_text,
)
return True
def _read_flow(self):
try:
value = self.hardware.get_flow(self.flow_channel)
@@ -400,6 +546,7 @@ class FlowControlLoop:
"device_connected": bool(getattr(self.hardware, "connected", False)),
"flow_failure_count": self.flow_failure_count,
"pressure_failure_count": self.pressure_failure_count,
"pid_mode": self.pid_mode,
}
def _bounded_opening(self, opening):