spacr.qt.widgets.measurement_scan_panel

Compare gene effect sizes across measured features and attached databases.

This panel renders spacr.measurement_scan beside regression runs. Model settings remain fixed while the dependent measurement changes. Each row shows both the within-measurement q-value and the correction across all scanned measurements; verdicts use the across-scan value, while ranking uses effect size. In the recorded permuted-label check, within-measurement correction flagged 83.5% of scans and across-scan correction flagged 5.0%.

DatabaseMergePanel also exposes the measurement databases attached to each regression plate. spacr.multi_database prevents pooling colliding plate identities, and spacr.merge_tables chooses aggregation and join behavior per table from measurement type and object cardinality. The panel reports the selected sources, dropped columns, aggregation policy, collisions, and other merge consequences rather than applying one global join rule.

Exceptions

QueueCancelled

The user stopped the queue between fits.

Classes

AttachedDatabase

One plate of the regression input table, seen from this tab.

ColumnFit

What one fit of the queue did.

ColumnRegressionPanel

Run one regression per selected column of a merged measurement table.

DatabaseMergePanel

The databases attached to the input table, and the join offered.

MeasurementScanPanel

The scan's result table, and the two numbers behind every row.

WorkflowStep

One numbered step of the Measurements workflow: a fold and a body.

WorkflowSteps

The numbered-step half of a Measurements panel, shared by both of them.

Functions

anchor_tables(→ Tuple[str, ...])

The subset of tables that can be an anchor.

attached_databases(→ Tuple[AttachedDatabase, ...])

The input table's rows as AttachedDatabase entries.

column_run_settings(→ Dict[str, Any])

The settings for ONE fit of the queue: this column, this score file.

default_aggregation_columns(→ Tuple[str, ...])

The columns NO AGGREGATION_RULES rule names.

describe_key_overlap(→ str)

Whether two frames' wells meet, and one example from each side if not.

displayed_plates(→ Tuple[str, ...])

Plate ids as the plates are CALLED, in their given order.

joinable_tables(→ Tuple[str, ...])

The object tables EVERY one of these databases has, in table order.

merge_across_databases(paths, tables, *[, policy, ...])

Every chosen table of every chosen database, on one anchor.

merge_evidence(→ str)

The lists behind merge_summary()'s counts. One click away.

merge_report(→ str)

The whole statement: merge_summary() and then its evidence.

merge_summary(→ str)

What the merge cost, as COUNTS. This is what fits in the box.

ordered_columns(→ list)

PREFERRED_COLUMNS this frame has, then everything else.

plate_id_notes(→ List[str])

Describe noncanonical plate identifiers that remain in a merge plan.

regressable_columns(→ Tuple[str, ...])

The columns of a merged frame a regression could take as its response.

resizable_box(owner, widget, layout, *, key, minimum, ...)

Give widget a user-draggable height instead of a hard cap.

run_column_fits(→ List[ColumnFit])

Fit response columns sequentially while isolating per-column failures.

step_header(number, title[, parent])

Create a numbered heading for one database-merge workflow step.

verdict_for(→ str)

One phrase per measurement, from BOTH corrections.

well_keys(→ Tuple[str, Tuple[str, ...]])

(what the key is called, the distinct well keys) for one frame.

write_merged_frame(→ str)

Stage the merged frame: offer it in memory, write the durable copy.

Module Contents

exception spacr.qt.widgets.measurement_scan_panel.QueueCancelled[source]

Bases: Exception

The user stopped the queue between fits.

Not an error and not a refusal, for the same reason spacr.multi_database.MergeCancelled is neither: wording a cancel like a failure puts “did not fit” in front of somebody who pressed Stop.

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

class spacr.qt.widgets.measurement_scan_panel.AttachedDatabase[source]

One plate of the regression input table, seen from this tab.

Parameters:
  • plate – the plate the input-table row names.

  • path – its measurements database, or "". A plate with no database is legal – the regression runs on the score and count CSVs, and the database is only what makes this tab possible for that plate – so an empty path is listed and disabled rather than refused.

  • screen – the screen the plate belongs to, when the project has more than one. Carried through to spacr.multi_database.read_merged() as screens=, which is what keeps two screens sharing plate1 apart as two identities instead of one collision.

property attached: bool[source]

Whether this plate has a database at all.

property label: str[source]

A readable name for the file, disambiguated by its folder.

Every plate’s database is usually called measurements.db, so the stem alone names all of them the same thing – the reason spacr.multi_database.describe_merge() folds the parent directory into its own labels.

property present: bool[source]

Whether the attached database is on disk right now.

Checked here rather than at run time: the design asks that a row whose database has gone missing says so BEFORE the run, not four minutes into it.

Asked of spacr.qt.path_probe rather than of os.path, and that is not a style choice. This property is read once per plate row by _fill_table, and again by paths, screens and describe – so a project with eight plates on a sleeping autofs mount was eight twenty-second stats on the GUI thread before the tab had drawn anything. The probe answers from a cache, reports a path it has not seen as present, and stats it on its own bounded worker; DatabaseMergePanel._follow_path_probes is the half that corrects the row when the real answer lands.

property status: str[source]

