spacr.qt.screens.train_compare

Training Runs — several runs’ curves on one axis, with the settings diffed.

The question this screen exists to answer is “why is run B better than run A”, which today is answered by opening two folders of PDFs in one window, two settings CSVs in another, and holding the difference in your head.

Layout:

┌──────────────────────────────────────────────────────────────────────┐
│ /data/screen1/model                     [Choose folder…]  [Scan]     │
├──────────────────────┬───────────────────────────────────────────────┤
│ Runs found (4)       │            ┌───────────────────────────┐      │
│ ☑ maxvit_t/…/ep_25   │            │   accuracy, 5 series      │      │
│   25 epochs · tr+val │            │   ╱‾‾‾‾  B val            │      │
│ ☑ maxvit_t/…/ep_10   │            │  ╱ ─ ─   A val            │      │
│ ☐ resnet50/…/ep_8    │            └───────────────────────────┘      │
│   8 ep x 3 folds     ├───────────────────────────────────────────────┤
│ ☐ maxvit_t/…/ep_3 !  │ 2 settings changed · 1 env drift · 0 drift    │
│                      │ bucket   setting        A        B            │
│ Metric [accuracy ▾]  │ changed  learning_rate  1e-4     1e-3         │
│ Folds  [per fold ▾]  │ changed  batch_size     64       32           │
│ [Overlay selected]   │ env      n_jobs         30       8            │
├──────────────────────┴───────────────────────────────────────────────┤
│ ! maxvit_t/…/epochs_3: no per-epoch curves in this folder            │
│ Clicked: maxvit_t/…/epochs_25 · val — best 0.87 @ 18, last 0.85 @ 25 │
└──────────────────────────────────────────────────────────────────────┘

Design notes:

  • Discovery runs off the GUI thread. A scan walks a model tree and parses every progress CSV under it, which on a real screen is hundreds of files. It goes through spacr.qt.bridge.make_thread() like every other spaCR job; tests pass threaded=False, which runs the same code inline. Drawing stays on the GUI thread — by then the data is already in memory.

  • No modal dialogs on any error path. A folder with no runs, a run with no curves, a metric nothing logged — all of it lands in the inline status and problem labels. A QMessageBox hangs a headless run.

  • Broken runs are listed, not hidden. A folder holding checkpoints but no train.csv still appears, marked, with its note in the problem line. A scan that silently drops the folder you were looking for is worse than one that says what is wrong with it.

  • The diff is bucketed, never flat. It renders spacr.train_compare.diff_settings(), which reuses the provenance bucketing from spacr.run_journal: environment drift (paths, hosts, worker counts) is shown in its own bucket instead of being counted as something the user changed, and schema drift is summarised. When two runs match, the table says “no differences” in words rather than going blank.

  • Every series says run · split · fold. Clicking a line names its run, folder and both its best and last epoch, because a legend that only carries the run id invites reading a train curve as a held-out result.

Classes

TrainCompareScreen

Compare training runs: overlaid curves plus the bucketed settings diff.

Functions

panel_canvas_class()

The FigureCanvasQTAgg subclass that sits ON the page.

Module Contents

class spacr.qt.screens.train_compare.TrainCompareScreen(parent=None, threaded: bool = True)[source]

Bases: PySide6.QtWidgets.QWidget

Compare training runs: overlaid curves plus the bucketed settings diff.

Parameters:
  • threaded – discover and load runs on a worker thread (the default). Tests pass False for deterministic, synchronous behaviour.

  • parent – parent widget; ownership only.

Variables:

last_error – text of the most recent failure, "" when the last operation succeeded. Errors are only ever reported here and in the inline status label — never in a modal dialog.

Build the run list, the curve plot and the settings diff.

Parameters:
  • parent – parent widget.

  • threaded – whether the scan runs on a worker.

active_jobs() → int[source]

How many background jobs this screen is running.

Returns:

the job count.

available_metrics() → List[str][source]

Which metrics the picker is currently offering.

Returns:

the metric names, in picker order.

closeEvent(event)[source]

Stop background work before going away.

Parameters:

event – the Qt close event.

comparison() → spacr.train_compare.Comparison | None[source]

The last comparison computed, if any.

Returns:

the comparison, or None before one has been run.

diff_headers() → List[str][source]

The settings-diff table’s column headers.

Returns:

the headers, in column order.

diff_rows() → List[List[str]][source]

The settings-diff table as it reads on screen.

Returns:

one row per differing setting.

figure()[source]

The live matplotlib figure (one per screen, reused every overlay).

fold_mode() → str[source]

How the cross-validation folds are being combined.

Returns:

the mode’s value.

identify_series(label: str) → str[source]

Name the run behind a series label and report it inline.

Parameters:

label – legend label of a plotted series, looked up in the figure’s spacr_series_by_label mapping.

Returns:

the description shown, or '' when the label is unknown.

is_busy() → bool[source]

Whether anything is still running.

Returns:

True while work is outstanding.

overlay() → bool[source]

Compare the ticked runs: draw their curves and fill the diff table.

Everything the drawing needs is already in memory after scan(), so this runs on the GUI thread.

Returns:

True when a comparison was produced.

picked_text() → str[source]

Description of the last clicked series.

problem_text() → str[source]

Every note from every discovered run, one per line.

root() → str[source]

The folder being scanned for training runs.

Returns:

the root path, as text.

run_ids() → List[str][source]

The identifiers of every run found.

Returns:

the run ids, in scan order.

run_rows() → List[str][source]

The run list exactly as it reads on screen.

Read off the WIDGET rather than rebuilt from the runs, so a test checks what the user sees rather than what the data says.

Returns:

one string per visible row.

runs() → List[spacr.train_compare.TrainingRun][source]

Every run the last scan found.

A LIST COPY, so a caller cannot reorder this screen’s runs by mutating what it was handed.

Returns:

the runs.

scan(root: Any) → bool[source]

Discover training runs under root and list them.

Runs off the GUI thread unless the screen was built with threaded=False. Every failure is reported inline.

Parameters:

root – folder to walk.

Returns:

True when the scan started (or, unthreaded, succeeded).

select_runs(run_ids: Sequence[str]) → bool[source]

Tick exactly these run ids. Unknown ids are reported inline.

Parameters:

run_ids – run ids to tick; every other run is unticked. Ids that match no listed run are reported and make the call return False.

selected_metric() → str[source]

Return the metric selected for the comparison plot.

This deliberately must not be named metric. QWidget inherits QPaintDevice.metric(PaintDeviceMetric), which Qt calls while laying out and painting the widget. A no-argument Python override here used to raise during show() and could take the whole GUI down.

selected_run_ids() → List[str][source]

The runs the user has ticked for comparison.

Returns:

the selected run ids.

series_labels() → List[str][source]

Labels of the currently drawn series, in draw order.

set_fold_mode(mode: str) → bool[source]

Choose how folds are combined.

Parameters:

mode – the mode’s value.

Returns:

True when the mode exists and was selected.

set_metric(name: str) → bool[source]

Pick the metric to draw; re-draws when a comparison exists.

Parameters:

name – metric name; it must be one of available_metrics(), otherwise an error is shown and False returned.

status_text() → str[source]

Whatever the status line is telling the user.

Returns:

the status text.

summary_text() → str[source]

The line above the diff table.

spacr.qt.screens.train_compare.panel_canvas_class()[source]

The FigureCanvasQTAgg subclass that sits ON the page.

FigureCanvasQT.__init__ sets WA_OpaquePaintEvent and a white palette, and the figure carries a solid facecolor. Three opaque things stacked: the area right of “Runs found” was a flat dark rectangle whatever the page-opacity slider said, with square corners where every other container on the page has rounded ones.

QSS cannot fix that — a WA_OpaquePaintEvent widget never lets the sheet’s background through, and a stylesheet cannot round a canvas matplotlib draws edge to edge. So the panel is drawn in paintEvent, underneath the figure, and the figure’s own patch is made fully transparent so the panel is what shows.

Built lazily and cached: no screen in this package imports matplotlib at module scope, and subclassing its Qt backend would do exactly that.

Nested helpers

TrainCompareScreen._run_job._job(payload: Dict[str, Any]) → None

Call the wrapped function, stashing its result in the payload.

spacr/qt/screens/train_compare.py:960

TrainCompareScreen.scan._job()

Find the training runs under a folder. Off the GUI thread.

spacr/qt/screens/train_compare.py:477

panel_canvas_class.PanelCanvas.__init__(self, figure)

Build the canvas non-opaque, so the panel shows through it.

spacr/qt/screens/train_compare.py:166

panel_canvas_class.PanelCanvas.paintEvent(self, event)

Draw the panel, then let matplotlib draw over it.

spacr/qt/screens/train_compare.py:176