spacr.qt.screens.plate_view

Plate Viewer — any measurement as a plate heatmap, with the edge-effect test sitting right next to it.

The heatmap is the delivery mechanism; the statistic beside it is the point. A screen hit in row A or column 24 is far more likely to be an evaporation artefact than biology, and until now spaCR gave nobody a way to notice that before the follow-up experiment failed.

Layout:

┌───────────────────────────────────────────────────────────────────┐
│ /data/plate1/measurements/measurements.db   [DB…] [Run folder…]   │
│ Table [cell ▾] Value [cell_area ▾] Plate [plate1 ▾] [mean ▾]      │
│ Colour [2–98 % ▾]  Min objects/well [20]            [Render]      │
├───────────────────────────────────┬───────────────────────────────┤
│      1  2  3  4 …             24  │ Plate plate1 — mean cell_area │
│  A  ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓  │ 384-well, 384 wells tested    │
│  B  ▓▓ ░░ ░░ ░░ ░░ ░░ ░░ ░░ ░░ ▓▓  │                               │
│  C  ▓▓ ░░ ▒▒ ▒▒ ▒▒ ▒▒ ▒▒ ░░ ░░ ▓▓  │ Edge effect: the outer ring   │
│  …                                │ reads +31.2 % vs the interior │
│  P  ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓ ▓▓  │ (δ = 0.78, p < 1e-4).         │
├───────────────────────────────────┴───────────────────────────────┤
│ C07 · 142 objects · mean cell_area = 1204.53 · ring 2 (interior)  │
│ scale 812.4 → 1655.9 (viridis, 2–98 %)     [Export well grid CSV…]│
└───────────────────────────────────────────────────────────────────┘

Design notes:

  • Read-only, structurally. Every query goes through spacr.plate_qc. It opens the file with file:…?mode=ro and PRAGMA query_only = ON, the same approach the Database Browser takes.

  • The statistics live outside the GUI. All of the analysis is in spacr.plate_qc, which imports neither torch nor cellpose, so it is testable headless and the screen stays a view.

  • An empty well is drawn as empty. Wells with no objects, or fewer than min_count, are hatched — never coloured as if they measured zero. The count of dropped wells is on screen, because a heatmap quietly missing a third of its wells looks exactly like data.

  • Only the columns needed are read. A spaCR feature table is 500 columns wide; the query pulls the well identifiers plus the one measurement being plotted.

  • No modal dialogs on any error path. “That column isn’t in this table”, “that file isn’t a database”, “this plate has no interior” — all of it lands in the inline status label. A QMessageBox would hang a headless run.

Classes

PlateGridWidget

A plate drawn as coloured wells, with row letters and column numbers.

PlateViewScreen

Plate heatmap + edge-effect QC for a spaCR measurements database.

Module Contents

class spacr.qt.screens.plate_view.PlateGridWidget(parent=None)[source]

Bases: PySide6.QtWidgets.QWidget

A plate drawn as coloured wells, with row letters and column numbers.

Painted directly rather than embedded as a matplotlib canvas for one reason that matters: a click has to map back to a well, exactly, and a hand-rolled grid gives that for free instead of via a coordinate round-trip through a figure’s data transform.

Parameters:

parent – parent widget.

Variables:

well_clicked – emitted with (row_index, column_index) (1-based) whenever the user clicks inside the grid.

Create an empty plate grid.

Parameters:

parent – parent widget, or None.

cell_rect(row_index: int, column_index: int) → PySide6.QtCore.QRectF[source]

Return the rectangle a well occupies, in widget coordinates.

Public because it is what makes a click testable: a test can aim at the centre of cell_rect(3, 7) instead of guessing at pixel arithmetic that would have to be kept in sync by hand.

Parameters:
  • row_index – 1-based plate row.

  • column_index – 1-based plate column.

clear() → None[source]

Drop the current plate and repaint empty.

grid_size() → Tuple[int, int][source]

Return (n_rows, n_cols) of the grid currently drawn.

has_plate() → bool[source]

True when there is something to paint.

mousePressEvent(event) → None[source]

Select the well under the pointer.

Parameters:

event – the Qt mouse event.

paintEvent(event) → None[source]

Draw the plate: one cell per well, coloured by its value.

Parameters:

event – the Qt paint event.

select(row_index: int | None, column_index: int | None = None) → None[source]

Highlight a well (or clear the highlight with None).

Parameters:
  • row_index – 1-based plate row, or None to clear.

  • column_index – 1-based plate column; None also clears.

selected_well() → Tuple[int, int] | None[source]

The highlighted (row_index, column_index), if any.

set_placeholder(text: str) → None[source]

Text shown when there is no plate to draw.

Parameters:

text – the message painted in place of the grid.

set_plate(layout: pandas.DataFrame | None, vmin: float = 0.0, vmax: float = 1.0, cmap: str = DEFAULT_CMAP, n_rows: int | None = None, n_cols: int | None = None) → None[source]

Show layout (a spacr.plate_qc.plate_layout() frame).

Parameters:
  • layout – tidy well frame, or None to clear.

  • vmin – value mapped to the bottom of the colormap.

  • vmax – value mapped to the top.

  • cmap – matplotlib colormap name.

  • n_rows – nominal plate rows; taken from layout.attrs when omitted.

  • n_cols – nominal plate columns; likewise.

well_at(point) → Tuple[int, int] | None[source]

Map a widget-coordinate point to a 1-based (row, column).

Parameters:

point – QPoint/QPointF in widget coordinates.

Returns:

the well under the point, or None when the point is in the margins or off the grid.

well_count(row_index: int, column_index: int) → int[source]

Objects behind a well; 0 when nothing survived filtering.

Parameters:
  • row_index – 1-based plate row.

  • column_index – 1-based plate column.

well_value(row_index: int, column_index: int) → float | None[source]

Value behind a well, or None when the well is blank.

Parameters:
  • row_index – 1-based plate row.

  • column_index – 1-based plate column.

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

Bases: spacr.qt.linked_selection.LinkedView, PySide6.QtWidgets.QWidget

Plate heatmap + edge-effect QC for a spaCR measurements database.

Joins the shared population through LinkedView: narrowing the Local Data Filter anywhere narrows the heatmap too. It subscribes for the filter only — a selection highlights individual objects, and a well is an aggregate of many, so there is nothing here for one to light up.

Parameters:
  • parent – parent widget.

  • threaded – run database work on a worker thread (the default). Tests pass False for deterministic, synchronous behaviour.

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 screen, arm its drop zone and join the shared selection.

Parameters:
  • parent – parent widget, or None.

  • threaded – run reads and aggregations on a worker thread. Set False in tests, which also removes the recompute coalescing timer, so an option change recomputes on the spot.

active_jobs() → int[source]

How many worker threads are still winding down.

closeEvent(event)[source]

Stop listening to the process-wide filter before going away.

The except is the one thing LinkedView.unlink_selection() does not do for us: it is flag-guarded, so a double close is already silent, but during interpreter teardown the C++ side of the process-wide LinkedSelection can be gone before this widget’s closeEvent runs, and PySide raises on the disconnect then.

Parameters:

event – the close event; passed on unchanged to the base class.

current_table() → str[source]

The table currently selected.

current_value_column() → str[source]

The measurement column currently selected.

export_csv(out_path: str) → bool[source]

Write the well grid to out_path as CSV.

The tidy grid is exported — one row per well with its object count, value, ring index and edge flag — because that is what anybody re-analysing the plate outside spaCR needs.

Parameters:

out_path – destination file.

Returns:

True on success; on failure the reason lands in the inline status label and last_error.

is_busy() → bool[source]

True while a database job is in flight.

on_linked_filter_changed(data_filter: spacr.selection.DataFilter) → None[source]

Re-draw for a new filter, without re-reading the database.

Silent when nothing is loaded: a filter change is not a reason to show an error on a screen the user has not pointed at a database yet.

Parameters:

data_filter – the filter that was published; not read here, as the redraw reads the link’s current filter itself.

open_database(path: str) → bool[source]

Open path read-only and list the tables it holds.

Parameters:

path – a measurements.db or a run folder containing one.

Returns:

True when the database opened.

recompute() → bool[source]

Rebuild the well grid + report from the frame already in memory.

The aggregation runs off the GUI thread. pqc.plate_layout is a full-frame groupby and detect_edge_effect a second pass over the result: 487 ms on a real measurement table, measured, and it fired on every tick of the min-objects spin box. Dragging that box was a sequence of half-second freezes.

THE CONTRACT. recompute() returns a bool that a caller reads, and it now means “a plate was drawn or is being drawn” rather than “a plate was drawn”. The refusals – no frame yet, and a failed aggregation – are unchanged and still synchronous, because the first does no work and the second is reported by the completion handler in exactly the place it was reported before. So the only caller-visible difference is that True may arrive before the grid is painted.

That was made safe rather than assumed safe: threaded=False (which every test in test_plate_view.py and test_plate_view_linked_filter.py constructs the screen with) runs the job inline through _run_job(), so those callers still get the old meaning exactly. A caller that needs to know the grid is up waits on active_jobs(), as render_plate’s callers already do – render_plate has returned “started, not finished” since the database read was threaded, and this is the same promise.

Returns:

True when a plate was drawn, or a draw was started.

render_plate() → bool[source]

Read the chosen measurement and draw the plate.

The database is only touched when the table or measurement changed; otherwise this is the same recompute the option controls trigger.

Returns:

True when the render was started/completed.

report_text() → str[source]

The rendered edge-effect report (test/introspection helper).

select_well(row_index: int, column_index: int) → str[source]

Report what is behind a well, and return the text shown.

Parameters:
  • row_index – 1-based plate row.

  • column_index – 1-based plate column.

Returns:

the readout line.

set_table(table: str) → None[source]

Select table and reload its numeric columns.

Parameters:

table – name of a table in the table box, converted with str.

set_value_column(column: str) → None[source]

Select column as the measurement to draw.

A name that is not in the current table is accepted and selected anyway — it is exactly what happens when a user picks a column and then switches to a table that does not have it. Rendering then fails with an inline explanation rather than the combo silently snapping back to something the user did not choose.

Parameters:

column – the measurement column name, converted with str.

status_text() → str[source]

Current inline status message (test/introspection helper).

well_info_text() → str[source]

The per-well readout line (test/introspection helper).

Nested helpers

PlateViewScreen._on_table_changed._done(columns: List[str]) → None

Offer the columns, guarded so refilling does not re-trigger this.

spacr/qt/screens/plate_view.py:1038

PlateViewScreen._on_table_changed._job()

Read the table’s numeric columns. Off the GUI thread.

spacr/qt/screens/plate_view.py:1034

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

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

The payload is how a value crosses back from the worker: a return would be swallowed by the runner.

spacr/qt/screens/plate_view.py:1378

PlateViewScreen.recompute._job()

Recompute the plate summary. Off the GUI thread.

spacr/qt/screens/plate_view.py:1185

PlateViewScreen.render_plate._done(frame: pd.DataFrame) → None

Keep the frame and draw it, remembering what it was keyed on.

spacr/qt/screens/plate_view.py:1116

PlateViewScreen.render_plate._job()

Load the plate frame. Off the GUI thread.

spacr/qt/screens/plate_view.py:1112