Why this plate is or is not in the merge, in words.

class spacr.qt.widgets.measurement_scan_panel.ColumnFit[source]

What one fit of the queue did.

Parameters:
  • column – the response it was asked to fit.

  • ok – whether it produced results.

  • folder – where it wrote, when it wrote anywhere.

  • error – why it did not, in the words the fit used.

  • n_results – how many coefficients came back.

describe() → str[source]

One line for the queue’s own list.

class spacr.qt.widgets.measurement_scan_panel.ColumnRegressionPanel(frame_provider=None, settings_provider=None, parent=None, *, score_provider=None, threaded: bool = True, fit=None)[source]

Bases: WorkflowSteps, PySide6.QtWidgets.QWidget

Run one regression per selected column of a merged measurement table.

Each column produces an independent run folder and Runs-tab entry. Jobs execute sequentially through JobRunner, can be stopped between fits, and continue after individual fit failures.

Parameters:
  • frame_provider (callable or None, optional) – Zero-argument callable returning the merged measurement frame.

  • settings_provider (callable or None, optional) – Zero-argument callable returning the regression screen’s current model, correction, and count settings. Each queued fit copies these settings and changes only the response column.

  • parent (QWidget or None, optional) – Parent widget.

  • score_provider (callable or None, optional) – Zero-argument callable returning the path to the saved merged frame. Every queued fit reads this same file.

  • threaded (bool, default=True) – Run fits through the background job runner. Set to False for synchronous tests or headless callers.

  • fit (callable or None, optional) – Function called with one fit’s settings. None uses the standard regression implementation. Supplying a callable keeps the queue independently testable and avoids importing the full statistics stack while the first window is constructed.

fit_started[source]

Emits (column, settings) as each fit begins.

Type:

Signal

fit_finished[source]

Emits (column, outcome) when a fit succeeds or fails.

Type:

Signal

queue_finished[source]

Emits (fitted, failed) when the queue ends.

Type:

Signal

queue_progress[source]

Emits (column, index, total) before each fit.

Type:

Signal

Initialize the sequential column-regression queue.
cancel(*_args) → bool[source]

Stop the queue after the fit that is running.

Returns:

whether there was one to stop.

NOT MID-FIT, and the honesty is the point: a regression stopped half-way has written part of a results folder, and there is no way to say what that folder means. The fits that finished are complete runs and stay in the Runs tab.

closeEvent(event)[source]

Do not let a queue outlive the widget it reports to.

Parameters:

event – the close event; it is passed on to the base class after the worker queue is stopped.

columns() → Tuple[str, ...][source]

Every column that can be regressed on.

is_running() → bool[source]

Whether a queue of fits is going right now.

outcomes() → Tuple[ColumnFit, ...][source]

What each fit of the last queue did.

refresh() → int[source]

Re-read the merged frame and offer its columns.

Returns:

how many columns can be regressed on.

THE SELECTION SURVIVES A REFRESH where the column survives with it. Re-merging with one more database must not silently empty a queue the user has just built.

selected_columns() → Tuple[str, ...][source]

The columns the user picked, in the list’s order.

The LIST’s order and not the click order, so the queue is read the same way the picker is – and two users who picked the same three columns get the same three runs in the same order.

set_selected_columns(names: Sequence[str]) → int[source]

Select exactly names. Returns how many were found.

Parameters:

names – column names to select, compared as strings; every other listed column is deselected, and None selects none.

start_regressions(*_args) → bool[source]

Fit every selected column, one run each, off the GUI thread.

Returns:

whether a queue was started. False when nothing is selected, when one is already going, or when the merged frame was never written – and each of those SAYS which it was, because a button that does nothing is the failure this file keeps fixing.

class spacr.qt.widgets.measurement_scan_panel.DatabaseMergePanel(database_provider=None, parent=None, *, threaded: bool = True, destination_provider=None)[source]

Bases: WorkflowSteps, PySide6.QtWidgets.QWidget

The databases attached to the input table, and the join offered.

One row per plate of the regression input table, whether or not it has a database – a plate with none is listed and disabled here, because it still runs in the regression and the user needs to see why it is absent from this tab.

WHAT IS NOT OFFERED IS AS DELIBERATE AS WHAT IS. There is no join-type control: the join follows object cardinality per table through spacr.merge_tables.MergePolicy.how_for(), and a blanket how is the finding the design raised. The two checkboxes here are the two settings that policy actually reads.

THE MERGE RUNS OFF THE GUI THREAD, and it did not used to. Four databases, 226,467 cell rows and three joined tables ran inside the button’s own click handler, so Qt could not paint, could not show a spinner and could not accept a cancel until it returned. The application was not hung; it was working, and had no way to say so – which is the design in full. JobRunner is the idiom every other long job here already uses, and this panel was the one that did not.

Variables:
  • databases_changed – emitted with the number of readable databases whenever the list is re-read.

  • merged – emitted with the merged frame.

  • merge_progress – emitted with (stage, rows done, rows total) as the merge moves. Always on the GUI thread – see _relay_progress().

  • merge_finished – emitted with the frame when a merge completes, or None when it was refused, failed or cancelled.

Parameters:
  • database_provider – called with no arguments for the input table’s rows. A callable rather than a stored list, for the same reason frame_provider is one: the tab must not go on showing the previous run’s inputs.

  • threaded – whether start_merge() runs off the GUI thread. False runs it inline through the same code path, emitting the same signals in the same order, so a test can drive the button synchronously without the behaviour diverging.

  • destination_provider – called with no arguments for the folder the merged frame is written into. Without one the merge still happens and simply leaves no artefact – the panel is used headless and in tests where there is nowhere to write.

  • parent – parent widget; ownership only.

anchor() → str[source]

The table a row of the merge means one of.

cancel_merge(*_args) → bool[source]

Stop a running merge. Nothing half-written survives it.

Returns:

whether there was one to stop.

closeEvent(event)[source]

Do not let a worker outlive the widget it reports to.

Parameters:

event – the close event; it is passed on to the base class after the workers and path probes are stopped.

describe() → str[source]

State what the merge WOULD do, before it is done.

Called on every click, and it CANNOT read the databases on every click. What it needs is sqlite metadata and the distinct plate ids, which is a millisecond on a local disk and was the reason the old comment here called it cheap – but the same read on a share that is asleep did not return at all, and this is reached from a checkbox. So it draws from what refresh asked for, waits at most READ_BUDGET_S for anything not back yet, and says “reading…” for the rest until _on_read_landed() puts it right.

THE COUNT GOES IN THE BOX AND THE NAMES GO BEHIND THE DISCLOSURE (154 B). Putting both in the box is what buried the three lines that matter under a hundred and seventy column names.

Returns:

the whole statement, summary and evidence, as one string – what the panel SAYS, wherever it puts it.

is_merging() → bool[source]

Whether a merge is running right now.

merge(**kwargs)[source]

Merge the chosen tables and report what it cost, RIGHT NOW.

This blocks the calling thread until the whole join is done. On four databases that is minutes, so the GUI must not call it: the Merge button goes through start_merge(), which runs this same work on a JobRunner. It is kept as the synchronous entry point for a headless caller and for a test that wants the frame back on the next line.

Returns:

the merged frame, or None when nothing was merged. A refusal is shown in full rather than summarised: it is an ANSWER, and it says what to do about it.

merged_frame_path() → str[source]

Where the merged frame was written, or "".

The artefact the design asks for: written ONCE when the merge finishes, named, and read by every fit in the column queue rather than the merge being redone per fit.

paths() → Tuple[str, ...][source]

The databases that are attached AND on disk, de-duplicated.

plan_evidence() → str[source]

The column names behind plan_summary()’s counts.

plan_summary() → str[source]

The pre-merge statement as COUNTS – what fits in the box.

plan_text() → str[source]

The whole pre-merge statement: the summary and then its evidence.

Kept whole for a caller that wants everything. What the PANEL shows is plan_summary() in the box and plan_evidence() behind the disclosure – the design.

policy() → spacr.merge_tables.MergePolicy[source]

The merge policy the controls describe.

Note what is NOT here: a join type. how_for derives it per table from cardinality, and these two checkboxes are the only settings that change it.

refresh() → int[source]

Re-read the provider and describe what is attached.

A RE-READ, so the previous paint’s answers are not reused: a measure run may have rewritten a database at a path this panel already knows, and _prepare_merge refreshes precisely to catch that. It is bounded rather than blocking – see _read_off_thread() – so a database that has not answered within the budget is drawn as it was, or as “reading…”, and put right the moment it does.

Returns:

the number of readable databases.

screens() → Dict[str, str] | None[source]

{path: screen} for the rows that named one, else None.

None rather than a dict of defaults: naming a screen for every database says the user is working in screens, and screens_were_named reads that to decide how a refusal is worded.

selected_tables() → Tuple[str, ...][source]

The tables the user has ticked, in table order.

set_anchor(name: str) → None[source]

Choose the anchor, if it is on offer.

Parameters:

name – name of the table to anchor on, set as the anchor box’s current text; a name not offered there leaves the choice unchanged.

set_database_provider(provider) → None[source]

Take a new source of input-table rows and re-read it.

Parameters:

provider – zero-argument callable returning the input table’s rows (see attached_databases() for the accepted shapes); it is called on every refresh, starting immediately.

set_selected_tables(names: Sequence[str]) → None[source]

Tick exactly names.

Parameters:

names – table names to tick, compared as strings; every other listed table is unticked.

show_aggregation_rules() → None[source]

The per-column rules, for the columns actually about to be merged.

Reuses the Gate Editor’s dialog rather than growing a second one: the rules are the same rules, and two editors of one decision is how they come to disagree.

THE PREVIEW READ IS OFF THE GUI THREAD, and it is the one the rest of this panel’s fix had left behind. Everything else here reads sqlite METADATA; this reads ROWS – read_merged opens every attached database and pulls PREVIEW_ROWS rows out of each – and it ran inside the button’s own click handler. On a local disk that is a blink; on the autofs mount this whole exercise came from it is the freeze again, on a button rather than on a tab.

So the click either opens the dialog straight away, as it always did, or says the button is reading and opens it from _on_read_landed() when the databases answer. Nothing is lost either way: the dialog the user asked for still arrives.

