完善流量控制前馈、稳态判定与阀门标定流程
- 前馈冻结改用可配置的 3 秒流量绝对误差滑窗,修复采样抖动造成的重复解冻,并保留下游扰动后的自动更新 - 支持非单调阀特性反解、最小可测面积以下直接全关,以及闭阀端精细扫点与辨识开关 - 调整双压力判稳、最长等待时间、在线入口全开收尾和闭环四联图 - 更新配置、README、阀模型及离线测试,归档本轮实验数据与诊断产物
This commit is contained in:
+97
-25
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user