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

- 前馈冻结改用可配置的 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
+87
View File
@@ -89,6 +89,18 @@ def test_valve_model_roundtrip():
assert _close(model.feedforward_stroke(q, 400.0, 300.0), 900.0, rel=0.01)
def test_area_below_measured_minimum_returns_fully_closed_stroke():
model = ValveModel(
[(800.0, 0.7358347), (980.0, 0.002094)],
motor_open=800.0,
motor_closed=1000.0,
)
assert model.stroke_from_area(0.002094) == 980.0
assert model.stroke_from_area(0.002093) == 1000.0
assert model.stroke_from_area(0.0) == 1000.0
def test_non_monotonic_raises():
table = [(800.0, 0.5), (900.0, 0.1), (1000.0, 0.3)]
try:
@@ -98,6 +110,48 @@ def test_non_monotonic_raises():
raise AssertionError("非单调表应抛 ValueError")
def test_monotonic_check_can_be_disabled():
import identify_valve
p1 = 400.0
denom = p1 + P_ATM # p2=0 时为阻塞流,F=1。
steady = [
(800.0, 0.5 * denom, p1, 0.0, None),
(900.0, 0.1 * denom, p1, 0.0, None),
(1000.0, 0.2 * denom, p1, 0.0, None),
]
checked, dropped, _, _ = identify_valve.compute_area_table(
steady,
check_monotonic=True,
)
unchecked, unchecked_dropped, _, _ = identify_valve.compute_area_table(
steady,
check_monotonic=False,
)
assert len(checked) == 2
assert len(dropped) == 1
assert len(unchecked) == 3
assert not unchecked_dropped
model = ValveModel(
unchecked,
motor_open=800.0,
motor_closed=1000.0,
enforce_monotonic=False,
)
assert not model.is_monotonic
# A_eff=0.15 有两个交点,选择更接近关闭端的 x=950。
assert _close(model.stroke_from_area(0.15), 950.0)
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / "nonmonotonic.json"
model.save(path)
loaded = ValveModel.load(path, 800.0, 1000.0)
assert not loaded.enforce_monotonic
assert _close(loaded.stroke_from_area(0.15), 950.0)
def test_identify_end_to_end():
import identify_valve
@@ -170,16 +224,49 @@ def test_resolve_csv_paths_glob():
assert [Path(p).name for p in paths] == ["a.csv", "b.csv"], paths
def test_open_loop_appends_fine_closed_end_strokes():
import config
import open_loop
points = open_loop.generate_scan_points(0.0, 100.0, 50.0, seed=42)
fine_strokes = open_loop.generate_fine_strokes(
config.OPEN_LOOP_FINE_STROKE_START,
config.MOTOR_CLOSED_POSITION,
config.OPEN_LOOP_FINE_STROKE_STEP,
)
assert open_loop.generate_fine_strokes(975.0, 1000.0, 5.0) == [
975.0,
980.0,
985.0,
990.0,
995.0,
1000.0,
]
fine_points = points[-len(fine_strokes):]
assert [position for _, position in fine_points] == fine_strokes
for opening, position in fine_points:
assert _close(
opening,
open_loop.motor_position_to_opening(
position,
config.MOTOR_OPEN_POSITION,
config.MOTOR_CLOSED_POSITION,
),
)
if __name__ == "__main__":
tests = [
test_f_ratio,
test_abs_pressure,
test_valve_model_roundtrip,
test_area_below_measured_minimum_returns_fully_closed_stroke,
test_non_monotonic_raises,
test_monotonic_check_can_be_disabled,
test_identify_end_to_end,
test_merge_multiple_csvs,
test_direction_tagging,
test_resolve_csv_paths_glob,
test_open_loop_appends_fine_closed_end_strokes,
]
for test in tests:
test()