start_merge(*_args, **kwargs) → bool[source]

Merge OFF the GUI thread, saying where it is and taking a cancel.

The whole of the design. Everything that touches a widget – re-reading the input table, printing the plan, showing the result – happens here on the GUI thread; the join itself happens on a worker, and the only thing that crosses back is a Signal.

Returns:

whether a merge was started. False when there is nothing to merge, or when one is already running – a second Merge click must not start a second join over the same databases.

statement() → str[source]

EVERYTHING the panel is saying: the box and the disclosure.

The box holds the counts and the disclosure holds the names, so a caller that wants to know whether the panel said something has to read both. Reading only the box would report a column as unnamed when it is one click away.

step_states() → Dict[int, str][source]

Return status text for the first three database-merge steps.

Step 1 reports attached databases, step 2 reports selected object tables and the anchor, and step 3 reports merge progress, output shape, and destination state.

Returns:

Mapping from step number to user-facing status text.

property databases: Tuple[AttachedDatabase, ...][source]

Every plate row, attached or not, in the input table’s order.

property frame[source]

The last merged frame, or None.

property overrides: Dict[str, str][source]

The user’s per-column aggregation choices, which beat every rule.

class spacr.qt.widgets.measurement_scan_panel.MeasurementScanPanel(frame_provider=None, parent=None, database_provider=None, *, threaded: bool = True, destination_provider=None, settings_provider=None, fit=None)[source]

Bases: PySide6.QtWidgets.QWidget

The scan’s result table, and the two numbers behind every row.

Variables:
  • measurement_selected – emitted with the measurement name of the selected row, so a host can draw it.

  • scanned – emitted with the number of measurements scanned.

Initialize the measurement scan and database-merge panel.

Parameters:
  • frame_provider (callable or None, optional) – Zero-argument callable returning the current well-level frame to scan. It is evaluated when needed so a newly loaded run replaces the previous frame.

  • parent (QWidget or None, optional) – Parent widget.

  • database_provider (callable or None, optional) – Zero-argument callable returning the regression input rows and their attached measurement databases.

  • threaded (bool, default=True) – Run database merges outside the GUI thread. Set to False for synchronous use in tests or headless callers.

  • destination_provider (callable or None, optional) – Zero-argument callable returning the directory where the merged measurement frame is written.

  • settings_provider (callable or None, optional) – Zero-argument callable returning the current regression settings. Column fits copy these settings and vary only the response.

  • fit (callable or None, optional) – Function used to fit one selected measurement column. None uses the standard regression implementation.

add_section(widget, title: str = '') → None[source]

Put widget in the tab as its own resizable, foldable section.

Anything added to this tab goes HERE and not into the layout: a widget appended to the layout takes its height out of the others, which is how the sections came to overlap.

Parameters:

widget – the widget to add as a foldable section; None does nothing.

databases_frame()[source]

The merged frame step 3 produced, or None.

A method rather than the attribute, so step 4 reads the CURRENT frame every time instead of a copy taken when it was built.

is_section_expanded(title: str) → bool[source]

Whether one result section is open.

Parameters:

title – the section’s title.

Returns:

True when expanded.

refresh_databases() → int[source]

Re-read the attached databases. Called when the tab is opened.

Returns:

how many readable databases are attached.

remember_section_layout() → None[source]

Store the folds and divider positions. Called when the tab closes.

restore_section_layout() → bool[source]

Put back the folds and divider positions from last time.

Called once the sections exist, including any added later by add_section(), which is why the host calls it rather than the constructor.

Returns:

whether anything was restored.

run_scan(**kwargs) → bool[source]

Scan whatever the provider is holding. Returns whether it ran.

scan(frame, **kwargs) → bool[source]

Scan frame and show the result.

Parameters:

frame – the measurement frame passed to spacr.measurement_scan.scan_measurements() together with kwargs.

section_is_shown(title: str) → bool[source]

Whether a section is on the tab at all – FOLDED OR NOT.

TWO DIFFERENT QUESTIONS, and they used to be asked with one call. _show_section HIDES a section that has nothing to show; a fold merely closes one that does. Testing the content’s visibility answers both at once, so when the sections started closed (2026-08-20) three tests about “the databases appear without a scan” began failing – the databases were there, the section was shown, and its content was simply folded away, which is what folded means.

Parameters:

title – the section’s title; an unknown title returns False.

section_titles() → tuple[source]

What can be folded, in the order the tab shows it.

sections() → tuple[source]

Return the folding sections in display order.

The expanding layout filler is excluded from the returned tuple.

set_database_provider(provider) → None[source]

Take a new source for the input table’s attached databases.

The same shape as set_frame_provider(), and for the same reason: the tab re-reads the rows rather than holding a copy of them.

Parameters:

provider – zero-argument callable returning the input table’s rows (see attached_databases() for the accepted shapes); it is called on every refresh by the databases section.

set_frame_provider(provider) → None[source]

Take a new source for the frame the scan runs on.

Parameters:

provider – zero-argument callable returning the frame the scan runs on, called each time the frame is needed.

set_result(result) → bool[source]

Show an already-computed ScanResult.

