spacr.qt.widgets.cell_montage_view¶
Show the measured cells represented by a regression coefficient.
spacr.cell_montage selects the objects and records the selection reason;
this module resolves and displays their image crops beside the run’s figures.
Each attached database resolves its own exported-PNG or merged/<fov>.npy
source so multi-plate montages can combine experiments with different storage
layouts. The crop mask plane and channels come from spacr.crops.CropSpec
or, by default, the run’s measurements.db metadata.
Image reads run through spacr.qt.job_runner.JobRunner and only the
GUI-thread completion handler updates widgets. Converting at most 300 crops to
QPixmap remains on the GUI thread because QPixmap is not transferable
across threads; the recorded 224-pixel thumbnail conversion takes about
21 milliseconds for 300 crops.
The tab stays visible when crops are unavailable and reports the reason in its status, tooltip, and disabled action. Crop-source discovery is cached after a load and invalidated when the coefficient, databases, or crop settings change.
Classes¶
The cells behind the selected coefficient, beside the run's figures. |
|
What came back from one load: the plans, the pixels, or the reason. |
|
Everything one montage load needs, as plain data. |
Functions¶
|
The border a likely cell wears, in the annotation app's own palette. |
|
Turn a clicked coefficient into |
|
The experiment folder holding |
|
Calculate the thumbnail grid capacity of a viewport. |
|
The fitted intercept out of the coefficient table, or |
|
Select the objects behind one coefficient and cut their crops. |
|
Draw the montage as one matplotlib figure, caption and all. |
|
Explain where spaCR looked for a per-object classification score. |
|
Read the channel box: |
|
The tab's own name: THE WELL AND THE gRNA, both. |
Module Contents¶
- class spacr.qt.widgets.cell_montage_view.CellMontageView(frame_provider: Callable[[], Any] | None = None, results_provider: Callable[[], str] | None = None, database_provider: Callable[[], Any] | None = None, parent=None, *, threaded: bool = True)[source]¶
Bases:
PySide6.QtWidgets.QWidgetThe cells behind the selected coefficient, beside the run’s figures.
Connect
set_coefficient()to the panel’s existing selection –RegressionResultsPanel.table.key_selected, which is the funnel every plot and the table already pass through. There is deliberately no second selection mechanism here: a montage of a different gene from the one the volcano is ringing is exactly the plausible-and-wrong output this feature is most at risk of.- Parameters:
frame_provider – called with no arguments for the coefficient table, which is where the fitted effect for a clicked key comes from.
results_provider – called with no arguments for the results CSV path, beside which
regression_data.csvis found.database_provider – called with no arguments for the run’s input table rows, which is where the measurement databases are attached. A callable rather than a snapshot, for the same reason the Measurements tab takes one: databases are attached after this widget is built, and a list captured at construction never grows.
threaded –
Falseruns the load inline, emitting the same signals in the same order, so a test drives the whole tab without the behaviour diverging.parent – parent widget; ownership only.
Build the montage tab.
Every piece of state a control reads is created before any signal is connected: a widget whose controls are live before its state exists is the crash that took the application down at launch, and the rule has its own test file.
The controls sit in a flow rather than a row, so they wrap before they clip – the four buttons alone measure over 550 px, and squeezing a required selector down to a few pixels was what met the nominal minimum before.
- Parameters:
frame_provider – called for the coefficient table.
results_provider – called for the run’s results path.
database_provider – called for the measurements database.
parent – parent widget, or
None.threaded – run loads on a worker thread. Remembered, so a panel built later runs the way the view was asked to – an unthreaded view that grew a threaded tab would put a
QThreadinto a test constructed to have none.
- annotate_the_cells(*_args)[source]¶
Open annotation strategies for the cells in the current montage.
- Returns:
The annotation-strategy panel, constructed on first use.
- apply_workspace_state(state) bool[source]¶
Put the montage’s settings back. Does NOT rebuild it.
Returns whether anything was applied.
DELIBERATELY NOT REBUILT. Loading the crops is the slow half – the first montage of a run reads images off disk for seconds – and a restore that started it would freeze a window the user had just opened to look around in. The settings are put back and the button is there; 155’s “the montage says how it chose” is on screen either way.
- Parameters:
state – mapping as saved with the workspace; its
picture_settings,widgets,picture_modeandcoefficiententries are each applied when present, and a non-dict applies nothing.
- build() bool[source]¶
Load the montage for the selected coefficient, off the GUI thread.
- Returns:
True when a load was started.
- build_every_selected() int[source]¶
Queue one montage for each selected coefficient.
Loads are chained in selection order because montage construction is asynchronous. Completed well tabs remain available, enabling comparison across coefficients while preserving each montage’s guide-specific crop ranking.
- Returns:
int – Number of selected coefficients queued for loading.
- cancel_loading()[source]¶
Cancel the active montage and queued selections without blocking.
- Returns:
whether there was a current montage request to cancel. An existing database or filesystem read may finish before exit; no later stage or stale result is shown.
- clear() None[source]¶
Empty every open well tab’s grid, leaving the tabs standing.
PUBLIC BECAUSE IT IS CALLED FROM OUTSIDE, which it was already: the Regression screen empties this view when the loaded run changes, because a montage answering a coefficient from the previous run’s table means nothing under the new one. It called
clear()– the obvious name, and the name the well tabs themselves use – and got an AttributeError every time, reported by a user on 2026-09-02 (issue 116) whose Cells tab kept showing the run before the one he had just loaded.
- clear_picking_override() None[source]¶
Clear a temporary picking fallback.
The next montage request will ask again if the selected picking method remains unavailable.
- closeEvent(event)[source]¶
Shut the loader down before the widget goes.
- Parameters:
event – the close event; passed to the base class after
shutdown().
- compare_a_measurement(*_args)[source]¶
Open the Compare tab for the cells selected by this montage.
Returns the shared comparison panel, or
Nonewhen no cells have been selected or the panel cannot be created. Reusing the tab keeps the comparison synchronized with the montage and avoids duplicate floating views.
- count_csvs() Tuple[str, ...][source]¶
The COUNT CSVs attached to the run’s input table.
THE SAME PROVIDER THE DATABASES COME FROM. The input table’s rows are
{"plate", "score", "count", "database"}, so the counts were always one field away – which is why requiring a run folder for the guide fractions was never necessary, only unexamined.
- crop_count() int[source]¶
Return the total number of crops held across all tab pages.
This differs from
len(thumbnails()), which counts only widgets on the pages currently displayed.
- databases() Tuple[str, ...][source]¶
The measurement databases attached to the run’s input table.
Read through
spacr.qt.widgets.measurement_scan_panel.attached_databases(), which is the same reader the Measurements tab uses – one vocabulary for “which plate has a database and is it still on disk”.
- edit_picture_settings() bool[source]¶
Open the settings window. Returns whether anything was changed.
- loaded_run_name() str[source]¶
The run this tab is describing, as a name a user recognises.
The run folder’s own basename –
ols_3– which is what the Runs tab calls it and what the figure grid heads its section with. “” when no run is loaded, so a caller can tell “no run” from “a run whose name I could not work out”.
- multivariate_shortfall(request=None) str[source]¶
Return why multivariate picking is unavailable, or
"".Multivariate picking requires a gene-by-measurement effects grid. The message directs the user to create that grid or explicitly choose rank-based picking rather than silently changing the selected method.
- picked_groups() dict[source]¶
{gene: the object index values this tab picked for it}.THE PICKER’S OWN ANSWER, read off the plans rather than recomputed –
montage_candidateis the columnselect_montagemarks, so whichever mode is in force (rank, attributed, assigned, multivariate) this is what the montage actually drew.
- picture_settings() dict[source]¶
How the cells are drawn. The annotator’s keys, the annotator’s defaults, and whatever the user has changed.
- reason() str[source]¶
Why the montage cannot be built right now, or
''.The rule: a control that cannot do anything is greyed out AND SAYS WHY. Every branch here is a sentence a user can act on, and the order is the order the inputs are needed in, so the first missing thing is the one named.
- refresh() None[source]¶
Re-read the providers. Call this when the tab is opened.
Databases are attached to the input table while this tab is behind another one, so what it can do changes without any signal reaching it – the same reason the Measurements tab re-reads on open.
The grid is re-flowed too. A montage built while this tab was behind another one was laid out against a viewport that had never been through a layout pass – the same trap that made a snapshot of the unshown volcano a 100x9 rectangle of one colour – so its column count is whatever fitted in that guess, which is one.
- remember_inventory(source=None, objects=None) None[source]¶
Keep what the last load resolved, so the settings window can offer THIS screen’s mask planes and object columns rather than free text.
- request() MontageRequest | None[source]¶
The request the button would submit, or
Noneif it cannot.Public because it is what a test drives and what the completion handler compares a landed answer against.
- resizeEvent(event)[source]¶
Reflow the grid after a resize settles.
- Parameters:
event – the resize event; passed to the base class, and the reflow timer is restarted so only the last of a burst reflows.
- rows_to_compare()[source]¶
Return object rows available to the measurement comparison panel.
- Returns:
pandas.DataFrame or None – The full object inventory when it contains every montage-plan index; otherwise only the concatenated plan rows.
Noneis returned when no plan contains object rows.
Notes
Control-well and other-well contrasts require objects that may not be displayed in the montage. The wider inventory is used only when its index preserves the plan rows’ group identities.
- save(path: str | None = None) str | None[source]¶
Write the montage as a figure, honouring the format preference.
- Parameters:
path – where to write.
Noneasks. The extension is left tospacr.plot.save_figure(), which corrects it to the format the user chose – naming one here is how a PNG ends up in a file called.pdf, which is a complaint this project has had twice.- Returns:
the path written, or
None.
- score_csvs() Tuple[str, ...][source]¶
The SCORE CSVs attached to the run’s input table.
The same provider and the same rows as
count_csvs()– the input table’s rows are{"plate", "score", "count", "database"}, so the scores were always one field away too.A database whose
png_listhas nopredcolumn is not necessarily a screen without scores: these files carry one row per cell and the fit was run on exactly those numbers.load_montage_objectsjoins them in memory when the database has none, and writes nothing.
- selected_coefficients() List[str][source]¶
Every coefficient in the current selection, in pick order.
- set_coefficient(key: str) None[source]¶
A coefficient was picked. THE SLOT TO CONNECT
key_selectedTO.Takes the feature string and nothing else, so the volcano, the Q-Q, the effect-rank plot and the coefficient table all reach it by the same one route the gene tile already uses.
It does NOT load. A montage is seconds of disk for a click whose usual purpose is to read a row, so the selection arms the button and the user asks for the pictures.
- Parameters:
key – the
featurethe panel emitted.
- set_coefficients(keys) None[source]¶
Store an ordered coefficient selection and show its latest member.
A montage represents one coefficient because crop ranking depends on that coefficient’s effect. The full selection remains available from
selected_coefficients();show_next_coefficient()cycles the displayed montage without discarding the other selected coefficients.- Parameters:
keys – every selected
feature, in pick order.
- show_next_coefficient() str | None[source]¶
Move the grid to the next coefficient in the selection.
- Returns:
the key now shown, or
Noneif fewer than two are selected and there is nowhere to step to.
- shutdown() None[source]¶
Stop waiting for a load, and let no QThread outlive this widget.
Qt aborts the whole process when a running
QThreadis destroyed, and a merged-source montage is seconds long – so leaving the screen mid-load is not a rare case.JobRunner.shutdowndrops the results and waits a bounded time rather than joining on the GUI thread, which is the freeze it exists to remove.
- thumbnails() Tuple[PySide6.QtWidgets.QWidget, ...][source]¶
Return thumbnails visible on the currently displayed tab pages.
Use
crop_count()to obtain the total number of objects held across all well tabs, including pages that are not displayed.
- workspace_state() dict[source]¶
The montage the session had open, as data.
THE SETTINGS AND THE CHOICE, NOT THE PIXELS. A montage is tens of megabytes of crops that the run’s own images regenerate exactly; what cannot be regenerated is which coefficient was on screen, how the cells were picked, and how they were drawn. Those are what go in.
The picked groups ride along as a RECORD, not as an input –
picked_groups()is what the picker chose given these settings, and restoring the settings reproduces it. Written down because a reader of a saved run wants to know which cells the claim rested on without re-running anything.
- write_scores_into_the_databases(*, confirm=None) dict[source]¶
Merge loaded per-object scores into attached databases on request.
The montage can use score files without modifying a database. This method writes only after explicit confirmation.
- Parameters:
confirm – Optional callable receiving
(databases, score_files)and returning whether to proceed. When omitted, spaCR displays a confirmation dialog.- Returns:
dict – Mapping of each updated database path to its matched-row count. Returns an empty mapping when no inputs are available, the user declines, or no database can be updated.
- class spacr.qt.widgets.cell_montage_view.MontageLoad[source]¶
What came back from one load: the plans, the pixels, or the reason.
- Parameters:
request – the request this answers, so a stale answer can be recognised and dropped.
plans – one
MontagePlanper montage – one for a summed gene, one per guide when the guides were asked for separately.images – the crops, one list per plan, aligned with that plan’s
objectsrows. An entry isNoneonly where a source returned nothing for a row.sources –
{experiment root: description}– which crop source drew each plate, in words.error – why there is no montage, or
''. A SENTENCE, not an exception: a tab that cannot be filled has to say why and stay on screen.unavailable –
Truewhen the reason is a permanent property of this run – no crop source anywhere – rather than a bad request. That is what lets the button grey itself out afterwards instead of inviting the same click again.shapes – the crop shapes EVERY plate’s route can actually cut. A shape one plate cannot produce is not offered, because a montage in which some crops follow the mask and some do not is a montage whose pictures are not comparable.
shape_reason – why a shape is missing from
shapes, for the disabled entry’s tooltip.objects – EVERY object row the load read, not just the ones the plans selected. The Compare panel’s wider contrasts (187 B) need the cells the montage did NOT pick – “against every other well” has nothing on its other side without them – and the join that makes every database measurement reachable (187 A) is keyed on these rows. A reference, not a copy: the frame is already in memory.
counts – the per-well guide fractions this load resolved. Carried for the same reason: naming a control (184) is a question about the COUNT data, and the panel cannot answer it from object rows.
- class spacr.qt.widgets.cell_montage_view.MontageRequest[source]¶
Everything one montage load needs, as plain data.
A frozen record rather than a pile of arguments because it crosses a thread boundary and is what the completion handler compares against to know whether the answer that arrived is still the one on screen.
- Parameters:
name – the gene or guide the coefficient names.
effect – its fitted coefficient.
level –
'gene'or'grna'.results_path – the results CSV, or the folder holding it. Either way
regression_data.csvin that FOLDER is what is read – seespacr.cell_montage.read_well_guide_fractions(), which refuses the two obvious wrong CSVs by name.databases – the
measurements.dbfiles attached to the run’s input table.count_csvs – the COUNT CSVs from the same input table. The fallback source of the per-well guide fractions, and the reason a run folder is no longer required: a fraction is
count / well total, which these files carry outright. Used only whenresults_pathyields nothing readable.object_type – which mask plane a crop is cut by.
channels – intensity planes for the picture, or
Nonefor the ones the run itself saved.prefer –
''/'png'/'merged'.score_column – the per-object classification score. A screen with more than one classifier output has more than one candidate.
cap – the largest montage to draw.
per_guide – one montage per guide instead of the gene’s guides summed. They are different questions – see
spacr.cell_montage.select_montage_per_guide()– and each plan says in its own caption which one it answers.half_widths – the score window’s half-width in robust scales – the direct stringency control.
0means the module’s own default. One value is applied to every coefficient in the screen to prevent gene-specific adjustment after the output has been inspected.baseline – the baseline to centre the window on, or
Nonefor the screen median.baseline_label – what to call that baseline in the caption.
crop_shape –
'object'or'bbox'– seeSHAPE_CHOICES.
- spacr.qt.widgets.cell_montage_view.candidate_colour() str[source]¶
The border a likely cell wears, in the annotation app’s own palette.
Class 1 – the first annotation colour – because these ARE the panel’s first class of thing: the cells consistent with the coefficient. Theme aware, because
label_to_hexis: the same hue deepens against a light tile so it stays readable, which is issue #6.
- spacr.qt.widgets.cell_montage_view.coefficient_from_frame(key: str, frame) Tuple[str, str, float | None][source]¶
Turn a clicked coefficient into
(name, level, effect).The join is on the KEY and the parse is
spacr.hits.guide_of()/spacr.hits.gene_of(), which is the same rule the volcano, the gene tile and the metadata join already use – a fourth copy of “which gene is this term” is how two surfaces start naming different guides for one dot.- Parameters:
key – the
featurethe panel emitted, e.g.fraction:grna[233460_1]orgene_fraction:gene[233460].frame – the coefficient table, for the fitted effect.
Noneis allowed and yieldsNonefor the effect rather than raising.
- Returns:
the gene or guide name,
'grna'or'gene', and the fitted coefficient. The name is''for a term that names neither – an Intercept or a row/column nuisance term – which is a real answer and the reason the montage is refused for it.
- spacr.qt.widgets.cell_montage_view.experiment_root(db_path: str) str[source]¶
The experiment folder holding
measurements/measurements.db.- Parameters:
db_path – the database an attached plate names.
- Returns:
the folder
spacr.crops.resolve_crop_source()wants – the one withmerged/anddata/under it. A database somewhere other thanmeasurements/yields its own folder, so a project laid out by hand still resolves instead of silently pointing one level up.
- spacr.qt.widgets.cell_montage_view.fits_on_a_page(width: int, height: int, thumb_px: int, spacing: int = 6) Tuple[int, int][source]¶
Calculate the thumbnail grid capacity of a viewport.
- Parameters:
- Returns:
int – Number of thumbnail columns.
int – Total thumbnails per page. Both values are at least one, including when the viewport is smaller than a thumbnail.
- spacr.qt.widgets.cell_montage_view.intercept_from_frame(frame) float | None[source]¶
The fitted intercept out of the coefficient table, or
None.The other baseline the score window can be centred on. Under the well-level model the intercept IS the score of a well carrying none of the guide, which is the same quantity the screen median estimates – so offering both is offering the model’s answer beside the data’s, and the caption says which produced the picture.
- Parameters:
frame – the coefficient table. Every spelling statsmodels and this project use is accepted (
Intercept,(Intercept),const), because a baseline silently not found would fall back to the median and the montage would saymedianwhile the user had asked for the intercept.- Returns:
the fitted value, or
Nonewhen the table names no intercept or its value is not a finite number.
- spacr.qt.widgets.cell_montage_view.load(request: MontageRequest, *, progress=None, cancelled=None) MontageLoad[source]¶
Select the objects behind one coefficient and cut their crops.
Runs on a worker thread and touches no widget. Every failure comes back as
MontageLoad.errorrather than as an exception, because the caller is a tab that must stay on screen and say why.- Parameters:
request – what to draw.
progress – optional callback receiving stage text on the calling thread. A GUI caller must relay it through a queued Qt signal.
cancelled – optional zero-argument predicate checked between reads and processing stages. An operation already in progress may finish.
- Returns:
the plans, the crops, and which source drew them.
- spacr.qt.widgets.cell_montage_view.montage_figure(plans: Sequence[Any], images: Sequence[Sequence[Any]], columns: int = 8)[source]¶
Draw the montage as one matplotlib figure, caption and all.
The caption is not decoration and is not optional: it is what stops a reader taking these for genotyped cells, and
caption()always ends with the sentence that says membership is inferred from a well-level fraction. A figure without it is the one output this feature must never produce, so it is drawn from the plan rather than passed in.- Parameters:
plans – the plans to draw, in order.
images – the crops for each plan.
columns – how many crops per row.
- Returns:
the
Figure. The caller writes it throughspacr.plot.save_figure(), which is what honours the user’s figure-format and resolution preferences – neversavefigdirectly, and never a hard-coded extension.
- spacr.qt.widgets.cell_montage_view.no_score_refusal(score_csvs, troubles=()) str[source]¶
Explain where spaCR looked for a per-object classification score.
- Parameters:
score_csvs – Score files loaded with the regression result.
troubles – Additional diagnostics from the attached databases.
- Returns:
str – A user-facing explanation that identifies both possible score sources and suggests the next useful action.
- spacr.qt.widgets.cell_montage_view.parse_channels(text: str) Tuple[int, ...] | None[source]¶
Read the channel box:
"0,1,2"->(0, 1, 2).- Parameters:
text – what the user typed. Empty means “as the run saved them”, which is
None– andNoneis not the same as(): it is what letsspacr.crops.resolve_crop_source()readpng_dimsback out ofmeasurements.dband reproduce that run’s own crops.- Returns:
the channel indices, or
Nonefor “leave it to the run”.- Raises:
ValueError – the box holds something that is not a channel index.
- spacr.qt.widgets.cell_montage_view.well_tab_label(well, guides, name='', level='gene')[source]¶
The tab’s own name: THE WELL AND THE gRNA, both.
A gene with several guides pulls the same well more than once, so two tabs called
p1_r3_c7are indistinguishable – and the whole point of a tab that outlives the selection is comparing one gene’s cells with another’s, which a label naming only the well makes impossible.THE COEFFICIENT IS NAMED TOO WHEN THE GUIDE ALONE DOES NOT IDENTIFY IT. Driving the real tab found the case: the guide-level coefficient
GRA14_1, and the GENEGRA14shown one guide at a time, both open a tab forGRA14_1in the same well – and they are DIFFERENT montages with different effects, different windows and different cells. Both readplate1_r1_c1 · GRA14_1and nothing on the tab bar told them apart.- Parameters:
well – the well key as the plan spells it.
guides – the guides this montage covers.
name – the coefficient’s own name, used when the guides are summed and there is therefore no single guide to name.
level –
'gene'or'grna'– which kind of coefficient this montage answers for.
- Returns:
e.g.
'plate1_r1_c1 · GRA14_1'for a guide term,'plate1_r1_c1 · GRA14_1 (of GRA14)'for that same guide inside a gene term, or'plate1_r1_c1 · GRA14 (2 guides)'for the sum.
Nested helpers¶
- CellMontageView.build.report(message)¶
Queue progress only for an active request and cancel if its Qt receiver has disappeared.
spacr/qt/widgets/cell_montage_view.py:2177
- CellMontageView.build.work()¶
Load the captured montage request and convert failures into a request-bound error result.
spacr/qt/widgets/cell_montage_view.py:2185
- _load._design_spelling(name: str) str¶
The design’s own spelling of a name, for display.
spacr/qt/widgets/cell_montage_view.py:775
- load.step(message)¶
Report a loading stage while checking cancellation before and after the callback.
spacr/qt/widgets/cell_montage_view.py:589