"""flow_control 前馈误差滑窗的离线自测(不连接硬件)。""" from collections import deque import pytest from flow_control import FlowControlLoop def _pid_threshold_loop(): """只构造测试 FAR/NEAR 阈值计算所需的状态。""" loop = FlowControlLoop.__new__(FlowControlLoop) loop.pid_near_error_min_slm = 1.0 loop.pid_near_error_max_slm = 3.0 loop.pid_near_error_target_ratio = 0.05 loop.pid_far_error_min_slm = 4.0 loop.pid_far_error_max_slm = 12.0 loop.pid_far_error_target_ratio = 0.20 return loop @pytest.mark.parametrize( ("target", "expected_near", "expected_far"), [ (10.0, 1.0, 4.0), # 比例值低于下限 (30.0, 1.5, 6.0), # 比例值位于上下限之间 (60.0, 3.0, 12.0), # 比例值恰好达到上限 (300.0, 3.0, 12.0), # 比例值超过上限 ], ) def test_pid_error_thresholds_are_clamped(target, expected_near, expected_far): loop = _pid_threshold_loop() near, far = loop._pid_error_thresholds(target) assert near == pytest.approx(expected_near) assert far == pytest.approx(expected_far) def _feedforward_window_loop(): """只构造测试滑窗所需的最小 FlowControlLoop 状态。""" loop = FlowControlLoop.__new__(FlowControlLoop) loop.target_flow_slm = 100.0 loop.feedforward_update_window_s = 3.0 loop._feedforward_error_window = deque() loop._feedforward_window_ready = False return loop def test_feedforward_window_stays_ready_despite_sampling_jitter(): loop = _feedforward_window_loop() # 首个完整 3 s 窗口内误差恒为 2 SLM。 sample_times = [0.0, 0.099] sample_times.extend(0.201 + 0.1 * index for index in range(28)) sample_times.append(3.0) means = [ loop._feedforward_window_mean_abs_error(timestamp, 98.0) for timestamp in sample_times ] assert all(value is None for value in means[:-1]) assert means[-1] == pytest.approx(2.0) assert loop._feedforward_window_ready # 下一周期淘汰旧样本后,队列首尾跨度仅 2.899 s。窗口仍应保持 # ready,并返回最近 3 s 的误差均值,不能因约 0.1 s 抖动返回 None。 mean = loop._feedforward_window_mean_abs_error(3.1, 98.0) span = ( loop._feedforward_error_window[-1][0] - loop._feedforward_error_window[0][0] ) assert span == pytest.approx(2.899) assert mean == pytest.approx(2.0) assert loop._feedforward_window_ready def test_ready_window_keeps_tracking_recent_error_and_can_reset(): loop = _feedforward_window_loop() loop._feedforward_window_mean_abs_error(0.0, 98.0) assert loop._feedforward_window_mean_abs_error(3.0, 98.0) == pytest.approx(2.0) # 就绪状态不会冻结均值本身;最近 3 s 都变成 10 SLM 误差后, # 返回的新均值会超过默认 5 SLM 阈值,从而触发上层前馈更新。 for index in range(1, 32): mean = loop._feedforward_window_mean_abs_error( 3.0 + index * 0.1, 90.0, ) assert mean == pytest.approx(10.0) loop._reset_feedforward_error_window() assert not loop._feedforward_window_ready assert not loop._feedforward_error_window assert loop._feedforward_window_mean_abs_error(10.0, 90.0) is None