补齐 Autocal 测点质量与独立 trial 验证
按已确认窗口比较重复性、加载卸载差异和零载恢复,并逐 trial 留出验证。55 项定向测试、已录视频复核闭环及代码文档检查通过。
This commit is contained in:
@@ -210,3 +210,114 @@ def test_baseline_image_matches_first_reviewed_zero_frame(session):
|
||||
assert baseline["status"] == "saved" and baseline["segment_frame"] == 1
|
||||
image = cv2.imread(str(saved.parent / baseline["image"]))
|
||||
assert 10 < image.mean() < 30
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def repeated_session(session):
|
||||
import shutil
|
||||
|
||||
source = session / "trials/trial-001"
|
||||
destination = session / "trials/trial-004"
|
||||
shutil.copytree(source, destination)
|
||||
metadata = json.loads((destination / "session.json").read_text(encoding="utf-8"))
|
||||
metadata["trial_id"] = "trial-004"
|
||||
write_json(destination / "session.json", metadata)
|
||||
with (destination / "forces.csv").open(encoding="utf-8") as handle:
|
||||
forces = list(csv.DictReader(handle))
|
||||
for row in forces:
|
||||
row["FN_raw"] = float(row["FN_raw"]) + 2
|
||||
_csv(destination / "forces.csv", forces)
|
||||
manifest = json.loads((session / "session.json").read_text(encoding="utf-8"))
|
||||
manifest["trials"].append({"trial_id": "trial-004", "site_id": "site-001",
|
||||
"status": "complete", "directory": "trials/trial-004"})
|
||||
write_json(session / "session.json", manifest)
|
||||
result = review_session(session)
|
||||
selections = {f"{w['trial_id']}:{w['step_index']}": {
|
||||
"t_start_host_mono_ns": w["t_start_host_mono_ns"],
|
||||
"t_end_host_mono_ns": w["t_end_host_mono_ns"], "stability_verified": True,
|
||||
} for site in result["sites"] for w in site["windows"]}
|
||||
return session, selections
|
||||
|
||||
|
||||
def test_quality_uses_distinct_trials_and_never_trains_on_held_out_data(repeated_session):
|
||||
import math
|
||||
from orisys_autocal.session_review import load_review
|
||||
|
||||
directory, selections = repeated_session
|
||||
result = review_session(directory, selections=selections)
|
||||
quality = result["sites"][0]["quality"]
|
||||
assert quality["reference_alignment_possible"] is False
|
||||
assert all(row["trials"] == 2 and row["raw_range"] == 2
|
||||
and row["raw_std_between_trials"] == pytest.approx(math.sqrt(2))
|
||||
for row in quality["repeatability"])
|
||||
assert len(quality["hysteresis"]) == 2
|
||||
assert all(row["unload_minus_load_raw"] == 1 for row in quality["hysteresis"])
|
||||
assert all(row["final_minus_initial_raw"] == 0 for row in quality["zero_return"])
|
||||
validation = quality["validation"]
|
||||
assert validation["evaluated_windows"] == 10
|
||||
assert validation["rmse_n"] == pytest.approx(math.sqrt(1.05))
|
||||
assert validation["mae_n"] == pytest.approx(1)
|
||||
assert validation["max_abs_error_n"] == pytest.approx(1.5)
|
||||
assert validation["outside_training_raw_range"] == 3
|
||||
for fold in validation["folds"]:
|
||||
assert fold["held_out_trial"] not in fold["training_trials"]
|
||||
assert len(fold["training_trials"]) == 1
|
||||
assert validation["folds"][0]["fit"]["offset_n"] == pytest.approx(-1.5)
|
||||
assert validation["folds"][1]["fit"]["offset_n"] == pytest.approx(-.5)
|
||||
second = result["sites"][1]["quality"]
|
||||
assert second["validation"]["rmse_n"] is None
|
||||
assert all(row["raw_range"] is None for row in second["repeatability"])
|
||||
saved = save_review(result)
|
||||
assert load_review(directory, saved)["sites"][0]["quality"] == quality
|
||||
|
||||
|
||||
def test_quality_ignores_unconfirmed_data_and_unmatched_loads(repeated_session):
|
||||
directory, selections = repeated_session
|
||||
result = review_session(directory)
|
||||
assert result["sites"][0]["quality"]["repeatability"] == []
|
||||
assert result["sites"][0]["quality"]["validation"]["rmse_n"] is None
|
||||
# Keep only one confirmed loading level in the other trial: no usable fit.
|
||||
for key in list(selections):
|
||||
if key.startswith("trial-004:") and key != "trial-004:1":
|
||||
selections[key]["stability_verified"] = False
|
||||
result = review_session(directory, selections=selections)
|
||||
quality = result["sites"][0]["quality"]
|
||||
assert quality["validation"]["folds"][0]["status"] == "insufficient_training"
|
||||
assert len(quality["hysteresis"]) == 1
|
||||
path = directory / "trials/trial-001/reference.csv"
|
||||
with path.open(encoding="utf-8") as handle:
|
||||
rows = list(csv.DictReader(handle))
|
||||
rows[3]["F_ref_normal_N"] = "5.01"
|
||||
_csv(path, rows)
|
||||
quality = review_session(directory, selections=selections)["sites"][0]["quality"]
|
||||
assert quality["hysteresis"] == []
|
||||
|
||||
|
||||
def test_quality_dialog_is_reachable_and_shows_validation_tables(qt_app, repeated_session, monkeypatch, tmp_path):
|
||||
from PySide6.QtWidgets import QTableWidget
|
||||
from orisys_autocal.ui.session_review_dialog import SessionReviewDialog
|
||||
from orisys_autocal.ui.quality_review_dialog import QualityReviewDialog
|
||||
|
||||
directory, selections = repeated_session
|
||||
review = SessionReviewDialog(directory)
|
||||
review.review = review_session(directory, selections=selections)
|
||||
review._show_site()
|
||||
opened = []
|
||||
monkeypatch.setattr(QualityReviewDialog, "exec", lambda dialog: opened.append(dialog))
|
||||
review.quality_button.click()
|
||||
assert len(opened) == 1
|
||||
dialog = opened[0]
|
||||
assert dialog.tabs.count() == 4
|
||||
repeat = dialog.findChild(QTableWidget, "quality_repeatability")
|
||||
validation = dialog.findChild(QTableWidget, "quality_validation")
|
||||
assert repeat.rowCount() == 5 and validation.rowCount() == 10
|
||||
assert validation.item(0, 0).text() == "trial-001"
|
||||
assert validation.item(0, 1).text() == "trial-004"
|
||||
dialog.show()
|
||||
qt_app.processEvents()
|
||||
assert dialog.grab().save(str(tmp_path / "repeatability.png"))
|
||||
dialog.tabs.setCurrentIndex(3)
|
||||
qt_app.processEvents()
|
||||
assert dialog.grab().save(str(tmp_path / "validation.png"))
|
||||
dialog.close()
|
||||
review.close()
|
||||
|
||||
Reference in New Issue
Block a user