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

- 前馈冻结改用可配置的 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
+99 -16
View File
@@ -2,7 +2,8 @@
本模块可独立运行,用于阀门开度—流量特性的开环标定/扫点。程序会:
1. 生成 0%~100%(默认 5% 间隔)共 21 个开度值并随机打乱
1. 生成 0%~100%(默认 5% 间隔)开度值并随机打乱、转换为行程,
然后在序列末尾追加配置指定的闭阀端精细行程点;
2. 逐个开度:写入电机行程 → 以固定周期采集控流阀前后 4 个传感器
(前压力、前流量、后压力、后流量)→ 等待“控流阀后流量”进入稳态后
切换到下一个开度;
@@ -56,7 +57,7 @@ RANDOM_SEED = 42 # 设为整数可复现同一打乱顺序
SAMPLE_PERIOD_S = 0.1 # 采样周期(秒)
STEADY_STATE_WINDOW_S = config.STEADY_STATE_WINDOW_S # 滑窗长度,复用 config
STEADY_STATE_STD_PCT = config.STEADY_STATE_STD_PCT # 相对标准差阈值(%
MAX_WAIT_S = 60.0 # 单个开度的最长等待时间
MAX_WAIT_S = config.STEADY_STATE_MAX_WAIT_S # 单个扫点的最长等待时间
MIN_ABS_STD_SLM = 0.05 # 近零流量时的绝对标准差下限
MAX_CONSECUTIVE_READ_FAILURES = 5 # 后流量连续读取失败判定设备断开
@@ -453,6 +454,71 @@ def generate_openings(min_pct, max_pct, step_pct, seed):
return openings
def motor_position_to_opening(position, motor_open_position, motor_closed_position):
"""把反向作用的电机行程换算为 0~100% 阀门开度。"""
position = float(position)
open_position = float(motor_open_position)
closed_position = float(motor_closed_position)
if open_position >= closed_position:
raise ValueError("打开端行程必须小于关闭端行程")
if not open_position <= position <= closed_position:
raise ValueError("电机行程超出打开端/关闭端范围")
return 100.0 * (closed_position - position) / (
closed_position - open_position
)
def generate_fine_strokes(start, end, step):
"""生成包含起点和终点的闭阀端精细行程序列。"""
start = float(start)
end = float(end)
step = float(step)
if step <= 0.0:
raise ValueError("精细行程间隔必须大于 0")
if start >= end:
raise ValueError("精细行程起点必须小于终点")
count = int(math.floor((end - start) / step))
strokes = [round(start + i * step, 6) for i in range(count + 1)]
if not strokes or abs(strokes[-1] - end) > 1e-6:
strokes.append(end)
return strokes
def generate_scan_points(min_pct, max_pct, step_pct, seed):
"""先生成并打乱开度点、转换为行程,再追加闭阀端精细行程点。"""
openings = generate_openings(min_pct, max_pct, step_pct, seed)
points = [
(
opening,
opening_to_motor_position(
opening,
config.MOTOR_OPEN_POSITION,
config.MOTOR_CLOSED_POSITION,
),
)
for opening in openings
]
fine_strokes = generate_fine_strokes(
config.OPEN_LOOP_FINE_STROKE_START,
config.MOTOR_CLOSED_POSITION,
config.OPEN_LOOP_FINE_STROKE_STEP,
)
points.extend(
(
motor_position_to_opening(
stroke,
config.MOTOR_OPEN_POSITION,
config.MOTOR_CLOSED_POSITION,
),
stroke,
)
for stroke in fine_strokes
)
return points
# ---------------------------------------------------------------------------
# 日志
# ---------------------------------------------------------------------------
@@ -530,12 +596,15 @@ def run(args):
motor_output_addr=config.MOTOR_OUTPUT_ADDR,
)
openings = generate_openings(
scan_points = generate_scan_points(
args.min, args.max, args.step, args.seed
)
logger.info(
"开度扫描顺序(已打乱: %s",
", ".join(f"{o:.1f}%" for o in openings),
"最终扫描顺序(随机开度转行程后追加精细行程: %s",
", ".join(
f"{opening:.2f}%/x={position:.1f}"
for opening, position in scan_points
),
)
connected = False
@@ -550,12 +619,16 @@ def run(args):
exit_code = 2
return exit_code
# 启动前先关阀
ok, _ = set_opening(hardware, 0.0)
# 连接后的全开动作发生在记录器及实验计时开始之前
ok, position = set_opening(hardware, 100.0)
if not ok:
logger.critical("启动前关阀失败,实验中止")
logger.critical("连接后设置阀门 100%% 开度失败,实验中止")
exit_code = 2
return exit_code
logger.info(
"PLC 已连接;阀门已设置为 100%% 开度(行程 %.1f),不计入实验计时",
position,
)
recorder = OpenLoopRecorder(
Path(__file__).resolve().parent / OUTPUT_DIRECTORY,
@@ -573,8 +646,10 @@ def run(args):
run_start = time.perf_counter()
read_failures = 0
for step_index, opening in enumerate(openings):
ok, position = set_opening(hardware, opening)
for step_index, (opening, position) in enumerate(scan_points):
ok = bool(
hardware.set_motor_position(position, channel=MOTOR_CHANNEL)
)
if not ok:
logger.critical(
"写入开度 %.1f%% 对应行程 %.1f 失败,实验中止", opening, position
@@ -584,7 +659,7 @@ def run(args):
logger.info(
"%d/%d:设置开度 %.1f%%(行程 %.1f),等待后流量稳定…",
step_index + 1, len(openings), opening, position,
step_index + 1, len(scan_points), opening, position,
)
detector.reset()
@@ -614,7 +689,10 @@ def run(args):
pressure_before, flow_before, pressure_after, flow_after
)
if fault is not None:
logger.critical("安全阈值触发:%s,实验中止并关阀", fault)
logger.critical(
"安全阈值触发:%s,实验中止并在断开前恢复全开",
fault,
)
exit_code = 2
break
@@ -665,13 +743,18 @@ def run(args):
finally:
if connected:
try:
close_ok, _ = set_opening(hardware, 0.0)
if not close_ok:
open_ok, position = set_opening(hardware, 100.0)
if open_ok:
logger.info(
"断开 PLC 前已设置阀门为 100%% 开度(行程 %.1f),不计入实验计时",
position,
)
else:
exit_code = max(exit_code, 4)
logger.critical("收尾安全关阀失败,请立即人工检查")
logger.critical("断开 PLC 前设置阀门 100%% 开度失败,请立即人工检查")
except Exception:
exit_code = max(exit_code, 4)
logger.exception("收尾安全关阀时发生异常")
logger.exception("断开 PLC 前设置阀门 100%% 开度时发生异常")
try:
hardware.disconnect()
except Exception: