Data transforms

The transform layer is the sequence of operations applied to raw per-detector histograms before any model fit sees them: deadtime correction (deadtime()), background subtraction (background()), grouping into forward and backward detector sums (apply_grouping()), asymmetry calculation with the chosen \(\alpha\) calibration constant (compute_asymmetry(), estimate_alpha()), and optional rebinning of the resulting asymmetry trace (rebin()). The order is fixed and matches musrfit’s PRunAsymmetry pipeline: deadtime → background → grouping → asymmetry. The user-facing documentation of these steps and of Asymmetry’s conventions (alpha applied to the backward group; Mantid-style uncertainty handling at zero denominator) is in Data processing.

Asymmetry calculation

Compute the μSR asymmetry from grouped histograms.

The corrected asymmetry is defined as

A(t) = [N_F(t) − α N_B(t)] / [β N_F(t) + α N_B(t)]

where N_F and N_B are the forward and backward group counts, α is the count-rate balance (detector efficiencies × solid angles) and β is the intrinsic-asymmetry balance β = A_{0,B}/A_{0,F} (musrfit asymmetry fit type 2; β multiplies the forward counts in our α-on-backward convention, which is algebraically identical to musrfit’s α-on-forward form with α_musrfit = 1/α — see docs/porting/beta-correction/). β = 1 recovers the standard formula A = (F − αB)/(F + αB).

asymmetry.core.transform.asymmetry.compute_asymmetry(forward: ndarray[tuple[int, ...], dtype[float64]], backward: ndarray[tuple[int, ...], dtype[float64]], alpha: float = 1.0, beta: float = 1.0) tuple[ndarray[tuple[int, ...], dtype[float64]], ndarray[tuple[int, ...], dtype[float64]]][source]

Calculate asymmetry and its statistical error.

Scale

This primitive returns the dimensionless fraction A [-1, 1] (and a fractional error), matching the textbook definition above. Note this differs from the loaded MuonDataset surface: ds.asymmetry / ds.error are on the percent scale (the loaders multiply this fraction by 100, the WiMDA-style convention the time-domain fit models also use). When you need an explicit scale from a dataset, use ds.asymmetry_fraction / ds.asymmetry_percent rather than assuming one — seeding a fit with fraction-scale amplitudes against percent-scale data converges to the wrong minimum.

param forward:

Counts in the forward and backward detector groups (same length).

param backward:

Counts in the forward and backward detector groups (same length).

param alpha:

Count-rate balance parameter (α).

param beta:

Intrinsic-asymmetry balance parameter (β = A_{0,B}/A_{0,F}); the default 1.0 is the standard formula.

returns:

Fractional arrays (A [-1, 1]) of the same length as the inputs.

rtype:

asymmetry, error

asymmetry.core.transform.asymmetry.compute_asymmetry_with_count_errors(forward: ndarray[tuple[int, ...], dtype[float64]], backward: ndarray[tuple[int, ...], dtype[float64]], forward_error: ndarray[tuple[int, ...], dtype[float64]], backward_error: ndarray[tuple[int, ...], dtype[float64]], alpha: float = 1.0, beta: float = 1.0) tuple[ndarray[tuple[int, ...], dtype[float64]], ndarray[tuple[int, ...], dtype[float64]]][source]

Calculate asymmetry from counts with supplied count uncertainties.

This is used for musrfit-style background-corrected histograms, where the count uncertainties are formed before or during background subtraction. The asymmetry convention remains Asymmetry’s convention, with alpha multiplying the backward group (and beta the forward group in the denominator).

asymmetry.core.transform.asymmetry.slice_to_good_window(asymmetry: ndarray[tuple[int, ...], dtype[float64]], error: ndarray[tuple[int, ...], dtype[float64]], grouping: dict, *, common_t0: int, bin_width: float) tuple[ndarray[tuple[int, ...], dtype[float64]], ndarray[tuple[int, ...], dtype[float64]], ndarray[tuple[int, ...], dtype[float64]]][source]

Slice reduced arrays to the grouping’s good-bin window and build the axis.

The shared tail of every F-B reduction: clip asymmetry/error to the inclusive [first_good_bin, last_good_bin] window (falling back to the full range when the window is degenerate) and form the time axis in microseconds, measured from common_t0. Used by both the loader-style reduction (asymmetry.core.simulate._reduce_histograms()) and the run-arithmetic subtraction reduction so the two agree by construction.

asymmetry.core.transform.asymmetry.estimate_alpha(forward: ndarray[tuple[int, ...], dtype[float64]], backward: ndarray[tuple[int, ...], dtype[float64]], *, first_good_bin: int | None = None, last_good_bin: int | None = None) float[source]

Estimate the detector-balance parameter alpha from grouped counts.

This follows the same approach used by Mantid’s AlphaCalc algorithm:

\[\alpha = \frac{\sum_i F_i}{\sum_i B_i}\]

where \(F_i\) and \(B_i\) are forward and backward grouped counts integrated over the selected good-bin window.

Parameters:
  • forward – Forward and backward grouped count arrays.

  • backward – Forward and backward grouped count arrays.

  • first_good_bin – Optional inclusive bin range for integration. If omitted, the full overlap of the two arrays is used.

  • last_good_bin – Optional inclusive bin range for integration. If omitted, the full overlap of the two arrays is used.

Returns:

Estimated alpha value. Returns 1.0 when the backward integral is not positive or when no valid bins are available.

Return type:

float

class asymmetry.core.transform.asymmetry.AlphaEstimate(alpha: float, alpha_error: float | None, method: str, n_bins_used: int, objective_value: float | None, ok: bool, message: str = '')[source]

Result of an alpha estimation.

alpha_error is a seeded Poisson-bootstrap standard deviation (None when bootstrapping was disabled or too few replicas survived). objective_value is the minimised objective for the optimising methods and None for the ratio method.

alpha: float
alpha_error: float | None
method: str
n_bins_used: int
objective_value: float | None
ok: bool
message: str = ''
__init__(alpha: float, alpha_error: float | None, method: str, n_bins_used: int, objective_value: float | None, ok: bool, message: str = '') None
asymmetry.core.transform.asymmetry.estimate_alpha_detailed(forward: ndarray[tuple[int, ...], dtype[float64]], backward: ndarray[tuple[int, ...], dtype[float64]], *, method: str = 'diamagnetic', time_us: ndarray[tuple[int, ...], dtype[float64]] | None = None, first_good_bin: int | None = None, last_good_bin: int | None = None, n_bootstrap: int = 200, seed: int = 0) AlphaEstimate[source]

Estimate alpha with an uncertainty, by one of three methods.

Methods (see docs/reference/data_reduction/alpha_calibration):

  • "diamagnetic" — minimise the weighted asymmetry power Σ(A/σ)² over a transverse-field calibration run (WiMDA’s diamagnetic estimate, run on internally packed equal-statistics bins).

  • "general" — flatness of the lifetime-corrected balanced count (F/√α + B√α)·exp(t/τ_μ), solved in closed form between two equal-statistics time windows; works on relaxing LF/ZF data, where no zero-mean oscillation exists, and fails informatively when the polarization does not relax. Requires time_us (bin centres relative to t0, microseconds).

  • "ratio" — ΣF/ΣB (Mantid AlphaCalc; the legacy estimate_alpha()). Only unbiased when the polarization integrates to zero over the window (many-cycle TF data).

The uncertainty is a Poisson bootstrap: per-bin counts are resampled as Poisson(observed) n_bootstrap times with a seeded generator and the estimator re-run; alpha_error is the robust (percentile) standard error of the replicas (None when n_bootstrap is 0 or fewer than 10 replicas survive). WiMDA reports a bare number; the uncertainty is an Asymmetry improvement (study divergence D2).

Parameters mirror estimate_alpha().

asymmetry.core.transform.asymmetry.compute_asymmetry(forward: ndarray[tuple[int, ...], dtype[float64]], backward: ndarray[tuple[int, ...], dtype[float64]], alpha: float = 1.0, beta: float = 1.0) tuple[ndarray[tuple[int, ...], dtype[float64]], ndarray[tuple[int, ...], dtype[float64]]][source]

Calculate asymmetry and its statistical error.

Scale

This primitive returns the dimensionless fraction A [-1, 1] (and a fractional error), matching the textbook definition above. Note this differs from the loaded MuonDataset surface: ds.asymmetry / ds.error are on the percent scale (the loaders multiply this fraction by 100, the WiMDA-style convention the time-domain fit models also use). When you need an explicit scale from a dataset, use ds.asymmetry_fraction / ds.asymmetry_percent rather than assuming one — seeding a fit with fraction-scale amplitudes against percent-scale data converges to the wrong minimum.

param forward:

Counts in the forward and backward detector groups (same length).

param backward:

Counts in the forward and backward detector groups (same length).

param alpha:

Count-rate balance parameter (α).

param beta:

Intrinsic-asymmetry balance parameter (β = A_{0,B}/A_{0,F}); the default 1.0 is the standard formula.

returns:

Fractional arrays (A [-1, 1]) of the same length as the inputs.

rtype:

asymmetry, error

asymmetry.core.transform.asymmetry.estimate_alpha(forward: ndarray[tuple[int, ...], dtype[float64]], backward: ndarray[tuple[int, ...], dtype[float64]], *, first_good_bin: int | None = None, last_good_bin: int | None = None) float[source]

Estimate the detector-balance parameter alpha from grouped counts.