Parameters:

result – the ScanResult to show; its frame() and rows fill the table, with a verdict per row.

set_section_expanded(title: str, expanded: bool) → None[source]

Fold or open one section by name. The hook a preference needs.

Parameters:
  • title – the section’s title; an unknown title is ignored.

  • expanded – True to open the section, False to fold it; coerced with bool().

what_is_available() → str[source]

One line naming both halves, and whether their wells meet.

Appended to a refusal, because “no ‘gene’ column” is true and does not say that the measurements next to it cannot be reached either.

why_nothing_to_scan(frame=None) → str[source]

Which half is missing, checked rather than asserted.

The old sentence named two things a well must carry, checked neither, and was shown while four measurement databases were loaded – so it was wrong about the half that was there and silent about the half that was not.

Parameters:

frame – whatever the provider returned, or None.

property result[source]

The last scan’s result, if any.

Returns:

the result, or None before a scan.

class spacr.qt.widgets.measurement_scan_panel.WorkflowStep(number: int, title: str, parent=None, expanded: bool = True)[source]

Bases: PySide6.QtWidgets.QWidget

One numbered step of the Measurements workflow: a fold and a body.

THE THIRD LEVEL OF NESTING ON THIS TAB. Its three panels are splitter children that fold (see MeasurementScanPanel), but the numbered steps inside them were bold labels with the step’s controls loose in the panel’s own column underneath. So “collapse the step I am not on” was not offered at all, and a step could only lose height to its neighbours – never take it back.

The heading is still the WorkflowStep QLabel step_header() makes, inside the header row rather than replacing it: it is what the stylesheet selects on and what a reader recognises. What is new beside it is a checkable arrow that hides the body, focusable so a keyboard reaches it, with an accessible name that says which step it folds.

Parameters:
  • number – one-based step number, as the tab counts them.

  • title – the step’s title, translated by step_header().

  • parent – parent widget; ownership only.

  • expanded – whether it starts open. Steps DO start open: unlike the tab’s three panels, a step is a stage of one procedure and hiding all of them would leave a panel that says nothing about what it does.

One numbered, collapsible step of the scan workflow.

Parameters:
  • number – the step’s position.

  • title – its caption.

  • parent – parent widget.

  • expanded – whether it starts open.

eventFilter(watched, event)[source]

Fold when the heading beside the arrow is clicked.

Parameters:
  • watched – the object the event is for.

  • event – the event.

Returns:

True when the click was consumed as a fold.

fold_button()[source]

The collapse control, for tests and for focus handling.

is_expanded() → bool[source]

Whether the step’s controls are showing.

READ OFF THE BUTTON, not a flag beside it, for the reason CollapsibleSection.is_expanded() gives: a cached copy can disagree with what the user sees.

number() → int[source]

Which step this is, one-based.

set_expanded(expanded: bool) → None[source]

Open or fold the step.

Parameters:

expanded – True to show the step’s controls.

title() → str[source]

The step’s title as written, before translation or numbering.

class spacr.qt.widgets.measurement_scan_panel.WorkflowSteps[source]

The numbered-step half of a Measurements panel, shared by both of them.

A MIXIN AND NOT A BASE CLASS: DatabaseMergePanel and ColumnRegressionPanel are both QWidget already, and the thing they share is bookkeeping rather than a widget. step_folds_changed stays declared on each of them – a Signal on a non-QObject mixin is not connectable.

box_heights() → dict[source]

The dragged heights, in px AT 100 % FONT SCALE.

Divided by the scale on the way out and multiplied on the way back in (set_box_heights()), so a height chosen at 200 % is the same number of lines when it is restored at 100 %.

register_box(key: str, grip) → None[source]

Record a height handle so its drag can be stored and restored.

Parameters:
  • key – stable, untranslated name for the box.

  • grip – the HeightGrip under it.

set_box_heights(heights) → None[source]

Put back what box_heights() returned.

A key this panel does not have is ignored, for the reason set_step_folds() gives.

Parameters:

heights – mapping of box key to height in pixels at 100 % font scale, as box_heights() returns; unknown keys are ignored and None changes nothing.

set_step_folds(folds) → None[source]

Put back what step_folds() returned.

A number this panel does not have is IGNORED rather than an error: a layout stored by a version with five steps must not stop this one from starting.

Parameters:

folds – mapping of step number to True (open) or False (folded), as step_folds() returns; unknown numbers are ignored and None changes nothing.

step_folds() → dict[source]

Which steps are open, by number. The half of the layout to store.

spacr.qt.widgets.measurement_scan_panel.anchor_tables(tables: Sequence[str]) → Tuple[str, ...][source]

The subset of tables that can be an anchor.

One row per cell, from spacr.object_roles.ONE_ROW_PER_CELL. Anchoring on a many-per-cell table would make a row of the merged frame mean one nucleus or one pathogen, with the cell’s own measurements repeated across its children – which is the fan-out the roll-up exists to prevent, arrived at from the other side.

Parameters:

tables – measurement table names; those holding one row per cell are kept, in their given order.

spacr.qt.widgets.measurement_scan_panel.attached_databases(rows: Any) → Tuple[AttachedDatabase, ...][source]

