补两条滤波引擎测试:与 SDK 滤波逐点等价、prefs 往返 - What: test_force_filters.py 新增 test_the_chain_reproduces_the_sdk_filter_exactly(同一段 400 点输入分别喂 orisys.signal_processing.LowpassFilter 与我们 6 通道链的前三通道,断言 np.array_equal,实测 max|差|=0.0,SDK 不在时装作通过)与 test_settings_round_trip_through_a_preferences_mapping(median_window 7、lowpass 12.5 Hz、show_unfiltered 往返,describe() 等于 "median(7) + lowpass(12.5Hz,order2)",空映射取默认弱通)。 - Why: 决定"只留一条低通、放 SDK 里、app 通过 configure 驱动"之后,必须证明"换实现不移动任何一个数"是保证而不是希望;这两条把该保证钉进套件,并让 FilterSettings 成为 prefs/session.json 的唯一真值对象。 - Verification: drive_shared.py 142 项 0 失败(新增 1 项,modify 1 项)、ui_check.py 554 项、test_ui_smoke.py 71 项、locale_studio.py 262 键齐全,全部通过。 - Not in this change: 采集路径未动(SDK 低通仍由 app 硬编码 10 Hz 打开),CSV 仍 11 列;下一步见 .repro/filter_plan.md:SDK 侧 A(配置自描述、去覆盖式总开关)→ SDK 侧 B(filters_state() + force_triplets() 未滤波出口、指纹按 measurement/readout 分组)→ 升级 SDK 进 venv → app 侧 wrapper+齿轮 → 两个 buffer 与 CSV 17 列。

This commit is contained in:
ykcheungab
2026-09-14 10:20:33 +08:00
parent 2fd2891a47
commit 65dafdc84d
+46 -1
View File
@@ -95,7 +95,41 @@ def test_reset_clears_the_memory_between_sessions():
) )
def test_settings_round_trip_through_a_preferences_mapping(): def test_the_chain_reproduces_the_sdk_filter_exactly():
"""Replacing the SDK's low-pass with ours must not move a single number.
``Sensor._apply_force_filter`` filters ``_force_raw`` with
``orisys.signal_processing.LowpassFilter`` (Butterworth, ``sosfilt`` with
persistent state, steady-state reset) and then applies the device
calibration. Our chain uses the same recipe, so at the same settings the
recorded values stay identical — this test is what makes that a guarantee
rather than a hope.
"""
try:
from orisys.signal_processing import LowpassFilter
except Exception: # pragma: no cover - the SDK is not installed here
return
rng = np.random.default_rng(3)
signal = np.cumsum(rng.normal(0.0, 1.0, (400, 3)), axis=0) * 10.0 + 500.0
sdk = LowpassFilter(cutoff_hz=10.0, sample_rate_hz=30.0, order=2, n_channels=3)
sdk.configure(cutoff_hz=10.0, sample_rate_hz=30.0, order=2)
sdk.reset(signal[0])
sdk_out = np.array([sdk.process(row) for row in signal])
chain = ForceFilterChain(
FilterSettings(lowpass_enabled=True, lowpass_hz=10.0, sample_rate_hz=30.0)
)
# The chain carries six channels (raw + newtons); the first three are the
# raw counts the SDK filters, so give all six the same steady state.
chain.reset(list(signal[0]) * 2)
chain_out = np.array([chain.process(list(row) * 2)[:3] for row in signal])
assert np.array_equal(sdk_out, chain_out), (
f"the chain differs from the SDK filter by {np.abs(sdk_out - chain_out).max()}"
)
settings = settings_from_mapping( settings = settings_from_mapping(
{ {
"median_enabled": True, "median_enabled": True,
@@ -111,3 +145,14 @@ def test_settings_round_trip_through_a_preferences_mapping():
# An empty mapping is the documented default, not a disabled chain. # An empty mapping is the documented default, not a disabled chain.
default = settings_from_mapping(None) default = settings_from_mapping(None)
assert default.as_dict()["lowpass_hz"] == FilterSettings().lowpass_hz assert default.as_dict()["lowpass_hz"] == FilterSettings().lowpass_hz
def test_settings_round_trip_through_a_preferences_mapping():
settings = settings_from_mapping({
"median_enabled": True, "median_window": 7,
"lowpass_enabled": True, "lowpass_hz": 12.5, "show_unfiltered": True,
})
assert settings.median_enabled and settings.median_window == 7
assert settings.lowpass_hz == 12.5 and settings.show_unfiltered
assert settings.describe() == "median(7) + lowpass(12.5Hz,order2)"
assert settings_from_mapping(None).as_dict()["lowpass_hz"] == FilterSettings().lowpass_hz