This follows the same approach used by Mantid’s AlphaCalc algorithm:

\[\alpha = \frac{\sum_i F_i}{\sum_i B_i}\]

where \(F_i\) and \(B_i\) are forward and backward grouped counts integrated over the selected good-bin window.

Parameters:
  • forward – Forward and backward grouped count arrays.

  • backward – Forward and backward grouped count arrays.

  • first_good_bin – Optional inclusive bin range for integration. If omitted, the full overlap of the two arrays is used.

  • last_good_bin – Optional inclusive bin range for integration. If omitted, the full overlap of the two arrays is used.

Returns:

Estimated alpha value. Returns 1.0 when the backward integral is not positive or when no valid bins are available.

Return type:

float

Grouping

Detector grouping utilities.

Groups individual detector histograms into forward / backward (or custom) groups by summing their counts.

asymmetry.core.transform.grouping.apply_grouping(histograms: list[Histogram], group_indices: list[int]) ndarray[tuple[int, ...], dtype[float64]][source]

Sum the counts of the listed histograms into a single group array.

Parameters:
  • histograms – All histograms from the run.

  • group_indices – 0-based indices of the histograms to include in this group.

Returns:

Summed counts array.

Return type:

NDArray

asymmetry.core.transform.grouping.EFFECTIVE_DETECTOR_T0_KEY = 'effective_detector_t0_bins'

Grouping-dict key carrying the T0Policy-resolved effective per-detector t0 bins (0-based, one per histogram). Distinct from the file-derived detector_t0_bins per-run fact so a manual policy can shift alignment without touching the file values. Absent for the from_file default.

asymmetry.core.transform.grouping.detector_t0_overrides(grouping: dict | None, n_histograms: int) list[int] | None[source]

Extract policy-resolved per-detector t0 overrides from a grouping dict.

Reads EFFECTIVE_DETECTOR_T0_KEY (a manual T0Policy writes it) and returns it as an int list when it lines up with the histogram count, else None (the from_file default aligns on Histogram.t0_bin).

asymmetry.core.transform.grouping.apply_grouping_aligned(histograms: list[Histogram], group_indices: list[int], *, common_t0_bin: int | None = None, detector_t0_bins: list[int] | None = None) ndarray[tuple[int, ...], dtype[float64]][source]

Sum detector counts after aligning each detector’s own t0_bin.

ISIS NeXus files normally provide one common t0 for every detector, so apply_grouping() is sufficient. PSI BIN/MDU data can carry different t0 values per detector. This helper shifts each selected detector so that its local t0_bin lands on common_t0_bin before summing.

detector_t0_bins optionally overrides each detector’s alignment t0 (indexed 0-based across the full histogram list) — the non-destructive route a manual T0Policy uses to shift the effective t0 without rewriting Histogram.t0_bin. When None, each histogram’s own t0_bin is used.

asymmetry.core.transform.grouping.common_t0_for_groups(histograms: list[Histogram], *group_indices: list[int], detector_t0_bins: list[int] | None = None) int[source]

Return a common t0 suitable for comparing multiple detector groups.

detector_t0_bins optionally overrides the per-detector t0 (see apply_grouping_aligned()) so a manual T0Policy can shift the common t0 without mutating Histogram.t0_bin.

asymmetry.core.transform.grouping.good_frames(grouping: dict | None, default: float = 1.0) float[source]

Positive good_frames from a grouping, falling back to default.

good_frames is the universal dead-time normaliser (rate = counts / (dt · good_frames)); a missing, unparseable or non-positive value collapses to default so it can never zero the correction. Pass default=0.0 (and treat a falsy result as “unknown”, e.g. good_frames(g, 0.0) or None) when the caller wants to fall back to a snapshot instead.

asymmetry.core.transform.grouping.good_event_count(histograms: list[Histogram] | None, grouping: dict | None) float | None[source]

Total raw counts in the good-bin range over the forward+backward groups.

Mirrors WiMDA’s logbook “good events” (LogbookUnit.pas): the sum of detector counts between first_good_bin and last_good_bin (inclusive) across the detectors of the forward and backward groups. Returns None when the grouping lacks a good-bin range or named forward/backward groups (e.g. an ungrouped run), so callers can fall back to a total-count display.

Detector ids in grouping['groups'] are 1-based (WiMDA convention), so histogram index = id − 1. This is the single source of truth for the good-range event total: the data-browser “Good Events” column and the export events_grouped header both call it, so they agree by construction.

asymmetry.core.transform.grouping.excluded_detector_indices(grouping: dict | None) frozenset[int][source]

0-based indices of detectors excluded by the grouping.

The excluded_detectors grouping key holds 1-based detector ids (WiMDA Group2.pas semantics). Exclusion is applied at grouping time — excluded detectors are dropped from every group sum — so the raw histograms stay intact and no reload is needed (study divergence D10; WiMDA zeroes the counts in its file-read path instead).

