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 loadedMuonDatasetsurface:ds.asymmetry/ds.errorare 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, useds.asymmetry_fraction/ds.asymmetry_percentrather 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
alphamultiplying the backward group (andbetathe 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/errorto 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 fromcommon_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
alphafrom grouped counts.This follows the same approach used by Mantid’s
AlphaCalcalgorithm:\[\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.0when the backward integral is not positive or when no valid bins are available.- Return type:
- 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_erroris a seeded Poisson-bootstrap standard deviation (Nonewhen bootstrapping was disabled or too few replicas survived).objective_valueis the minimised objective for the optimising methods andNonefor theratiomethod.
- 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. Requirestime_us(bin centres relative to t0, microseconds)."ratio"— ΣF/ΣB (MantidAlphaCalc; the legacyestimate_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_bootstraptimes with a seeded generator and the estimator re-run;alpha_erroris the robust (percentile) standard error of the replicas (Nonewhenn_bootstrapis 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 loadedMuonDatasetsurface:ds.asymmetry/ds.errorare 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, useds.asymmetry_fraction/ds.asymmetry_percentrather 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
alphafrom grouped counts.This follows the same approach used by Mantid’s
AlphaCalcalgorithm:\[\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.0when the backward integral is not positive or when no valid bins are available.- Return type:
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_binsper-run fact so a manual policy can shift alignment without touching the file values. Absent for thefrom_filedefault.
- 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, elseNone(thefrom_filedefault aligns onHistogram.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
t0for every detector, soapply_grouping()is sufficient. PSI BIN/MDU data can carry differentt0values per detector. This helper shifts each selected detector so that its localt0_binlands oncommon_t0_binbefore summing.detector_t0_binsoptionally 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 rewritingHistogram.t0_bin. WhenNone, each histogram’s ownt0_binis 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_binsoptionally overrides the per-detector t0 (seeapply_grouping_aligned()) so a manual T0Policy can shift the common t0 without mutatingHistogram.t0_bin.
- asymmetry.core.transform.grouping.good_frames(grouping: dict | None, default: float = 1.0) float[source]
Positive
good_framesfrom a grouping, falling back to default.good_framesis 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. Passdefault=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 betweenfirst_good_binandlast_good_bin(inclusive) across the detectors of the forward and backward groups. ReturnsNonewhen 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 exportevents_groupedheader 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_detectorsgrouping key holds 1-based detector ids (WiMDAGroup2.passemantics). 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. RaisesValueErroron 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 beintorstr.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
.mdufile with only nine histograms. Reduction sums over the detectors that ARE present (effective_group_indices()withn_histogramsdrops 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_idinto the 0-based detector indices it will sum MUST go through here, so detector exclusion (theexcluded_detectorsgrouping 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 carryexcluded_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_refis 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_namesmap from the run’s grouping payload (keyed byintor 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.
- 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 balancesalphaandbeta(leniently, defaulting to1.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
ValueErrorwhen the grouping lacks agroupsdefinition 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’sPRunAsymmetryconvention: the mean count over a pre-t0 bin range (fallback range0.1·t0to0.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.
- __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_usis the flat background rate (counts/µs for the group);amplitude_per_usthe muon-decay rate extrapolated to t0.consistent_with_zeroflags 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.
- 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. Useavailable_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_fitandreference_runapply everywhere;rangeneeds 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_modewins; 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
estBGconvention 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·referenceand 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_modeselects the mode (seeresolve_background_mode()for the back-compatible default);background_fixed_valuessupplies fixed values andbackground_ranges/background_rangebin 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_runmode (resolution from thebackground_runpayload 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_runmode (resolution from thebackground_runpayload 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_runmode (resolution from thebackground_runpayload 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
countfitworkflow: 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
Calbutton behavior: fit each detector histogram independently using the samecountfitmodel 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-detectordead_time_usso the next reduction applies it.additiveaccumulates onto the existing value (WiMDA’sDTmodelChanges) instead of replacing it;detector_indicesrestricts the write to the fitted group’s detectors (default: all). The correction is enabled and thedeadtime_methodmarkedvalueby default; passmethodto record a different provenance label (e.g."maxent_fit"for the MaxEnt calibration route).dead_time_us_valueis 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 indexi; targets beyond its length get0.0.For a polynomial or power-law count-loss fit, pass the loss
modeland its higher-orderextra_terms(DT1/C2/C3/C4). The dominantDT0term still drives the per-detectordead_time_usthat 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 indeadtime_model/deadtime_model_termsso the full calibration round-trips with the run.additiveaccumulates 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
Truewhen grouping metadata contains file deadtime values.
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(), thebunching_factorgrouping key);variable — output width grows exponentially from
bin0_usat t = 0 tobin10_usat 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.factoris clamped tomax(1, int(factor)); a no-op factor or a series shorter than the factor returnsfloat64copies 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 tofixed.
- 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
Regrouploop, snapped to integer raw-bin boundaries.t_start_usis 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_factorraw bins per output bin (trailing remainder dropped, likerebin()); the non-fixed modes usebinning_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 conventionrebin()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)