spacr.qt.widgets.motility_preview

Motility live preview — tracks, velocity, straightness, infection split.

Point it at a plate folder holding merged/*.npy (or at the merged folder itself) and it rebuilds, for a handful of frames, exactly what spacr.timelapse.automated_motility_assay() computes: per-track velocity and straightness from the cell-mask centroids, split by infection state read off the pathogen mask.

Two things this preview refuses to fudge.

Units. The assay converts pixels per frame into physical units with factor = (1 / pixels_per_um) * (60 / seconds_per_frame) and only when both are known; otherwise it reports px/frame. A preview that printed “velocity 4.2” while the user was thinking in µm/s would be worse than printing nothing, so the calibration fields start unset, every velocity carries its unit, and while the calibration is unknown the panel says so in words instead of quietly borrowing the 1.78 px/µm default.

Short tracks. Mean step length and straightness are wildly unstable on a three-point track — straightness is exactly 1.0 for any two-point track, no matter what the cell did. So the track-length distribution is drawn beside the velocity plot with the cutoff marked, and the minimum length is a live setting: it is the knob that actually decides whether the numbers mean anything.

The expensive half (reading merged arrays and extracting centroids) runs once in a worker thread and is cached as a point table. Every metric setting — minimum length, max displacement, straightness threshold, and both calibration fields — recomputes from that cached table on the GUI thread, instantly.

Exceptions

MotilityInputError

The chosen folder is not a usable motility input, with the reason.

Classes

Calibration

Pixel size and frame interval, either of which may be unknown.

MotilityPreviewPanel

Interactive motility preview — Motility Assay module.

MotilityRequest

One read of merged arrays into a point table.

MotilitySummary

Everything the panel pins under the plots.

Functions

build_motility_preview_card(host)

Build the Motility preview card + panel pair.

build_point_table(merged_dir, metas, n_channels, ...)

Extract one row per object per frame from a group of merged arrays.

default_plane_layout(→ Tuple[int, Optional[int]])

Guess (tracked mask plane, pathogen mask plane) from the plane count.

group_merged_files(→ Dict[tuple, List[dict]])

Group merged/*.npy by (plate, well, field) and sort each by time.

read_plane_count(→ int)

Planes held by one merged array. No Qt: worker-safe.

render_motility_figure(→ numpy.ndarray)

Render the three panels the user tunes against, as an RGB array.

resolve_merged_dir(→ str)

Return the merged directory for path.

run_motility_pass(req)

Build the cached point table for one (plate, well, field) group.

scan_plate_payload(→ Dict[str, Any])

Resolve a plate's merged folder and group it. No Qt: worker-safe.

smooth_and_filter_tracks(points, max_displacement)

Apply the assay's centroid QC: fix teleports, drop impossible tracks.

summarise(→ MotilitySummary)

Reduce a per-track table to the numbers shown under the plots.

track_metrics(points, calibration[, min_length])

Per-track velocity and straightness, using the assay's own formulae.

Module Contents

exception spacr.qt.widgets.motility_preview.MotilityInputError[source]

Bases: ValueError

The chosen folder is not a usable motility input, with the reason.

Initialize self. See help(type(self)) for accurate signature.

class spacr.qt.widgets.motility_preview.Calibration[source]

Pixel size and frame interval, either of which may be unknown.

Variables:
  • pixels_per_um – image scale in px/µm, or None when unset.

  • seconds_per_frame – frame interval in seconds, or None.

caveat() → str[source]

The sentence shown when the calibration is incomplete.

property factor: float[source]

Multiplier from px/frame to unit.

Identical to the assay’s own conversion, so a preview number and a run number are the same number.

property known: bool[source]

Whether physical units can be reported at all.

property unit: str[source]

"µm/min" when calibrated, else "px/frame".

class spacr.qt.widgets.motility_preview.MotilityPreviewPanel(parent=None, *, threaded: bool = True)[source]

Bases: spacr.qt.widgets.preview_contract.LivePreviewContract, PySide6.QtWidgets.QWidget

Interactive motility preview — Motility Assay module.

Same contract as LivePreviewPanel, and the shared half is the same code: standalone QWidget, QThread worker emitting over signals, LivePreviewContract for the run/cancel/status protocol, set_propagate_callback() to push tuned values back into the main settings panel, and a build_*_card factory.

Parameters:
  • parent – parent widget.

  • threaded – whether the panel’s jobs run off the GUI thread. False runs each one inline, emitting the same signals in the same order, so a test can drive the panel synchronously without the behaviour diverging.

Build the motility preview panel.

Two job runners, and the separation is load-bearing twice: the pending work on the main one is what reports “a plate is still being scanned”, and a plane-count read is not a plate scan; and the second is marked not user-visible so the Home run banner does not announce a read the user never started.

Parameters:
  • parent – parent widget, or None.

  • threaded – run scans and reads on worker threads. Set False in tests: each job then runs inline, emitting the same signals in the same order, so the panel can be driven synchronously without the behaviour diverging.

apply_settings(settings: dict) → None[source]

Seed the preview from the main Motility settings dict.

Parameters:

settings – the Motility settings dict. tracked_object, max_displacement, straightness_threshold, drop_straight_tracks, channels, pixels_per_um and seconds_per_frame are read when present; errors are logged, not raised.

calibration() → Calibration[source]

The current calibration, with unset fields as None.

closeEvent(event)[source]

Let a running read finish before the widget is torn down.

A QThread collected while running aborts the process; the worker outlives the emit that produced its result by a few instructions.

Parameters:

event – the close event, passed on to the base class after any running worker has been waited for (up to five seconds each).

current_params() → dict[source]

Snapshot for tests + external callers.

display_channel() → int | None[source]

Merged-array plane the preview reads objects from.

dragEnterEvent(event)[source]

Accept a drag carrying a tracked timelapse to measure motility from.

Parameters:

event – the Qt drag event.

dragMoveEvent(event)[source]

Keep accepting while a tracked timelapse to measure motility from stays over the panel.

Parameters:

event – the Qt drag event.

dropEvent(event)[source]

Take the dropped input and preview it.

Parameters:

event – the Qt drop event.

load_folder(path) → bool[source]

Synchronously open a plate (or merged) folder.

For programmatic callers and tests, mirroring LivePreviewPanel.load_image. The GUI uses load_folder_async().

Parameters:

path – a plate or merged folder, scanned with scan_plate_payload(); a failure or a plate with no time series is shown in the status line.

load_folder_async(path) → bool[source]

Scan a plate on a worker, then install it on the GUI thread.

Both GUI entry points – the drop handler and the Choose-plate dialog – come through here.

Parameters:

path – a plate or merged folder; None or an empty path submits nothing and returns False.

Returns:

True when a job was submitted.

propagate_settings() → None[source]

Push the current settings to the main panel, if wired.

recompute() → None[source]

Re-score the cached point table. No file is re-read.

run_preview() → None[source]

Read the merged arrays into the cached point table, then score.

The guard, the refusals and the busy state are the shared ones — see LivePreviewContract.

sample_note() → str[source]

The sentence stating this preview is a sample of N of M sets.

set_propagate_callback(cb) → None[source]

Register a callback(dict) used to push tuned settings back.

Parameters:

cb – callable taking one settings dict, or None to push nothing.

settings_for_propagation() → dict[source]

Map the preview’s widgets onto real Motility Assay setting keys.

pixels_per_um / seconds_per_frame are only propagated when the user actually set them — pushing a fabricated calibration into the run would be exactly the mistake this panel exists to prevent.

shutdown() → None[source]

Abandon anything in flight and leave no QThread behind.

BOTH runners – the plate scan and the plane-count read. Qt aborts the process when a running QThread is destroyed, so a runner left out of here is a crash at teardown rather than a leak.

class spacr.qt.widgets.motility_preview.MotilityRequest[source]

One read of merged arrays into a point table.

class spacr.qt.widgets.motility_preview.MotilitySummary[source]

Everything the panel pins under the plots.

summary() → str[source]

One monospace block: counts, then velocities with their unit.

spacr.qt.widgets.motility_preview.build_motility_preview_card(host)[source]

Build the Motility preview card + panel pair.

Mirrors spacr.qt.screens.hyperparam.build_hyperparam_card: returns the pair without adding it to any layout.

Parameters:

host – the AppScreen asking for the card.

Returns:

(panel, card).

spacr.qt.widgets.motility_preview.build_point_table(merged_dir: str, metas: List[dict], n_channels: int, tracked_plane: int, pathogen_plane: int | None, max_frames: int = 12)[source]

Extract one row per object per frame from a group of merged arrays.

Loads each .npy memory-mapped and touches only the two planes it needs, so a 10-plane 2048² merged array costs two planes of I/O, not ten. Objects keep their mask label as cellID — merged arrays written by the Timelapse module are already relabelled by track id, which is the same assumption the assay makes.

Parameters:
  • merged_dir – folder holding the merged .npy arrays.

  • metas – parsed file names of one (plate, well, field) group in time order; each supplies filename, plateID, wellID and fieldID, and its position becomes frame.

  • n_channels – number of intensity channels, used to orient each array.

  • tracked_plane – index of the label plane whose objects are tracked.

  • pathogen_plane – index of the pathogen mask plane; an object overlapping it is infected. None marks every object uninfected.

Returns:

DataFrame with plateID, wellID, fieldID, cellID, frame, x, y, area and infected.

spacr.qt.widgets.motility_preview.default_plane_layout(n_planes: int, n_channels: int) → Tuple[int, int | None][source]

Guess (tracked mask plane, pathogen mask plane) from the plane count.

Mirrors the layout spacr.timelapse._load_masks_from_merged() documents: intensity channels first, then the cell mask, then optionally a nucleus mask, then optionally the pathogen mask.

Parameters:
  • n_planes – number of planes in a merged array.

  • n_channels – number of intensity channels stored before the mask planes; values below 1 count as 1.

Returns:

(cell_plane, pathogen_plane_or_None).

spacr.qt.widgets.motility_preview.group_merged_files(merged_dir: str) → Dict[tuple, List[dict]][source]

Group merged/*.npy by (plate, well, field) and sort each by time.

Shares the lightweight parser behind spacr.timelapse._parse_merged_filename(), so grouping agrees with the assay without importing plotting or model dependencies.

Parameters:

merged_dir – folder of merged .npy arrays; only groups with at least two time points are returned.

spacr.qt.widgets.motility_preview.read_plane_count(merged_dir: str, filename: str) → int[source]

Planes held by one merged array. No Qt: worker-safe.

np.load(..., mmap_mode="r") sounds free and is not. Before it maps anything it OPENS the file and reads the .npy header, so it is a filesystem round trip on a path the user supplied – and an open is no cheaper than the stat that started this exercise. Measured on one workstation, one stat under /nas_mnt – an autofs mount whose share was asleep – had not returned after twenty seconds.

Parameters:
  • merged_dir – the merged folder holding the array.

  • filename – the array’s basename inside it.

Returns:

the plane count, or 0 when the array cannot be read. Zero is the same soft failure the two inline except branches used to give: the spinners keep the values they had and the channel dropdown empties, rather than the panel raising at the user.

spacr.qt.widgets.motility_preview.render_motility_figure(points, tracks, calibration: Calibration, min_length: int, straightness_threshold: float, width_px: int = 1180, height_px: int = 380) → numpy.ndarray[source]

Render the three panels the user tunes against, as an RGB array.

  1. Tracks, drawn from the origin so paths are comparable, coloured by infection state.

  2. Track-length distribution, with the minimum-length cutoff drawn on it — this is the plot that says whether the velocity numbers can be trusted.

  3. Velocity and straightness, split by infection state, with the unit in the axis label so px/frame is never mistaken for µm/s.

Parameters:
  • points – point table as returned by build_point_table(), one row per object per frame; drawn as tracks from the origin.

  • tracks – per-track table as returned by track_metrics(), for the length histogram and the velocity panel.

  • calibration – supplies the velocity axis unit; an uncalibrated one is labelled px/frame on the plot.

  • min_length – minimum track length in frames, drawn as the cutoff on the length histogram.

  • straightness_threshold – straightness threshold of the current settings; not drawn.

spacr.qt.widgets.motility_preview.resolve_merged_dir(path) → str[source]

Return the merged directory for path.

Accepts the plate folder (which holds merged/) or the merged folder itself, which is what a user dragging a folder in will most likely grab.

Parameters:

path – a plate folder holding merged/, the merged folder itself, or a file inside either (its parent folder is used).

Raises:

MotilityInputError – when neither exists or it holds no .npy.

spacr.qt.widgets.motility_preview.run_motility_pass(req: MotilityRequest)[source]

Build the cached point table for one (plate, well, field) group.

Parameters:

req – the merged folder, group file names, channel count, plane indices and frame cap passed to build_point_table().

spacr.qt.widgets.motility_preview.scan_plate_payload(path) → Dict[str, Any][source]

Resolve a plate’s merged folder and group it. No Qt: worker-safe.

The expensive half of opening a plate: resolve_merged_dir lists the candidate folder and group_merged_files reads every name in merged/ and parses it – thousands of entries on a 384-well plate.

Parameters:

path – a plate folder or its merged folder; any failure is caught and reported in the error entry.

Returns:

{path, merged, groups, error}.

spacr.qt.widgets.motility_preview.smooth_and_filter_tracks(points, max_displacement: float)[source]

Apply the assay’s centroid QC: fix teleports, drop impossible tracks.

A single frame whose steps in and out both exceed max_displacement while its neighbours are within it is a segmentation glitch, and the assay interpolates it. Any remaining step over the limit means the track links two different objects, and the assay drops the whole track. This reproduces both, so the preview’s track count matches a run’s.

Parameters:
  • points – point table as returned by build_point_table(), one row per object per frame; tracks are keyed by plate, well, field and cellID. None or empty is returned as is.

  • max_displacement – largest plausible step between frames, in pixels.

Returns:

(points, n_glitches_fixed, n_tracks_dropped).

spacr.qt.widgets.motility_preview.summarise(tracks, calibration: Calibration, min_length: int, straightness_threshold: float, glitches: int = 0, dropped: int = 0) → MotilitySummary[source]

Reduce a per-track table to the numbers shown under the plots.

Parameters:
  • tracks – per-track table as returned by track_metrics(); tracks flagged too_short are left out of the means.

  • calibration – supplies the velocity unit and whether it is calibrated.

  • min_length – the minimum track length in frames, recorded in the summary.

  • straightness_threshold – straightness at or above which a used track counts as highly straight.

spacr.qt.widgets.motility_preview.track_metrics(points, calibration: Calibration, min_length: int = 3)[source]

Per-track velocity and straightness, using the assay’s own formulae.

v_px_per_frame is the mean step length; straightness is net displacement over path length. Velocity is v_px_per_frame * factor and carries Calibration.unit.

Tracks shorter than min_length frames are kept in the table and flagged too_short rather than silently dropped, so the length distribution plot can show what the cutoff is discarding.

Parameters:
  • points – point table as returned by build_point_table(), one row per object per frame.

  • calibration – pixel size and frame interval; its factor converts px/frame to the reported velocity unit.