asymmetry.core.transform.grouping.filter_excluded_indices(indices: list[int], grouping: dict | None) list[int][source]

Drop excluded detectors (0-based) from a group index list.

asymmetry.core.transform.grouping.parse_detector_list(text: str) list[int][source]

Parse a WiMDA-style detector list, e.g. "1,5,10-15".

Separators may be commas or whitespace; ranges use - and may run in either direction ("15-10" equals "10-15"). Returns sorted unique 1-based detector ids. Raises ValueError on unparseable fragments so typos surface instead of silently excluding nothing.

asymmetry.core.transform.grouping.format_detector_list(ids: list[int]) str[source]

Format detector ids compactly with ranges, e.g. "1,5,10-15".

asymmetry.core.transform.grouping.resolve_group_indices(groups: dict, group_id: int) list[int][source]

Return zero-based detector indices for group_id.

Grouping entries are 1-based detector numbers (matching the convention in fourier.grouped / grouped_time_domain); they are converted to zero-based histogram indices here. Group keys may be int or str.

This is the single source of truth shared by the time-domain F-B asymmetry representation and the time-integral observable, so the two cannot decode a run’s grouping differently.

asymmetry.core.transform.grouping.group_detectors_outside_run(groups: dict, group_id: int, n_histograms: int) list[int][source]

Return the 1-based detector numbers in group_id absent from the run.

A grouping (typically an instrument preset or a saved profile) may name detectors a particular run does not contain — e.g. a full HAL-9500 preset referencing the backward ring (detectors 10-17) applied to a forward-ring-only .mdu file with only nine histograms. Reduction sums over the detectors that ARE present (effective_group_indices() with n_histograms drops the rest); this helper names the dropped detectors so the GUI can report them — as an informational note when the group still has present detectors, or as the skip reason when it has none and no asymmetry can be formed.

Returns the sorted detector numbers whose 0-based index is >= n_histograms (or < 1), empty when every referenced detector is present.

asymmetry.core.transform.grouping.effective_group_indices(grouping: dict | None, group_id: int, *, n_histograms: int | None = None) list[int][source]

Resolve a group’s detector indices for reduction, exclusion applied.

This is the single exclusion-aware chokepoint: every reduction path that turns a grouping + group_id into the 0-based detector indices it will sum MUST go through here, so detector exclusion (the excluded_detectors grouping key) can never be silently skipped at a new call site.

The raw decoder resolve_group_indices() is reserved for non-reduction uses that legitimately want every named detector regardless of exclusion (synthetic-run generation, NeXus writing); reduction code should not call it directly.

Parameters:
  • grouping – An effective grouping dict (must carry groups; may carry excluded_detectors).

  • group_id – The forward/backward (or custom) group id to resolve.

  • n_histograms – When given, indices outside [0, n_histograms) are dropped too — a grouping or preset may name detectors a particular run does not contain.

asymmetry.core.transform.grouping.effective_grouping(run: Run, grouping_ref: dict | None = None) dict[source]

Merge a run’s grouping with an optional override (override wins).

grouping_ref is the recipe-level grouping override (forward/backward group ids, alpha, good-bin window, …). Returning a fresh dict keeps the run’s stored grouping immutable.

asymmetry.core.transform.grouping.group_names(run: Run) dict[int, str][source]

Return {group_id: display name} for a run’s grouping.

Reads the optional group_names map from the run’s grouping payload (keyed by int or stringified id), falling back to "Group <id>" for any group without an explicit name. Returns {} when the run has no group definitions.

class asymmetry.core.transform.grouping.GroupedForwardBackward(forward: ndarray[tuple[int, ...], dtype[float64]], backward: ndarray[tuple[int, ...], dtype[float64]], common_t0: int, alpha: float, forward_gid: int, backward_gid: int, beta: float = 1.0)[source]

Forward/backward grouped counts plus the metadata to reduce them.

forward: ndarray[tuple[int, ...], dtype[float64]]
backward: ndarray[tuple[int, ...], dtype[float64]]
common_t0: int
alpha: float
forward_gid: int
backward_gid: int
beta: float = 1.0
__init__(forward: ndarray[tuple[int, ...], dtype[float64]], backward: ndarray[tuple[int, ...], dtype[float64]], common_t0: int, alpha: float, forward_gid: int, backward_gid: int, beta: float = 1.0) None
asymmetry.core.transform.grouping.group_forward_backward(histograms: list[Histogram], grouping: dict) GroupedForwardBackward[source]

Form aligned forward/backward groups from a (effective) grouping dict.

This is the shared core of the time-domain F-B asymmetry and the time-integral observable: it resolves the forward/backward group ids to detector indices, aligns each detector to a common t0, sums the groups, and reads the balances alpha and beta (leniently, defaulting to 1.0). Callers own the good-bin window, the time axis, compute_asymmetry, and any rebinning, so the two reductions agree on grouping by construction.

Raises ValueError when the grouping lacks a groups definition or the forward/backward groups reference no detectors.

asymmetry.core.transform.grouping.apply_grouping(histograms: list[Histogram], group_indices: list[int]) ndarray[tuple[int, ...], dtype[float64]][source]

Sum the counts of the listed histograms into a single group array.

Parameters:
  • histograms – All histograms from the run.

  • group_indices – 0-based indices of the histograms to include in this group.

Returns:

Summed counts array.

Return type:

NDArray

Background

Background subtraction for grouped raw histograms.

Four modes are supported, selected by the background_mode grouping key (with back-compatible derivation from the older keys when absent):

  • "fixed" — subtract user-supplied per-group constants.

  • "range" — musrfit’s PRunAsymmetry convention: the mean count over a pre-t0 bin range (fallback range 0.1·t0 to 0.6·t0), only meaningful at continuous sources where a pre-t0 region exists.

  • "tail_fit" — WiMDA’s pulsed-source mode (Group.pas estBG/BGfit): a bin-integrated muon exponential plus a flat rate fitted to the late-time spectrum; the flat rate is subtracted. Asymmetry fits by Poisson maximum likelihood instead of WiMDA’s √N weights (study divergence D4) and reports the rate with an uncertainty.

  • "reference_run" — WiMDA’s File BG: subtract a designated reference run’s grouped counts scaled by the good-frame ratio (sample holder / silver / laser-off references), with error propagation (divergence D7).

class asymmetry.core.transform.background.BackgroundCorrectionResult(forward: ndarray[tuple[int, ...], dtype[float64]], backward: ndarray[tuple[int, ...], dtype[float64]], forward_error: ndarray[tuple[int, ...], dtype[float64]] | None, backward_error: ndarray[tuple[int, ...], dtype[float64]] | None, applied: bool, method: str, values: tuple[float, float] | None = None, ranges: tuple[tuple[int, int], tuple[int, int]] | None = None, details: dict[str, Any] | None = None)[source]

Result of grouped background subtraction.

forward: ndarray[tuple[int, ...], dtype[float64]]
backward: ndarray[tuple[int, ...], dtype[float64]]
forward_error: ndarray[tuple[int, ...], dtype[float64]] | None
backward_error: ndarray[tuple[int, ...], dtype[float64]] | None
applied: bool
method: str
values: tuple[float, float] | None = None
ranges: tuple[tuple[int, int], tuple[int, int]] | None = None
details: dict[str, Any] | None = None
__init__(forward: ndarray[tuple[int, ...], dtype[float64]], backward: ndarray[tuple[int, ...], dtype[float64]], forward_error: ndarray[tuple[int, ...], dtype[float64]] | None, backward_error: ndarray[tuple[int, ...], dtype[float64]] | None, applied: bool, method: str, values: tuple[float, float] | None = None, ranges: tuple[tuple[int, int], tuple[int, int]] | None = None, details: dict[str, Any] | None = None) None
class asymmetry.core.transform.background.TailFitResult(rate_per_us: float, rate_error_per_us: float | None, amplitude_per_us: float, window: tuple[int, int], n_bins: int, ok: bool, consistent_with_zero: bool, message: str = '')[source]

Late-time exponential + flat fit to one grouped count spectrum.

rate_per_us is the flat background rate (counts/µs for the group); amplitude_per_us the muon-decay rate extrapolated to t0. consistent_with_zero flags a background below two standard errors — the expected outcome on pulsed data, where the duty factor suppresses the uncorrelated rate (textbook §14.3); a significantly non-zero value is itself a diagnostic.

rate_per_us: float
rate_error_per_us: float | None
amplitude_per_us: float
window: tuple[int, int]
n_bins: int
ok: bool
consistent_with_zero: bool
message: str = ''
__init__(rate_per_us: float, rate_error_per_us: float | None, amplitude_per_us: float, window: tuple[int, int], n_bins: int, ok: bool, consistent_with_zero: bool, message: str = '') None
asymmetry.core.transform.background.supports_background_correction(*, metadata: dict[str, Any] | None = None, source_file: str = '') bool[source]

Return whether pre-t0 range-average background subtraction applies.

Kept for back-compatibility: this is the gate for the "range" mode only. Use available_background_modes() for the full mode list.

asymmetry.core.transform.background.available_background_modes(*, metadata: dict[str, Any] | None = None, source_file: str = '') tuple[str, ...][source]

Background modes applicable to a dataset.

fixed, tail_fit and reference_run apply everywhere; range needs a pre-t0 region, which only continuous-source data provide (pulsed ISIS files start at the muon pulse).

