fixed calibration data not loading due to path

This commit is contained in:
ykcheungab
2026-09-20 11:00:33 +08:00
parent 14fbfec62e
commit 1cf78c9d71
5 changed files with 448 additions and 25 deletions
+77 -1
View File
@@ -243,8 +243,84 @@ def test_missing_cal_path_raises():
def test_parse_shipped_box_file_directly():
cal = parse(json.loads(_box_cal_path().read_text(encoding="utf-8")), source_path=_box_cal_path())
assert cal.schema_version == 1
assert cal.schema_version == 2
assert cal.convert(10000.0, 36000.0, -36000.0) == pytest.approx((0.377, 1.19, -1.19))
# The shipped file names both shear axes outright: no shared block, no clone.
assert set(cal.channels) == {"fn", "fx", "fy"}
fx, fy = cal.get("FX"), cal.get("FY")
assert fx is not None and fy is not None and fx is not fy
for curve in (fx, fy):
assert curve.signed is True
assert curve.extrapolate is True
assert curve.clamp_output_min == pytest.approx(0.0)
assert len(curve.points) == 3
assert fx.raw_key == "fshearx"
assert fy.raw_key == "fsheary"
def test_the_shipped_box_shear_axes_can_drift_apart(tmp_path: Path):
"""The whole point of the split: FX and FY are independent curves.
A document with the same shape as the shipped one but different per-axis
points must apply each axis's own curve. Re-collapsing the axes into one
shared ``shear`` block makes this fail.
"""
doc = {
"schema_version": 2,
"unit": "N",
"channels": {
"normal": {
"method": "piecewise_linear",
"points": [[0.0, 0.0], [1000.0, 1.0]],
},
"shear_x": {
"method": "piecewise_linear",
"signed": True,
"input": "components",
"extrapolate": True,
"clamp_output_min": 0.0,
"points": [[0.0, 0.0], [500.0, 2.0]],
},
"shear_y": {
"method": "piecewise_linear",
"signed": True,
"input": "components",
"extrapolate": True,
"clamp_output_min": 0.0,
"points": [[0.0, 0.0], [500.0, 3.0]],
},
},
}
path = tmp_path / "split.json"
path.write_text(json.dumps(doc), encoding="utf-8")
cal = load(path)
assert cal is not None
assert cal.convert(0.0, 500.0, 500.0)[1:] == pytest.approx((2.0, 3.0))
assert cal.convert(0.0, -500.0, 500.0)[1:] == pytest.approx((-2.0, 3.0))
def test_out_of_range_readings_extend_instead_of_being_dropped():
"""Outside the calibrated range the nearest segment is extended.
This pins the "extend, never ignore, never reuse a previous coefficient"
behaviour of the shipped box file, so flipping ``extrapolate`` has to be a
deliberate edit that fails this test first.
"""
cal = load(sensor_type="box")
assert cal is not None
fx, fy, fn = cal.get("FX"), cal.get("FY"), cal.get("FN")
assert fx is not None and fy is not None and fn is not None
# 136000 = 2 x the top raw break (68000 -> 2.06): the last segment's slope is
# applied again, so the value keeps growing past the calibrated range.
(r0, n0), (r1, n1) = fx.points[-2], fx.points[-1]
extended = n1 + (n1 - n0) / (r1 - r0) * (136000.0 - r1)
assert extended > n1
assert fx.convert_component(136000.0) == pytest.approx(extended)
assert fy.convert_component(136000.0) == pytest.approx(extended)
# A negative beyond the range keeps its sign.
assert fx.convert_component(-136000.0) == pytest.approx(-extended)
# Below the first point the normal channel floors at clamp_output_min.
assert fn.convert(500.0) == pytest.approx(0.0)
# ------------------------------------------------------- filter fingerprint
+156 -1
View File
@@ -2,11 +2,13 @@
from __future__ import annotations
import json
import numpy as np
import pytest
from scipy.signal import butter, filtfilt
from orisys import Sensor
from orisys import Sensor, configs
from orisys.signal_processing import (
NORMAL_FORCE_CAL_OFFSET,
NORMAL_FORCE_CAL_SLOPE,
@@ -263,3 +265,156 @@ def test_configure_hhd_downsample_invalidates_solver_cache():
sensor.configure(hhd_downsample="nope")
finally:
sensor.disconnect()
# ------------------------------------------------- bundle resolution anchors
#
# The bundle is chosen by the *running* root's folder layout, not by the root's
# name and not by whichever checkout happens to be importable. These build small
# real trees, because ``_runtime_roots`` asks the filesystem whether a directory
# carries ``examples/`` beside ``sensors/``.
_CAL_DOC = {
"schema_version": 2,
"unit": "N",
"channels": {
"normal": {"method": "piecewise_linear", "points": [[0.0, 0.0], [1000.0, 1.0]]},
"shear_x": {
"method": "piecewise_linear",
"signed": True,
"points": [[0.0, 0.0], [500.0, 2.0]],
},
"shear_y": {
"method": "piecewise_linear",
"signed": True,
"points": [[0.0, 0.0], [500.0, 3.0]],
},
},
}
def _make_root(base, *, name="anything", sensor_json=True, calibration=True):
"""A runnable SDK root: ``examples/`` beside ``sensors/<name>/``."""
root = base / name
(root / "examples").mkdir(parents=True, exist_ok=True)
if sensor_json:
(root / "examples" / "basic_pipeline.py").write_text("# demo\n", encoding="utf-8")
bundle = root / "sensors" / "mini"
bundle.mkdir(parents=True, exist_ok=True)
if sensor_json:
(bundle / "sensor.json").write_text(
json.dumps({"name": "prototype"}), encoding="utf-8"
)
if calibration:
(bundle / "force_calibration.json").write_text(
json.dumps(_CAL_DOC), encoding="utf-8"
)
return root
def test_runtime_root_anchor_keys_on_layout_not_on_the_folder_name(tmp_path):
named = _make_root(tmp_path, name="some-random-name")
assert configs._is_sdk_runtime_root(named) is True
no_examples = tmp_path / "dist-like"
(no_examples / "sensors" / "mini").mkdir(parents=True)
assert configs._is_sdk_runtime_root(no_examples) is False
neither = tmp_path / "plain"
neither.mkdir()
assert configs._is_sdk_runtime_root(neither) is False
def test_runtime_roots_walk_up_from_a_subdirectory(tmp_path, monkeypatch):
root = _make_root(tmp_path, name="renamed-release")
subdir = root / "examples"
monkeypatch.setattr(configs.Path, "cwd", classmethod(lambda cls: subdir))
roots = [str(path) for path in configs._runtime_roots()]
assert str(root) in roots
def test_the_running_root_bundle_beats_a_calibration_less_checkout(
tmp_path, monkeypatch
):
"""The reported bug: a nearer bundle without coefficients must not win."""
running = _make_root(tmp_path, name="running-root")
checkout = _make_root(tmp_path, name="importable-checkout", calibration=False)
monkeypatch.setattr(configs, "_runtime_roots", lambda: [running, checkout])
bundle = configs.resolve_sensor_bundle("mini")
assert bundle is not None
assert bundle.root == (running / "sensors" / "mini").resolve()
assert bundle.force_calibration_json == (
running / "sensors" / "mini" / "force_calibration.json"
).resolve()
# The whole point: the running root's document is the one that reaches the
# sensor, split shear and all.
sensor = Sensor("external", config_name="mini", verbose=False, backend="cpu")
try:
assert sensor.force_calibrated is True
assert sensor.force_unit == "N"
assert sensor.calibration.source_path == (
running / "sensors" / "mini" / "force_calibration.json"
).resolve()
finally:
sensor.disconnect()
def test_a_nearer_bundle_wins_even_without_coefficients(tmp_path, monkeypatch):
"""Nearest bundle wins, so a calibration-less nearer one hides a farther one.
That is the chosen precedence (the running root is authoritative), and the
diagnostic exists precisely so this stops being silent: this is the shape of
the original report, where a checkout's ``mini`` has only ``sensor.json``.
"""
checkout = _make_root(tmp_path, name="checkout", calibration=False)
running = _make_root(tmp_path, name="run-root")
monkeypatch.setattr(configs, "_runtime_roots", lambda: [checkout, running])
bundle = configs.resolve_sensor_bundle("mini")
assert bundle is not None
assert bundle.root == (checkout / "sensors" / "mini").resolve()
assert bundle.force_calibration_json is None
info = configs.describe_bundle_resolution("mini")
assert info["found_root"] == (checkout / "sensors" / "mini").resolve()
assert info["force_calibration_json"] is False
def test_describe_bundle_resolution_names_the_bundle_and_the_searched_roots(
tmp_path, monkeypatch
):
checkout = _make_root(tmp_path, name="checkout", calibration=False)
running = _make_root(tmp_path, name="run-root")
roots = [checkout / "sensors", running / "sensors"]
# Patched at the root list, so no real bundle on this machine can satisfy
# the lookup while the "nothing found" case is being asserted.
monkeypatch.setattr(configs, "_candidate_sensor_roots", lambda: list(roots))
info = configs.describe_bundle_resolution("mini")
assert info["found_root"] == (checkout / "sensors" / "mini").resolve()
assert info["force_calibration_json"] is False
assert [str(path) for path in info["searched"]] == [str(p) for p in roots]
(checkout / "sensors" / "mini" / "sensor.json").unlink()
(running / "sensors" / "mini" / "sensor.json").unlink()
missing = configs.describe_bundle_resolution("mini")
assert missing["found_root"] is None
assert missing["sensor_json"] is None
assert missing["force_calibration_json"] is False
def test_the_calibration_note_names_the_bundle_that_was_found(
tmp_path, monkeypatch
):
checkout = _make_root(tmp_path, name="checkout", calibration=False)
monkeypatch.setattr(configs, "_runtime_roots", lambda: [checkout])
sensor = Sensor("external", config_name="mini", verbose=False, backend="cpu")
try:
assert sensor.calibration is None
# Still None: callers read a non-None value here as "calibrated".
assert sensor._force_cal_sensor_name is None
note = sensor._force_calibration_note()
assert "has no force_calibration.json" in note
assert str((checkout / "sensors" / "mini").resolve()) in note
assert "roots searched" in note
finally:
sensor.disconnect()