修改问题.md,且新增data_logger.py画图模块

This commit is contained in:
2026-08-13 14:28:03 +08:00
parent 84b2bdb35d
commit 64be6d7c7a
11 changed files with 226 additions and 10 deletions
+189
View File
@@ -0,0 +1,189 @@
"""逐周期保存控制数据,并在运行结束时生成三联曲线图。"""
import csv
from datetime import datetime
import math
from pathlib import Path
CSV_FIELDS = (
"time_s",
"timestamp",
"target_flow_slm",
"measured_flow_slm",
"opening_pct",
"pressure_kpa",
"error_slm",
"motor_position",
"actual_dt_s",
"status",
)
class FlowRunRecorder:
"""把每个控制周期写入 CSV,并在结束时输出一张三联图。"""
def __init__(self, output_directory, *, plot_dpi=160):
output_dir = Path(output_directory)
output_dir.mkdir(parents=True, exist_ok=True)
run_id = datetime.now().strftime("%Y%m%d_%H%M%S")
self.csv_path = output_dir / f"flow_run_{run_id}.csv"
self.image_path = output_dir / f"flow_run_{run_id}.png"
self.plot_dpi = int(plot_dpi)
self._file = self.csv_path.open("w", newline="", encoding="utf-8-sig")
self._writer = csv.DictWriter(self._file, fieldnames=CSV_FIELDS)
self._writer.writeheader()
self._file.flush()
self._first_timestamp = None
self.sample_count = 0
self._closed = False
def record(self, result):
"""保存一个 ControlStepResult;每次写入后立即刷新到磁盘。"""
if self._closed:
raise RuntimeError("记录器已经关闭")
if self._first_timestamp is None:
self._first_timestamp = float(result.timestamp)
elapsed_s = max(0.0, float(result.timestamp) - self._first_timestamp)
self._writer.writerow(
{
"time_s": f"{elapsed_s:.6f}",
"timestamp": f"{float(result.timestamp):.6f}",
"target_flow_slm": self._format_value(result.target_flow_slm),
"measured_flow_slm": self._format_value(
result.measured_flow_slm
),
"opening_pct": self._format_value(result.opening_pct),
"pressure_kpa": self._format_value(result.pressure_kpa),
"error_slm": self._format_value(result.error_slm),
"motor_position": self._format_value(result.motor_position),
"actual_dt_s": self._format_value(result.actual_dt_s),
"status": result.status,
}
)
self._file.flush()
self.sample_count += 1
def finalize(self):
"""关闭 CSV 并生成 PNG;无采样点时只保留 CSV。"""
self.close()
if self.sample_count == 0:
return None
self._create_plot()
return self.image_path
def close(self):
if not self._closed:
self._file.flush()
self._file.close()
self._closed = True
def _create_plot(self):
# 使用无界面后端,保证从终端运行和无显示器环境都能保存图片。
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
time_s = []
target_flow = []
measured_flow = []
opening = []
pressure = []
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"]))
target_flow.append(self._float_or_nan(row["target_flow_slm"]))
measured_flow.append(
self._float_or_nan(row["measured_flow_slm"])
)
opening.append(self._float_or_nan(row["opening_pct"]))
pressure.append(self._float_or_nan(row["pressure_kpa"]))
figure, axes = plt.subplots(
3,
1,
figsize=(12, 10),
sharex=True,
constrained_layout=True,
)
figure.suptitle("Gas Flow Control Response", fontsize=15)
axes[0].plot(
time_s,
measured_flow,
color="#1565C0",
linewidth=1.4,
label="Measured flow",
)
axes[0].plot(
time_s,
target_flow,
color="#D84315",
linewidth=1.5,
linestyle="--",
label="Target flow",
)
axes[0].set_ylabel("Flow (SLM)")
axes[0].set_title("Flow response")
axes[0].legend(loc="best")
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)
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",
)
axes[2].set_ylabel("Pressure (kPa)")
axes[2].set_xlabel("Time (s)")
axes[2].set_title("Pressure")
for axis in axes:
axis.grid(True, alpha=0.3, linestyle="--")
axis.margins(x=0)
figure.savefig(self.image_path, dpi=self.plot_dpi, bbox_inches="tight")
plt.close(figure)
@staticmethod
def _format_value(value):
if value is None:
return ""
number = float(value)
return "" if not math.isfinite(number) else f"{number:.6f}"
@staticmethod
def _float_or_nan(value):
if value in (None, ""):
return math.nan
try:
return float(value)
except (TypeError, ValueError):
return math.nan