asymmetry.core.transform.background.resolve_background_mode(grouping: dict[str, Any] | None) str[source]

Resolve the active background mode from a grouping payload.

An explicit background_mode wins; otherwise the pre-existing keys decide (fixed values → fixed; anything else → range, which is the historical behaviour of the single enable flag).

asymmetry.core.transform.background.fit_tail_background(counts: ndarray[tuple[int, ...], dtype[float64]], *, bin_width_us: float, t0_bin: int, last_good_bin: int | None = None, fit_start_bin: int | None = None) TailFitResult[source]

Fit exponential + flat background to the late-time spectrum.

The model is WiMDA’s BGfit (Group.pas:1114), the muon exponential averaged across each bin plus a flat rate:

µ_i = [p₁ · e^{−t_i/τ_µ} · sinh(w/2τ_µ)/(w/2τ_µ) + p₂] · w

with w the bin width and t_i the bin centre relative to t0. Estimation is by Poisson maximum likelihood — the intensity is linear in (p₁, p₂), so the negative log-likelihood is convex and the minimum unique. WiMDA instead weights by √N and deletes bins with ≤ 4 counts, which biases the rate low exactly where this fit operates (study divergence D4). The rate uncertainty comes from the expected information matrix at the optimum.

The default window is the late half of (t0, last_good) — WiMDA’s estBG convention expressed on raw bins rather than display bins.

asymmetry.core.transform.background.subtract_scaled_counts(counts: ndarray[tuple[int, ...], dtype[float64]], reference_counts: ndarray[tuple[int, ...], dtype[float64]], scale: float) tuple[ndarray[tuple[int, ...], dtype[float64]], ndarray[tuple[int, ...], dtype[float64]]][source]

Subtract a scaled reference spectrum with error propagation.

Returns counts scale·reference and per-bin errors √(counts + scale²·reference) (both inputs treated as Poisson; WiMDA subtracts the raw reference without touching the errors — study divergence D7). Arrays are truncated to their common length. This is the shared frame-scaled arithmetic seam the run-arithmetic project reuses.

asymmetry.core.transform.background.apply_grouped_background_correction(forward: ndarray[tuple[int, ...], dtype[float64]], backward: ndarray[tuple[int, ...], dtype[float64]], *, grouping: dict[str, Any] | None, t0_bin: int, bin_width_us: float, facility: str = '', last_good_bin: int | None = None, reference_forward: ndarray[tuple[int, ...], dtype[float64]] | None = None, reference_backward: ndarray[tuple[int, ...], dtype[float64]] | None = None, reference_scale: float | None = None) BackgroundCorrectionResult[source]

Subtract background from grouped counts by the active mode.

Parameters:
  • forward – Grouped forward/backward count arrays.

  • backward – Grouped forward/backward count arrays.

  • grouping – Grouping payload. background_mode selects the mode (see resolve_background_mode() for the back-compatible default); background_fixed_values supplies fixed values and background_ranges/background_range bin ranges (inclusive, matching musrfit).

  • t0_bin – Common time-zero bin (musrfit fallback range; tail-fit time axis).

  • bin_width_us – Histogram bin width in microseconds.

  • facility – Optional facility label. PSI and TRIUMF ranges are shortened to an integer number of accelerator periods, following musrfit.

  • last_good_bin – End of the good window; bounds the tail-fit window.

  • reference_forward – Grouped counts of the resolved background reference run and the good-frame ratio, required by the reference_run mode (resolution from the background_run payload happens at the caller, which has loader access).

  • reference_backward – Grouped counts of the resolved background reference run and the good-frame ratio, required by the reference_run mode (resolution from the background_run payload happens at the caller, which has loader access).

  • reference_scale – Grouped counts of the resolved background reference run and the good-frame ratio, required by the reference_run mode (resolution from the background_run payload happens at the caller, which has loader access).

Deadtime

Deadtime correction utilities for raw detector histograms.

asymmetry.core.transform.deadtime.apply_deadtime_correction(counts, tau_us: float, bin_width_us: float, *, num_good_frames: float = 1.0)[source]

Apply the musrfit/Mantid non-paralyzable deadtime correction.

N_corr = N / (1 - N * tau / (dt * n_frames))

asymmetry.core.transform.deadtime.estimate_deadtime_from_histograms(histograms: list[Histogram], *, t_good_offset: int = 0, last_good_bin: int | None = None, num_good_frames: float = 1.0, max_bins: int = 400) float | None[source]

Estimate a uniform deadtime value from early-time detector counts.

The fit mirrors WiMDA’s countfit workflow: average the early-time count rate per detector and fit a muon-lifetime decay with a deadtime-loss term. The returned value is a single detector deadtime in microseconds that can be broadcast across grouped histograms.