The input table’s rows as AttachedDatabase entries.

Parameters:

rows – what the host’s database provider returned. The shape the input table emits is a list of {"plate", "score", "count", "database"} dicts; a (plate, path) pair, a (plate, path, screen) triple and a bare path are accepted too, so a caller with a plainer list does not have to build dicts to be understood.

Returns:

one entry per row, IN THE ROW ORDER, including the rows with no database – they are the plates this tab has to disable rather than drop, and dropping them here would make them invisible instead.

spacr.qt.widgets.measurement_scan_panel.column_run_settings(base: Dict[str, Any] | None, column: str, score_path: str) → Dict[str, Any][source]

The settings for ONE fit of the queue: this column, this score file.

Parameters:
  • base – the regression screen’s own settings, or None.

  • column – the response to fit.

  • score_path – the merged frame written by write_merged_frame().

A COPY, never the caller’s dict. Twelve fits built by mutating one dict are twelve fits of whatever the last one asked for – and the base is the live settings panel, which the user may be editing while the queue runs.

THE COUNT SIDE IS LEFT ALONE. What varies between these runs is the RESPONSE and nothing else, which is what makes them comparable in the Runs tab; the guides each well got are the same guides.

spacr.qt.widgets.measurement_scan_panel.default_aggregation_columns(columns: Sequence[str], *, overrides: Dict[str, str] | None = None) → Tuple[str, ...][source]

The columns NO AGGREGATION_RULES rule names.

third bullet: a measurement nobody thought about is exactly the one worth naming. These fall through to DEFAULT_AGGREGATION, which is MEAN – right more often than not for an unrecognised number, and silently wrong for a total.

Computed by re-walking the rule table, so it cannot drift from it: a list written here would go stale the first time a rule was added.

Parameters:
  • columns – the column names about to be aggregated.

  • overrides – the user’s explicit choices, which win over every rule and are therefore not fall-throughs.

spacr.qt.widgets.measurement_scan_panel.describe_key_overlap(left_name: str, left, right_name: str, right) → str[source]

Whether two frames’ wells meet, and one example from each side if not.

The sentence the design asks for, and it is computed rather than asserted. "" when the two do overlap, because then the join is not the problem and saying anything about it would send the user the wrong way.

Parameters:
  • left_name – how the first frame is named in the sentence, such as 'merged measurements'.

  • left – the first frame; its well keys come from well_keys().

  • right_name – how the second frame is named in the sentence.

  • right – the second frame; its well keys come from well_keys().

spacr.qt.widgets.measurement_scan_panel.displayed_plates(plates: Sequence[str]) → Tuple[str, ...][source]

Plate ids as the plates are CALLED, in their given order.

Parameters:

plates – plate ids, each passed through canonical_plate_id.

spacr.qt.widgets.measurement_scan_panel.joinable_tables(paths: Sequence[str]) → Tuple[str, ...][source]

The object tables EVERY one of these databases has, in table order.

The intersection, not the union, and this is not a nicety: spacr.multi_database.describe_merge() reads a row count from every path, so asking it for a table one database lacks raises a bare sqlite3.OperationalError naming the table and nothing else. Offering only what all of them have means the user never picks that.

Parameters:

paths – measurement databases.

Returns:

names from spacr.merge_tables.OBJECT_TABLES – the object-role registry, cell/nucleus/pathogen/cytoplasm and every organelle slot, rather than four names typed here – followed by png_list where every database has it.

Raises:

sqlite3.Error – a path that is not a readable database.

spacr.qt.widgets.measurement_scan_panel.merge_across_databases(paths: Sequence[str], tables: Sequence[str], *, policy: spacr.merge_tables.MergePolicy | None = None, screens: Any = None, columns: str = 'common', report=None, limit_per_source: int | None = None, progress=None, cancelled=None, on_ambiguous_identifier: str = 'refuse')[source]

Every chosen table of every chosen database, on one anchor.

THE COMPOSITION OF THE TWO MERGES THAT ALREADY EXIST, and deliberately nothing else. spacr.multi_database.read_merged() is many databases, one table; spacr.merge_tables.merge_tables() is one database, many tables and takes a path, so it cannot be handed a frame that already spans databases. This runs the first per chosen table and then joins them with the second’s own roll_up() and how_for(). There is no sum and no mean written here; every number comes from the rules.

THE ROLL-UP KEYS CARRY THE SCREEN AND THE SOURCE. Omit them and two screens legitimately sharing plate1 – the case describe_merge() deliberately permits – would collapse into one parent, reintroducing one layer up the exact pooling spacr.multi_database exists to prevent.

