feat: ForceStreamOutput 新增同幀未濾波量測欄位

App 需要濾波前的 raw 與 N 供錄製、分析與標定取樣;診斷幀保留 raw 特徵、N 為 NaN。已以帶標定、中值加低通的會話逐幀對照直通結果驗證。
This commit is contained in:
ykcheungab
2026-10-23 10:41:05 +08:00
parent ad7b0d3e26
commit ad4c3ba6b9
3 changed files with 105 additions and 0 deletions
+81
View File
@@ -669,3 +669,84 @@ def test_fn_only_model_uncalibrated_session_stays_explicit() -> None:
assert all(np.isnan(value) for value in results[-1].newton)
assert set(results[-1].newton_status) == {"uncalibrated"}
assert results[-1].quality == "uncalibrated"
def test_unfiltered_measurement_fields_survive_the_output_filter() -> None:
inputs = [100.0, 100.0, 100.0, 900.0, 100.0, 120.0, 140.0, 160.0]
filtered = _new_chain(settings=helpers.legacy_settings(median_window=5))
passthrough = _new_chain(settings=helpers.legacy_settings(median_window=1))
for frame_id, value in enumerate(inputs, start=1):
one = filtered.process(_measurement(frame_id, (value, 0.0, 0.0)))
two = passthrough.process(_measurement(frame_id, (value, 0.0, 0.0)))
assert one.measurement_raw == (value, 0.0, 0.0)
assert two.raw == (value, 0.0, 0.0)
assert two.measurement_raw == one.measurement_raw
assert one.measurement_newton == two.measurement_newton
assert filtered.process(_measurement(9, (900.0, 0.0, 0.0))).raw[0] != 900.0
assert passthrough.process(_measurement(9, (900.0, 0.0, 0.0))).raw[0] == 900.0
def test_diagnostic_rows_keep_the_unfiltered_measurement() -> None:
session = _new_chain(settings=helpers.legacy_settings(median_window=5))
session.process(_measurement(20, _raw_for(20)))
duplicate = session.process(_measurement(20, _raw_for(20)))
assert duplicate.quality == "duplicate"
assert duplicate.measurement_raw == tuple(float(value) for value in _raw_for(20))
assert duplicate.raw == duplicate.measurement_raw
assert all(np.isnan(value) for value in duplicate.measurement_newton)
assert all(np.isnan(value) for value in duplicate.newton)
def test_calibrated_measurement_newton_is_unfiltered() -> None:
filt = _new_chain(
helpers.box_cal(),
helpers.legacy_settings(
median_window=5, lowpass_hz=3.0, lowpass_order=2, sample_rate_hz=30.0
),
)
passthrough = _new_chain(
helpers.box_cal(), helpers.legacy_settings(median_window=1)
)
for session in (filt, passthrough):
session.request_zero(
reference_id="r",
declared_unloaded=True,
expected_reference_generation=0,
)
rows = []
pass_rows = []
for frame_id in range(1, 80):
raw = list(_raw_for(1))
if frame_id == 60:
raw[0] += 20000.0
rows.append(filt.process(_measurement(frame_id, tuple(raw))))
pass_rows.append(passthrough.process(_measurement(frame_id, tuple(raw))))
for filtered, plain in zip(rows, pass_rows):
for measured, reference in zip(
filtered.measurement_newton, plain.newton
):
assert (np.isnan(measured) and np.isnan(reference)) or measured == reference
assert filtered.measurement_raw == plain.raw
spike = rows[59]
assert spike.newton[0] != spike.measurement_newton[0]
def test_late_frame_carries_unfiltered_measurement_fields() -> None:
session = _new_chain(
helpers.box_cal(),
helpers.legacy_settings(
median_window=5, lowpass_hz=3.0, lowpass_order=2, sample_rate_hz=30.0
),
)
session.request_zero(
reference_id="r", declared_unloaded=True, expected_reference_generation=0
)
for frame_id in range(1, 32):
session.process(_measurement(frame_id, _raw_for(frame_id)))
raw = _raw_for(32)
late = session.process(
_measurement(32, raw, capture_time_ns=31 * FRAME_NS)
)
assert late.late is True
assert late.measurement_raw == tuple(float(value) for value in raw)
assert all(np.isnan(value) for value in late.measurement_newton)