"""Saved-session review uses frame identity and excludes ramps and invalid data.""" from __future__ import annotations import csv import json import pytest from orisys_autocal.calibration_session import write_json from orisys_autocal.session_review import review_session, save_review def _csv(path, rows): with path.open("w", newline="", encoding="utf-8") as handle: writer = csv.DictWriter(handle, fieldnames=list(rows[0])) writer.writeheader() writer.writerows(rows) @pytest.fixture def session(tmp_path): manifest = { "format_version": "autocal-session-1.0", "session_id": "test-session", "status": "incomplete", "uv_definition": {"mapping_sha256": "test-map"}, "sites": [{"site_id": "site-001", "uv_xy_px": [1, -2]}, {"site_id": "site-002", "uv_xy_px": [-3, 4]}], "trials": [], } for i, sensitivity in enumerate((2.0, 4.0), 1): trial_id, site_id = f"trial-{i:03d}", f"site-{i:03d}" entry = {"trial_id": trial_id, "site_id": site_id, "status": "complete", "directory": f"trials/{trial_id}"} manifest["trials"].append(entry) path = tmp_path / entry["directory"] path.mkdir(parents=True) write_json(path / "session.json", { "trial_id": trial_id, "site_id": site_id, "session_id": 7, "calibration_session_id": "test-session", "trial_status": "complete", "reference_alignment_possible": False, "uv_definition": manifest["uv_definition"], }) frames, forces, references = [], [], [] for step, (branch, load) in enumerate(( ("zero", 0), ("load", 5), ("load", 10), ("unload", 5), ("unload", 0))): start = (step * 20 + 1) * 1_000_000_000 end = start + 10_000_000_000 references.append({"step_index": step, "branch": branch, "t_start_host_mono_ns": start, "t_end_host_mono_ns": end, "F_ref_normal_N": load, "hold_complete": 1, "stability_verified": 0}) raw = 1 + sensitivity * load + (1 if branch == "unload" and load else 0) for offset, value, valid, dropped in ( (0, raw, 1, 0), (1_000_000_000, raw, 1, 0), (2_000_000_000, 999, 0, 0), (3_000_000_000, 999, 1, 1), (10_000_000_000, 999, 1, 0)): frame_id = len(frames) + 1 frames.append({"frame_id": frame_id, "t_host_mono_ns": start + offset, "dropped": dropped}) forces.append({"session_id": 7, "frame_id": frame_id, "FN_raw": value, "valid": valid}) _csv(path / "frames.csv", frames) _csv(path / "forces.csv", list(reversed(forces))) _csv(path / "reference.csv", references) manifest["trials"].append({"trial_id": "trial-003", "status": "incomplete"}) write_json(tmp_path / "session.json", manifest) return tmp_path def test_review_fits_each_site_without_ramps_invalid_samples_or_unloading_bias(session): result = review_session(session) assert result["status"] == "exploratory" assert result["issues"] == [{"trial_id": "trial-003", "reason": "incomplete_trial"}] first, second = result["sites"] assert first["fit"]["gain_n_per_raw"] == pytest.approx(.5) assert first["fit"]["offset_n"] == pytest.approx(-.5) assert second["fit"]["sensitivity_raw_per_n"] == pytest.approx(4) assert all(w["samples"] == 2 and w["raw_std"] == 0 for s in result["sites"] for w in s["windows"]) assert first["windows"][3]["raw_mean"] == 12 assert not first["windows"][0]["reference_alignment_possible"] assert not first["windows"][0]["stability_verified"] before = (session / "session.json").read_bytes() a, b = save_review(result), save_review(result) assert a != b and a.is_file() and b.is_file() assert (session / "session.json").read_bytes() == before def test_no_samples_does_not_produce_a_coefficient(session): path = session / "trials/trial-001/forces.csv" _csv(path, [{"session_id": 7, "frame_id": 1, "FN_raw": "", "valid": 0}]) first = review_session(session)["sites"][0] assert first["fit"] is None assert all(w["raw_mean"] is None and w["status"] == "insufficient_samples" for w in first["windows"]) def test_review_refuses_mismatched_identity_and_paths_outside_session(session): path = session / "trials/trial-001/session.json" document = json.loads(path.read_text(encoding="utf-8")) document["site_id"] = "wrong-site" write_json(path, document) with pytest.raises(ValueError, match="mismatch"): review_session(session) manifest_path = session / "session.json" manifest = json.loads(manifest_path.read_text(encoding="utf-8")) manifest["trials"][0]["directory"] = "../outside" write_json(manifest_path, manifest) with pytest.raises(ValueError, match="outside"): review_session(session) def test_review_dialog_shows_saved_windows_charts_and_new_export(qt_app, session, tmp_path): from orisys_autocal.ui.session_review_dialog import SessionReviewDialog dialog = SessionReviewDialog(session) assert dialog.sites.count() == 2 assert dialog.table.rowCount() == 5 assert len(dialog.chart.series()) == 4 assert len(dialog.uv_chart.series()) == 2 dialog.sites.setCurrentIndex(1) assert "4" in dialog.summary.text() dialog.uv_chart.series()[0].clicked.emit(dialog.uv_chart.series()[0].at(0)) assert dialog.sites.currentIndex() == 0 dialog.save_button.click() assert len(list(session.glob("derived/*/review.json"))) == 1 dialog.show() qt_app.processEvents() assert dialog.grab().save(str(tmp_path / "review.png")) dialog.close()