GUI components

The GUI is a PySide6 application organised around a MainWindow that hosts the four persistent panels (data browser, plot, fit, Fourier) plus a log dock and a workspace selector for the time / individual-groups / frequency views. Non-modal dialog windows (Grouping, Detector Layout, Fit Wizard, Global Fit Wizard, Run Info, GLE setup) are launched from the main window. The end-user reference for the GUI lives in GUI usage; this API page lists the classes for developers who need to embed or extend the components, not for day-to-day use.

Main application

Application entry point for the Asymmetry GUI.

Launch with:

python -m asymmetry.gui.app
# or via the installed console script:
asymmetry-gui
asymmetry.gui.app.main() None[source]

Main window

class asymmetry.gui.mainwindow.MainWindow[source]

Bases: QMainWindow

Top-level application window for the Asymmetry μSR analysis GUI.

Orchestrates all panels (data browser, plot, fit, Fourier, log) and manages the central data flow between them. Also owns project-file save/load and the recent-projects list stored in QSettings.

Project files (.asymp)

The collect_project_state / restore_project_state pair serialises and deserialises the full application state as a versioned JSON file. Source data files are referenced rather than embedded, so each relevant data file must remain accessible at its original path (or at the same relative path from the .asymp file) for the project to reopen cleanly. Missing files are skipped with a WARNING log entry; the rest of the session restores normally.

Saved state includes:

  • List of loaded datasets with source-file paths and field overrides

  • Co-added (combined) dataset group definitions

  • Data browser sort column, active filters, and selected runs

  • Plot panel axis limits and fit-curve overlay

  • Single-fit and global-fit model selection and parameter table contents

  • Fourier panel window, zero-pad factor, and display mode

Previously not saved (now persisted where available):

  • Raw asymmetry / time / error arrays (reloaded from source files)

  • Fit result statistics (χ², uncertainties) — only the fitted parameter values that were written back to the parameter table

  • Fourier transform output (cached spectra are now saved when computed)

__init__() None[source]
showEvent(event) None[source]
set_compact_mode(enabled: bool) None[source]

Legacy compatibility shim after compact-mode removal.

eventFilter(obj: QObject, event: QEvent) bool[source]

Record inspector-dock closes into the per-representation memory.

The dock X button sends a Close event; programmatic hide() does not — so this captures exactly the user closes consumed by _apply_inspector_for_domain.

collect_project_state() dict[source]

Return a full serialisable snapshot of the current application state.

Returns:

Project state suitable for passing to save_project().

Return type:

dict

restore_project_state(state: dict, project_path: str) None[source]

Restore the full application state from a project file state dict.

Owns the _restoring_project flag for its duration (the open-project caller also sets it — idempotent) so direct callers get the same suppressions: no dirty-marking, and no FFT compute-on-view seeding. Restore replays domain/selection changes against a HALF-restored window — the workspace state lands before the project model — so a mid-restore seed would record a recipe into the outgoing model, and its in-flight compute would then block the real recipe recompute against the restored one.

Parameters:
  • state (dict) – Validated project state as returned by load_project().

  • project_path (str) – Absolute path to the project file (used to resolve relative paths).

closeEvent(event) None[source]

Stop background work and save plot axis ranges before closing.