Parameters:
  • paths – measurement databases. Repeats are read once.

  • tables – the object tables to join. The anchor is added if absent.

  • policy – how each measurement combines and what happens to a cell with no children. policy.primary IS the anchor and defaults to DEFAULT_ANCHOR.

  • screens – screen label per database – a sequence parallel to paths or a mapping from path. Passed to both the plan and the read.

  • columns – 'common' (default) or 'union', as read_merged.

  • report – called with one line per thing the merge cost.

  • limit_per_source – row cap per database, for a preview.

  • progress – called progress(stage, done, total) as the merge moves. stage names the table and the database; done/total are ROWS, against the same total the plan prints. Runs on whatever thread the merge does, so a GUI caller relays it rather than touching a widget in it.

  • cancelled – called between stages; a true answer raises MergeCancelled. Nothing is written anywhere until this function RETURNS, so a cancelled merge leaves the previous result exactly where it was.

  • on_ambiguous_identifier – 'refuse' (default) leaves out a text identifier that differs within a roll-up group and names it; 'first' restores the old silent pick. See spacr.plate_measurements.ambiguous_identifiers().

Returns:

one row per anchor object, with frame.attrs carrying what the merge cost – see merge_report(), which renders it.

Raises:
spacr.qt.widgets.measurement_scan_panel.merge_evidence(frame) → str[source]

The lists behind merge_summary()’s counts. One click away.

Every name the summary counted, so that a user who wants to check the claim can, and one who does not is not made to read it.

Parameters:

frame – the merged frame; the default_aggregation, identifier_columns and dropped_columns entries of its attrs are listed.

spacr.qt.widgets.measurement_scan_panel.merge_report(frame) → str[source]

The whole statement: merge_summary() and then its evidence.

Kept as one string for a caller that wants everything – a log line, a test, a headless script. The PANEL shows the two halves in two places, which is the whole of the design.

Parameters:

frame – the merged frame, passed to merge_summary() and merge_evidence().

spacr.qt.widgets.measurement_scan_panel.merge_summary(frame) → str[source]

What the merge cost, as COUNTS. This is what fits in the box.

The old report put eighty-five column names inline and then another eighty-five, so the three lines that matter – what joined how, how many rows, what the anchor is – were buried in nucleus_channel_2_channel_3_M2_correlation_85 and its brothers.

The COUNT is the sentence. The LIST is the evidence, and evidence goes behind a disclosure.

A refusal is the exception and stays here whatever its length: it is not evidence for a claim, it IS the claim, and a user who never opens the disclosure still has to be told a column was left out.

Parameters:

frame – the output of merge_across_databases().

spacr.qt.widgets.measurement_scan_panel.ordered_columns(frame) → list[source]

PREFERRED_COLUMNS this frame has, then everything else.

Ordering, not filtering – the columns nobody thought to list are still the user’s own numbers.

Parameters:

frame – the frame whose columns are ordered; None returns an empty list.

spacr.qt.widgets.measurement_scan_panel.plate_id_notes(plan) → List[str][source]

Describe noncanonical plate identifiers that remain in a merge plan.

Plate identifiers are normally canonicalized while each database is read. A value that still differs from canonical_plate_id() at this stage was not repaired during import and may fail to match score or count CSVs, whose identifiers are normalized by spacr.utils.correct_metadata().

Parameters:

plan – Merge plan containing database sources and their plate IDs.

Returns:

One warning per affected database, or an empty list when all identifiers are canonical.

spacr.qt.widgets.measurement_scan_panel.regressable_columns(frame) → Tuple[str, ...][source]

The columns of a merged frame a regression could take as its response.

Parameters:

frame – a merged measurement frame, or None.

Returns:

the numeric measurement columns, in the frame’s own order.

NUMERIC AND NOT IDENTITY, and both halves are checked rather than assumed. A merged frame carries plateID, object_label, source_database and the text identifiers merge_across_databases carries through; a picker that offered those would offer a fit onto a well name.

A column of one value is left out too. It has no variance, so the fit is degenerate – and every backend reports that differently, which turns one unusable choice into N different-looking failures.

spacr.qt.widgets.measurement_scan_panel.resizable_box(owner, widget, layout, *, key: str, minimum: int, default: int, maximum: int, name: str)[source]

Give widget a user-draggable height instead of a hard cap.

WHAT THIS REPLACES, and why it was wrong twice over. Every tall box on this tab was pinned with setMaximumHeight(N) at a literal N chosen against a 100 % font. Measured offscreen at a 200 % font scale – a supported accessibility setting – each one lost about half its content and could not be dragged back:

box

cap

@100 %

@200 %

attached databases

170 px

4 rows

3 rows

join list

72 px

4 rows

2 rows

merge report

190 px

11 lines

5 lines

merge evidence

220 px

12 lines

6 lines

column picker

180 px

10 rows

5 rows

outcomes

120 px

7 lines

3 lines

So the height now starts at default SCALED BY THE FONT PREFERENCE – at 200 % the report opens twice as tall and keeps its eleven lines – and a HeightGrip under it drags it anywhere between minimum and maximum. Home’s release-notes panel has had that handle for a while; this is the same class, moved to spacr.qt.widgets.height_grip rather than written a second time.

Parameters:
  • owner – the panel the box belongs to. The handle is registered on it under key so the tab can store and restore the height.

  • widget – the box to make resizable. Added to layout here.

  • layout – the column the box and its handle go into.

  • key – stable name for the stored height. NOT name: that one is read aloud and will be translated one day, and a stored layout keyed on a translated string is a layout that is lost when the user switches language.

  • minimum – floor in px at 100 % font scale.

  • default – opening height in px at 100 % font scale.

  • maximum – ceiling in px at 100 % font scale.

  • name – accessible name for the handle – which box it resizes.

