"""Calibration diagnostics must describe the model that is exported. The review found the panel reporting a private least-squares fit while the exported file carried a piecewise-linear curve, validation points leaking into the build, and repeated loads being overwritten so only the last one reached the model. """ from __future__ import annotations import pytest from orisys_app_shared.force.calibration_io import ( CalSample, CalSessionStore, build_export_model, build_force_calibration_v2, merge_samples, predict_channel, ) from orisys_app_shared.force.calibration_recorder import CalibrationRecorder from orisys_app_shared.force.session import SessionSample def _point(recorder, load: float, raw: float, *, channel: str = "fn"): recorder.start(channel=channel, reference_load_n=load, duration_s=0.05) for index in range(8): recorder.feed( SessionSample( index, index * 0.01, fn_raw=raw, fx_raw=raw, fy_raw=raw, fn_n=raw / 10.0, fx_n=raw / 10.0, fy_n=raw / 10.0, ) ) return recorder.finish_now() def test_the_diagnosed_model_is_the_exported_model(): recorder = CalibrationRecorder() for load, raw in ((1.0, 100.0), (2.0, 400.0), (3.0, 300.6), (5.0, 500.9)): _point(recorder, load, raw) fit = recorder.fit("fn") assert fit.usable assert fit.method == "piecewise_linear" assert fit.breakpoints == 4 # A piecewise curve passes through the points it was built from. assert fit.max_residual_n == pytest.approx(0.0, abs=1e-9) def test_validation_points_are_held_out_and_measured(): recorder = CalibrationRecorder() points = [ _point(recorder, 1.0, 100.0), _point(recorder, 2.0, 400.0), _point(recorder, 3.0, 300.6), _point(recorder, 5.0, 500.9), ] recorder.set_validation(points[1], True) fit = recorder.fit("fn") assert fit.n_points == 3, "a validation point entered the build" assert fit.validation_points == 1 assert fit.validation_error_n > 0.05, "the off-trend point was not measured" assert fit.breakpoints == 3 assert points[1].residual_n != points[1].residual_n # NaN: not in the fit def test_excluded_points_leave_the_build(): recorder = CalibrationRecorder() points = [_point(recorder, 1.0, 100.0), _point(recorder, 5.0, 500.0)] outlier = _point(recorder, 3.0, 900.0) recorder.set_excluded(outlier, True) fit = recorder.fit("fn") assert fit.n_points == 2 store = CalSessionStore() for point in recorder.all_points(): if point.excluded or point.is_validation: continue store.aggregate( point.channel, CalSample( weight_n=float(point.reference_load_n), raw_mean=float(point.loaded_stats().mean), n=int(point.loaded_stats().n), ), ) doc, _warnings = build_force_calibration_v2(store) points_in_doc = doc["channels"]["normal"]["points"] assert [point[0] for point in points_in_doc] == [100.0, 500.0], ( "the excluded point reached the exported coefficients" ) assert points # silence linters about the unused list def test_validation_points_never_reach_the_exported_coefficients(): recorder = CalibrationRecorder() _point(recorder, 1.0, 100.0) _point(recorder, 5.0, 500.0) held_out = _point(recorder, 3.0, 300.0) recorder.set_validation(held_out, True) store = CalSessionStore() for point in recorder.all_points(): if point.excluded or point.is_validation: continue store.aggregate( point.channel, CalSample( weight_n=float(point.reference_load_n), raw_mean=float(point.loaded_stats().mean), n=int(point.loaded_stats().n), ), ) doc, _ = build_force_calibration_v2(store) raws = [point[0] for point in doc["channels"]["normal"]["points"]] assert 300.0 not in raws, "a validation point was used to build the model" def test_repeated_loads_are_merged_not_overwritten(): """Two samples of one load carry scatter the model should see.""" first = CalSample(weight_n=2.0, raw_mean=100.0, n=10, std=1.0) second = CalSample(weight_n=2.0, raw_mean=110.0, n=30, std=2.0) merged = merge_samples(first, second) assert merged.n == 40 assert merged.raw_mean == pytest.approx((100.0 * 10 + 110.0 * 30) / 40) assert merged.std > 2.0, "the scatter between the two samples was lost" store = CalSessionStore() store.aggregate("fn", first) store.aggregate("fn", second) assert len(store.channel("fn")) == 1 assert store.channel("fn")[0].n == 40 def test_predict_channel_applies_the_exported_curve(): store = CalSessionStore() store.aggregate("fn", CalSample(weight_n=0.0, raw_mean=0.0, n=10)) store.aggregate("fn", CalSample(weight_n=10.0, raw_mean=100.0, n=10)) model = build_export_model({key: store.channel(key) for key in ("fn", "fx", "fy")}) assert predict_channel(model, "fn", 50.0) == pytest.approx(5.0, abs=1e-6) def test_a_channel_with_one_point_is_not_exported(): store = CalSessionStore() store.aggregate("fn", CalSample(weight_n=1.0, raw_mean=10.0, n=10)) with pytest.raises(ValueError): build_force_calibration_v2(store)