增加两档PID控制,针对流量误差大和小的情况分别控制,在误差大时快速调节,误差小时慢慢调节以减少震荡和稳态误差。
This commit is contained in:
+147
@@ -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 -> %s:Kp=%.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):
|
||||
|
||||
Reference in New Issue
Block a user