Returns:

the HeightGrip that was installed.

spacr.qt.widgets.measurement_scan_panel.run_column_fits(columns: Sequence[str], settings_for, fit, *, progress=None, cancelled=None, on_result=None) → List[ColumnFit][source]

Fit response columns sequentially while isolating per-column failures.

Parameters:
  • columns (sequence of str) – Response columns in execution order.

  • settings_for (callable) – Called with a column and returns settings for that fit.

  • fit (callable) – Called with the settings and returns the pipeline result.

  • progress (callable, optional) – Called as progress(column, index, total) before each fit.

  • cancelled (callable, optional) – Called between fits; a true result stops the queue.

  • on_result (callable, optional) – Called with each ColumnFit when it completes or fails.

Returns:

list of ColumnFit – One result for every attempted column. A failed fit does not prevent later columns from running.

spacr.qt.widgets.measurement_scan_panel.step_header(number: int, title: str, parent=None)[source]

Create a numbered heading for one database-merge workflow step.

The title is translated before it is uppercased and prefixed with the step number. The returned label uses the WorkflowStep object name for shared stylesheet selection.

Parameters:
  • number – One-based workflow step number.

  • title – Source title to translate and display.

  • parent – Optional Qt parent.

Returns:

Bold, word-wrapped QLabel for the step.

spacr.qt.widgets.measurement_scan_panel.verdict_for(row) → str[source]

One phrase per measurement, from BOTH corrections.

Parameters:

row – one scan row; its survives_across_scan and survives_within_run attributes are read, and missing ones count as false.

spacr.qt.widgets.measurement_scan_panel.well_keys(frame) → Tuple[str, Tuple[str, ...]][source]

(what the key is called, the distinct well keys) for one frame.

prc when the frame carries it, otherwise built from plateID/rowID/columnID – which is what prc IS, and the reason a measurements table with no prc column is still comparable to a regression frame that has one.

Parameters:

frame – the frame to read; its prc column is used when present, else plateID, rowID and columnID joined with underscores. None or a frame without columns gives ("", ()).

Returns:

("", ()) for a frame carrying no well identity at all, which is itself the answer to “why did nothing join”.

spacr.qt.widgets.measurement_scan_panel.write_merged_frame(frame, folder: str, name: str = MERGED_FRAME_NAME, *, report=print) → str[source]

Stage the merged frame: offer it in memory, write the durable copy.

Parameters:
  • frame – the merged frame.

  • folder – where to put it. Created if it is not there.

  • name – what the artefact is called. Only its STEM is used – the suffix belongs to the writer, so the same call produces Parquet where an engine is installed and CSV where none is, and a reader dispatches on what it finds.

  • report – called with one line naming what was written and what it cost; None to say nothing.

Returns:

the path written, or "" when there was nothing to write.

THE ARTEFACT IS STILL WRITTEN. A user can open it, and every fit of a queue then reads the same numbers, which is what made it an artefact rather than a preview in the first place.

WHAT CHANGES IS THAT NOTHING PARSES IT BACK. The frame is already in this process when it is written, and the fits run in this process too, so spacr.frame_handoff.stage() offers it under the path it wrote BEFORE the write returns. A four-plate screen merges to about 2.75 GB: the CSV write cost around 160 seconds before anything read it, and each fit then parsed the whole file back out of the page cache – minutes of CPU with no disk I/O at all, which is what made a working run look hung.

The offer is a WEAK reference, so it cannot keep a multi-gigabyte frame alive after the panel that merged it lets go, and a reader that was offered nothing reads the file exactly as before.

Nested helpers

DatabaseMergePanel._follow_path_probes.corrected(path: str, _answer: bool) → None

Redraw when path is one of ours, ignore every other.

Parameters:
  • path – the path whose answer just changed.

  • _answer – what it changed to; the rows are drawn from paths(), which reads the probe’s cache itself.

spacr/qt/widgets/measurement_scan_panel.py:1984

DatabaseMergePanel._follow_path_probes.let_go(*_args) → None

Drop the probe connection as the panel is destroyed.

Only while still connected: closeEvent may have unfollowed already, and disconnecting twice warns.

Parameters:

_args – whatever destroyed sends; unused.

spacr/qt/widgets/measurement_scan_panel.py:2000

DatabaseMergePanel.show_aggregation_rules.preview()

Read the preview. WORKER THREAD; touches no widget.

Returns:

what read_merged() returns for the chosen table.

spacr/qt/widgets/measurement_scan_panel.py:2904

describe_key_overlap._canonical(keys)

Keys with their plate id canonicalised, so two spellings match.

spacr/qt/widgets/measurement_scan_panel.py:3017

merge_across_databases._say(stage: str) → None

Report the current stage, if anyone is listening.

spacr/qt/widgets/measurement_scan_panel.py:593

merge_across_databases._stop(where: str) → None

Raise if the caller has cancelled, naming where it stopped.

Checked BETWEEN stages rather than only at the start: a merge across databases runs for minutes, and a cancel that is only noticed at the end is not a cancel.

spacr/qt/widgets/measurement_scan_panel.py:598