实现流量控制前馈 + 增益调度(MFC 结构)

按 plan.md 新增三个交付物,全部默认关闭以保持与纯 PID 的 A/B 兼容:

- valve_model.py:阀特性模型 Q_ss = A_eff(x)·P1_abs·F(r),含 ISO 6358
  椭圆 F(r)、行程/面积互逆插值、单调性校验、JSON 存取。
- identify_valve.py:从 open_loop.py 扫点 CSV 拟合 A_eff 表并输出 JSON。
- 控制器集成:config/flow_control/main/data_logger 新增阀前压读取
  (channel 2)、前馈打底 + PI 修残差、按 P1 增益调度,并记录/绘制前馈项。

新增配置 FEEDFORWARD_ENABLED / GAIN_SCHEDULE_ENABLED 默认 False,
VALVE_MODEL_PATH 默认空,未加载模型时行为与原先纯 PID 完全一致。
This commit is contained in:
2026-08-27 16:00:51 +08:00
parent d7e712ede7
commit a03735387c
7 changed files with 958 additions and 20 deletions
+51 -5
View File
@@ -23,6 +23,7 @@ from controllers import IncrementalPID
from data_logger import FlowRunRecorder
from flow_control import FlowControlFault, FlowControlLoop
from performance_reporter import PerformanceReporter
from valve_model import ValveModel
class ConsoleGate(logging.Filter):
@@ -61,7 +62,27 @@ def build_logger():
return logger, console_gate
def _load_valve_model(logger):
"""按配置加载阀特性模型;配置为空或文件缺失/损坏时返回 None(纯 PID)。"""
if not config.VALVE_MODEL_PATH:
return None
model_path = Path(__file__).resolve().parent / config.VALVE_MODEL_PATH
if not model_path.exists():
logger.warning("阀特性模型文件不存在:%s,退化为纯 PID", model_path)
return None
try:
return ValveModel.load(
str(model_path),
config.MOTOR_OPEN_POSITION,
config.MOTOR_CLOSED_POSITION,
)
except (ValueError, OSError) as exc:
logger.warning("加载阀特性模型失败(%s):%s,退化为纯 PID", model_path, exc)
return None
def build_control_loop(hardware, logger):
valve_model = _load_valve_model(logger)
pid = IncrementalPID(
kp=config.PID_FAR_KP,
ki=config.PID_FAR_KI,
@@ -77,6 +98,7 @@ def build_control_loop(hardware, logger):
period_s=config.CONTROL_PERIOD_S,
flow_channel=config.FLOW_INPUT_CHANNEL,
pressure_channel=config.PRESSURE_INPUT_CHANNEL,
pressure_before_channel=config.PRESSURE_BEFORE_CHANNEL,
motor_channel=config.MOTOR_OUTPUT_CHANNEL,
motor_open_position=config.MOTOR_OPEN_POSITION,
motor_closed_position=config.MOTOR_CLOSED_POSITION,
@@ -112,6 +134,15 @@ def build_control_loop(hardware, logger):
pid_mode_switch_confirm_cycles=(
config.PID_MODE_SWITCH_CONFIRM_CYCLES
),
valve_model=valve_model,
feedforward_enabled=config.FEEDFORWARD_ENABLED,
feedforward_correction_band_pct=config.FEEDFORWARD_CORRECTION_BAND_PCT,
gain_schedule_enabled=config.GAIN_SCHEDULE_ENABLED,
gain_schedule_ref_abs_kpa=config.GAIN_SCHEDULE_REF_ABS_KPA,
gain_schedule_floor_abs_kpa=config.GAIN_SCHEDULE_FLOOR_ABS_KPA,
gain_schedule_scale_change_threshold=(
config.GAIN_SCHEDULE_SCALE_CHANGE_THRESHOLD
),
logger=logger,
)
@@ -139,12 +170,27 @@ def format_status(result):
"--" if result.pressure_kpa is None
else f"{result.pressure_kpa:.2f}"
)
return (
f"状态={result.status} 目标={result.target_flow_slm:.2f} SLM "
f"实际={flow_text} SLM 压力={pressure_text} kPa "
f"开度={result.opening_pct:.2f}% "
f"行程={result.motor_position:.1f} dt={result.actual_dt_s:.3f}s"
pressure_before_text = (
"--" if result.pressure_before_kpa is None
else f"{result.pressure_before_kpa:.2f}"
)
parts = [
f"状态={result.status}",
f"目标={result.target_flow_slm:.2f} SLM",
f"实际={flow_text} SLM",
f"压力={pressure_text} kPa",
f"阀前压={pressure_before_text} kPa",
f"开度={result.opening_pct:.2f}%",
]
if result.feedforward_pct is not None:
parts.append(f"前馈={result.feedforward_pct:.2f}%")
if result.correction_pct is not None:
parts.append(f"修正={result.correction_pct:+.2f}%")
if result.gain_scale is not None and abs(result.gain_scale - 1.0) > 1e-9:
parts.append(f"scale={result.gain_scale:.3f}")
parts.append(f"行程={result.motor_position:.1f}")
parts.append(f"dt={result.actual_dt_s:.3f}s")
return " ".join(parts)
def run(target_flow_slm):