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

- 前馈冻结改用可配置的 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
+107 -24
View File
@@ -1,4 +1,4 @@
"""手动设置阀门开度,并每秒显示流量、压力和电机行程。"""
"""手动设置阀门开度,并每秒显示阀前、阀后传感器数据和电机行程。"""
import contextlib
import csv
@@ -30,9 +30,10 @@ class ManualValveRecorder:
CSV_FIELDS = (
"time_s",
"timestamp",
"flow_slm",
"flow_after_slm",
"opening_pct",
"pressure_kpa",
"pressure_before_kpa",
"pressure_after_kpa",
"motor_position",
)
@@ -49,16 +50,25 @@ class ManualValveRecorder:
self.sample_count = 0
self._closed = False
def record(self, sample_time, flow, opening, pressure, position):
def record(
self,
sample_time,
flow_after,
opening,
pressure_before,
pressure_after,
position,
):
if self._start_time is None:
self._start_time = sample_time
self._writer.writerow(
{
"time_s": f"{sample_time - self._start_time:.6f}",
"timestamp": datetime.now().isoformat(timespec="milliseconds"),
"flow_slm": self._format_number(flow),
"flow_after_slm": self._format_number(flow_after),
"opening_pct": self._format_number(opening),
"pressure_kpa": self._format_number(pressure),
"pressure_before_kpa": self._format_number(pressure_before),
"pressure_after_kpa": self._format_number(pressure_after),
"motor_position": self._format_number(position),
}
)
@@ -85,15 +95,21 @@ class ManualValveRecorder:
import matplotlib.pyplot as plt
time_s = []
flow = []
flow_after = []
opening = []
pressure = []
pressure_before = []
pressure_after = []
with self.csv_path.open("r", newline="", encoding="utf-8-sig") as file:
for row in csv.DictReader(file):
time_s.append(float(row["time_s"]))
flow.append(self._float_or_nan(row["flow_slm"]))
flow_after.append(self._float_or_nan(row["flow_after_slm"]))
opening.append(self._float_or_nan(row["opening_pct"]))
pressure.append(self._float_or_nan(row["pressure_kpa"]))
pressure_before.append(
self._float_or_nan(row["pressure_before_kpa"])
)
pressure_after.append(
self._float_or_nan(row["pressure_after_kpa"])
)
figure, axes = plt.subplots(
3,
@@ -104,19 +120,33 @@ class ManualValveRecorder:
)
figure.suptitle("Manual Valve Test", fontsize=15)
axes[0].plot(time_s, flow, color="#1565C0", linewidth=1.4)
axes[0].set_ylabel("Flow (SLM)")
axes[0].set_title("Flow")
axes[0].plot(time_s, flow_after, color="#1565C0", linewidth=1.4)
axes[0].set_ylabel("Flow after valve (SLM)")
axes[0].set_title("Flow after valve")
axes[1].plot(time_s, opening, color="#2E7D32", linewidth=1.4)
axes[1].set_ylabel("Opening (%)")
axes[1].set_title("Valve opening")
axes[1].set_ylim(-2, 102)
axes[2].plot(time_s, pressure, color="#6A1B9A", linewidth=1.4)
axes[2].plot(
time_s,
pressure_before,
color="#6A1B9A",
linewidth=1.4,
label="Pressure before valve",
)
axes[2].plot(
time_s,
pressure_after,
color="#D84315",
linewidth=1.4,
label="Pressure after valve",
)
axes[2].set_ylabel("Pressure (kPa)")
axes[2].set_xlabel("Time (s)")
axes[2].set_title("Pressure")
axes[2].set_title("Pressure before and after valve")
axes[2].legend()
for axis in axes:
axis.grid(True, alpha=0.3, linestyle="--")
@@ -156,6 +186,25 @@ def format_value(value, unit):
return f"{number:.2f} {unit}"
def format_analog_raw(value, physical_range, analog_raw_max):
"""把物理量反算为 PLC 的 0~analog_raw_max 模拟量原始值。"""
try:
number = float(value)
full_scale = float(physical_range)
raw_maximum = float(analog_raw_max)
except (TypeError, ValueError):
return "--"
if (
not math.isfinite(number)
or not math.isfinite(full_scale)
or not math.isfinite(raw_maximum)
or full_scale <= 0.0
or raw_maximum <= 0.0
):
return "--"
return str(round(number / full_scale * raw_maximum))
def motor_position_for(opening_pct):
return opening_to_motor_position(
opening_pct,
@@ -180,17 +229,22 @@ def set_opening(hardware, opening_pct):
def read_sensors(hardware, quiet=False):
"""读取一次传感器;输入模式中丢弃底层硬件代码的终端输出。"""
"""读取阀后流量及阀前、阀后压力;输入模式中隐藏硬件输出。"""
output = io.StringIO()
stream = output if quiet else sys.stdout
with contextlib.redirect_stdout(stream):
flow = hardware.get_flow(config.FLOW_AFTER_ADDR)
pressure = (
flow_after = hardware.get_flow(config.FLOW_AFTER_ADDR)
pressure_before = (
None
if config.PRESSURE_BEFORE_ADDR is None
else hardware.get_pressure(config.PRESSURE_BEFORE_ADDR)
)
pressure_after = (
None
if config.PRESSURE_AFTER_ADDR is None
else hardware.get_pressure(config.PRESSURE_AFTER_ADDR)
)
return flow, pressure
return flow_after, pressure_before, pressure_after
def main():
@@ -199,7 +253,7 @@ def main():
return 1
try:
from PcControl import Easy521ModbusClient
from PcControl import Easy521ModbusClient, raw_max as analog_raw_max
except ModuleNotFoundError as exc:
print(f"无法导入硬件客户端: {exc}")
return 2
@@ -234,6 +288,13 @@ def main():
print("连接失败。")
return 3
# 先全开,再创建记录器和启动采样计时。
opening, position = set_opening(hardware, 100.0)
print(
f"连接后已设置为 100% 开度(行程 {position:.1f}),"
"不计入采样计时。"
)
recorder = ManualValveRecorder(OUTPUT_DIRECTORY)
print("手动阀门控制已启动:按 S 输入新开度,按 Ctrl+C 退出。")
print(
@@ -295,14 +356,27 @@ def main():
now = time.perf_counter()
if now >= next_sample_time:
flow, pressure = read_sensors(hardware, quiet=input_mode)
recorder.record(now, flow, opening, pressure, position)
flow_after, pressure_before, pressure_after = read_sensors(
hardware, quiet=input_mode
)
recorder.record(
now,
flow_after,
opening,
pressure_before,
pressure_after,
position,
)
if not input_mode:
timestamp = time.strftime("%H:%M:%S")
print(
f"[{timestamp}] "
f"流量={format_value(flow, 'SLM')} "
f"压力={format_value(pressure, 'kPa')} "
f"阀后流量={format_value(flow_after, 'SLM')}"
f"(模拟量={format_analog_raw(flow_after, config.FLOW_AFTER_RANGE_SLM, analog_raw_max)} "
f"阀前压力={format_value(pressure_before, 'kPa')}"
f"(模拟量={format_analog_raw(pressure_before, config.PRESSURE_BEFORE_RANGE_KPA, analog_raw_max)} "
f"阀后压力={format_value(pressure_after, 'kPa')}"
f"(模拟量={format_analog_raw(pressure_after, config.PRESSURE_AFTER_RANGE_KPA, analog_raw_max)} "
f"开度={opening:.2f}% 行程={position:.1f}"
)
# 不追补错过的终端输出;输入期间的样本只写入 CSV。
@@ -321,6 +395,15 @@ def main():
exit_code = 4
finally:
if connected:
try:
opening, position = set_opening(hardware, 100.0)
print(
f"断开前已设置为 100% 开度(行程 {position:.1f}),"
"不计入采样计时。"
)
except Exception as exc:
print(f"警告:断开前设置阀门 100% 开度失败:{exc}")
exit_code = max(exit_code, 4)
try:
hardware.disconnect()
except Exception as exc: