四个滤波预设落成机器可读的配置文件,并加上防漂移测试 - What: 新增 examples/filters/filters_{off,low_latency,balanced,low_noise}.json —— 每个都是完整可整块替换的 filters 段(五键齐全:flow_ema_alpha / force_median_window / force_lowpass_cutoff_hz / force_lowpass_order / force_lowpass_sample_rate_hz),数值即 FILTER_PRESETS 展开后的结果;examples/filters/README.md 说明这四份副本的存在与用途;新增 tests/test_filter_presets.py(12 项):预设名恰为四个、每个示例文件 == 默认块叠加该预设(改预设或改文件都会红)、每份都是含三个开关键的真实 filters 段、三个出厂 bundle(box/mini/finger)的 filters 段仍为全关、且用 external-frame 模式构造真实 Sensor 核对 filters_state() 报的也是全关(文件与设备必须一致)。 - Why: 之前的实现计划里写的是"四个 sensor_<preset>.json 整机配置",但整机配置要按传感器各复制一份、且必然随 bundle 漂移;README 里内嵌的 JSON 又无法被测试覆盖,等于把"建议用哪档滤波"这句话留在了一处会过时的散文里。改成"filters 片段 + 断言与 FILTER_PRESETS 逐值一致的测试",既不谎称是完整配置,也不会静默过时。 - Verification: pytest tests/test_filter_presets.py → 12 passed;SDK 全量 335 passed / 15 skipped(唯一失败仍是需要 1200 帧而样例只有 577 帧的 longrun 测试)。 - Not in this change: 未生成按传感器复制的整机配置;docs/filters.md 的预设表未纳入测试(只有这四个文件与代码常量对齐);app 侧未动。
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
"""The shipped filter presets and the example configs that document them.
|
||||
|
||||
``json.load`` cannot carry comments, so the answer to "which filter should I
|
||||
use?" lives in three places that must not drift apart: ``FILTER_PRESETS`` in the
|
||||
sensor, the table in ``docs/filters.md``, and the copy-paste files in
|
||||
``examples/filters/``. The presets are the source of truth; these tests fail
|
||||
when a copy stops matching them.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from orisys import Sensor
|
||||
from orisys.calibration import filters_fingerprint
|
||||
from orisys.sensor import FILTER_PRESETS, available_filter_presets
|
||||
from orisys import configs as orisys_configs
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
EXAMPLES = REPO_ROOT / "examples" / "filters"
|
||||
BUNDLES = ("box", "mini", "finger")
|
||||
|
||||
#: What every shipped bundle says: filtering is opt-in, so a fresh install
|
||||
#: records the raw reading and the operator turns a stage on deliberately.
|
||||
ALL_OFF = {
|
||||
"flow_ema_alpha": 1.0,
|
||||
"force_median_window": 1,
|
||||
"force_lowpass_cutoff_hz": 0.0,
|
||||
"force_lowpass_order": 2,
|
||||
"force_lowpass_sample_rate_hz": "auto",
|
||||
}
|
||||
|
||||
|
||||
def test_the_preset_names_are_the_documented_ones():
|
||||
assert sorted(available_filter_presets()) == [
|
||||
"balanced",
|
||||
"low_latency",
|
||||
"low_noise",
|
||||
"off",
|
||||
]
|
||||
# ``available_filter_presets`` hands out copies: a UI cannot edit the truth.
|
||||
snapshot = available_filter_presets()
|
||||
snapshot["off"]["flow_ema_alpha"] = 0.0
|
||||
assert FILTER_PRESETS["off"]["flow_ema_alpha"] == 1.0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("preset", sorted(FILTER_PRESETS))
|
||||
def test_every_example_file_still_matches_its_preset(preset: str):
|
||||
path = EXAMPLES / f"filters_{preset}.json"
|
||||
assert path.is_file(), f"missing example for preset {preset!r}: {path}"
|
||||
block = json.loads(path.read_text(encoding="utf-8"))
|
||||
# A whole ``filters`` block: the preset's own keys on top of the defaults
|
||||
# every config needs, and nothing else.
|
||||
expected = dict(ALL_OFF)
|
||||
expected.update(FILTER_PRESETS[preset])
|
||||
assert block == expected, f"{path.name} drifted from preset {preset!r}"
|
||||
|
||||
|
||||
def test_every_example_file_is_a_real_filters_block():
|
||||
"""A file that cannot be dropped into a config is documentation that lies."""
|
||||
for preset in FILTER_PRESETS:
|
||||
block = json.loads((EXAMPLES / f"filters_{preset}.json").read_text(encoding="utf-8"))
|
||||
assert isinstance(block, dict) and block, preset
|
||||
for key in ("flow_ema_alpha", "force_median_window", "force_lowpass_cutoff_hz"):
|
||||
assert key in block, f"{preset}: {key} missing"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bundle", BUNDLES)
|
||||
def test_shipped_bundles_ship_with_filtering_off(bundle: str):
|
||||
"""Nothing filters unless it was asked for."""
|
||||
path = orisys_configs.resolve_sensor_config_path(bundle)
|
||||
assert path is not None and path.is_file(), f"missing bundle: {bundle}"
|
||||
cfg = json.loads(path.read_text(encoding="utf-8"))
|
||||
assert cfg.get("filters") == ALL_OFF, f"{bundle} no longer ships with filtering off"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("bundle", BUNDLES)
|
||||
def test_shipped_bundles_report_all_off(bundle: str):
|
||||
"""The file and the running device have to agree about the shipped default."""
|
||||
sensor = Sensor("external", config_name=bundle, verbose=False, backend="cpu")
|
||||
try:
|
||||
fp = filters_fingerprint(sensor.filters_state())
|
||||
finally:
|
||||
sensor.disconnect()
|
||||
assert fp["measurement"] == {"flow_ema_alpha": 1.0}
|
||||
assert fp["readout"]["force_median_window"] == 1
|
||||
assert fp["readout"]["force_lowpass_cutoff_hz"] == 0.0
|
||||
Reference in New Issue
Block a user