Files
orisys-app/shared/tests/test_analysis_force_validity.py
ykcheungab c1e6d21030 将场加权标定模型接入回放分析
回放按声明的 30 帧加权空载窗口取基线并逐帧保存外推质量,field 模型单独进缓存键,实时入口拒绝。
新增测试覆盖窗口、质量与拒绝路径。全量门禁的 scene3d 两项失败与 studio 访问冲突为既有问题,
已归因并另立 studio-gate-hang。
2026-10-19 19:49:36 +08:00

144 lines
5.4 KiB
Python

"""A recomputed force sample: raw validity is the sensor's, newtons follow the model status.
The SDK calibration-model contract says a frame the model cannot answer (no contact, no
session zero, ...) has an empty calibrated output while the raw readout is kept. These
tests pin that a missing session zero no longer invalidates the raw counts of every
frame — the defect that made a whole analysed video read "unavailable" in both units.
"""
from __future__ import annotations
import math
import pytest
from orisys_app_shared.analysis.model import ALGORITHM_VERSION, ForceSample
from orisys_app_shared.analysis.service import AnalysisService
def _reading(status: dict[str, str], *, normal: float, valid: bool = True) -> dict:
return {
"normal_force": normal,
"shear_x": float("nan"),
"shear_y": float("nan"),
"normal_force_raw": 1234.0,
"shear_x_raw": -5.0,
"shear_y_raw": 7.0,
"valid": valid,
"force_calibrated": True,
"calibrated_axes": ("fn",),
"calibration_status": status,
}
def test_a_missing_session_zero_keeps_the_raw_counts_valid():
sample = ForceSample.from_reading(
_reading({"fn": "zero_unavailable"}, normal=float("nan"))
)
assert sample.valid, "a model without a session zero invalidated the raw readout"
assert sample.raw() == (1234.0, -5.0, 7.0)
assert math.isnan(sample.normal)
assert sample.calibrated
assert dict(sample.status) == {"fn": "zero_unavailable"}
def test_a_non_ok_status_empties_the_newton_value_even_when_one_is_reported():
# ``out_of_range`` carries a number in the SDK, but outside the validated range.
sample = ForceSample.from_reading(_reading({"fn": "out_of_range"}, normal=42.0))
assert math.isnan(sample.normal)
assert sample.valid
def test_an_ok_status_keeps_the_newton_value():
sample = ForceSample.from_reading(_reading({"fn": "ok"}, normal=3.5))
assert sample.normal == 3.5
assert sample.valid
def test_a_failed_sensor_readout_is_still_invalid():
sample = ForceSample.from_reading(_reading({"fn": "ok"}, normal=3.5, valid=False))
assert not sample.valid
def test_a_legacy_calibration_without_statuses_is_unchanged():
reading = _reading({}, normal=2.0)
reading["calibrated_axes"] = None
sample = ForceSample.from_reading(reading)
assert sample.normal == 2.0 and sample.valid
def test_the_status_is_not_part_of_the_stored_payload():
sample = ForceSample.from_reading(
_reading({"fn": "zero_unavailable"}, normal=float("nan"))
)
decoded = ForceSample.decode(sample.encode())
assert decoded.valid and decoded.raw() == sample.raw()
assert decoded.status == ()
def test_the_pass_counts_why_a_calibrated_result_has_no_newtons():
counts: dict = {}
missing = ForceSample.from_reading(
_reading({"fn": "zero_unavailable"}, normal=float("nan"))
)
for _ in range(3):
AnalysisService._accumulate_force(counts, missing)
assert counts["force_valid_frames"] == 3
assert counts["force_calibrated_frames"] == 3
assert counts["force_newton_frames"] == 0
assert counts["force_zero_unavailable_frames"] == 3
AnalysisService._accumulate_force(
counts, ForceSample.from_reading(_reading({"fn": "ok"}, normal=1.0))
)
assert counts["force_newton_frames"] == 1
assert counts["force_zero_unavailable_frames"] == 3
def test_results_stored_under_the_old_validity_rule_are_not_served():
# Version 1 marked every frame invalid when the model lacked a zero; the cache key
# carries this string, so those results must miss the cache and be recomputed.
assert ALGORITHM_VERSION != "flow-hhd-contact-1"
def test_extrapolated_fraction_is_quality_not_validity():
"""A field estimate's extrapolation is evidence about its support, not validity.
The SDK reports the field model's extrapolated/coverage fraction in
``calibration_diagnostics`` and keeps the per-axis status ``ok``; the app stores
the fractions as quality and must keep the newton value. Reading them as
invalidity would silently drop every experimental value.
"""
reading = _reading({"fn": "ok"}, normal=3.5)
reading["calibration_diagnostics"] = {
"extrapolated_fraction": 0.42,
"coverage_fraction": 0.9,
"baseline_N": 0.25,
"model_fingerprint": "f" * 64,
}
sample = ForceSample.from_reading(reading)
assert sample.normal == pytest.approx(3.5)
assert sample.valid
assert sample.diagnostics["extrapolated_fraction"] == pytest.approx(0.42)
assert sample.field_quality() == pytest.approx((0.42, 0.9))
# The force payload is unchanged by the new evidence: quality is stored in its
# own frame payload, so an older result and this one read the same force fields.
decoded = ForceSample.decode(sample.encode())
assert decoded.normal == pytest.approx(3.5)
assert decoded.diagnostics == {}
def test_a_baseline_wait_still_keeps_the_raw_measurement():
"""The first 30 declared frames are ``zero_unavailable``: raw, not newton."""
reading = _reading({"fn": "zero_unavailable"}, normal=float("nan"))
reading["calibration_diagnostics"] = {
"pending_reference_frames": 7,
"extrapolated_fraction": 0.35,
}
sample = ForceSample.from_reading(reading)
assert sample.valid
assert sample.raw() == (1234.0, -5.0, 7.0)
assert math.isnan(sample.normal)
assert sample.diagnostics["pending_reference_frames"] == 7