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