e2cac0ae40
多点标定未测零点时,分析把整帧判无效,原始值与牛顿值都显示"不可用";现按 SDK 标定模型约定只清空该轴牛顿值,并在回放提示与分析设置中说明需要零点。算法版本升为 flow-hhd-contact-2,旧缓存不再复用;新增测试在旧代码上失败、新代码通过,studio/shared 全量与 HEAD 失败集合一致。
100 lines
3.6 KiB
Python
100 lines
3.6 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
|
|
|
|
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"
|