added calibration data handling functions
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
"""Tests for orisys.calibration load/parse/write and Sensor apply."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from orisys import Sensor
|
||||
from orisys.calibration import (
|
||||
CalSample,
|
||||
CalibrationError,
|
||||
build,
|
||||
check,
|
||||
load,
|
||||
parse,
|
||||
write,
|
||||
)
|
||||
|
||||
|
||||
def _box_cal_path() -> Path:
|
||||
# Editable checkout: repo_root/sensors/box/force_calibration.json
|
||||
here = Path(__file__).resolve()
|
||||
candidate = here.parents[1] / "sensors" / "box" / "force_calibration.json"
|
||||
assert candidate.is_file(), f"missing shipped cal: {candidate}"
|
||||
return candidate
|
||||
|
||||
|
||||
def test_parse_box_v1_piecewise():
|
||||
cal = load(sensor_type="box")
|
||||
assert cal is not None
|
||||
assert cal.get("FN") is not None
|
||||
assert cal.get("FN").convert(10000.0) == pytest.approx(0.377)
|
||||
assert cal.get("FX").convert_component(36000.0) == pytest.approx(1.19)
|
||||
assert cal.get("FX").convert_component(-36000.0) == pytest.approx(-1.19)
|
||||
assert cal.get("FY").convert_component(36000.0) == pytest.approx(1.19)
|
||||
|
||||
|
||||
def test_parse_v2_tmp_file(tmp_path: Path):
|
||||
doc = {
|
||||
"schema_version": 2,
|
||||
"unit": "N",
|
||||
"channels": {
|
||||
"normal": {
|
||||
"method": "piecewise_linear",
|
||||
"points": [[0.0, 0.0], [1000.0, 1.0]],
|
||||
"extrapolate": True,
|
||||
"clamp_output_min": 0.0,
|
||||
},
|
||||
"shear_x": {
|
||||
"method": "piecewise_linear",
|
||||
"signed": True,
|
||||
"points": [[0.0, 0.0], [500.0, 2.0]],
|
||||
"extrapolate": True,
|
||||
"clamp_output_min": 0.0,
|
||||
},
|
||||
"shear_y": {
|
||||
"method": "piecewise_linear",
|
||||
"signed": True,
|
||||
"points": [[0.0, 0.0], [500.0, 3.0]],
|
||||
"extrapolate": True,
|
||||
"clamp_output_min": 0.0,
|
||||
},
|
||||
},
|
||||
"meta": {"sensor_type": "box", "sn": "TEST-001"},
|
||||
}
|
||||
path = tmp_path / "force_calibration_v2.json"
|
||||
path.write_text(json.dumps(doc), encoding="utf-8")
|
||||
cal = load(path)
|
||||
assert cal is not None
|
||||
assert check(cal).ok
|
||||
assert cal.convert(1000.0, 500.0, -500.0) == pytest.approx((1.0, 2.0, -3.0))
|
||||
|
||||
|
||||
def test_build_write_load_roundtrip(tmp_path: Path):
|
||||
samples = {
|
||||
"fn": [
|
||||
CalSample(weight_n=0.0, raw_mean=0.0, n=10),
|
||||
CalSample(weight_n=1.0, raw_mean=1000.0, n=10),
|
||||
],
|
||||
"fx": [
|
||||
CalSample(weight_n=0.0, raw_mean=0.0, n=10),
|
||||
CalSample(weight_n=2.0, raw_mean=500.0, n=10),
|
||||
],
|
||||
"fy": [
|
||||
CalSample(weight_n=0.0, raw_mean=0.0, n=10),
|
||||
CalSample(weight_n=3.0, raw_mean=500.0, n=10),
|
||||
],
|
||||
}
|
||||
cal = build(
|
||||
samples,
|
||||
identity={"sensor_type": "box", "sn": "RT-1"},
|
||||
pipeline={"dis_mode": 1},
|
||||
origin="user",
|
||||
)
|
||||
path = tmp_path / "out.json"
|
||||
write(cal, path, overwrite=True)
|
||||
loaded = load(path)
|
||||
assert loaded is not None
|
||||
assert loaded.convert(1000.0, 500.0, -500.0) == pytest.approx((1.0, 2.0, -3.0))
|
||||
|
||||
|
||||
def test_check_rejects_bad_schema_and_short_points():
|
||||
bad_schema = {"schema_version": 99, "channels": {"normal": {"method": "linear", "slope": 1, "offset": 0}}}
|
||||
result = check(bad_schema)
|
||||
assert not result.ok
|
||||
assert any("schema_version" in e for e in result.errors)
|
||||
|
||||
short = {
|
||||
"schema_version": 2,
|
||||
"channels": {
|
||||
"normal": {
|
||||
"method": "piecewise_linear",
|
||||
"points": [[0.0, 0.0]],
|
||||
}
|
||||
},
|
||||
}
|
||||
result2 = check(short)
|
||||
assert not result2.ok
|
||||
|
||||
|
||||
def test_sensor_cal_path_v2_applies_to_read_info(tmp_path: Path):
|
||||
doc = {
|
||||
"schema_version": 2,
|
||||
"unit": "N",
|
||||
"channels": {
|
||||
"normal": {
|
||||
"method": "piecewise_linear",
|
||||
"points": [[0.0, 0.0], [100.0, 5.0]],
|
||||
"extrapolate": True,
|
||||
"clamp_output_min": 0.0,
|
||||
},
|
||||
"shear_x": {
|
||||
"method": "piecewise_linear",
|
||||
"signed": True,
|
||||
"points": [[0.0, 0.0], [100.0, 7.0]],
|
||||
"extrapolate": True,
|
||||
"clamp_output_min": 0.0,
|
||||
},
|
||||
"shear_y": {
|
||||
"method": "piecewise_linear",
|
||||
"signed": True,
|
||||
"points": [[0.0, 0.0], [100.0, 9.0]],
|
||||
"extrapolate": True,
|
||||
"clamp_output_min": 0.0,
|
||||
},
|
||||
},
|
||||
}
|
||||
path = tmp_path / "v2.json"
|
||||
path.write_text(json.dumps(doc), encoding="utf-8")
|
||||
sensor = Sensor(
|
||||
"external",
|
||||
config_name="box",
|
||||
cal_path=str(path),
|
||||
verbose=False,
|
||||
backend="cpu",
|
||||
)
|
||||
try:
|
||||
assert sensor.force_calibrated
|
||||
assert sensor.force_unit == "N"
|
||||
sensor._force_raw = (100.0, 100.0, -100.0)
|
||||
sensor._apply_force_filter()
|
||||
fn, fx, fy = sensor.read_info(
|
||||
sensor.info.FNORMAL,
|
||||
sensor.info.FSHEARX,
|
||||
sensor.info.FSHEARY,
|
||||
)
|
||||
fn_raw, fx_raw, fy_raw = sensor.read_info(
|
||||
sensor.info.FNORMAL_RAW,
|
||||
sensor.info.FSHEARX_RAW,
|
||||
sensor.info.FSHEARY_RAW,
|
||||
)
|
||||
assert (fn_raw, fx_raw, fy_raw) == pytest.approx((100.0, 100.0, -100.0))
|
||||
assert (fn, fx, fy) == pytest.approx((5.0, 7.0, -9.0))
|
||||
finally:
|
||||
sensor.disconnect()
|
||||
|
||||
|
||||
def test_set_calibration_swaps_curve(tmp_path: Path):
|
||||
path_a = tmp_path / "a.json"
|
||||
path_b = tmp_path / "b.json"
|
||||
path_a.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"schema_version": 2,
|
||||
"channels": {
|
||||
"normal": {
|
||||
"method": "piecewise_linear",
|
||||
"points": [[0.0, 0.0], [10.0, 1.0]],
|
||||
}
|
||||
},
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
path_b.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"schema_version": 2,
|
||||
"channels": {
|
||||
"normal": {
|
||||
"method": "piecewise_linear",
|
||||
"points": [[0.0, 0.0], [10.0, 2.0]],
|
||||
}
|
||||
},
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
sensor = Sensor(
|
||||
"external",
|
||||
config_name="box",
|
||||
cal_path=str(path_a),
|
||||
verbose=False,
|
||||
backend="cpu",
|
||||
)
|
||||
try:
|
||||
sensor._force_raw = (10.0, 0.0, 0.0)
|
||||
sensor._apply_force_filter()
|
||||
assert sensor.fnormal == pytest.approx(1.0)
|
||||
sensor.set_calibration(path_b)
|
||||
sensor._apply_force_filter()
|
||||
assert sensor.fnormal == pytest.approx(2.0)
|
||||
finally:
|
||||
sensor.disconnect()
|
||||
|
||||
|
||||
def test_missing_cal_path_raises():
|
||||
with pytest.raises(CalibrationError):
|
||||
Sensor(
|
||||
"external",
|
||||
config_name="box",
|
||||
cal_path="D:/no/such/force_calibration.json",
|
||||
verbose=False,
|
||||
backend="cpu",
|
||||
)
|
||||
|
||||
|
||||
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.convert(10000.0, 36000.0, -36000.0) == pytest.approx((0.377, 1.19, -1.19))
|
||||
@@ -0,0 +1,48 @@
|
||||
"""Tests for sensor-bundle export helpers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from orisys.configs import (
|
||||
export_sensor_bundle,
|
||||
export_sensor_bundles,
|
||||
list_sensor_bundles,
|
||||
resolve_sensor_bundle,
|
||||
)
|
||||
|
||||
|
||||
def test_export_sensor_bundle_json_only(tmp_path: Path) -> None:
|
||||
assert resolve_sensor_bundle("box") is not None
|
||||
|
||||
dest = export_sensor_bundle("box", tmp_path, include_assets=False)
|
||||
assert dest == tmp_path / "box"
|
||||
assert (dest / "sensor.json").is_file()
|
||||
assert (dest / "force_calibration.json").is_file()
|
||||
assert not (dest / "assets").exists()
|
||||
|
||||
|
||||
def test_export_sensor_bundles_default_names(tmp_path: Path) -> None:
|
||||
available = list_sensor_bundles()
|
||||
assert "box" in available
|
||||
|
||||
exported = export_sensor_bundles(tmp_path, include_assets=False)
|
||||
names = {path.name for path in exported}
|
||||
assert "box" in names
|
||||
assert "mini" in names
|
||||
for path in exported:
|
||||
assert (path / "sensor.json").is_file()
|
||||
assert not (path / "assets").exists()
|
||||
|
||||
|
||||
def test_export_sensor_bundle_no_overwrite(tmp_path: Path) -> None:
|
||||
export_sensor_bundle("box", tmp_path, overwrite=True)
|
||||
with pytest.raises(FileExistsError):
|
||||
export_sensor_bundle("box", tmp_path, overwrite=False)
|
||||
|
||||
|
||||
def test_export_sensor_bundle_missing_name(tmp_path: Path) -> None:
|
||||
with pytest.raises(FileNotFoundError):
|
||||
export_sensor_bundle("not-a-real-sensor", tmp_path)
|
||||
@@ -224,12 +224,13 @@ def test_box_read_info_applies_force_calibration_json():
|
||||
|
||||
def test_read_info_force_stays_raw_without_calibration_json(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
"orisys.sensor.configs.load_force_calibration",
|
||||
lambda _name: None,
|
||||
"orisys.calibration.load",
|
||||
lambda *args, **kwargs: None,
|
||||
)
|
||||
sensor = Sensor("external", config_name="box", verbose=False, backend="cpu")
|
||||
try:
|
||||
assert sensor._force_cal_sensor_name is None
|
||||
assert sensor.calibration is None
|
||||
sensor._force_raw = (111.0, -22.0, 33.0)
|
||||
sensor._apply_force_filter()
|
||||
fn, fx, fy = sensor.read_info(
|
||||
|
||||
Reference in New Issue
Block a user