Files
orisys-sdk/tests/test_calibration_models.py
ykcheungab ff7614a6a0 接入力标定 schema 3 与多点空间增益模型
保留旧格式和 cal_path,校验 SN、特征身份及坐标附件,按帧返回逐轴状态。相关测试 263 项通过,已核对存量预测并实测 180 帧录像。
2026-10-18 12:56:40 +08:00

180 lines
7.3 KiB
Python

"""Schema 3 model validation and deterministic inference."""
import hashlib
from copy import deepcopy
import numpy as np
import pytest
from orisys.calibration import CalibrationError
from orisys.calibration_models import parse_model
def document():
return {
"schema_version": 3, "unit": "N",
"identity": {"sensor_type": "finger", "sn": "DD02-GL"},
"pipeline": {
"stitch_resolution": [700, 750], "dis_mode": 4,
"hhd_downsample": 3, "source_scale": 0.5,
"filters": {"measurement": {"flow_ema_alpha": 1.0}},
"feature_signature_sha256": "a" * 64,
"geometry_sha256": "b" * 64,
"coordinates": {"frame": "autocal_flat", "unit": "px",
"origin_xy": [511, 1021], "map_sha256": "c" * 64},
},
"model": {
"type": "spatial_gain/1", "input_profile": "fn_delta_autocal_uv/1",
"output_frame": "sdk_force/1", "outputs": ["fn"],
"params": {"points_uv": [[0, 0], [10, 0], [0, 10]],
"gains_N_per_raw": [0.001, 0.002, 0.003],
"triangles": [[0, 1, 2]], "valid_force_range_N": [0, 3]},
},
}
def features(**overrides):
return dict(delta_raw=1000, u=2.5, v=2.5, contact_found=True,
zero_valid=True, mapping_valid=True, **overrides)
def test_interpolates_gain_not_sensitivity_and_roundtrips():
data = document()
model = parse_model(data)
result = model.predict(1, 0.1, features(), "zero-1")
assert result.values_N == {"fn": pytest.approx(1.75)}
assert result.status == {"fn": "ok"}
assert model.to_dict() == data
assert "fx" not in model.channels
data["model"]["params"]["gains_N_per_raw"][0] = 999
assert model.predict(2, 0.2, features(), "zero-1").values_N["fn"] == 1.75
def test_legacy_scalar_convert_explicitly_rejected():
with pytest.raises(CalibrationError, match="schema 3 requires frame features"):
parse_model(document()).convert(1, 2, 3)
@pytest.mark.parametrize("change,status", [
({"u": 20}, "outside_coverage"), ({"contact_found": False}, "no_contact"),
({"zero_valid": False}, "zero_unavailable"),
({"mapping_valid": False}, "coordinate_unmapped"),
({"delta_raw": float("nan")}, "invalid_input"),
({"delta_raw": "1000"}, "invalid_input"),
({"single_contact": False}, "invalid_input"),
])
def test_frame_invalidity(change, status):
values = features()
values.update(change)
result = parse_model(document()).predict(1, 0.1, values, "zero")
assert result.status == {"fn": status}
assert result.values_N == {"fn": None}
def test_out_of_range_retains_value():
values = features()
values["delta_raw"] = 3000
result = parse_model(document()).predict(1, 0, values, "zero")
assert result.values_N["fn"] == 5.25
assert result.status["fn"] == "out_of_range"
@pytest.mark.parametrize("mutate", [
lambda d: d.update(schema_version=True),
lambda d: d.update(extra="ignored"),
lambda d: d["identity"].update(sn=""),
lambda d: d["model"].update(outputs=["fn", "fx"]),
lambda d: d["model"]["params"].update(gains_N_per_raw=[0.001, 0, 0.003]),
lambda d: d["model"]["params"].update(triangles=[[0, 1, 1]]),
lambda d: d["model"]["params"].update(triangles=[[0, 1, 2], [2, 1, 0]]),
lambda d: d["pipeline"].pop("geometry_sha256"),
lambda d: d["pipeline"]["coordinates"].update(map_sha256="bad"),
])
def test_rejects_bad_documents(mutate):
data = document()
mutate(data)
with pytest.raises(ValueError):
parse_model(data)
def test_rejects_gap_and_overlapping_equal_area_meshes():
data = document()
params = data["model"]["params"]
params.update(points_uv=[[0, 0], [10, 0], [10, 10], [0, 10]],
gains_N_per_raw=[0.001] * 4, triangles=[[0, 1, 2]])
with pytest.raises(ValueError, match="triang|mesh|hull"):
parse_model(data)
params["triangles"] = [[0, 1, 2], [0, 1, 3]]
with pytest.raises(ValueError, match="overlap"):
parse_model(data)
params["triangles"] = [[0, 1, 2], [0, 2, 3]]
assert parse_model(data).predict(1, 0, features(), "zero").status["fn"] == "ok"
def test_global_gain_and_context():
data = document()
data["pipeline"].pop("coordinates")
data["pipeline"].pop("geometry_sha256")
data["model"].update(type="global_gain/1", input_profile="force_delta/1",
outputs=["fn", "fx"], params={
"gains_N_per_raw": {"fn": 0.002, "fx": 0.004},
"valid_force_range_N": [-3, 3]})
model = parse_model(data)
result = model.predict(1, 0, {"delta_raw_by_axis": {"fn": 100, "fx": -100},
"zero_valid": True, "contact_found": True}, "z")
assert result.values_N == {"fn": 0.2, "fx": -0.4}
model.validate_for_context(data["pipeline"])
wrong = deepcopy(data["pipeline"])
wrong["source_scale"] = 0.42
with pytest.raises(ValueError, match="source_scale"):
model.validate_for_context(wrong)
model.reset("new-zero")
data["pipeline"].pop("source_scale")
assert parse_model(data).input_profile == "force_delta/1"
assert model.predict(2, None, {"force_delta_raw": {"fn": 100, "fx": -100},
"zero_valid": True}, "z").values_N == {"fn": 0.2, "fx": -0.4}
assert model.predict(3, None, {"force_delta_raw": {"fn": 100, "fx": -100},
"zero_valid": True, "contact_found": False}, "z").status == {
"fn": "no_contact", "fx": "no_contact"}
def test_unknown_backend_fails_explicitly():
data = document()
data["model"]["type"] = "onnx/1"
with pytest.raises(ValueError, match="unsupported.*onnx/1"):
parse_model(data)
def test_coordinate_artifact_hash_and_paths(tmp_path):
path = tmp_path / "map.npz"
np.savez(path, uv_by_output_px=np.zeros((2, 3, 2)), valid_mask=np.ones((2, 3), dtype=bool),
origin_xy=np.array([511, 1021]))
digest = hashlib.sha256(path.read_bytes()).hexdigest()
data = document()
data["pipeline"]["coordinates"]["map_sha256"] = digest
data["model"]["artifacts"] = [{"role": "coordinate_map", "path": "map.npz", "sha256": digest}]
model = parse_model(data, tmp_path / "calibration.json")
assert model.coordinate_map.shape == (2, 3, 2)
assert len(model.fingerprint) == 64
assert not model.coordinate_map.flags.writeable
for unsafe in ("../map.npz", "https://example.org/x.npz", "C:\\map.npz", "/map.npz"):
data["model"]["artifacts"][0]["path"] = unsafe
with pytest.raises(ValueError, match="relative"):
parse_model(data, tmp_path / "calibration.json")
data["model"]["artifacts"][0]["path"] = "map.npz"
path.write_bytes(b"corrupt")
with pytest.raises(ValueError, match="SHA-256"):
parse_model(data, tmp_path / "calibration.json")
def test_many_points_complete_mesh():
from scipy.spatial import Delaunay
data = document()
points = np.array([[x, y] for x in range(5) for y in range(5)])
data["model"]["params"].update(points_uv=points.tolist(), gains_N_per_raw=[0.001] * 25,
triangles=Delaunay(points).simplices.tolist())
model = parse_model(data)
values = features()
values.update(u=2, v=2)
assert model.predict(1, 0, values, "z").values_N["fn"] == 1.0