新增统一力读数会话 ForceSession

把同帧测量、空载归零、模型推断与共用后处理收进一个 SDK 会话,Studio 显示与记录使用同一样本;旧 Sensor 读数接口保持兼容。以 force session 与受影响 SDK 套件、lint 与文档门禁验证,硬件与真实载荷精度未验证。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CpCfJF6SvonMKmPZhzKZHb
This commit is contained in:
ykcheungab
2026-10-20 07:07:22 +00:00
parent dc8a630df3
commit ee830d8505
17 changed files with 4175 additions and 29 deletions
+219
View File
@@ -0,0 +1,219 @@
"""HHD downsample compatibility diagnostics and non-divisible grid integration.
The pipeline contract allows a model calibrated at one ``hhd_downsample`` to run at
another, but the difference is an accuracy risk that must be visible instead of a
silent pass. These tests pin the report, the unchanged refusal of every other
identity mismatch, and the analytic block integration on grids that do not divide
the output image evenly.
"""
from __future__ import annotations
import numpy as np
import pytest
from orisys.calibration import CalibrationError, parse
from orisys.calibration_context import build_context
from orisys.calibration_models import ModelCalibration
from orisys.field_calibration import FieldFrame, _block_starts, build_field_gain_map, reduce_field
from orisys.force_readout import OutputSettings
from orisys.force_session import ForceSession, FrameStamp
from orisys.sensor import FrameMeasurement
FRAME = np.zeros((2, 2, 3), dtype=np.uint8)
def _config(downsample=1, *, dis_mode=4):
return {
"camera": {"resolution_raw": [1280, 720], "resolution_stitch": [700, 750]},
"tracker": {"dis_mode": dis_mode, "hhd_downsample": downsample},
}
def _document(config) -> dict:
return {
"schema_version": 3,
"unit": "N",
"identity": {"sensor_type": "box", "sn": "A1"},
"pipeline": dict(build_context(config)),
"model": {
"type": "global_gain/1",
"input_profile": "force_delta/1",
"output_frame": "sdk_force/1",
"outputs": ["fn"],
"params": {
"gains_N_per_raw": {"fn": 0.001},
"valid_force_range_N": [0, 3],
},
},
}
def _model(config) -> ModelCalibration:
loaded = parse(_document(config))
assert isinstance(loaded, ModelCalibration)
return loaded
# ------------------------------------------------------------------ the report
def test_downsample_only_difference_runs_and_is_reported():
model = _model(_config(2))
runtime = build_context(_config(3))
model.validate_for_context(runtime) # allowed: no exception
report = model.pipeline_compatibility(runtime)
assert report["ok"] is True
downsample = report["downsample"]
assert downsample["declared"] == 2
assert downsample["actual"] == 3
assert downsample["match"] is False
assert downsample["allowed"] is True
assert downsample["accuracy_risk"]
assert model.downsample_report(runtime).known is True
def test_a_matching_downsample_has_no_accuracy_risk():
model = _model(_config(3))
report = model.pipeline_compatibility(build_context(_config(3)))
assert report["downsample"]["match"] is True
assert report["downsample"]["accuracy_risk"] == ""
def test_an_unknown_runtime_downsample_is_unknown_not_a_match():
model = _model(_config(2))
report = model.downsample_report({})
assert report.actual is None
assert report.match is False
assert report.allowed is True
assert "unknown" in report.accuracy_risk
def test_every_other_identity_mismatch_is_still_refused():
model = _model(_config(2))
runtime = build_context(_config(2, dis_mode=5))
report = model.pipeline_compatibility(runtime)
assert report["ok"] is False
assert "calibration pipeline mismatch: dis_mode" in report["mismatches"].values()
with pytest.raises(CalibrationError, match="calibration pipeline mismatch: dis_mode"):
model.validate_for_context(runtime)
@pytest.mark.parametrize("raw, expected", [(None, 1), ("bad", 1), (0, 1), (-3, 1), ("3", 3), (3.7, 3)])
def test_context_reports_its_actual_downsample(raw, expected):
config = _config(1)
config["tracker"]["hhd_downsample"] = raw
assert build_context(config).actual_downsample == expected
# ------------------------------------------------- non-divisible grid integration
def _identity_coordinates(rows: int, cols: int) -> np.ndarray:
uv = np.empty((rows, cols, 2), dtype=np.float64)
uv[..., 0] = np.arange(cols)[None, :]
uv[..., 1] = np.arange(rows)[:, None]
return uv
@pytest.mark.parametrize("grid_shape", [(1, 1), (2, 3), (3, 4), (4, 4)])
def test_a_constant_field_integrates_exactly_on_a_non_divisible_grid(grid_shape):
rows, cols = 37, 53
uv = _identity_coordinates(rows, cols)
mask = np.ones((rows, cols), dtype=bool)
points = np.array(
[
[0.0, 0.0],
[cols - 1.0, 0.0],
[cols - 1.0, rows - 1.0],
[0.0, rows - 1.0],
]
)
gains = np.ones(4, dtype=np.float64)
triangles = np.array([[0, 1, 2], [0, 2, 3]], dtype=int)
# The block layout is an exact partition of the output image, for every grid
# size that does not divide it: no lost pixels and no double counting.
for divisions, size in ((grid_shape[0], rows), (grid_shape[1], cols)):
starts, ends = _block_starts(divisions, size)
assert starts[0] == 0
assert ends[-1] == size
assert np.array_equal(starts[1:], ends[:-1])
assert np.all(ends > starts)
gain_map = build_field_gain_map(points, gains, triangles, uv, mask, grid_shape, mask)
row_starts, row_ends = _block_starts(grid_shape[0], rows)
col_starts, col_ends = _block_starts(grid_shape[1], cols)
block_area = np.outer(row_ends - row_starts, col_ends - col_starts)
# A constant unit gain and a fully mapped mask: the area-weighted coverage is
# the whole image, exactly (analytic area, not a sampled sum).
assert float(np.sum(gain_map.coverage * block_area)) == pytest.approx(rows * cols)
assert np.allclose(gain_map.gain, 1.0)
assert np.allclose(gain_map.inside, 1.0)
field = FieldFrame(
d=np.ones((grid_shape[0], grid_shape[1], 2), dtype=np.float32),
valid_mask=np.ones(grid_shape, dtype=bool),
area_scale=1.0,
grid_shape=grid_shape,
reference_shape=None,
)
reduction = reduce_field(field, gain_map)
# q = |d_x| + |d_y| = 2 per active HHD cell; the gain map's block sums are exact
# whatever the block boundaries are, so the integral is exactly 2 * grid cells.
cells = grid_shape[0] * grid_shape[1]
assert reduction.weighted_N == pytest.approx(2.0 * cells)
assert reduction.extrapolated_fraction == 0.0
assert reduction.coverage_fraction == pytest.approx(1.0)
# ------------------------------------------------------- session output diagnostics
class _Source:
"""The measurement-source surface ForceSession needs; no camera involved."""
def __init__(self) -> None:
self.session_enabled = False
self.reset_count = 0
def enable_measurement_session(self) -> None:
self.session_enabled = True
def disable_measurement_session(self) -> None:
self.session_enabled = False
def reset_tracker(self) -> None:
self.reset_count += 1
def read_measurement_frame(
self, source_frame, *, frame_id, timestamp_s=None, want_field=False, want_contact=False
) -> FrameMeasurement:
del source_frame, timestamp_s, want_field, want_contact
return FrameMeasurement(
raw=(10.0, 20.0, 30.0),
raw_valid=(True, True, True),
)
def test_the_session_output_states_the_downsample_risk():
model = _model(_config(2))
session = ForceSession(
source=_Source(),
sensor_family="box",
context=build_context(_config(3)),
calibration=model,
settings=OutputSettings(sample_rate_hz=30.0),
)
output = session.process_frame(
FRAME,
FrameStamp(
session_id="s1",
frame_id=0,
source_frame_id=0,
signal_time_s=0.0,
time_basis="recorded_signal",
),
)
diagnostics = dict(output.measurement.diagnostics)
assert diagnostics["hhd_downsample_calibrated"] == 2.0
assert diagnostics["hhd_downsample_actual"] == 3.0
assert diagnostics["hhd_downsample_mismatch"] == 1.0
assert diagnostics["downsample_accuracy_risk"]
assert session.calibration_compatibility["downsample"]["actual"] == 3