spacr.qt.screens.gate_editor¶
Workflow inputs and outputs¶
Gate Editor¶
Load a table or use Merge tables to create a working set with validated keys and aggregation. Define threshold or polygon gates on actual feature/coordinate columns, then apply the saved gate to compatible objects. Use Annotate to write the displayed gates to an annotation column; review the selected objects and choose binary or multiclass labels.
Open: Home → Gate Editor.
Inputs and outputs below include conditional alternatives. The guidance and handoff notes say which route applies.
Inputs
Measured objects — measurements/measurements.db; object tables depend on the enabled cell, nucleus, pathogen and organelle masks. Relevant tables, depending on the route:
cell,nucleus,pathogen,cytoplasm. Relevant columns, depending on the route:plateID,rowID,columnID,fieldID.Projection and clusters — Image UMAP/PCA coordinate tables, selected clusters and figures for the loaded measurement data.
Outputs
Reusable gates — Saved threshold/polygon gate definitions or a selected object set; apply a gate to the same feature definitions.
Training annotations — A chosen annotation column in measurements/measurements.db, table png_list; labels belong to object identities. Relevant tables, depending on the route:
png_list. Relevant columns, depending on the route:prcfo.
Before this module
Image UMAP: Supply the matching coordinate/feature columns when defining a selection.
Measure: Use the actual measured feature definitions and units.
After this module
Annotate: Apply compatible gates, then review candidate labels.
Classify: Write reviewed gate selections to an annotation column, then select that same column and object population in Classify. Keep validation objects separate from training labels.
V2 — Gate Editor: draw a region, name it, and it becomes a filter.
The flow-cytometry gesture, on spaCR measurement tables. Drag a threshold
across a histogram or a polygon round the cloud on a two-parameter scatter, name
it, and the shape becomes a spacr.selection.DataFilter clause that
every open view honours — the UMAP, the plate map, the crop grid, the Graph
Builder, Small Multiples.
What it is for. Selecting a population by its measurements once Measure has run: the cells above an intensity threshold, or one cluster in a two-feature scatter. Gates can sit inside other gates, and each shows its object count and its percentage of both its parent gate and the whole table.
What it needs. A measurement table: one table of a measurements.db,
where the object tables are offered first, or a CSV or TSV file. Several
databases can be loaded as one table; plate identifiers that collide between
them are reported rather than silently pooled. Database tables are read as a
sample set by sample_fraction and capped by max_points, both in the
Gate Editor settings. Merge tables beside the table picker opens the same
validated default/custom merge workflow as Graph Builder. Name the result,
inspect row counts, unmatched records and the aggregation preview, then create
it. Multiple child tables are aggregated independently onto composite image,
time and object keys. Saved gates embed the derived-table definition and
reconstruct it against the same source/schema. Export evaluates the full merged
table and writes gates using the original base object’s identity. External
tabular merges remain gateable; image annotation/export are unavailable when
image/object provenance cannot be verified.
What it produces. Select measurement rows with threshold, rectangle,
ellipse, polygon, or wand tools. Use box, cylinder, or prism shapes in
the 3D view. Combine existing gates to define additional populations.
Publishing a gate restricts each linked view to its selected population.
Save the selection strategy to a JSON file and load it for another plate.
The default filename is gates.json. Export each gate as a column of the
filters table in the measurements database. Save the graph as PNG or PDF.
Before export, compare the current gates, including unsaved edits, with
analysis locks that contain their saved strategy. Record the verdict for
each gate in filter_export_provenance. A missing lock does not mean
the gates were verified. These records describe only the exported gates.
They do not verify unrecorded merge definitions or unrelated pipeline settings.
What to do next. Look at the gated population in the views that follow the shared filter, such as Image UMAP, Graph Builder and the crop grid, and load the saved gating strategy on the next plate.
Assembles:
spacr.qt.widgets.gate_editor.GateEditorPanel— the canvas, the gate tools and the hierarchy with its percentages;spacr.qt.widgets.data_filter_panel.DataFilterPanel— the Local Data Filter, which the gate composes onto rather than replacing;spacr.qt.widgets.formula_editor.FormulaPanel— so a gate can be drawn on a computed column;Save / Load, because a gating strategy is the reusable part: the whole point of a gate over a lasso is that it re-applies to the next plate.
register() is not called at import; read its docstring.
Classes¶
A table, two axis pickers, the gating surface, and save/load. |
Functions¶
|
Factory handed to |
|
Put the Gate Editor in the app registry. Idempotent. |
Module Contents¶
- class spacr.qt.screens.gate_editor.GateEditorScreen(parent=None, *, link=None, threaded: bool = True)[source]¶
Bases:
spacr.qt.widgets.derived_table_source.DerivedTableSource,PySide6.QtWidgets.QWidgetA table, two axis pickers, the gating surface, and save/load.
- Parameters:
parent – parent widget.
link – the
LinkedSelectionthis screen’s views join, so a selection made here reaches the others.Nonejoins the shared one; pass a private one in a test.threaded – whether the work runs off the GUI thread. False runs it inline, which is what makes a test deterministic.
Build the screen: header, working-set chips, axis pickers, plot and side panels.
- Parameters:
parent – parent widget, or
None.link – shared selection link, passed through to the gate panel and the filter panel so both answer to the same selection.
threaded – run loads and exports on a worker thread. Set
Falsein tests so a load finishes before it returns.
- align_side_panel() int[source]¶
Line the Filter / Search page up with the graph and gate table.
Settings above the graph share one row. The tabs of the panel on the right sit level with that row, and the containers under both start at the same height. The row is the graph panel’s own tool row – the table chips and the X / Y / Z pickers were moved into it – so the tabs already start level with it; what differs is where each container begins below its row. The tab page is moved down by that difference, measured each time, so whatever the row gains (the 3D plane controls under it) the page follows.
A tab bar given a fixed height does not move the page under it – QTabWidget places the page from the bar’s size hint – so the page’s own offset is what is set.
- Returns:
the offset applied to the page, in pixels.
- annotate_from_gates() None[source]¶
Label every object from the gates currently shown.
The SHOWN gates, not all of them: ticking a gate on and off is already how the user says which ones count, so asking again in a dialog would be asking a question they have already answered.
- apply_settings(settings: spacr.qt.widgets.gate_settings.GateEditorSettings) None[source]¶
Take new settings, re-reading the table only if one of them needs it.
Two settings cost a read – the sample fraction and the row cap. The rest are drawing, and re-reading a large table because the user nudged a colour map is the lag this dialog exists to remove.
- Parameters:
settings – the complete new
GateEditorSettings; it replaces the current settings and is passed to the gate canvas and the search panel.
- ask_axis_cutoffs(axis: str) Tuple | None[source]¶
Ask for the lowest and highest value
axisshould show.Returns the pair that was applied, or
Nonewhen the request was cancelled or could not be read.- Parameters:
axis –
"x"or"y"; an axis with no measurement chosen returnsNonewithout asking.
- axis_column(axis: str) str[source]¶
The measurement drawn on
"x"or"y", or"".- Parameters:
axis –
"x"or"y"; converted tostr, and any other value gives"".
The axis menu as data, so its CONTENTS can be tested offscreen.
Separated from the QMenu for the same reason
graph_menu_items()is: an offscreen Qt cannot grab for a popup, so a test that builds a real menu hangs.- Parameters:
axis –
"x"or"y"; the menu is built for the measurement currently drawn on it.
- axis_under(point) str | None[source]¶
Which axis a right-click at
pointlanded on, orNone.pointis in the canvas widget’s own coordinates, which is what Qt hands a custom context menu. Getting from there to the figure means two conversions and both are easy to get wrong: the matplotlib canvas is a CHILD of the gate canvas rather than the same widget, and matplotlib’s display coordinates count upward from the BOTTOM while Qt counts downward from the top.Returns
Nonefor a click inside the plotting rectangle, which is where the plot’s own menu belongs.- Parameters:
point – the click position as a
QPointin the gate canvas widget’s own coordinates.
- clear_axis_cutoffs(axis: str) bool[source]¶
Let
axisfollow the data again. Returns whether it was cut.- Parameters:
axis –
"x"or"y"; an axis with no measurement chosen, or with no cutoffs set, returns False.
- closeEvent(event)[source]¶
Let the panel close first, so it can unlink its canvas.
- Parameters:
event – the Qt close event.
- database_labels() List[str][source]¶
The name each loaded database carries in the provenance column.
Asked of
spacr.multi_databaserather than derived here, so a chip and thesource_databasevalue it stands for cannot disagree – a chip readingplate1beside a legend readingmeasurements (2)is provenance the user cannot follow.
- eventFilter(watched, event) bool[source]¶
Keep the side panel’s page level with the graph as rows come and go.
- Parameters:
watched – the graph’s splitter or the side tabs.
event – the event.
- Returns:
False, so the event is handled as usual.
The plot menu as data:
[(label, enabled, callback, why)].Separated from the QMenu so the CONTENTS can be tested without a display. An offscreen Qt cannot grab for a popup, so a test that builds a real menu hangs – which is how this method came to exist.
Every callback is an EXISTING method. The menu is a second route to the same code, never a second implementation.
- is_busy() bool[source]¶
Whether anything is still running.
What the window asks before closing: a gate applied to a table that is still loading would be applied to half of it.
- Returns:
True while work is outstanding.
- load_filters(path: str) List[str][source]¶
Apply a saved filter set. Reports columns this table does not have.
Saying so matters more here than it looks. A filter set saved against one plate and loaded against another is an ordinary thing to do, and a set that half-applies selects the wrong rows while looking like it worked.
- Parameters:
path – a JSON filter-set file written by
save_filters(). A file that cannot be read is reported on the source line and gives an empty list.
- load_gates(path: str) bool[source]¶
Read a gating strategy and apply it to the loaded table.
A strategy naming a measurement this table does not carry loads anyway and reports the problem: the gates are still there to look at and fix, which is more use than refusing the file.
- Parameters:
path – a JSON gate file written by
save_gates(). A file that cannot be read is reported on the source line and gives False.
- load_path(path: str, table: str | None = None) None[source]¶
Read a CSV or one table of a measurement database, off the GUI thread.
- Parameters:
path – a CSV, TSV or TXT file (by extension), read as one table; any other path is opened as a SQLite measurement database and its tables are listed in the picker. It becomes the only database in the working set.
table – the database table to read, also selected in the picker when the database has it;
Nonereads the picker’s current table.
- load_paths(paths, table: str | None = None) None[source]¶
Load several measurement databases as one frame.
Every decision that can go quietly wrong – plate-id collisions, mismatched column sets, provenance – is delegated to
spacr.multi_database, so this screen and Image UMAP cannot disagree about them.A collision is REPORTED, not resolved. Two databases that each hold a plate called
plate1are two experiments, and pooling them would compute every per-well number over both at once with nothing on screen to say so. The user is told which plate ids clash, because they are the only one who can say whether they are the same plate.- Parameters:
paths – the SQLite measurement databases to merge, as paths; each is converted to
str, and an empty list does nothing.table – the table to read from every database;
Noneuses the first table of the first database.
- open_settings() None[source]¶
Show the settings window.
Not modal, and not re-created: a settings window you have to close to see what it did is a settings window you cannot tune anything with. The same dialog is raised again so its tab and scroll position survive, which is the difference between adjusting a value and hunting for it.
- reduce_to_components() str | None[source]¶
Project every measurement onto components, and gate on those.
This is what xD MEANS here. More measurements than can be drawn is not a drawing problem to be solved with another axis – past three there is no fourth to add – so the measurements are projected and the projection is gated.
The components come back as ORDINARY COLUMNS, so every existing tool works on them unchanged: the same rectangle, oval, polygon, wand and cluster, saved and exported the same way. A gate on PC1 vs PC2 is the same kind of object as a gate on area vs intensity.
- Returns:
an error to show, or None on success.
- remove_database(name: str) None[source]¶
Drop one database from the working set and re-merge the rest.
By its CHIP’s label or by its path, because the chip shows the label and a caller usually holds the path.
This is also the resolution the screen offers for a plate-id collision, and the reason it does not offer
on_collision='qualify'instead: qualifying rewritesplate1torunA-plate1, which makes the keys unique by hiding which experiment a plate belongs to inside its own id, where nothing can block on it or colour by it. Dropping one of the two databases keeps every remaining number meaning what it says.- Parameters:
name – the database’s chip label or its path. An unknown name, or the last remaining database, does nothing.
- remove_table(name: str) None[source]¶
Drop a table from the working set.
The last one cannot be dropped: a gate editor with no table is a screen with nothing on it, and the user’s next move would be to load the same table again.
- Parameters:
name – the table name. A table not in the working set, or the last remaining table, does nothing.
- save_filters(path: str) str[source]¶
Write the current filter set to
path.- Parameters:
path – the file to write the current filter set to, as JSON; an existing file is overwritten.
- save_gates(path: str) str[source]¶
Write the gating strategy to
path.- Parameters:
path – the file to write the gating strategy to, as JSON; an existing file is overwritten.
- save_graph(path: str = '') str[source]¶
Write the current graph to a PNG or PDF.
The format comes from the figure-format PREFERENCE rather than from a setting of this screen’s own; the design is explicit that a second place to answer “am I making PDFs” is one too many. The file dialog still lets a single save differ, because “save as” is when a user thinks about format.
Rendering goes through
render_figure_to_png, the same helper the figure queue uses, rather thansavefig: it applies the figure colour, line and text-size preferences, caps the display raster, and in PDF mode writes a genuine vector page beside the PNG with its fonts embedded as TrueType. Calling matplotlib directly would give none of that.THE FILE GETS THE PRINT STYLE. Decision 2026-09-25: “saved graphs (PDF/PNG) get a WHITE PRINT STYLE (white background, dark text/axes/lines) whatever the screen theme”. The render passes
for_print=True, which styles a detached copy white with dark ink, so the graph on screen keeps the theme’s colours. The extension the user picked decides whether the PDF is written, so choosing PDF in the dialog under a PNG preference still gives a PDF.- Parameters:
path – destination. Empty opens a file dialog.
- Returns:
the path written, or “” when cancelled or nothing is drawn.
- set_axis_cutoffs(axis: str, low, high) None[source]¶
Show only
lowtohighof the measurement onaxis.Either end may be
None, meaning the data decides it.- Parameters:
axis –
"x"or"y"; an axis with no measurement chosen does nothing.low – the lowest value to show, or
None.high – the highest value to show, or
None.
- Raises:
spacr.qt.widgets.gate_canvas.CutoffError – when the low end is not below the high one.
- set_axis_scale(axis: str, scale: str) None[source]¶
Lay
axisout onscale, from the menu or from the window.The menu is a second ROUTE to the scale the settings window already holds, never a second copy of it: this writes the same field, so the two cannot come to disagree about how the plot is drawn.
- Parameters:
axis –
"x"or"y"; it names the<axis>_scalesettings field, and a value with no such field does nothing.scale – a matplotlib axis scale, one of
AXIS_SCALES("linear","log","symlog"or"logit").
- set_frame(frame: pandas.DataFrame, *, label: str = '') None[source]¶
Point the screen at a table to gate.
- Parameters:
frame – the rows, or None to clear.
- settings() spacr.qt.widgets.gate_settings.GateEditorSettings[source]¶
The screen’s settings, in the shape a settings file wants.
- Returns:
the settings dict.
- spacr.qt.screens.gate_editor.make_gate_editor_screen(app_key: str | None = None) PySide6.QtWidgets.QWidget[source]¶
Factory handed to
spacr.qt.app.register_app().
- spacr.qt.screens.gate_editor.register() bool[source]¶
Put the Gate Editor in the app registry. Idempotent.
The row itself – the key, the name, the blurb, the section, the “no headless run” sentence, the API doc link and the nine translations of the display name – is declared in
spacr.qt.app_catalog.spacr.qt.app.register_app()distributes those into the four tables each used to need a hand-edit in, and this function’s whole job is to name which row. That is what lets the app be registered without importing this module at all: the launch reads the table, and the screen is imported when somebody opens it.- Returns:
Trueif this call is what registered it.