asymmetry.core.transform.deadtime.calibrate_deadtime_from_histograms(histograms: list[Histogram], *, t_good_offset: int = 0, last_good_bin: int | None = None, num_good_frames: float = 1.0, max_bins: int = 400) list[float] | None[source]

Calibrate per-detector deadtime values from early-time count data.

This mirrors WiMDA’s Cal button behavior: fit each detector histogram independently using the same countfit model and return one deadtime value per detector in microseconds.

asymmetry.core.transform.deadtime.parse_deadtime_calibration_text(text: str, *, n_histograms: int | None = None) list[float][source]

Parse a WiMDA-style deadtime calibration text file.

The accepted format is the simple line-oriented form written by WiMDA:

  • optional first line with the histogram count

  • subsequent lines of <index> <tau_us>

  • optional trailing metadata lines such as Run 1234

asymmetry.core.transform.deadtime.DEADTIME_MODEL_TERM_NAMES: tuple[str, ...] = ('DT1', 'C2', 'C3', 'C4')

Higher-order count-loss coefficients carried alongside the promoted DT0.

asymmetry.core.transform.deadtime.promote_deadtime_to_grouping(grouping: dict, dead_time_us_value: float | list[float] | tuple[float, ...] | ndarray, *, n_histograms: int, detector_indices: list[int] | None = None, additive: bool = False, model: str | None = None, extra_terms: dict[str, float] | None = None, method: str = 'value') dict[str, dict[int, float]][source]

Write a fitted deadtime (µs) into the grouping correction (WiMDA Send-to-Group).

Mirrors WiMDA’s SendToGroupClick: a deadtime obtained from a count fit is promoted into the grouping’s per-detector dead_time_us so the next reduction applies it. additive accumulates onto the existing value (WiMDA’s DTmodelChanges) instead of replacing it; detector_indices restricts the write to the fitted group’s detectors (default: all). The correction is enabled and the deadtime_method marked value by default; pass method to record a different provenance label (e.g. "maxent_fit" for the MaxEnt calibration route).

dead_time_us_value is either a single deadtime broadcast to every target detector (the count-fit DT₀ case) or a per-detector sequence — one value per detector index — for calibrators that fit each detector independently (the MaxEnt route). A per-detector sequence aligns to detector index i; targets beyond its length get 0.0.

For a polynomial or power-law count-loss fit, pass the loss model and its higher-order extra_terms (DT1/C2/C3/C4). The dominant DT0 term still drives the per-detector dead_time_us that the reduction applies (Asymmetry’s grouping stores a single non-paralyzable deadtime per histogram), while the model name and the higher-order coefficients are recorded in deadtime_model / deadtime_model_terms so the full calibration round-trips with the run. additive accumulates the extra terms too.

Returns {"before": {idx: value}, "after": {idx: value}} for the affected detectors, so the GUI can show the change before/after.

asymmetry.core.transform.deadtime.has_file_deadtime(grouping: dict, n_histograms: int = 0) bool[source]

Return True when grouping metadata contains file deadtime values.

asymmetry.core.transform.deadtime.has_resolved_deadtime(grouping: dict, n_histograms: int = 0) bool[source]

Return True when grouping metadata contains usable deadtime values.

asymmetry.core.transform.deadtime.prepare_histograms_with_deadtime(histograms: list[Histogram], grouping: dict, use_deadtime: bool) tuple[list[Histogram], bool][source]

Return histograms with optional deadtime correction applied.

Rebinning

Rebinning utilities for time-domain data.

Three binning modes (WiMDA Group.pas:1411–1418, textbook Fig. 15.7):

  • fixed — every output bin merges the same number of raw bins (rebin(), the bunching_factor grouping key);

  • variable — output width grows exponentially from bin0_us at t = 0 to bin10_us at t = 10 µs (WiMDA’s formula folds 1/(10·λ_µ) into the exponent with a rounded constant, 0.22; the exact law is implemented here — study divergence D8, < 0.6 % difference over a 32 µs window);

  • constant_error — output width grows as exp(t/τ_µ), so each output bin holds roughly equal counts and the Poisson error per bin stays flat while the polarization varies slowly.

All modes are display/fit-input transformations on the reduced data: raw histograms are never modified (provenance invariant). Every mode bins the counts and then forms the asymmetry per output bin (via binned_fb_asymmetry()) — the order WiMDA, musrfit, and Mantid all use. At late times the raw bins hold few or zero counts, where a mean of per-bin asymmetry ratios is undefined (and its σ = 1 no-information sentinels inflate merged error bars); summed counts stay exactly Poisson. rebin() remains the curve-level combiner for data without raw histograms (curve-only datasets, period-combined curves, display bunching of an already-reduced curve).