staticMetaObject = PySide6.QtCore.QMetaObject("MainWindow" inherits "QMainWindow": Methods:   #39 type=Signal, signature=_background_log(QString), parameters=QString )

Panels

Data Browser

class asymmetry.gui.panels.data_browser.DataBrowserPanel(parent: QWidget | None = None)[source]

Bases: QWidget

Logbook-style run table with grouping, sorting, filtering and co-add.

dataset_selected
selection_changed
group_selected
get_info_requested
grouping_requested
assign_profile_requested

(run_numbers, profile_name) — assign the runs to the named grouping profile (schema v17); MainWindow re-resolves and records the change.

extra_columns_changed
refit_coadded_requested
combine_status
datasets_changed
fit_group_requested
show_group_series_requested
ungroup_requested
group_membership_changed
__init__(parent: QWidget | None = None) None[source]
batch_updates()[source]

Defer table rebuilds while adding or removing many datasets.

Inside the block, _rebuild_table, _sort_table and _resize_columns_to_content record that they were requested instead of running; the outermost exit replays each at most once. Callers must not read table rows inside the block (model state — _datasets, _display_order — is always current). Nesting is safe.

set_project_model(project_model: ProjectModel) None[source]

Bind the panel to the canonical group registry (D6).

MainWindow calls this whenever it (re)creates the project model — on construction and after a project load replaces the model — so the panel always reads and mutates the live data_groups. Collapse flags for ids no longer present are dropped; the group view is rebuilt from the (possibly freshly loaded) registry.

sync_groups_from_project_model() None[source]

Rebuild the group view from the canonical registry (D6).

Ensures every group carried by a legacy browser_state.data_groups block (a pre-registry project, or a standalone panel restore) exists in the registry, then integrates the registry’s groups into the display order, re-derives multi-membership, and repaints. Idempotent — safe to call after a load rebinds a fresh model or after a browser-only restore.

all_datasets() list[MuonDataset][source]

Return the browser’s datasets in insertion order.

next_derived_run_number() int[source]

Reserve a run number for a synthetic or degraded run.

Numbers start at 90001 and skip anything already loaded or reserved this session. Real ISIS run numbers can exceed this (modern runs are six digits), so a file loaded later could carry the same number — add_dataset keys entries by run number and would replace the derived run. Acceptable for session-scoped derived data; revisit if derived runs gain a persistent identity.

release_derived_run_number(number: int) None[source]

Return a reserved derived run number that was never used.

Lets a dialog hand back the number it reserved when generation fails, so a failed Generate does not leave a permanent gap in the SIM series. A number already claimed by a loaded dataset is left untouched.

add_dataset(dataset: MuonDataset) None[source]
create_data_group(run_numbers: list[int], name: str | None = None, group_id: str | None = None, collapsed: bool = False, kind: str = 'user', order_key: str = 'run') str | None[source]

Create a group over run_numbers in the canonical registry (D6).

Multi-membership (D2): this adds a group; it never strips its runs from any group they already belong to. Requires at least two valid (loaded) runs. kind is "user" or "auto" (a machine-minted batch group, D3); order_key seeds the group’s member ordering. Returns the new group id, or None when fewer than two runs are valid or group_id already exists.

ungroup(group_id: str, *, orphan_series: bool = True) list[str][source]

Dissolve group_id entirely, returning its now-orphaned runs to top level.

Members still belonging to another group keep their rows; only members left with no group return to the ungrouped list (at the group’s former slot). orphan_series (D7) selects the disposition of any series the group owns: True freezes each to a standalone legacy analysis (group_id=None), False deletes them. The host prompts for this choice (see ungroup_requested); the browser passes it straight through and returns the removed batch_ids (empty when freezing) so the host can clean up their fit slots / panels.

add_runs_to_group(run_numbers: list[int], group_id: str) bool[source]

Add existing dataset runs into an existing group as a new membership (D2).

Multi-membership: a run already in another group keeps that membership and gains this one (rendered as a marked copy); it is never stolen. A run gaining its first membership leaves the ungrouped list.

Returns:

True if at least one run was added.

Return type:

bool

remove_runs_from_group(run_numbers: list[int]) bool[source]

Remove each run’s primary group membership (D2).

A run in several groups loses only its primary membership; the earliest remaining copy is promoted to primary. A run’s last membership removal returns it to the ungrouped list. Returns True when at least one membership was removed.

multi_period_run_numbers() set[int][source]

Return run numbers of loaded datasets that carry more than one period.

resizeEvent(event) None[source]

Keep the columns filling the panel as the dock is resized.

Deferred one event-loop turn: the table’s viewport geometry settles via the layout pass after this event, so an immediate fit would read the previous width.

changeEvent(event) None[source]

Re-apply two-line row heights when fonts change.

The heights are explicit pixels computed from the table font at rebuild time; a font change (UI scale) would otherwise leave the comment line clipped until the next rebuild.

extra_columns() list[ExtraColumn][source]

Return a copy of the current extra-column definitions (custom + metadata).

custom_label_fields() list[tuple[str, str]][source]

Return (label, "custom:<id>") pairs for the user’s custom columns.

These are the columns offered as the plot legend label and the parameter trend x-axis. Their per-run values live in dataset.metadata["custom_fields"] keyed by the same id, so consumers resolve them without reaching back into the browser.

custom_values_by_run() dict[int, dict[str, str]][source]

Map each known run number to its live custom-column values.

The source of truth for custom columns is dataset.metadata["custom_fields"] (keyed by column id). Exposing it per run lets trend consumers re-link existing batch results to a column that was added or populated after the fit completed (the ordering trap) without re-running the batch. A run with no custom values maps to an empty dict so consumers can clear stale snapshots, not just add.

Combined (co-added/subtracted) datasets are included here too: their dataset object lives in _datasets under the combined run number, same as any regular run, so a single pass over _datasets already covers them. _combined_datasets maps a combined run number to its list of source run numbers, not to a dataset, and must not be iterated as (run_number, dataset) pairs.

angle_x_field() tuple[str, str] | None[source]

Return (label, id) for the special Angle field, or None if absent.

The Angle field is promoted to a first-class trend x-axis rather than offered among the generic custom columns, so consumers route it separately.

add_extra_column(field_key: str) None[source]

Add a metadata-backed dynamic column to the browser table.

add_custom_column(label: str) ExtraColumn | None[source]

Create an empty, user-editable custom column and return its definition.

has_angle_column() bool[source]

Return True when the singleton special “Angle” field exists.

add_angle_column() ExtraColumn | None[source]

Add the singleton special “Angle (°)” field (numeric degrees per run).

Returns the existing Angle field if one is already present, so the action is idempotent and never produces a duplicate.

rename_extra_column(column_id: str, new_label: str) bool[source]

Rename a column’s gui-facing label (its source_key is untouched).

remove_extra_column(field_key_or_id: str) None[source]

Remove an extra column by id or (for metadata columns) source key.

get_extra_columns() list[str][source]

Return the metadata source keys currently shown (for inclusion tracking).

Custom columns have no metadata source and are intentionally excluded — this drives the Get Info “Include in Data Browser” checkboxes, which only concern NeXus/metadata fields. The from-log pseudo-keys are appended to mirror the fixed T/B columns’ include state.

set_use_temperature_from_log(enabled: bool) None[source]

Set the global temperature-from-log display option.

use_temperature_from_log() bool[source]

Return the global temperature-from-log display option.

set_dataset_temperature_from_log(run_number: int, enabled: bool) None[source]

Override temperature-from-log display for a single dataset.

dataset_uses_temperature_from_log(run_number: int) bool[source]

Return whether one dataset is configured to show log temperature.

set_use_field_from_log(enabled: bool) None[source]

Set the global field-from-log display option (B from the data log).

The analogue of set_use_temperature_from_log() (WiMDA’s Bfromaveinblog): show the mean of the magnetic-field log channel in the B column instead of the header scalar.

use_field_from_log() bool[source]

Return the global field-from-log display option.

set_dataset_field_from_log(run_number: int, enabled: bool) None[source]

Override field-from-log display for a single dataset.

dataset_uses_field_from_log(run_number: int) bool[source]

Return whether one dataset is configured to show log field.

displayed_coordinate(run_number: int) dict[str, float | None][source]

The field/temperature the browser currently displays for a run.

Honours the log toggles and per-dataset overrides — the same resolution as the fixed B/T columns (_field_for_display(), _temperature_for_display()) — so a batch trend and its CSV export plot each run at the abscissa the user sees in the table, not the parked header setpoint. When the browser shows a logged value it replaces the scalar; an axis with no recorded value is None (off that axis), never 0 — preserving the trend’s “missing coordinate” semantics rather than the columns’ 0.0 fallback.

custom_column_value(dataset: MuonDataset, column_id: str) str[source]

Return the stored per-run text for a custom column ("" if unset).

get_selected_group_ids() list[str][source]
get_current_selection_key() int | str | None[source]

Return the key for the current row, if any.

get_current_dataset() MuonDataset | None[source]

Return dataset on the current table row when that row is a run.

is_single_group_selected() bool[source]

Return True when the selection contains exactly one group header row.

get_group_name(group_id: str) str | None[source]
get_group_id_for_run(run_number: int) str | None[source]

Return run_number’s primary data-group id, if any (D2).

Compatibility accessor: under multi-group membership a run may belong to several groups; this returns only the primary (first) membership. Callers that need every membership use get_group_ids_for_run(). (Phase 3 reworks the fit-flow callers that still assume single membership.)

get_group_ids_for_run(run_number: int) list[str][source]

Return every group id run_number belongs to, primary first (D2).

get_group_member_run_numbers(group_id: str) list[int][source]

Return run numbers currently belonging to group_id.

get_all_group_ids() list[str][source]

Return every current group id, from the canonical registry (D6).

get_dataset(run_number: int) MuonDataset | None[source]
set_grouping_profile_provider(provider: Callable[[], list] | None) None[source]

Inject a callable returning the project’s live grouping profiles.

The panel derives the per-run profile marker and the Assign-Grouping-Profile context menu from it; a provider (rather than a copied list) keeps the panel current when MainWindow rebuilds the profile list on Apply.

set_highlighted_runs(run_numbers: set[int] | None) None[source]

Tint run_numbers with the FitSeries membership indicator (red tint).

Pass an empty set or None to clear all highlights. The tint is purely decorative — it never alters the table’s selection state.

select_runs(run_numbers: set[int] | list[int]) None[source]

Perform a true selection of run_numbers in the browser table.

This is a real selection (drives selection_changed, updates the fit panel, etc.), distinct from the decorative red tint applied by set_highlighted_runs(). Existing selection is replaced. Runs not currently visible in the table are silently skipped.

get_selected_datasets() list[MuonDataset][source]
get_all_datasets() list[MuonDataset][source]

Return all datasets currently present in the browser.

is_combined_dataset(run_number: int) bool[source]

Return True when run_number refers to a combined row.

get_combined_source_datasets(run_number: int) list[MuonDataset][source]

Return hidden source datasets for a combined row.

rebuild_combined_dataset(run_number: int) MuonDataset | None[source]

Recompute one combined dataset from its hidden source datasets.

render_logbook_tsv() tuple[str, int][source]

Render the TSV logbook to a string, reading widget/grouping state.

Returns (content, exported_rows). The render reads the table model (columns, grouping, every dataset) so it must run on the GUI thread; the caller writes content to disk (off-thread where it matters). The export includes rows hidden by filters or collapsed groups.

export_logbook_tsv(path: str) int[source]

Render and write the TSV logbook to path (synchronous helper).

render_logbook_rtf() tuple[str, int][source]

Render the RTF logbook to a string, reading widget/grouping state.

Returns (content, exported_rows). Like render_logbook_tsv(), the render reads the table model and must run on the GUI thread; the caller writes the bytes. Includes rows hidden by filters/collapsed groups.

export_logbook_rtf(path: str) int[source]

Render and write the RTF logbook to path (synchronous helper).

apply_degrade_statistics(run_number: int, factor: float, seed: int) MuonDataset | None[source]

Resample run_number’s histograms by factor; add the derived run.

Returns the new browser dataset, or None when the source has no detector histograms or the factor is invalid. The source run is never modified (WiMDA degraded in place; see docs/porting/simulate-mode/).

eventFilter(watched, event)[source]
sort_by_run_number_if_unordered() bool[source]

Apply an ascending run-number sort when rows aren’t already in run order.

Called after a multi-file load (F8): an OS file picker can hand files back in a scrambled order, so the browser’s insertion order need not be ascending by run — which makes a shift-range selection silently non-contiguous. This only nudges the default case: if the user has already chosen a sort column, or the rows are already ascending by run, it does nothing (and never reorders a run inside a data group). Returns True when it applied the sort.

shutdown_workers(timeout_ms: int = 10000) None[source]

Stop background combine work (call from the main window’s closeEvent).

The browser is a docked widget, so its own closeEvent never fires; the main window shuts it down alongside the other panel runners.

clear() None[source]
add_combined_dataset(source_run_numbers: list[int], *, sign: int = 1, operation: str | None = None) int | None[source]

Recreate a combined row programmatically (.asymp load).

sign=+1 co-adds; sign=-1 subtracts. operation disambiguates the subtractions: "subtract_signed" is the symmetric N-run signed co-subtract (sample − every other source), any other value (or None with sign=-1) is the two-run reference subtraction. Co-add requires identical grouping; the subtractions only need the count-level invariants (combine_runs checks them), so the stricter grouping gate is skipped for them.

get_state() dict[source]
restore_state(state: dict) None[source]
staticMetaObject = PySide6.QtCore.QMetaObject("DataBrowserPanel" inherits "QWidget": Methods:   #33 type=Signal, signature=dataset_selected(int), parameters=int   #34 type=Signal, signature=selection_changed()   #35 type=Signal, signature=group_selected(QString), parameters=QString   #36 type=Signal, signature=get_info_requested(int), parameters=int   #37 type=Signal, signature=grouping_requested(int), parameters=int   #38 type=Signal, signature=assign_profile_requested(PyObject,QString), parameters=PyObject, QString   #39 type=Signal, signature=extra_columns_changed()   #40 type=Signal, signature=refit_coadded_requested(PyObject), parameters=PyObject   #41 type=Signal, signature=combine_status(QString), parameters=QString   #42 type=Signal, signature=datasets_changed()   #43 type=Signal, signature=fit_group_requested(QString), parameters=QString   #44 type=Signal, signature=show_group_series_requested(QString), parameters=QString   #45 type=Signal, signature=ungroup_requested(QString), parameters=QString   #46 type=Signal, signature=group_membership_changed() )

Plot panel

class asymmetry.gui.panels.plot_panel.PlotPanel(parent: QWidget | None = None, *, domain: str = 'time')[source]

Bases: QWidget

Matplotlib canvas for time- and frequency-domain plots.

Notes

The bunch factor controls both the plotted representation and the dataset prepared for fitting in the GUI. The stored source dataset remains unchanged, and any rebinned fit dataset is produced as a temporary copy.

bunch_factor_changed
fit_range_changed
view_limits_changed
polarization_axis_changed
fit_target_projection_changed

Emitted with the projection label when a stacked subplot is clicked to become the single-fit target (multi-subplot / ALL view only).

overlay_toggled
waterfall_changed

Emitted when the waterfall toggle or manual-Δ field changes and the host should re-render the overlay (mirrors overlay_toggled).

time_view_changed
cursor_coords_changed
moments_window_changed

window in canonical MHz.

Type:

Spectral-moments overlay drags (frequency panel)

moments_cutoff_changed
moments_drag_finished

A moments-handle drag has ended (mouse released after an actual move): the signal for “recompute with full bootstrap uncertainty now”.

grouping_hint_requested

The user clicked the “Open Grouping…” link in the transverse-field grouping-hint bar (see set_grouping_hint()); the host opens the grouping dialog for the current run.

grouping_hint_dismissed

The user dismissed the grouping-hint bar (the ✕ button); the host records the dismissal so the nudge stays hidden for that run.

__init__(parent: QWidget | None = None, *, domain: str = 'time') None[source]
set_grouping_hint(text: str | None) None[source]

Show or hide the transverse-field grouping-nudge bar.

text may contain rich text with an <a href=…>Open Grouping…</a> link; a falsy value hides the bar. Idempotent and safe to call on a matplotlib-less panel (it simply no-ops).

set_frequency_axis_mode(mode: str) None[source]

Set the frequency x-axis transform mode programmatically.

mode is one of "absolute", "shift", or "relative_ppm".

frequency_axis_mode() str[source]

Return the current frequency x-axis transform mode.

is_frequency_axis_relative_to_reference() bool[source]

Return whether the frequency x axis is a reference-shifted axis.

True for both shift modes ("shift" and "relative_ppm"); the phase-estimation window logic narrows around the reference whenever the view is reference-shifted, so this stays a single predicate.

get_frequency_view_window_mhz(*, reference_dataset: MuonDataset | None = None) tuple[float, float] | None[source]

Return the current frequency-view x window in canonical absolute MHz.

The window is inverted through the ACTIVE dataset’s resolved reference — the phase-estimation window this feeds is a per-active-run concept, so an explicit reference_dataset (when the caller knows the active run) overrides the stored active reference.

decimation_chip_text(ax=None) str | None[source]

Return the decimation chip text on ax (None when not displayed).

active_domain() str[source]

Return the fixed domain represented by this panel.

has_plot_content() bool[source]

Return True when the panel currently holds plotted datasets.

set_custom_label_fields(fields: list[tuple[str, str]]) None[source]

Set the data-browser custom columns offered as legend-label options.

fields is a list of (display_label, "custom:<id>") pairs. The combo is rebuilt (selection preserved) and, if the active label field is a custom column whose label changed, the plot is redrawn so the legend tracks the rename.

set_custom_values_by_run(values_by_run: dict[int, dict[str, str]]) None[source]

Set the run_number -> {column-id: value} custom-label fallback.

Datasets that do not carry custom_fields inline — averaged FFT spectra copy their metadata from a per-group source that has none — can still resolve a custom-column label from this host-provided map, keyed by run number. Redraws when a custom field is active so the change is reflected immediately (e.g. a value edited after the spectrum was drawn).

current_time_view_mode() str[source]

Return current main time-domain plot mode.

set_time_view_modes(modes: list[str], current_mode: str | None = None) None[source]

Update the explicit time-domain view selector.

set_current_time_view_mode(mode: str, *, emit_signal: bool = False) None[source]

Select the active time-domain view mode.

is_overlay_enabled() bool[source]

Return whether multi-selection overlays are currently enabled.

set_overlay_enabled(enabled: bool, *, emit_signal: bool = False) None[source]

Set overlay mode from state restore or external UI events.

is_waterfall_enabled() bool[source]

Return whether waterfall stacking is currently enabled.

set_waterfall_enabled(enabled: bool, *, emit_signal: bool = False) None[source]

Set waterfall mode from state restore or external UI events.

waterfall_offset() float | None[source]

Return the manual waterfall Δ, or None when spacing is automatic.

set_waterfall_offset(offset: float | None, *, emit_signal: bool = False) None[source]

Set the manual waterfall Δ (None for auto), updating the field.

set_active_label_group(group_id: str | None) None[source]

Switch legend label-field context between ungrouped and Data Group views.

set_counts_rebunch_provider(provider: Callable[[MuonDataset, int], tuple[ndarray, ndarray, ndarray] | None] | None) None[source]

Install the counts-first re-reduction hook for display bunching.

provider(dataset, factor) re-runs the dataset’s recorded reduction (deadtime/background corrections included) with the panel’s bunch factor multiplied onto the grouping bunching_factor, returning (time, asymmetry, error) on the percent scale — or None when counts-first bunching does not apply (no raw histograms, non-fixed binning, period-combined curves), in which case get_analysis_dataset() falls back to the curve-level rebin.

get_analysis_dataset(dataset: MuonDataset | None) MuonDataset | None[source]

Return the dataset that should be used for plotting and fitting.

When the bunch factor is 1, the original dataset is returned. For a larger bunch factor, a rebinned MuonDataset copy is returned with the same metadata and run association. Runs that still carry raw histograms are re-reduced counts-first through the host-injected provider; histogram-less curves (co-added data, G±R combinations) keep the curve-level rebin().

update_frequency_reference(dataset: MuonDataset | None) None[source]

Public entry-point: sync the reference field from dataset metadata.

Call this whenever the active dataset changes so the Reference spinbox shows the correct value even before an FFT has been computed.

get_fit_dataset(dataset: MuonDataset | None) MuonDataset | None[source]

Return dataset restricted to the currently selected fit range.

get_fit_range() tuple[float | None, float | None][source]

Return the active fit range as (x_min, x_max).

get_view_limits() tuple[float, float, float, float][source]

Return the currently displayed x/y limits from the toolbar fields.

view_reframed_on_last_draw() bool[source]

Whether the most recent plot draw first-paint framed the axes.

Lets a caller that re-applies preserved limits after a draw (the frequency render path) tell a same-content recompute — which must keep the user’s window — apart from a genuine first paint, whose freshly computed smart framing must survive rather than be overwritten by stale preserved defaults.

set_view_limits(x_min: float, x_max: float, y_min: float, y_max: float) None[source]

Apply x/y limits through the existing toolbar-backed controls.

get_bunch_factor() int[source]

Return the currently configured plot-panel bunch factor.

decimation_enabled() bool[source]

Return whether display-only plot decimation is enabled.

set_decimation_enabled(enabled: bool, *, redraw: bool = True) None[source]

Enable or disable display-only plot decimation.

set_bunch_factor(value: int, *, emit_signal: bool = True) None[source]

Set the plot-panel bunch factor and optionally emit the normal signal.

set_custom_x_axis_label(label: str | None) None[source]

Pin (or clear) a custom x-axis label that survives replots.

A replot recomputes and overwrites the x-axis label, so a caller that wants a bespoke label — for example a Fourier annotation workflow labelling a hyperfine-coupling axis — pins it here. It is re-applied after every render (including the show-time replot) instead of being wiped. Pass None to restore the computed label.

add_persistent_frequency_marker(freq_mhz: float, label: str = '') None[source]

Pin a vertical frequency-line marker that survives replots.

freq_mhz is a canonical (absolute) frequency in MHz; it is display-unit converted at draw time so the marker tracks the active axis unit. The marker is recreated on every render, so — unlike a one-off axvline drawn straight onto the axis — it is not wiped by the show-time replot. Use for Fourier line labels (ν₁, ν₂, A_µ, γ_μ·B).

clear_persistent_frequency_markers() None[source]

Remove every pinned frequency-line marker and redraw.

get_current_polarization_axis() str | None[source]

Return current polarization selector key (P_* or ALL).

set_projections(projections: list[dict], selected: list[str] | None = None) None[source]

Show/update the projection chip bar, or hide it when unavailable.

projections is an ordered list of {"label", "tint"?} dicts; selected is the subset of labels to show as subplots (defaults to all). The bar (and any multi-projection behaviour) is suppressed when fewer than two projections exist.

selected_projection_labels() list[str][source]

Return the projection labels currently selected.

The chip bar is the source of truth once populated; the stored copy is the fallback during project restore, before the bar has been rebuilt.

projection_tint(label: str | None) str | None[source]

Return the frame/identity tint for a projection label, if any.

fit_target_projection() str | None[source]

Return the projection whose subplot is the active single-fit target.

Only meaningful in the stacked multi-subplot view; None otherwise.

set_fit_target_projection(label: str | None, *, emit: bool = True) None[source]

Mark label’s subplot as the single-fit target and redraw its box.

The fit target is also the focus for the manual Y-limit controls, so a change caches the outgoing subplot’s limits and surfaces the incoming subplot’s into the Y fields.

plot_vector_subplots(datasets_by_axis: dict[str, list[MuonDataset]]) None[source]

Render the selected projections as stacked subplots.

plot_grouped_time_domain_subplots(datasets: list[MuonDataset]) None[source]

Render grouped time-domain traces as stacked subplots.

plot_maxent_reconstruction(datasets: list[MuonDataset], *, combined: bool = False) None[source]

Overlay the MaxEnt time-domain reconstruction on the measured data.

Each dataset carries the observed signal in asymmetry and the forward-model reconstruction + weighted residual in metadata['maxent_model'] / ['maxent_residual']. Two layouts share the same data and χ²:

  • per-group (default): one main axis (data points + model line) above a residuals strip per group, stacked vertically;

  • combined (combined=True): every group’s data+model overlaid on a single colour-coded axis above one shared residuals strip.

The displayed χ² is the engine’s by construction (the residuals are (data model)/σ), so both layouts report the same total.

set_fit_range(x_min: float, x_max: float) None[source]

Set fit range limits and refresh visual handles.

plot_datasets(datasets: list[MuonDataset]) None[source]

Plot multiple datasets on the same axes with per-dataset colours.

Each dataset is assigned a colour from matplotlib’s default cycle (C0, C1, …). Any stored fit curve for a run is drawn in a matching style, with high-contrast overrides for period-mode datasets. Low-count (grey) points are still drawn at reduced opacity. The axes limits are initialised from the combined data extent on the first call, then held fixed on subsequent redraws.

Delegates to plot_dataset() when datasets has exactly one entry so that the single-dataset code path (limit initialisation, fit-range handling, etc.) is exercised unchanged.

set_diamagnetic_overlay(time_us: ndarray | None, signal: ndarray | None, *, run_number: int | None = None) None[source]

Set (or clear) the diamagnetic-fit curve drawn on the time-domain view.

Passing None for the arrays clears it. The overlay is tagged with run_number and only drawn while that run is displayed, so it never lingers on an unrelated run; it is redrawn on the next plot_dataset() call for a time-domain panel.

plot_dataset(dataset: MuonDataset) None[source]

Plot a dataset, optionally rebinned according to the bunch factor.

The input dataset is stored unchanged as the current dataset. If the bunch factor is greater than 1, temporary rebinned arrays are created for plotting. The source dataset itself is never mutated.

current_frequency_dataset() MuonDataset | None[source]

Return the active frequency-domain dataset, or None.

active_spectrum_for_moments() tuple[ndarray, ndarray, ndarray | None, str] | None[source]

W15 accessor: (x, amplitude, errors, x_unit) for the active spectrum.

x is the canonical absolute MHz axis (the unit-invariant the moments feature works in) and x_unit is "mhz"; the caller converts to its chosen field unit. Returns None when no spectrum is shown or the active spectrum is a correlation axis (a hyperfine coupling, not a field).

set_moments_overlay(*, window_mhz: tuple[float, float] | None, cutoff_fraction: float, peak_amplitude: float | None, visible: bool) None[source]

Show/update the moment window + cutoff line on the spectrum plot.

window_mhz is the analysis window in canonical absolute MHz; the cutoff line is drawn at peak_amplitude · cutoff_fraction. Drawn only on the frequency panel.

clear_moments_overlay() None[source]

Hide the moment window/cutoff overlay.

plot_fit(t_fit, y_fit, label: str = 'Fit', component_curves: list[tuple[str, object]] | None = None, fit_result: object | None = None, fit_function: str | None = None, run_number: int | None = None) None[source]

Overlay a fit curve on the current plot.

The fit curve will be retained even when bunching or limits change.

Parameters:
  • t_fit (array) – Time points for the fit curve.

  • y_fit (array) – Fitted asymmetry values.

  • label (str, optional) – Label for the fit curve in the legend.

  • fit_result (object, optional) – FitResult containing chi_squared, parameters, uncertainties, etc.

  • run_number (int, optional) – Run the fit was computed for. In a multi-run overlay stacked view _current_dataset points at the first projection’s last run, not the fitted (selected) run, so the caller passes the fitted run explicitly to keep the overlay key and the persisted single-fit slot on the same run. Defaults to inferring it from _current_dataset.

set_global_fits(fit_curves_dict: dict) None[source]

Set fit curves from global fitting.

Parameters:

fit_curves_dict (dict) – Dictionary mapping run_number -> (t_fit, y_fit, label, component_curves), (t_fit, y_fit, label, component_curves, fit_result), or (t_fit, y_fit, label, component_curves, fit_result, fit_function), or (t_fit, y_fit, label, component_curves, fit_result, fit_function, axis_key).

clear(*, message: str | None = None, preserve_view_state: bool = False) None[source]

Clear the plot and reset stored data.

When message is given, draw it as a centred grey placeholder over the (axis-off) plot area — the empty-state pattern used by asymmetry.gui.panels.alc_panel.AlcPanel.clear(). The frequency panel passes a message to prompt the user to compute a spectrum (an FFT is computed on demand, never automatically); every other caller leaves message None and gets an unchanged blank plot.

preserve_view_state keeps the frame latches (_limits_initialized, _limits_user_locked, _framed_identity) rather than resetting them. It is for transient empty states — browsing past an uncomputed run, or an empty-spectrum render — where the blank is momentary and the user’s chosen window must survive to the next compute. Genuine teardown (project close/new, dataset removal) leaves it False so a fresh view reframes from scratch. It does not touch the spin fields, which clear() never reset; callers re-apply the preserved limits explicitly.

resizeEvent(event) None[source]

Keep the canvas width aligned with the viewport during grouped scrolling.

clear_fit() None[source]

Clear all fit curves and redraw the plot.

clear_fits_for_runs(run_numbers: list[int]) int[source]

Clear stored fit overlays for the provided run numbers.

get_current_plot_export_data(datasets: list[MuonDataset] | None = None) list[dict] | None[source]

Return export payloads for all displayed datasets.

Returns a list of dicts (one per displayed dataset), or None when nothing is available to export.

export_plotted_data_as_text() None[source]

Export the plotted curve(s) as plain text, without the GLE machinery.

Reuses the same .dat/.fit writers (and their provenance header) as the GLE export, but writes only the text files — no .gleplot folder, GLE script, or compile. The content switch (data only / data + fit / fit only) mirrors WiMDA’s stData/stBoth/stFit.

export_plots_to_gle() None[source]

Export current main-plot view as GLE using gleplot.

Time-domain data is plotted with error bars (no connecting lines) and fit curves with lines (no markers). Frequency-domain (FFT/MaxEnt) views mirror the on-screen spectrum instead: a piecewise-linear line in the current display unit plus a light shaded ±1σ band. File names are derived from the Label dropdown value for each dataset.

export_current_plot() None[source]

Export current main-plot view as GLE (with optional compiled output).

shutdown_workers(timeout_ms: int = 10000) None[source]

Stop this panel’s background tasks (GLE export compiles).

set_rrf_feature_enabled(enabled: bool) None[source]

Enable/disable the whole RRF surface (the Advanced toggle, app-level).

Forwards to the RRF controls (which appear/disappear under the usual view condition) and redraws so the display reverts to or from the rotating frame immediately.

rrf_has_active_parameters() bool[source]

True when the RRF controls carry an active frame (enabled + ν₀ > 0).

Independent of the feature toggle: used on project open to decide whether a stored RRF configuration should auto-enable the toggle so the user’s analysis is not silently hidden.

rrf_fit_frequency_mhz() float | None[source]

Frame frequency ν₀ (MHz) when an RRF fit should be performed, else None.

The fit auto-couples to the plot’s RRF controls: a single composite fit runs in the rotating frame exactly when the RRF display is active (the feature is on, the controls are enabled with ν₀ > 0, and the FB-asymmetry time view is showing). The fit consumes raw data with this offset and reports lab-frame frequencies (δν + ν₀).

get_state() dict[source]

Return a serialisable snapshot of the plot panel state.

This captures the bunch factor, axis limits, the currently displayed run number, and any stored fit curves. Fit curve arrays are serialised as plain Python lists for JSON compatibility.

Returns:

Plot state suitable for inclusion in a project file.

Return type:

dict

restore_state(state: dict, dataset: MuonDataset | None = None) None[source]

Restore plot panel state from a saved dict.

Parameters:
  • state (dict) – Plot state as returned by get_state().

  • dataset (MuonDataset, optional) – Dataset to re-plot after restoring limits. If None no plot is drawn, but all other state (limits, bunch factor, fit curves) is still applied.

staticMetaObject = PySide6.QtCore.QMetaObject("PlotPanel" inherits "QWidget": Methods:   #33 type=Signal, signature=bunch_factor_changed(int), parameters=int   #34 type=Signal, signature=fit_range_changed(double,double), parameters=double, double   #35 type=Signal, signature=view_limits_changed(double,double,double,double), parameters=double, double, double, double   #36 type=Signal, signature=polarization_axis_changed(QString), parameters=QString   #37 type=Signal, signature=fit_target_projection_changed(QString), parameters=QString   #38 type=Signal, signature=overlay_toggled(bool), parameters=bool   #39 type=Signal, signature=waterfall_changed()   #40 type=Signal, signature=time_view_changed(QString), parameters=QString   #41 type=Signal, signature=cursor_coords_changed(PyObject), parameters=PyObject   #42 type=Signal, signature=moments_window_changed(double,double), parameters=double, double   #43 type=Signal, signature=moments_cutoff_changed(double), parameters=double   #44 type=Signal, signature=moments_drag_finished()   #45 type=Signal, signature=grouping_hint_requested()   #46 type=Signal, signature=grouping_hint_dismissed() )

Fit panel

class asymmetry.gui.panels.fit_panel.FitPanel(parent: QWidget | None = None)[source]

Bases: QWidget

Fit setup and results panel with tabbed interface.

Contains tabs for single dataset fitting and global (multi-dataset) fitting.

fit_completed
preview_requested
global_fit_started
global_fit_completed
grouped_fit_completed
add_single_fit_to_series_requested
fit_range_edit_committed
batch_seeding_mode_changed
tab_changed
__init__(parent: QWidget | None = None) None[source]
set_active_projection_label(projection: str | None, tint: str | None = None) None[source]

Show/hide the ‘Fitting: <projection>’ echo for the bound projection.

tint colours the text to match the projection’s subplot frame.

set_batch_seeding_mode(mode: str) None[source]

Forward the batch-series seeding mode to the Batch tab.

domain() str[source]

Return the current fitting domain.

set_rrf_frequency_provider(provider: Callable[[], float | None]) None[source]

Forward the rotating-frame ν₀ provider to the single-fit tab.

set_domain(domain: str) None[source]

Switch the fit panel between time- and frequency-domain workflows.

clear() None[source]

Reset all fit-panel domain state.

set_dataset(dataset: MuonDataset | None) None[source]

Set the current dataset for single fitting tab.

D5 “refresh-unless-fitted”: three branches decide what the form shows.

  1. PROTECTED — the restore mediator (_single_fit_restore_provider) hands back a non-empty payload: a genuine recorded fit for this (run, representation, projection) slot, restored verbatim (provenance own_slot, no badge). This covers both a single fit and a batch/global write-back — the mediator reconstructs a payload from a batch member’s persisted FitSlot when it has no ui_state of its own (see MainWindow._single_fit_restore_payload), so protection here is driven purely by “did the mediator hand back content”, with no separate probe needed. Never auto-overwritten.

  2. An empty mediator payload: the mediator has a real opinion this exact slot was checked and confirmed unfit (a genuine projection, or the default slot once another projection has been fit). REFRESHABLE: once any fit exists in the session, refresh onto it (D5 — even a “domain default” is refreshable); otherwise blank to the domain default exactly as before (this slot was positively checked, so there is nothing to carry from a previous run either).

  3. No mediator opinion at all (None — a legacy/no-mediator run, or the default slot with no other projection info): REFRESHABLE via _refresh_single_fit_form, which prefers the session’s most recently fitted state, falling back to today’s pre-D5 behaviour (this run’s own cached form, or the previously active run’s displayed form) only once nothing has been fitted yet.

A dismissable badge (D2/F6) surfaces (B)/(C) whenever a source run is named — carry-forward itself is kept (useful for run series), but the panel must say so instead of silently presenting another run’s values as this run’s own.

set_single_fit_restore_provider(provider: Callable[[MuonDataset | None], dict | None] | None) None[source]

Install the per-projection single-fit restore mediator (or clear it).

The main window passes a callable that maps the dataset being bound to the persisted single-fit form payload for the active (run, representation, projection) slot — or None to defer to the panel’s own run-keyed state (the default / legacy-project path).

get_single_form_state() dict[source]

Return the single-fit form payload (no per-run/domain wrapping).

This is exactly what restore_single_fit_ui() consumes, so it is the payload the main window stores as a slot’s ui_state.

restore_single_fit_ui(payload: dict | None) None[source]

Restore (or blank) the single-fit form from a slot ui_state payload.

A populated dict restores the form verbatim; an empty dict (or None) blanks it — an unfit projection must never inherit another projection’s fit. The run-keyed blob is deliberately not touched: it stays the per-run store that global seeding reads, while the per-projection slot is the source of truth for the single-fit form.

set_datasets(datasets: list[MuonDataset]) None[source]

Set the datasets for the global fitting tab, tracking all datasets fed in.

batch_datasets() list[MuonDataset][source]

Return the datasets configured for the batch/integral-scan.

set_bound_group(group_id: str | None, name: str | None = None) None[source]

Bind the Batch tab to a data group (“Fit this group…”, D1).

clear_bound_group() None[source]

Clear the Batch tab’s group binding (ad-hoc selection, D1).

bound_group_id() str | None[source]

Return the id of the data group the Batch tab is bound to, or None.

set_frequency_missing_spectra_status(missing_run_numbers: list[int], cached_count: int) None[source]

Show frequency-domain global fit status for selected uncached runs.

is_grouped_time_domain_mode() bool[source]

Return whether the global tab is in grouped time-domain mode.

set_fit_blocked(blocked: bool, reason: str = '') None[source]

Apply fit-action blocking to both single and global tabs.

set_fit_range_display(x_min: float | None, x_max: float | None) None[source]

Forward fit-range display update to both single and global tabs.

single_fit_formula_string() str | None[source]

Return the active single-fit formula string, if available.

single_fit_model_and_seed() tuple[CompositeModel, ParameterSet][source]

Return the active single-fit model and seed (for headless re-fits).

global_fit_formula_string() str | None[source]

Return the active global-fit formula string, if available.

clear_fits_for_runs(run_numbers: list[int]) int[source]

Clear cached single/global fit state for specific dataset runs.

get_single_state_for_run(run_number: int) dict | None[source]

Return current single-fit state for one run, if available.

get_single_fit_wizard_cache_for_run(run_number: int) tuple[FitWizardRecommendation | None, dict[str, object] | None, str][source]
persist_single_fit_wizard_cache_for_run(run_number: int, recommendation: FitWizardRecommendation, *, signature: dict[str, object] | None = None, log_text: str = '') None[source]
build_single_fit_payload_from_slot(model: dict | None, template_parameters: list[dict] | None, result: dict | None) dict | None[source]

Reconstruct a single-fit-tab restore payload from a persisted FitSlot.

Batch/global-member FitSlot``s are "pointer slots": they carry a genuine fitted ``model/result but never a ui_state (only the single-fit GUI path writes one). D5 protects any run with a recorded result, so MainWindow._single_fit_restore_payload calls this to synthesise a payload rather than treating such a run as unfit — most relevantly across a project reload, before this session’s register_global_fit_results cache exists. Returns None when there is nothing usable to build from.

register_global_fit_results(results_by_run: dict[int, tuple[object, object, object]]) None[source]

Persist per-run single-tab state using the latest successful global fit.

get_single_state() dict[source]

Return serialisable state of the single-fit tab.

get_domain_state(domain: str) dict[source]

Return serialisable fit state for one fitting domain.

restore_domain_state(domain: str, state: dict | None) None[source]

Restore serialisable fit state for one fitting domain.

The blob’s domain tag (written by get_domain_state()) must match the requested domain — a mismatch means a stale/mis-routed payload and is refused rather than silently applied to the wrong form (F21c).

restore_single_state(state: dict) None[source]

Restore single-fit tab state from a saved dict.

get_global_state() dict[source]

Return serialisable state of the global-fit tab.

get_grouped_state() dict[source]

Return the grouped-fit classification (physics roles + nuisance block).

single_fit_range_text() str | None[source]

Active single-fit range as a provenance string (see SingleFitTab).

batch_fit_range_text() str | None[source]

Active batch/global/grouped fit range as a provenance string.

send_single_model_to_batch() bool[source]

Copy the single-fit tab’s model and current seeds into the Batch tab.

Returns True when a model was sent. The Single ⇄ Batch flow: build a model in Single, send it to seed a batch over the selected runs. The batch parameter seeds are taken from the single tab’s current table values (which reflect the latest fit once one has run), so the batch starts from the values the user just set rather than model defaults or stale preserved state (BUG B8c).

restore_global_state(state: dict) None[source]

Restore global-fit tab state from a saved dict.

get_ui_state() dict[source]

Return serialisable UI state for the fit panel container.

restore_ui_state(state: dict) None[source]

Restore serialisable UI state for the fit panel container.

shutdown_workers() None[source]

Cancel running fits on both tabs and wait for their threads.

staticMetaObject = PySide6.QtCore.QMetaObject("FitPanel" inherits "QWidget": Methods:   #33 type=Signal, signature=fit_completed(PyObject,PyObject,PyObject), parameters=PyObject, PyObject, PyObject   #34 type=Signal, signature=preview_requested(PyObject,PyObject,PyObject), parameters=PyObject, PyObject, PyObject   #35 type=Signal, signature=global_fit_started()   #36 type=Signal, signature=global_fit_completed(PyObject,PyObject), parameters=PyObject, PyObject   #37 type=Signal, signature=grouped_fit_completed(PyObject,PyObject), parameters=PyObject, PyObject   #38 type=Signal, signature=add_single_fit_to_series_requested()   #39 type=Signal, signature=fit_range_edit_committed(double,double), parameters=double, double   #40 type=Signal, signature=batch_seeding_mode_changed(QString), parameters=QString   #41 type=Signal, signature=tab_changed(int), parameters=int )

Fourier panel

class asymmetry.gui.panels.fourier_panel.FourierPanel(parent: QWidget | None = None, *, settings: QSettings | None = None)[source]

Bases: QWidget

Controls for frequency-domain analysis.

settings_changed

Emitted (debounced) whenever a control feeding get_state() changes, except the spectral-moments widget (it does not affect the computed spectrum). Suppressed during programmatic restores — see _suppress_settings_signal_scope.

__init__(parent: QWidget | None = None, *, settings: QSettings | None = None) None[source]
set_compute_scope(count: int) None[source]

Reflect the compute target count in the primary button’s label.

"Compute FFT" for a single-run scope; "Compute FFT (N runs)" when the Data-Browser selection puts N > 1 runs in scope — the button always computes the full selection, and the label makes that scope visible before clicking.

exclusion_ranges() list[tuple[float, float]][source]

Return the editable (centre, half-width) exclusion ranges.

set_background_hint(text: str | None) None[source]

Show the inherited grouping-background state above the FFT button (F3).

text is the resolved mode description (e.g. "tail-fit") or None when no grouping background correction is active. Rendered as the footer hint.

set_fft_status(message: str, *, success: bool = False) None[source]

Set the footer status line for a computed-spectrum outcome.

A successful compute is reported with the green success chip; other messages fall through as an informational (accent) chip.

set_stale(reason: str | None) None[source]

Show or hide the “spectrum out of date” banner.

A non-empty reason shows the banner with that reason folded into the fixed message; None or an empty string hides it.

is_stale() bool[source]

Return whether the stale-spectrum banner is currently shown.

set_overlay_mismatch(mismatched: bool) None[source]

Show or hide the “overlay mixes settings” banner.

Independent of set_stale(): an overlay can mix spectra computed under different settings even when the active run’s own displayed spectrum is in sync with the live panel state, so the two banners can be shown together.

is_overlay_mismatched() bool[source]

Return whether the overlay-mismatch banner is currently shown.

group_phase_table() dict[int, float][source]

Return the per-group phase table as {group_id: phase_deg}.

group_enabled_table() dict[int, bool][source]

Return the per-group inclusion table as {group_id: enabled}.

selected_group_ids() list[int][source]

Return the ordered list of detector groups enabled for grouped FFTs.

set_group_definitions(group_names: dict[int, str], phase_table: dict[int, float] | None = None, enabled_table: dict[int, bool] | None = None) None[source]

Populate the editable group-phase table for the active run.

set_group_phases(phase_table: dict[int, float], *, auto_filled: bool = False) None[source]

Update existing group-phase rows without rebuilding the table.

set_apodisation_suggestion(window: str, time_constant_us: float) None[source]

Fill a matched-apodisation suggestion into the filter controls.

Deliberately does NOT suppress settings_changed: filling the values is itself a settings change, and the staleness banner engaging is the designed flow (suggestion -> banner -> explicit Compute FFT applies it).

set_group_enabled(enabled_table: dict[int, bool]) None[source]

Update existing group-inclusion rows without rebuilding the table.

group_auto_filled_ids() set[int][source]

Return group IDs whose phase cells were last auto-estimated.

group_phase_state() dict[str, object][source]

Return the current group-phase UI state for one dataset/run.

restore_group_phase_state(state: dict[str, object] | None, group_names: dict[int, str]) None[source]

Restore per-run group-phase state into the table.

clear_average_summary() None[source]

Clear the averaged-spectrum summary text.

set_average_summary(*, mean_error: float, peak_signal_to_noise: float, group_count: int) None[source]

Show a short summary for an averaged grouped FFT spectrum.

get_state() dict[source]

Return a serialisable snapshot of the Fourier panel settings.

property moments_widget: SpectralMomentsWidget

The spectral-moments control hosted in the advanced stack.

restore_state(state: dict) None[source]

Restore Fourier panel settings from a saved dict.

staticMetaObject = PySide6.QtCore.QMetaObject("FourierPanel" inherits "QWidget": Methods:   #33 type=Signal, signature=settings_changed() )

Log panel

class asymmetry.gui.panels.log_panel.LogPanel(parent: QWidget | None = None)[source]

Bases: QWidget

Scrollable log with per-category tag colouring.

Capped to the most recent _MAX_LOG_BLOCKS entries via QTextDocument.setMaximumBlockCount so a long-running session’s log cannot grow the widget (and the app’s memory footprint) without bound.

entry_count_changed

Emitted with the running entry count (feeds the dock header’s badge).

__init__(parent: QWidget | None = None) None[source]
log(message: str, *, tag: str = '') None[source]

Append a timestamped entry, optionally categorised by tag.

entry_count() int[source]

Return the number of entries logged since the last clear.

clear() None[source]

Clear all log entries.

to_plain_text() str[source]
staticMetaObject = PySide6.QtCore.QMetaObject("LogPanel" inherits "QWidget": Methods:   #33 type=Signal, signature=entry_count_changed(int), parameters=int )

Dialogs

Grouping dialog

class asymmetry.gui.windows.grouping_dialog.GroupingDialog(datasets: list[MuonDataset], *, profiles: list[GroupingProfile] | None = None, overridden_run_numbers: list[int] | None = None, assigned_profiles: dict[int, str] | None = None, selected_run_number: int | None = None, selected_run_numbers: list[int] | None = None, parent=None)[source]

Bases: QDialog

Profile editor for detector grouping.

The dialog edits an in-memory draft grouping profile (a GroupingProfile). Each run of a fingerprint (instrument, histogram_count) follows its assigned profile — several profiles can be in concurrent use (schema v17, e.g. one per sample), with one flagged the fingerprint’s default for newly loaded runs. The scope panel moves runs between profiles (“Assign to ▸”); per-run divergence is an explicit “release from profile” exception, with the assignment kept as the base profile Reattach returns the run to. Applying the edited profile reaches only the runs following it. Nothing touches the project or runs until Apply.

The scope panel is the selector: the run selected there is the one the form previews and edits, and the editing target follows that run’s status. An inheriting run edits the profile draft (through a flat run.grouping payload — see asymmetry.gui.windows.grouping.profile_bridge); an overridden run edits that run’s own override draft, seeded once from its stored payload and kept in self._override_drafts. Selecting never prompts — each target keeps its draft — and Apply commits everything dirty: the profile to every inheriting run plus each edited override to its own run. The only guard is closing the window with uncommitted changes.

Parameters:
  • datasets – Datasets available in the active project. Datasets without raw histograms are ignored.

  • profiles – The project’s existing grouping profiles. When omitted (or empty for the current fingerprint), the draft is synthesized from the current/reference run’s payload as "Default (<instrument>)".

  • assigned_profiles – Optional {run_number: profile_name} map of each run’s recorded profile assignment (schema v17). Runs without an entry — or whose entry no longer names a profile of their fingerprint — are treated as following the fingerprint’s default profile.

  • selected_run_number – Optional run number used as the initially selected run.

  • selected_run_numbers – Optional run numbers; only the first choice steers the initial selected run / fingerprint. No longer a broadcast selection.

  • parent – Parent Qt widget.

__init__(datasets: list[MuonDataset], *, profiles: list[GroupingProfile] | None = None, overridden_run_numbers: list[int] | None = None, assigned_profiles: dict[int, str] | None = None, selected_run_number: int | None = None, selected_run_numbers: list[int] | None = None, parent=None) None[source]

Create a grouping profile editor for project datasets.

reject() None[source]

Reject with an unsaved-draft guard (Cancel / Esc).

closeEvent(event) None[source]

Close with an unsaved-draft guard (window ✕).

done(result: int) None[source]

Tear the preview + background-configure runners down on dismissal.

staticMetaObject = PySide6.QtCore.QMetaObject("GroupingDialog" inherits "QDialog": )
get_grouping_result() dict[str, Any] | None[source]

Return the applied grouping payload for inheriting runs.

The dialog is a profile editor: on Apply the draft is saved as the active profile and resolved onto every inheriting run of the instrument. The returned dict is the flat grouping payload the main window’s _apply_grouping_settings_to_dataset consumes, with run_numbers set to the inheriting runs. Per-run override edits are carried separately in get_profile_result().

Returns:

Grouping payload plus run_numbers (inheriting runs). None when no run histograms are available.

Return type:

dict or None

get_profile_result() dict[str, Any] | None[source]

Return the profile-editor result for the main window to reconcile.

Returns:

{"profile": GroupingProfile, "inheriting": set[int], "released": set[int], "newly_released": set[int], "newly_reattached": set[int], "preview_run_number": int, "override_edits": {run_number: payload}} — the saved draft profile, the scope reconciliation, and every dirty per-run override payload to apply to its run alone. None when no run is available.

Return type:

dict or None

property draft_profile: GroupingProfile

The current draft profile (a copy synced from the form).

Detector layout dialog

class asymmetry.gui.windows.detector_layout_dialog.DetectorLayoutDialog(instrument: InstrumentLayout, groups: dict[int, list[int]], group_names: dict[int, str] | None = None, initial_preset_name: str | None = None, forward_group: int = 1, backward_group: int = 2, excluded_detectors: list[int] | None = None, projections: list[dict] | None = None, field_direction: str | None = None, parent: QWidget | None = None)[source]

Bases: QDialog

Visual detector grouping editor.

Parameters:
  • instrument – Pre-selected instrument layout. Users can override this via the instrument combo inside the dialog.

  • groups – Current group definitions mapping group ID (1-based) to list of 1-based detector IDs.

  • group_names – Optional mapping from group ID to human-readable group name.

  • forward_group – Group ID currently designated as the forward group.

  • backward_group – Group ID currently designated as the backward group.

  • field_direction – Applied-field geometry of the run ("Transverse" / "Longitudinal" / "Zero field" / None). When the run is transverse-field but the current preset is not the recommended transverse one, the dialog shows a non-blocking hint and pre-selects the recommended preset in the combo (the user still clicks Apply Grouping).

  • parent – Parent Qt widget.

__init__(instrument: InstrumentLayout, groups: dict[int, list[int]], group_names: dict[int, str] | None = None, initial_preset_name: str | None = None, forward_group: int = 1, backward_group: int = 2, excluded_detectors: list[int] | None = None, projections: list[dict] | None = None, field_direction: str | None = None, parent: QWidget | None = None) None[source]
showEvent(event) None[source]
eventFilter(watched, event) bool[source]

Highlight a group’s detectors in the schematic while its row is hovered.

get_result() dict[str, Any][source]

Return the editing result as a plain dictionary.

Returns:

Keys:

"groups"

Mapping from group ID (int, 1-based) to list of 1-based detector IDs. Only non-empty groups are included.

"group_names"

Mapping from group ID (int, 1-based) to name string.

"forward_group"

Group ID of the designated forward group.

"backward_group"

Group ID of the designated backward group.

"instrument"

Instrument name (str).

Return type:

dict

staticMetaObject = PySide6.QtCore.QMetaObject("DetectorLayoutDialog" inherits "QDialog": )

Widgets

Detector schematic

class asymmetry.gui.widgets.detector_schematic.DetectorSchematicWidget(instrument: InstrumentLayout, parent: QWidget | None = None)[source]

Bases: QWidget

Interactive matplotlib-backed detector schematic.

Parameters:
  • instrument – Instrument layout to render.

  • parent – Parent Qt widget.

  • Signals

  • -------

  • detector_toggled(int – Emitted when the user clicks a detector segment. Arguments are the 1-based detector_id and True if the detector is now included in the active group, False if it has been removed.

  • bool) – Emitted when the user clicks a detector segment. Arguments are the 1-based detector_id and True if the detector is now included in the active group, False if it has been removed.

detector_toggled: Signal
detector_exclusion_toggled: Signal

(1-based detector id, now_excluded).

Type:

Emitted in exclude mode

__init__(instrument: InstrumentLayout, parent: QWidget | None = None) None[source]
set_active_group(group_id: int) None[source]

Change which group is currently active (receives click toggles).

Parameters:

group_id – 1-based group identifier.

set_group_detectors(group_id: int, detector_ids: set[int]) None[source]

Set detector membership for one group and refresh the display.

Parameters:
  • group_id – 1-based group identifier.

  • detector_ids – Set of 1-based detector IDs belonging to the group.

set_all_groups(groups: dict[int, list[int]], active_group: int) None[source]

Replace all group assignments and set the active group.

Parameters:
  • groups – Mapping from group ID (1-based) to list of 1-based detector IDs.

  • active_group – Group ID that should be considered active.

set_excluded_detectors(detector_ids: set[int]) None[source]

Mark detectors (1-based ids) as excluded and refresh the display.

set_exclude_mode(enabled: bool) None[source]

Toggle click-to-exclude mode (clicks edit the exclusion set).

get_excluded_detectors() set[int][source]

Return the current exclusion set (1-based ids).

set_group_names(group_names: dict[int, str]) None[source]

Set the gid -> display-name mapping used to label hover tooltips.

Purely cosmetic (tooltip text only); does not affect group membership or colour. The layout dialog calls this whenever its name-edit fields change so hovering shows “Forward” rather than “Group 1”.

set_group_highlight(group_id: int | None) None[source]

Temporarily emphasise group_id’s detectors (edge highlight).

Used by the layout dialog to highlight a group’s detectors in the schematic when the user hovers that group’s row. Pass None to clear the highlight.

get_filled_detectors() set[int][source]

Return the set of detectors currently in the active group.

set_instrument(instrument: InstrumentLayout) None[source]

Replace the displayed instrument layout.

Parameters:

instrument – New instrument layout to render.

sizeHint() QSize[source]
staticMetaObject = PySide6.QtCore.QMetaObject("DetectorSchematicWidget" inherits "QWidget": Methods:   #33 type=Signal, signature=detector_toggled(int,bool), parameters=int, bool   #34 type=Signal, signature=detector_exclusion_toggled(int,bool), parameters=int, bool )