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¶
The user stopped the queue between fits. |
Classes¶
One plate of the regression input table, seen from this tab. |
|
What one fit of the queue did. |
|
Run one regression per selected column of a merged measurement table. |
|
The databases attached to the input table, and the join offered. |
|
The scan's result table, and the two numbers behind every row. |
|
One numbered step of the Measurements workflow: a fold and a body. |
|
The numbered-step half of a Measurements panel, shared by both of them. |
Functions¶
|
The subset of |
|
The input table's rows as |
|
The settings for ONE fit of the queue: this column, this score file. |
|
The columns NO |
|
Whether two frames' wells meet, and one example from each side if not. |
|
Plate ids as the plates are CALLED, in their given order. |
|
The object tables EVERY one of these databases has, in table order. |
|
Every chosen table of every chosen database, on one anchor. |
|
The lists behind |
|
The whole statement: |
|
What the merge cost, as COUNTS. This is what fits in the box. |
|
|
|
Describe noncanonical plate identifiers that remain in a merge plan. |
|
The columns of a merged frame a regression could take as its response. |
|
Give |
|
Fit response columns sequentially while isolating per-column failures. |
|
Create a numbered heading for one database-merge workflow step. |
|
One phrase per measurement, from BOTH corrections. |
|
|
|
Stage the merged frame: offer it in memory, write the durable copy. |
Module Contents¶
- exception spacr.qt.widgets.measurement_scan_panel.QueueCancelled[source]¶
Bases:
ExceptionThe user stopped the queue between fits.
Not an error and not a refusal, for the same reason
spacr.multi_database.MergeCancelledis 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()asscreens=, which is what keeps two screens sharingplate1apart as two identities instead of one collision.
- 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 reasonspacr.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_proberather than ofos.path, and that is not a style choice. This property is read once per plate row by_fill_table, and again bypaths,screensanddescribe– so a project with eight plates on a sleepingautofsmount 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_probesis the half that corrects the row when the real answer lands.
- 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.
- 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.QWidgetRun 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
Falsefor synchronous tests or headless callers.fit (callable or None, optional) – Function called with one fit’s settings.
Noneuses 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.
- 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.
- 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
Noneselects none.
- start_regressions(*_args) bool[source]¶
Fit every selected column, one run each, off the GUI thread.
- Returns:
whether a queue was started.
Falsewhen 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.QWidgetThe 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 blankethowis 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.
JobRunneris 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
Nonewhen 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_provideris one: the tab must not go on showing the previous run’s inputs.threaded – whether
start_merge()runs off the GUI thread.Falseruns 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.
- 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
refreshasked for, waits at mostREAD_BUDGET_Sfor 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.
- 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 aJobRunner. 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
Nonewhen 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.
- plan_evidence() str[source]¶
The column names behind
plan_summary()’s counts.
- 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 andplan_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_forderives 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_mergerefreshes 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, elseNone.Nonerather than a dict of defaults: naming a screen for every database says the user is working in screens, andscreens_were_namedreads that to decide how a refusal is worded.
- 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_mergedopens every attached database and pullsPREVIEW_ROWSrows out of each – and it ran inside the button’s own click handler. On a local disk that is a blink; on theautofsmount 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.
Falsewhen 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.
- 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.QWidgetThe 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
Falsefor 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.
Noneuses the standard regression implementation.
- add_section(widget, title: str = '') None[source]¶
Put
widgetin 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;
Nonedoes 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.
- scan(frame, **kwargs) bool[source]¶
Scan
frameand show the result.- Parameters:
frame – the measurement frame passed to
spacr.measurement_scan.scan_measurements()together withkwargs.
- 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_sectionHIDES 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.
- 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
ScanResultto show; itsframe()androwsfill 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 –
Trueto open the section,Falseto fold it; coerced withbool().
- 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.
- class spacr.qt.widgets.measurement_scan_panel.WorkflowStep(number: int, title: str, parent=None, expanded: bool = True)[source]¶
Bases:
PySide6.QtWidgets.QWidgetOne 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
WorkflowStepQLabelstep_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.
- 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.
- 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:
DatabaseMergePanelandColumnRegressionPanelare bothQWidgetalready, and the thing they share is bookkeeping rather than a widget.step_folds_changedstays declared on each of them – aSignalon a non-QObjectmixin 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
HeightGripunder 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 andNonechanges 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) orFalse(folded), asstep_folds()returns; unknown numbers are ignored andNonechanges nothing.
- spacr.qt.widgets.measurement_scan_panel.anchor_tables(tables: Sequence[str]) Tuple[str, ...][source]¶
The subset of
tablesthat 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
AttachedDatabaseentries.- 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_RULESrule 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 baresqlite3.OperationalErrornaming 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 bypng_listwhere 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 ownroll_up()andhow_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 casedescribe_merge()deliberately permits – would collapse into one parent, reintroducing one layer up the exact poolingspacr.multi_databaseexists 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.primaryIS the anchor and defaults toDEFAULT_ANCHOR.screens – screen label per database – a sequence parallel to
pathsor a mapping from path. Passed to both the plan and the read.columns –
'common'(default) or'union', asread_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.stagenames the table and the database;done/totalare 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. Seespacr.plate_measurements.ambiguous_identifiers().
- Returns:
one row per anchor object, with
frame.attrscarrying what the merge cost – seemerge_report(), which renders it.- Raises:
MergeError – the anchor is not one row per cell, or carries no object label.
spacr.multi_database.MergeRefused – a plate id appears twice within one screen.
spacr.multi_database.MergeCancelled – the caller asked it to stop.
- 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_columnsanddropped_columnsentries of itsattrsare 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()andmerge_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_85and 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_COLUMNSthis 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;
Nonereturns 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 byspacr.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_databaseand the text identifiersmerge_across_databasescarries 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
widgeta 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
defaultSCALED BY THE FONT PREFERENCE – at 200 % the report opens twice as tall and keeps its eleven lines – and aHeightGripunder it drags it anywhere betweenminimumandmaximum. Home’s release-notes panel has had that handle for a while; this is the same class, moved tospacr.qt.widgets.height_griprather than written a second time.- Parameters:
owner – the panel the box belongs to. The handle is registered on it under
keyso the tab can store and restore the height.widget – the box to make resizable. Added to
layouthere.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
HeightGripthat 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
ColumnFitwhen 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
WorkflowStepobject 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
QLabelfor 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_scanandsurvives_within_runattributes 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.prcwhen the frame carries it, otherwise built fromplateID/rowID/columnID– which is whatprcIS, and the reason a measurements table with noprccolumn is still comparable to a regression frame that has one.- Parameters:
frame – the frame to read; its
prccolumn is used when present, elseplateID,rowIDandcolumnIDjoined with underscores.Noneor 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;
Noneto 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
pathis 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:
closeEventmay have unfollowed already, and disconnecting twice warns.- Parameters:
_args – whatever
destroyedsends; 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