Files
orisys-app/apps/autocal/tests/test_summary_fit.py
T
ykcheungab 19426c3c4a Fix repeat ownership and close out capture evidence
Start each repeat after its own manifest entry, refuse packets from a restarted
or stopped camera, keep a simulated repeat from reading as recorded evidence,
and decide completeness from the writer report. Add the operator summary (four
files plus an atomic pointer) and read-only video review of a hold window.

Verified with the full Autocal suite (567 passed), the shared recorder/playback
files (57 passed), the lint gate and the documentation gate; the two pre-existing
panel_check layout failures also fail on a clean HEAD worktree.
2026-10-17 10:44:30 +08:00

167 lines
5.9 KiB
Python

"""The summary fit is a forward regression of raw on the declared reference.
Hand-computed lines, one nonlinear set, and the proof that its slope is not the
reciprocal of the review's inverse fit (``N = gain * raw + offset``).
"""
from __future__ import annotations
import math
import pytest
from orisys_autocal.summary import fit_axis
def _window(load: float, fn: float | None, *, branch: str = "load", verified: bool = False,
eligible: bool = True, site: str = "P1", trial: str = "t1") -> dict:
return {
"session_id": "session-1", "site_id": site, "trial_id": trial,
"repeat_index": 1, "step_index": 0, "branch": branch,
"reference_load_n": load, "eligible": eligible,
"stability_verified": verified,
"FN_mean": fn, "FX_mean": 0.0, "FY_mean": 0.0,
}
def test_a_straight_line_is_recovered_exactly():
fit = fit_axis([(0.0, 100.0), (2.0, 180.0), (4.0, 260.0)])
assert fit["status"] == "available"
assert fit["slope_raw_per_N"] == pytest.approx(40.0)
assert fit["intercept_raw"] == pytest.approx(100.0)
assert fit["R2"] == pytest.approx(1.0)
assert fit["rmse_raw"] == pytest.approx(0.0)
assert fit["reference_min_N"] == 0.0 and fit["reference_max_N"] == 4.0
assert fit["window_count"] == 3
def test_a_nonlinear_set_matches_the_hand_computation():
# x mean 1, y mean 166.667; slope 80; intercept 86.667; SSE 1066.667;
# SST 13866.667 -> R2 1 - 1066.667 / 13866.667 = 0.923077.
fit = fit_axis([(0.0, 100.0), (1.0, 140.0), (2.0, 260.0)])
assert fit["slope_raw_per_N"] == pytest.approx(80.0)
assert fit["intercept_raw"] == pytest.approx(86.6666666667, rel=1e-9)
assert fit["R2"] == pytest.approx(0.9230769231, rel=1e-9)
assert fit["rmse_raw"] == pytest.approx(math.sqrt(1066.6666667 / 3), rel=1e-9)
def test_a_constant_signal_has_no_coefficient_of_determination():
fit = fit_axis([(0.0, 5.0), (1.0, 5.0), (2.0, 5.0)])
assert fit["status"] == "flat_signal"
assert fit["R2"] is None
assert fit["slope_raw_per_N"] == pytest.approx(0.0)
assert fit["intercept_raw"] == pytest.approx(5.0)
def test_one_reference_level_cannot_be_fitted():
fit = fit_axis([(5.0, 300.0), (5.0, 310.0)])
assert fit["status"] == "insufficient_reference_levels"
assert "slope_raw_per_N" not in fit
def test_no_finite_sample_is_reported_as_missing_data():
fit = fit_axis([])
assert fit["status"] == "no_data"
def test_repeated_reference_levels_are_weighted_but_counted():
fit = fit_axis([(0.0, 100.0), (5.0, 300.0), (5.0, 320.0), (10.0, 520.0)])
assert fit["window_count"] == 4
assert fit["reference_min_N"] == 0.0 and fit["reference_max_N"] == 10.0
assert fit["slope_raw_per_N"] == pytest.approx(42.0)
def test_the_forward_slope_is_not_the_reciprocal_of_the_inverse_fit():
"""``N = gain*raw + offset`` and ``raw = a*N + b`` answer different questions."""
import numpy as np
from orisys_autocal.session_review import _fit
raw = [100.0, 300.0, 520.0]
loads = [0.0, 5.0, 10.0]
forward = fit_axis(list(zip(loads, raw)))
inverse = _fit([
{"raw_mean": value, "reference_n": load, "status": "candidate", "trial_id": "t1"}
for value, load in zip(raw, loads)
])
gain, _offset = np.polyfit(np.array(raw), np.array(loads), 1)
assert forward["slope_raw_per_N"] == pytest.approx(42.0)
assert inverse["sensitivity_raw_per_n"] == pytest.approx(1.0 / gain)
assert abs(forward["slope_raw_per_N"] - 1.0 / gain) > 1e-3
def test_unloading_is_shown_but_never_fitted():
from orisys_autocal.summary import build_sensitivity
windows = [
_window(0.0, 100.0, branch="baseline"),
_window(5.0, 300.0),
_window(5.0, 340.0, branch="unload"),
_window(0.0, 120.0, branch="unload"),
]
rows = build_sensitivity(windows)
assert {row["component"] for row in rows} == {"FN", "FX", "FY"}
fn = next(row for row in rows if row["component"] == "FN")
assert fn["window_count"] == 2
assert fn["slope_raw_per_N"] == pytest.approx(40.0)
# The unloading values are far higher; a fit that included them would not be 40.
assert fn["endpoint_raw_per_N"] == pytest.approx(40.0)
def test_endpoint_and_initial_step_use_the_declared_loads():
from orisys_autocal.summary import build_sensitivity
windows = [
_window(0.0, 100.0, branch="baseline"),
_window(2.0, 180.0),
_window(5.0, 300.0),
]
fn = next(row for row in build_sensitivity(windows) if row["component"] == "FN")
# endpoint: (raw at the highest load - raw at zero) / highest load.
assert fn["endpoint_raw_per_N"] == pytest.approx((300.0 - 100.0) / 5.0)
# initial step: the lowest positive load minus zero.
assert fn["initial_step_raw"] == pytest.approx(80.0)
def test_a_missing_zero_load_leaves_the_derived_numbers_empty():
from orisys_autocal.summary import build_sensitivity
fn = next(row for row in build_sensitivity([_window(5.0, 300.0), _window(10.0, 500.0)])
if row["component"] == "FN")
assert fn["slope_raw_per_N"] == pytest.approx(40.0)
assert fn["endpoint_raw_per_N"] is None
assert fn["initial_step_raw"] is None
def test_confirmed_and_candidate_windows_are_never_fitted_together():
from orisys_autocal.summary import build_sensitivity
windows = [
_window(0.0, 100.0, branch="baseline"),
_window(5.0, 300.0),
_window(0.0, 100.0, branch="baseline", verified=True),
_window(10.0, 500.0, verified=True),
]
rows = build_sensitivity(windows)
by_review = {}
for row in rows:
if row["component"] == "FN":
by_review[row["review_status"]] = row
assert set(by_review) == {"candidate", "confirmed"}
assert by_review["candidate"]["window_count"] == 2
assert by_review["candidate"]["slope_raw_per_N"] == pytest.approx(40.0)
assert by_review["confirmed"]["window_count"] == 2
assert by_review["confirmed"]["slope_raw_per_N"] == pytest.approx(40.0)