add session browsing, calibration diagnostics and the 2D layout preset
Phase 2 of the review: browse the whole session instead of only the rolling window, diagnose a calibration from the recorded points, and provide the reviewed two-panel layout. What - SessionStore gains extent(), visible(t0, t1, max_points) and statistics(t0, t1, unit). visible() walks only the blocks that overlap the window and decimates by taking each bucket's minimum and maximum, so a spike survives decimation and browsing a long session does not copy all of it. - ForcePlotsPanel gains follow/browse modes, a whole-session overview strip with the current window, pan and wheel zoom, a rubber-band zoom toggle, a shared time cursor across all three channels, and a selection readout (duration, valid samples, per-axis peak/mean/std). - CalibrationPoint records a bounded per-axis trace, an excluded flag, a validation flag and its fit residual. CalibrationRecorder gains set_excluded/set_validation/fit; the fit is a least-squares line over the non-excluded, non-validation points with per-point residuals and an independent validation error. - The calibration table becomes the reviewed diagnostic view (reference load | FN | FX | FY | stability | state) with the residual shown next to the loaded axis, plus exclude/restore and mark-validation actions and a fit summary line. Double clicking a row opens the point's three-axis trace. - Add a layout preset: free layout or 2D mode (depth map on the left, force charts on the right, calibration folded away), persisted in preferences. Why - Browsing is a display pause, not a capture pause: the record channel keeps running while the charts show an older window. - Excluding a point removes it from the fit and from the exported calibration but keeps it on record, so the evidence for the decision is not destroyed. - The non-loaded axes are recorded but never given a reference load: their true load is unknown during a single-axis calibration, so it stays empty. Verification - shared/tests/test_record_channel.py grows to 18 cases, covering extent, peak-preserving decimation, windowed reads, inverted/empty windows, unit-aware statistics and the time origin. - tools/ui_check.py gains "session browsing" and "layout preset" sections (223 checks, 0 failures overall). - apps/studio/tests/test_ui_smoke.py covers the same invariants. Not in this change: synchronised playback of video, depth and curves. It needs a decision on how depth is stored for replay (per-frame PNG, reduced-rate PNG, or offline recomputation); the cursor plumbing (cursor_moved / set_cursor) is already in place for it.
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
"""Offscreen UI smoke tests: construction, layout adaptation, theme tokens."""
|
||||
"""Offscreen UI smoke tests: construction, layout adaptation, theme tokens."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
|
||||
import pytest
|
||||
|
||||
import ui_check
|
||||
@@ -199,7 +201,7 @@ def test_toolbar_run_button_is_labelled(make_window):
|
||||
window = _show(make_window(), (1400, 880))
|
||||
label = window.toolbar.btn_run.text()
|
||||
assert label.strip()
|
||||
assert label != "â–¶"
|
||||
assert label != "â–¶"
|
||||
window.toolbar.set_run_checked(True)
|
||||
assert window.toolbar.btn_run.text() != label
|
||||
|
||||
@@ -264,14 +266,92 @@ def test_cal_panel_records_and_lists_samples(make_window):
|
||||
cal.set_running(True)
|
||||
assert cal.btn_record.isEnabled()
|
||||
assert cal.btn_record.text()
|
||||
assert cal.table.columnCount() == 6, "the diagnostic columns changed"
|
||||
|
||||
from orisys_app_shared.force.calibration_io import CalSample
|
||||
|
||||
cal._store.upsert("fn", CalSample(weight_n=1.0, raw_mean=9.7, n=120, std=0.02))
|
||||
cal._refresh_table()
|
||||
assert cal.table.rowCount() == 1
|
||||
_add_calibration_point(window, load=1.0, raw=100.2)
|
||||
_add_calibration_point(window, load=2.0, raw=400.0) # off-trend
|
||||
_add_calibration_point(window, load=3.0, raw=300.6)
|
||||
assert cal.table.rowCount() == 3
|
||||
assert not cal.empty_hint.isVisible()
|
||||
|
||||
headers = [
|
||||
cal.table.horizontalHeaderItem(i).text() for i in range(cal.table.columnCount())
|
||||
]
|
||||
from orisys_app_shared.i18n import t
|
||||
|
||||
assert headers == [
|
||||
t("cal.col_weight"),
|
||||
t("cal.col_fn"),
|
||||
t("cal.col_fx"),
|
||||
t("cal.col_fy"),
|
||||
t("cal.col_stability"),
|
||||
t("cal.col_state"),
|
||||
]
|
||||
|
||||
|
||||
def _add_calibration_point(window, *, load: float, raw: float, samples: int = 40) -> None:
|
||||
"""Drive one point through the recorder and into the panel's table."""
|
||||
from orisys_app_shared.force.session import SessionSample
|
||||
|
||||
window.cal_recorder.start(channel="fn", reference_load_n=load, duration_s=1.0)
|
||||
for index in range(samples):
|
||||
window.cal_recorder.feed(
|
||||
SessionSample(
|
||||
frame_id=index,
|
||||
t_rel_s=index / 100.0,
|
||||
fn_raw=raw,
|
||||
fx_raw=0.4,
|
||||
fy_raw=-0.2,
|
||||
valid=True,
|
||||
)
|
||||
)
|
||||
window.cal_panel.on_record_finished(window.cal_recorder.finish_now())
|
||||
|
||||
|
||||
def test_calibration_diagnostics_drive_the_fit(make_window):
|
||||
window = _show(make_window(), (1400, 880))
|
||||
window.cal_panel.set_running(True)
|
||||
_add_calibration_point(window, load=1.0, raw=100.2)
|
||||
_add_calibration_point(window, load=2.0, raw=400.0)
|
||||
_add_calibration_point(window, load=3.0, raw=300.6)
|
||||
_add_calibration_point(window, load=5.0, raw=500.9)
|
||||
|
||||
recorder = window.cal_recorder
|
||||
fit = recorder.fit("fn")
|
||||
assert fit.usable and fit.n_points == 4
|
||||
worst = fit.max_residual_n
|
||||
assert worst > 0.05, "the off-trend point produced no residual"
|
||||
assert window.cal_panel.fit_label.text()
|
||||
|
||||
window.cal_panel.table.setCurrentCell(1, 0)
|
||||
window.cal_panel._on_toggle_excluded()
|
||||
assert recorder.fit("fn").max_residual_n < worst, "exclusion did not tighten the fit"
|
||||
assert recorder.points[1].excluded
|
||||
# An excluded point leaves the export store but stays on record.
|
||||
assert len(window.cal_panel._store.channel("fn")) == 3
|
||||
|
||||
window.cal_panel.table.setCurrentCell(2, 0)
|
||||
window.cal_panel._on_toggle_validation()
|
||||
held_out = recorder.fit("fn")
|
||||
assert held_out.validation_points == 1
|
||||
assert recorder.points[2].is_validation
|
||||
assert np.isnan(recorder.points[2].residual_n), "validation point entered the fit"
|
||||
|
||||
|
||||
def test_calibration_point_dialog_shows_three_axes(make_window):
|
||||
window = _show(make_window(), (1400, 880))
|
||||
_add_calibration_point(window, load=1.0, raw=100.2)
|
||||
point = window.cal_recorder.points[0]
|
||||
assert set(point.trace) == {"fn", "fx", "fy"}
|
||||
assert point.trace_dt_s > 0
|
||||
|
||||
from orisys_studio.ui.cal_point_dialog import CalibrationPointDialog
|
||||
|
||||
dialog = CalibrationPointDialog(point, theme="dark")
|
||||
assert dialog.windowTitle()
|
||||
assert len(dialog.plot_widget.ci.items) == 3, "not one trace per axis"
|
||||
dialog.close()
|
||||
|
||||
|
||||
def test_force_plots_render_demo_data(make_window):
|
||||
window = _show(make_window(), (1440, 880))
|
||||
@@ -516,3 +596,4 @@ def test_recording_leaves_unavailable_values_empty(make_window, record_root):
|
||||
_finish(window, stuck)
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user