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

- 前馈冻结改用可配置的 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
+97 -25
View File
@@ -1,4 +1,4 @@
"""逐周期保存控制数据,并在运行结束时生成联曲线图。"""
"""逐周期保存控制数据,并在运行结束时生成联曲线图。"""
import csv
from datetime import datetime
@@ -6,6 +6,10 @@ import math
from pathlib import Path
ATMOSPHERIC_PRESSURE_KPA = 101.325
CRITICAL_PRESSURE_RATIO = 0.528
CSV_FIELDS = (
"time_s",
"timestamp",
@@ -14,6 +18,7 @@ CSV_FIELDS = (
"opening_pct",
"pressure_kpa",
"pressure_before_kpa",
"P_abs_ratio",
"error_slm",
"motor_position",
"feedforward_pct",
@@ -25,7 +30,7 @@ CSV_FIELDS = (
class FlowRunRecorder:
"""把每个控制周期写入 CSV,并在结束时输出一张联图。"""
"""把每个控制周期写入 CSV,并在结束时输出一张联图。"""
def __init__(self, output_directory, *, plot_dpi=160):
output_dir = Path(output_directory)
@@ -53,6 +58,10 @@ class FlowRunRecorder:
self._first_timestamp = float(result.timestamp)
elapsed_s = max(0.0, float(result.timestamp) - self._first_timestamp)
p_abs_ratio = self._absolute_pressure_ratio(
result.pressure_before_kpa,
result.pressure_kpa,
)
self._writer.writerow(
{
"time_s": f"{elapsed_s:.6f}",
@@ -66,6 +75,7 @@ class FlowRunRecorder:
"pressure_before_kpa": self._format_value(
result.pressure_before_kpa
),
"P_abs_ratio": self._format_value(p_abs_ratio),
"error_slm": self._format_value(result.error_slm),
"motor_position": self._format_value(result.motor_position),
"feedforward_pct": self._format_value(result.feedforward_pct),
@@ -104,7 +114,9 @@ class FlowRunRecorder:
measured_flow = []
opening = []
feedforward = []
pressure = []
pressure_before = []
pressure_after = []
p_abs_ratio = []
with self.csv_path.open("r", newline="", encoding="utf-8-sig") as file:
for row in csv.DictReader(file):
@@ -117,12 +129,20 @@ class FlowRunRecorder:
feedforward.append(
self._float_or_nan(row.get("feedforward_pct"))
)
pressure.append(self._float_or_nan(row["pressure_kpa"]))
before = self._float_or_nan(row["pressure_before_kpa"])
after = self._float_or_nan(row["pressure_kpa"])
pressure_before.append(before)
pressure_after.append(after)
ratio = self._float_or_nan(row.get("P_abs_ratio"))
if not math.isfinite(ratio):
ratio_value = self._absolute_pressure_ratio(before, after)
ratio = math.nan if ratio_value is None else ratio_value
p_abs_ratio.append(ratio)
figure, axes = plt.subplots(
3,
4,
1,
figsize=(12, 10),
figsize=(12, 14),
sharex=True,
constrained_layout=True,
)
@@ -168,26 +188,42 @@ class FlowRunRecorder:
axes[1].set_title("Valve opening")
axes[1].set_ylim(-2, 102)
if any(math.isfinite(value) for value in pressure):
axes[2].plot(
time_s,
pressure,
color="#6A1B9A",
linewidth=1.4,
)
else:
axes[2].text(
0.5,
0.5,
"No pressure data",
transform=axes[2].transAxes,
ha="center",
va="center",
color="gray",
)
self._plot_series(
axes[2],
time_s,
pressure_before,
"Pressure before valve",
"#1565C0",
)
self._plot_series(
axes[2],
time_s,
pressure_after,
"Pressure after valve",
"#D84315",
)
axes[2].set_ylabel("Pressure (kPa)")
axes[2].set_xlabel("Time (s)")
axes[2].set_title("Pressure")
axes[2].set_title("Pressure across control valve")
axes[2].legend(loc="best")
self._plot_series(
axes[3],
time_s,
p_abs_ratio,
"P_abs_ratio (after/before)",
"#EF6C00",
)
axes[3].axhline(
CRITICAL_PRESSURE_RATIO,
color="#B71C1C",
linestyle="--",
linewidth=1.2,
label=f"Critical ratio {CRITICAL_PRESSURE_RATIO:.3f}",
)
axes[3].set_ylabel("P_abs_ratio")
axes[3].set_xlabel("Time (s)")
axes[3].set_title("Absolute pressure ratio across control valve")
axes[3].legend(loc="best")
for axis in axes:
axis.grid(True, alpha=0.3, linestyle="--")
@@ -196,6 +232,42 @@ class FlowRunRecorder:
figure.savefig(self.image_path, dpi=self.plot_dpi, bbox_inches="tight")
plt.close(figure)
@staticmethod
def _plot_series(axis, time_s, series, label, color):
if any(math.isfinite(value) for value in series):
axis.plot(
time_s,
series,
color=color,
linewidth=1.4,
label=label,
)
else:
axis.text(
0.5,
0.5,
f"No {label.lower()} data",
transform=axis.transAxes,
ha="center",
va="center",
color="gray",
)
@staticmethod
def _absolute_pressure_ratio(pressure_before_kpa, pressure_after_kpa):
"""返回阀后绝对压力 / 阀前绝对压力;无效读数返回 None。"""
if pressure_before_kpa is None or pressure_after_kpa is None:
return None
try:
before_abs = float(pressure_before_kpa) + ATMOSPHERIC_PRESSURE_KPA
after_abs = float(pressure_after_kpa) + ATMOSPHERIC_PRESSURE_KPA
except (TypeError, ValueError):
return None
if before_abs <= 0.0:
return None
ratio = after_abs / before_abs
return ratio if math.isfinite(ratio) else None
@staticmethod
def _format_value(value):
if value is None: