spacr.qt.screens.pipeline_graph

Workflow inputs and outputs

Pipeline Graph

Read recorded artifact dependencies and stale/missing status. The graph describes provenance and does not regenerate missing artifacts.

Open: the application’s Help/tools menus.

Inputs and outputs below include conditional alternatives. The guidance and handoff notes say which route applies.

Inputs

  • Run history and artifacts — Project run records, settings, output paths, artifact provenance, status and logs.

Outputs

  • Figures and table exports — The output location chosen by the tool; exports describe the selected data and filters.

API reference.

Module tutorial.

Pipeline Graph — what produced what, and which of it is still true.

A spaCR project accumulates files faster than anyone can hold in their head: masks, a measurements database, crops, model weights, predictions, regression tables, figures. Some of those were made from each other. Some were made from a previous version of each other, because the mask step was re-run with a different diameter on Tuesday and nothing downstream was. Nothing in the GUI showed that, and a stale number does not look stale — it looks like a number.

This screen draws the DAG:

┌──────────────┐   ┌────────────────┐   ┌───────────────┐
│ mask         │──▶│ measure        │──▶│ classify      │
│ masks   OK   │   │ db      STALE  │   │ preds   STALE │
└──────────────┘   └────────────────┘   └───────────────┘

Boxes are artifacts, arrows are “was made from”, and the colour is the verdict spacr.artifacts.Registry.is_stale() returns — with its reasons shown in the panel beside the graph, because “stale” on its own is an accusation, not an explanation. Click a box and the panel names the run that produced it, the settings digest, the spaCR version, and what re-running it would invalidate.

Three deliberate choices:

The graph is drawn from provenance, not from the module list. What the pipeline is supposed to do is spacr.ports.PORTS, and that is drawn too — dimmed, underneath, as the “declared order” strip — but the boxes and arrows are what actually happened. A project where measure read last week’s masks does not look like the tidy diagram, and that difference is the entire value of the screen.

Missing is not stale. A deleted file is red and says so; an outdated one is amber. spacr.artifacts refuses to conflate them and neither does this.

Nothing here writes. No re-run button, no “mark as fresh”, no delete. The screen answers a question; acting on the answer is the module’s job, and a one-click re-run wired to a graph is how somebody overwrites the artifact they were trying to check.

The layout is computed by a pure function (layout_rects()) off the graph’s layers, so the arrangement is testable without pixels, and the heavy part — opening the registry and asking it about every artifact — runs through spacr.qt.job_runner.JobRunner, off the GUI thread.

Classes

GraphCanvas

Draws one PipelineGraph.

PipelineGraphScreen

Pick a project; see its provenance DAG with staleness marked.

Functions

canvas_size(→ Tuple[int, int])

The pixel size the whole graph needs, as (width, height).

layout_rects(→ Dict[str, PySide6.QtCore.QRect])

Place every node of graph on a grid: one column per layer.

make_pipeline_graph_screen(→ PySide6.QtWidgets.QWidget)

Factory the registry calls to build this screen.

register(→ bool)

Add Pipeline Graph to the app registry. Idempotent.

Module Contents

class spacr.qt.screens.pipeline_graph.GraphCanvas(parent=None)[source]

Bases: PySide6.QtWidgets.QWidget

Draws one PipelineGraph.

Custom painting rather than QGraphicsScene: the graph is a few dozen boxes with straight-ish edges, and a scene brings its own selection model, its own coordinate system and its own set of ways to leak a C++ object into a Python-owned widget. The whole drawing is one paintEvent over a dict of rectangles that a test can read directly.

Parameters:

parent – Qt parent.

Variables:

selected – artifact id of the box the user last clicked, or "".

Create the empty pipeline-graph canvas.

Parameters:

parent – parent widget, or None.

graph() → spacr.pipeline_graph.PipelineGraph | None[source]

The graph currently drawn, or None.

mousePressEvent(event) → None[source]

Select whatever box was clicked.

Parameters:

event – the mouse press; its position() picks the box, and it is then passed on to the base-class handler.

node_at(x: int, y: int) → str[source]

Artifact id of the box containing this point, or "".

Parameters:
  • x – horizontal widget coordinate, in pixels.

  • y – vertical widget coordinate, in pixels.

node_rects() → Dict[str, PySide6.QtCore.QRect][source]

{artifact id: rectangle} for every box currently drawn.

paintEvent(event) → None[source]

Paint the edges, then the boxes, then the selection ring.

Parameters:

event – the paint event; not read, the whole canvas is repainted.

select(artifact_id: str) → None[source]

Select a box by id ("" clears) and emit node_clicked.

Parameters:

artifact_id – the artifact to select; an id that is not drawn clears the selection.

set_graph(graph: spacr.pipeline_graph.PipelineGraph | None, visible: set | None = None) → None[source]

Show graph, drawing only the ids in visible when given.

Parameters:
  • graph – the pipeline graph to lay out and draw, or None for an empty canvas.

  • visible – artifact ids to draw; None draws every node, and ids not in the graph are ignored.

class spacr.qt.screens.pipeline_graph.PipelineGraphScreen(parent=None, project: str = '', threaded: bool = True)[source]

Bases: PySide6.QtWidgets.QWidget

Pick a project; see its provenance DAG with staleness marked.

Parameters:
  • parent – Qt parent.

  • project – open straight onto this project root, skipping the folder picker.

  • threaded – False builds the graph inline, so a test drives the screen synchronously without the behaviour diverging.

Variables:

last_error – text of the most recent failure, "" when the last operation worked. Failures land here and in the banner — never in a modal dialog, which hangs a headless run.

Build the screen and arm its drop zone.

Parameters:
  • parent – parent widget, or None.

  • project – a project folder to draw immediately; empty leaves the screen asking for one.

  • threaded – scan on a worker thread. Set False in tests so load_project finishes before it returns.

active_jobs() → int[source]

How many worker threads are still winding down.

closeEvent(event) → None[source]

Drain the worker before the widget goes.

Parameters:

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

describe(artifact_id: str) → str[source]

The detail block for one artifact, as plain text.

Split out from the widget so a test can assert on the content without reading a QTextEdit back.

Parameters:

artifact_id – the artifact to describe; an empty or unknown id, or no graph loaded, gives "".

graph() → spacr.pipeline_graph.PipelineGraph | None[source]

The graph currently drawn, or None.

is_busy() → bool[source]

True while a graph is still being built.

load_project(project: str) → None[source]

Build and draw the graph for project, off the GUI thread.

Returns as soon as the job is submitted; graph_loaded fires when the graph is drawn. A project with no registry is not a failure — it draws the declared module order and says nothing has run.

Parameters:

project – the spaCR project folder; stripped, and an empty value only asks for a folder.

spacr.qt.screens.pipeline_graph.canvas_size(graph: spacr.pipeline_graph.PipelineGraph) → Tuple[int, int][source]

The pixel size the whole graph needs, as (width, height).

spacr.qt.screens.pipeline_graph.layout_rects(graph: spacr.pipeline_graph.PipelineGraph) → Dict[str, PySide6.QtCore.QRect][source]

Place every node of graph on a grid: one column per layer.

Column is spacr.pipeline_graph.Node.depth, which is the longest distance from a root — so an arrow never points backwards. Row is the node’s position within its layer, in the order the graph already sorted them (newest first), so re-building the same graph twice draws the same picture.

Parameters:

graph – the graph to lay out.

Returns:

{artifact_id: QRect}, empty for an empty graph.

spacr.qt.screens.pipeline_graph.make_pipeline_graph_screen(app_key: str | None = None) → PySide6.QtWidgets.QWidget[source]

Factory the registry calls to build this screen.

spacr.qt.screens.pipeline_graph.register() → bool[source]

Add Pipeline Graph to the app registry. Idempotent.

Returns:

True when this call added the row, False when it was already there — which is what a second import, or a plugin that pulls the module in again, must not treat as an error.