asymmetry.core.transform.rebin.rebin(time: ndarray[tuple[int, ...], dtype[float64]], values: ndarray[tuple[int, ...], dtype[float64]], errors: ndarray[tuple[int, ...], dtype[float64]], factor: int) tuple[ndarray[tuple[int, ...], dtype[float64]], ndarray[tuple[int, ...], dtype[float64]], ndarray[tuple[int, ...], dtype[float64]]][source]

Rebin data by combining factor consecutive bins.

Parameters:
  • time – Equal-length arrays of the original data.

  • values – Equal-length arrays of the original data.

  • errors – Equal-length arrays of the original data.

  • factor – Number of bins to merge into one.

Return type:

(time_rebinned, values_rebinned, errors_rebinned)

asymmetry.core.transform.rebin.rebin_counts(time: ndarray[tuple[int, ...], dtype[float64]], counts: ndarray[tuple[int, ...], dtype[float64]], factor: int) tuple[ndarray[tuple[int, ...], dtype[float64]], ndarray[tuple[int, ...], dtype[float64]]][source]

Return count-preserving bunched traces (sum counts, mean times).

Unlike rebin(), which is value-domain (mean of values, errors in quadrature ÷ factor), this merges neighbouring bins by summing the counts while moving the time coordinate to the centre of the wider effective bin — the right combiner for grouped count-like signals (fit-input traces, grouped Fourier inputs). The two are deliberately distinct: a value-domain mean would corrupt Poisson statistics on counts.

factor is clamped to max(1, int(factor)); a no-op factor or a series shorter than the factor returns float64 copies of the inputs.

asymmetry.core.transform.rebin.resolve_binning_mode(grouping: dict[str, Any] | None) tuple[str, float, float][source]

Return (mode, bin0_us, bin10_us) from a grouping payload.

Defaults mirror WiMDA’s InitializeGlobalVars: bin0 = 0.08 µs, bin10 = 0.25 µs. Unknown modes fall back to fixed.

asymmetry.core.transform.rebin.binning_slice_edges(n_bins: int, *, mode: str, bin_width_us: float, t_start_us: float, bin0_us: float = 0.08, bin10_us: float = 0.25) ndarray[tuple[int, ...], dtype[int64]][source]

Output-bin boundaries (indices 0..n_bins) for a non-fixed binning mode.

Raw bins are accumulated until the running edge passes the target width at the output bin’s start time — WiMDA’s Regroup loop, snapped to integer raw-bin boundaries. t_start_us is the time (relative to t0) of the window’s first raw-bin edge; widths before t = 0 use the t = 0 target.

asymmetry.core.transform.rebin.binned_fb_asymmetry(forward: ndarray[tuple[int, ...], dtype[float64]], backward: ndarray[tuple[int, ...], dtype[float64]], *, grouping: dict[str, Any] | None, common_t0: int, bin_width_us: float, alpha: float, first_good_bin: int, last_good_bin: int, forward_error: ndarray[tuple[int, ...], dtype[float64]] | None = None, backward_error: ndarray[tuple[int, ...], dtype[float64]] | None = None, beta: float = 1.0) tuple[ndarray[tuple[int, ...], dtype[float64]], ndarray[tuple[int, ...], dtype[float64]], ndarray[tuple[int, ...], dtype[float64]]][source]

Reduce grouped counts to a binned asymmetry curve (all binning modes).

Counts (and, when supplied, count variances) are summed onto the output bins first and the asymmetry formed per output bin — the counts-then- ratio order WiMDA, musrfit, and Mantid all use. This matters beyond tidiness: forming the asymmetry per raw bin and then averaging gives one-sided raw bins (F·B = 0) the σ = 1 no-information sentinel, which leaks into every merged bin’s quadrature and inflates late-time errors on sparse data. Fixed mode merges bunching_factor raw bins per output bin (trailing remainder dropped, like rebin()); the non-fixed modes use binning_slice_edges(). Returns (time, asymmetry, error) in µs relative to t0; the asymmetry is fractional (callers scale to percent). Output times are the mean of the merged raw bins’ reduction time stamps (k t0)·w — the same convention rebin() uses, so switching binning modes never shifts the time axis.

asymmetry.core.transform.rebin.rebin(time: ndarray[tuple[int, ...], dtype[float64]], values: ndarray[tuple[int, ...], dtype[float64]], errors: ndarray[tuple[int, ...], dtype[float64]], factor: int) tuple[ndarray[tuple[int, ...], dtype[float64]], ndarray[tuple[int, ...], dtype[float64]], ndarray[tuple[int, ...], dtype[float64]]][source]

Rebin data by combining factor consecutive bins.

Parameters:
  • time – Equal-length arrays of the original data.

  • values – Equal-length arrays of the original data.

  • errors – Equal-length arrays of the original data.

  • factor – Number of bins to merge into one.

Return type:

(time_rebinned, values_rebinned, errors_rebinned)