Negative-muon analysis API (experimental)

Warning

Experimental — work in progress. The asymmetry.core.negmu package is unvalidated against real μ⁻ data; see Negative-muon capture-lifetime analysis (experimental) for the full disclaimer, physics background, and worked example.

The asymmetry.core.negmu package provides a scriptable API for μ⁻ capture-lifetime elemental analysis on raw single-histogram counts. It is not registered in the GUI fit builders and has no plot-mode or panel exposure.

Element lifetimes

EXPERIMENTAL — WORK IN PROGRESS. Negative-muon (μ⁻) capture-lifetime analysis.

This API is UNVALIDATED against real μ⁻ elemental-analysis data. No μ⁻ corpus exists in this project; every result here has been exercised only against synthetic histograms. The element lifetime values are literature-anchored (Suzuki, Measday & Roalsvig, Phys. Rev. C 35, 2212 (1987), via Blundell et al., Muon Spectroscopy: An Introduction, OUP 2022, Table C.1), but the fitting, capture-ratio, and background machinery have NOT been checked against an established tool (WiMDA, Mantid) on measured data. The API, parameter names, and return shapes MAY CHANGE without notice. Do not rely on results for publication without independent verification. This feature is deliberately NOT exposed in the GUI fit builders. Promotion trigger for a GUI: real ISIS μ⁻ data AND a user.

Negative-muon capture lifetimes: an element-keyed table of muonic-atom total disappearance lifetimes τ(Z) = 1/(Λ_capture + Λ_bound-decay), the seeds for the multi-exponential capture fit.

Source: Blundell, De Renzi, Lancaster & Pratt, Muon Spectroscopy: An Introduction (OUP, 2022), Appendix C, Table C.1 — values combined from T. Suzuki, D. F. Measday & J. P. Roalsvig, Phys. Rev. C 35, 2212 (1987). A few entries marked WiMDA-provisional are from WiMDA’s older table where Table C.1 has no value. WiMDA’s table is NOT trusted for values (see the porting study, comparison.md §5).

Provenance note: WiMDA’s mystrings table (NegMuAnalyse.pas:104–120) was the workflow reference, not the value source. It has a 69-vs-67 length mismatch, the 'Ti'``→’Tl’`` symbol bug, and several value divergences (Ne 1.520→1.461, Zn 0.169→0.161, Sr 0.142→0.132, Ba 0.072→0.0949). Adopt Table C.1; cite Suzuki/Measday/Roalsvig 1987.

class asymmetry.core.negmu.lifetimes.ElementLifetime(symbol: str, z: int, name: str, tau_us: float, sigma_us: float | None, source: str)[source]

Literature capture lifetime for one element.

symbol: str
z: int
name: str
tau_us: float
sigma_us: float | None
source: str
__init__(symbol: str, z: int, name: str, tau_us: float, sigma_us: float | None, source: str) None
asymmetry.core.negmu.lifetimes.DECAY_BACKGROUND_LABEL: str = 'decayBG'

Reserved label for the free-μ⁻ decay-background component (τ = MUON_LIFETIME_US).

asymmetry.core.negmu.lifetimes.ELEMENT_LIFETIMES: dict[str, ElementLifetime] = {'Ag': ElementLifetime(symbol='Ag', z=47, name='Silver', tau_us=0.0906, sigma_us=0.0007, source='SuzukiMeasdayRoalsvig1987'), 'Al': ElementLifetime(symbol='Al', z=13, name='Aluminium', tau_us=0.864, sigma_us=0.001, source='SuzukiMeasdayRoalsvig1987'), 'Ar': ElementLifetime(symbol='Ar', z=18, name='Argon', tau_us=0.537, sigma_us=0.032, source='SuzukiMeasdayRoalsvig1987'), 'As': ElementLifetime(symbol='As', z=33, name='Arsenic', tau_us=0.153, sigma_us=0.001, source='SuzukiMeasdayRoalsvig1987'), 'Au': ElementLifetime(symbol='Au', z=79, name='Gold', tau_us=0.0728, sigma_us=0.0005, source='SuzukiMeasdayRoalsvig1987'), 'B': ElementLifetime(symbol='B', z=5, name='Boron', tau_us=2.097, sigma_us=0.003, source='SuzukiMeasdayRoalsvig1987'), 'Ba': ElementLifetime(symbol='Ba', z=56, name='Barium', tau_us=0.0949, sigma_us=0.0006, source='SuzukiMeasdayRoalsvig1987'), 'Be': ElementLifetime(symbol='Be', z=4, name='Beryllium', tau_us=2.16747, sigma_us=0.00089, source='SuzukiMeasdayRoalsvig1987'), 'Bi': ElementLifetime(symbol='Bi', z=83, name='Bismuth', tau_us=0.0735, sigma_us=0.0004, source='SuzukiMeasdayRoalsvig1987'), 'Br': ElementLifetime(symbol='Br', z=35, name='Bromine', tau_us=0.133, sigma_us=0.001, source='SuzukiMeasdayRoalsvig1987'), 'C': ElementLifetime(symbol='C', z=6, name='Carbon', tau_us=2.03, sigma_us=0.001, source='SuzukiMeasdayRoalsvig1987'), 'Ca': ElementLifetime(symbol='Ca', z=20, name='Calcium', tau_us=0.336, sigma_us=0.001, source='SuzukiMeasdayRoalsvig1987'), 'Cd': ElementLifetime(symbol='Cd', z=48, name='Cadmium', tau_us=0.0906, sigma_us=0.0007, source='SuzukiMeasdayRoalsvig1987'), 'Ce': ElementLifetime(symbol='Ce', z=58, name='Cerium', tau_us=0.084, sigma_us=0.0006, source='SuzukiMeasdayRoalsvig1987'), 'Cl': ElementLifetime(symbol='Cl', z=17, name='Chlorine', tau_us=0.561, sigma_us=0.002, source='SuzukiMeasdayRoalsvig1987'), 'Co': ElementLifetime(symbol='Co', z=27, name='Cobalt', tau_us=0.186, sigma_us=0.001, source='SuzukiMeasdayRoalsvig1987'), 'Cr': ElementLifetime(symbol='Cr', z=24, name='Chromium', tau_us=0.259, sigma_us=0.002, source='SuzukiMeasdayRoalsvig1987'), 'Cs': ElementLifetime(symbol='Cs', z=55, name='Cesium', tau_us=0.088, sigma_us=0.002, source='SuzukiMeasdayRoalsvig1987'), 'Cu': ElementLifetime(symbol='Cu', z=29, name='Copper', tau_us=0.164, sigma_us=0.001, source='SuzukiMeasdayRoalsvig1987'), 'Er': ElementLifetime(symbol='Er', z=68, name='Erbium', tau_us=0.0749, sigma_us=0.0006, source='SuzukiMeasdayRoalsvig1987'), 'F': ElementLifetime(symbol='F', z=9, name='Fluorine', tau_us=1.461, sigma_us=0.005, source='SuzukiMeasdayRoalsvig1987'), 'Fe': ElementLifetime(symbol='Fe', z=26, name='Iron', tau_us=0.206, sigma_us=0.001, source='SuzukiMeasdayRoalsvig1987'), 'Ga': ElementLifetime(symbol='Ga', z=31, name='Gallium', tau_us=0.163, sigma_us=0.002, source='SuzukiMeasdayRoalsvig1987'), 'Gd': ElementLifetime(symbol='Gd', z=64, name='Gadolinium', tau_us=0.0806, sigma_us=0.0008, source='SuzukiMeasdayRoalsvig1987'), 'Ge': ElementLifetime(symbol='Ge', z=32, name='Germanium', tau_us=0.167, sigma_us=0.001, source='SuzukiMeasdayRoalsvig1987'), 'H': ElementLifetime(symbol='H', z=1, name='Hydrogen', tau_us=2.1948, sigma_us=6e-05, source='SuzukiMeasdayRoalsvig1987'), 'He': ElementLifetime(symbol='He', z=2, name='Helium', tau_us=2.188, sigma_us=None, source='WiMDA-provisional'), 'Hf': ElementLifetime(symbol='Hf', z=72, name='Hafnium', tau_us=0.075, sigma_us=0.001, source='SuzukiMeasdayRoalsvig1987'), 'Hg': ElementLifetime(symbol='Hg', z=80, name='Mercury', tau_us=0.076, sigma_us=0.001, source='SuzukiMeasdayRoalsvig1987'), 'Ho': ElementLifetime(symbol='Ho', z=67, name='Holmium', tau_us=0.079, sigma_us=0.001, source='SuzukiMeasdayRoalsvig1987'), 'I': ElementLifetime(symbol='I', z=53, name='Iodine', tau_us=0.0856, sigma_us=0.0006, source='SuzukiMeasdayRoalsvig1987'), 'Ir': ElementLifetime(symbol='Ir', z=77, name='Iridium', tau_us=0.074, sigma_us=None, source='WiMDA-provisional'), 'K': ElementLifetime(symbol='K', z=19, name='Potassium', tau_us=0.435, sigma_us=0.001, source='SuzukiMeasdayRoalsvig1987'), 'Kr': ElementLifetime(symbol='Kr', z=36, name='Krypton', tau_us=0.136, sigma_us=None, source='WiMDA-provisional'), 'La': ElementLifetime(symbol='La', z=57, name='Lanthanum', tau_us=0.0899, sigma_us=0.0007, source='SuzukiMeasdayRoalsvig1987'), 'Li': ElementLifetime(symbol='Li', z=3, name='Lithium', tau_us=2.1869, sigma_us=0.0004, source='SuzukiMeasdayRoalsvig1987'), 'Mg': ElementLifetime(symbol='Mg', z=12, name='Magnesium', tau_us=1.069, sigma_us=0.002, source='SuzukiMeasdayRoalsvig1987'), 'Mn': ElementLifetime(symbol='Mn', z=25, name='Manganese', tau_us=0.231, sigma_us=0.001, source='SuzukiMeasdayRoalsvig1987'), 'Mo': ElementLifetime(symbol='Mo', z=42, name='Molybdenum', tau_us=0.104, sigma_us=0.001, source='SuzukiMeasdayRoalsvig1987'), 'N': ElementLifetime(symbol='N', z=7, name='Nitrogen', tau_us=1.92, sigma_us=0.002, source='SuzukiMeasdayRoalsvig1987'), 'Na': ElementLifetime(symbol='Na', z=11, name='Sodium', tau_us=1.204, sigma_us=0.002, source='SuzukiMeasdayRoalsvig1987'), 'Nb': ElementLifetime(symbol='Nb', z=41, name='Niobium', tau_us=0.092, sigma_us=0.001, source='SuzukiMeasdayRoalsvig1987'), 'Nd': ElementLifetime(symbol='Nd', z=60, name='Neodymium', tau_us=0.0784, sigma_us=0.0007, source='SuzukiMeasdayRoalsvig1987'), 'Ne': ElementLifetime(symbol='Ne', z=10, name='Neon', tau_us=1.461, sigma_us=0.009, source='SuzukiMeasdayRoalsvig1987'), 'Ni': ElementLifetime(symbol='Ni', z=28, name='Nickel', tau_us=0.157, sigma_us=0.001, source='SuzukiMeasdayRoalsvig1987'), 'Np': ElementLifetime(symbol='Np', z=93, name='Neptunium', tau_us=0.072, sigma_us=0.0007, source='SuzukiMeasdayRoalsvig1987'), 'O': ElementLifetime(symbol='O', z=8, name='Oxygen', tau_us=1.795, sigma_us=0.002, source='SuzukiMeasdayRoalsvig1987'), 'Os': ElementLifetime(symbol='Os', z=76, name='Osmium', tau_us=0.078, sigma_us=None, source='WiMDA-provisional'), 'P': ElementLifetime(symbol='P', z=15, name='Phosphorus', tau_us=0.616, sigma_us=0.001, source='SuzukiMeasdayRoalsvig1987'), 'Pb': ElementLifetime(symbol='Pb', z=82, name='Lead', tau_us=0.0747, sigma_us=0.0004, source='SuzukiMeasdayRoalsvig1987'), 'Pd': ElementLifetime(symbol='Pd', z=46, name='Palladium', tau_us=0.0885, sigma_us=0.0006, source='SuzukiMeasdayRoalsvig1987'), 'Pr': ElementLifetime(symbol='Pr', z=59, name='Praseodymium', tau_us=0.0721, sigma_us=0.0006, source='SuzukiMeasdayRoalsvig1987'), 'Pt': ElementLifetime(symbol='Pt', z=78, name='Platinum', tau_us=0.074, sigma_us=None, source='WiMDA-provisional'), 'Rb': ElementLifetime(symbol='Rb', z=37, name='Rubidium', tau_us=0.137, sigma_us=0.003, source='SuzukiMeasdayRoalsvig1987'), 'Re': ElementLifetime(symbol='Re', z=75, name='Rhenium', tau_us=0.076, sigma_us=None, source='WiMDA-provisional'), 'Rh': ElementLifetime(symbol='Rh', z=45, name='Rhodium', tau_us=0.096, sigma_us=0.0006, source='SuzukiMeasdayRoalsvig1987'), 'Ru': ElementLifetime(symbol='Ru', z=44, name='Ruthenium', tau_us=0.0958, sigma_us=0.0006, source='SuzukiMeasdayRoalsvig1987'), 'S': ElementLifetime(symbol='S', z=16, name='Sulfur', tau_us=0.555, sigma_us=0.001, source='SuzukiMeasdayRoalsvig1987'), 'Sb': ElementLifetime(symbol='Sb', z=51, name='Antimony', tau_us=0.0924, sigma_us=0.0009, source='SuzukiMeasdayRoalsvig1987'), 'Sc': ElementLifetime(symbol='Sc', z=21, name='Scandium', tau_us=0.317, sigma_us=0.003, source='SuzukiMeasdayRoalsvig1987'), 'Se': ElementLifetime(symbol='Se', z=34, name='Selenium', tau_us=0.163, sigma_us=0.001, source='SuzukiMeasdayRoalsvig1987'), 'Si': ElementLifetime(symbol='Si', z=14, name='Silicon', tau_us=0.759, sigma_us=0.001, source='SuzukiMeasdayRoalsvig1987'), 'Sm': ElementLifetime(symbol='Sm', z=62, name='Samarium', tau_us=0.079, sigma_us=0.001, source='SuzukiMeasdayRoalsvig1987'), 'Sn': ElementLifetime(symbol='Sn', z=50, name='Tin', tau_us=0.0907, sigma_us=0.0008, source='SuzukiMeasdayRoalsvig1987'), 'Sr': ElementLifetime(symbol='Sr', z=38, name='Strontium', tau_us=0.132, sigma_us=0.002, source='SuzukiMeasdayRoalsvig1987'), 'Ta': ElementLifetime(symbol='Ta', z=73, name='Tantalum', tau_us=0.0755, sigma_us=0.0006, source='SuzukiMeasdayRoalsvig1987'), 'Tb': ElementLifetime(symbol='Tb', z=65, name='Terbium', tau_us=0.0762, sigma_us=0.0007, source='SuzukiMeasdayRoalsvig1987'), 'Tc': ElementLifetime(symbol='Tc', z=43, name='Technetium', tau_us=0.095, sigma_us=None, source='WiMDA-provisional'), 'Te': ElementLifetime(symbol='Te', z=52, name='Tellurium', tau_us=0.104, sigma_us=0.001, source='SuzukiMeasdayRoalsvig1987'), 'Th': ElementLifetime(symbol='Th', z=90, name='Thorium', tau_us=0.078, sigma_us=0.0003, source='SuzukiMeasdayRoalsvig1987'), 'Ti': ElementLifetime(symbol='Ti', z=22, name='Titanium', tau_us=0.329, sigma_us=0.001, source='SuzukiMeasdayRoalsvig1987'), 'Tl': ElementLifetime(symbol='Tl', z=81, name='Thallium', tau_us=0.0704, sigma_us=0.0008, source='SuzukiMeasdayRoalsvig1987'), 'Tm': ElementLifetime(symbol='Tm', z=69, name='Thulium', tau_us=0.074, sigma_us=0.002, source='SuzukiMeasdayRoalsvig1987'), 'U': ElementLifetime(symbol='U', z=92, name='Uranium', tau_us=0.0775, sigma_us=0.0002, source='SuzukiMeasdayRoalsvig1987'), 'V': ElementLifetime(symbol='V', z=23, name='Vanadium', tau_us=0.28, sigma_us=0.002, source='SuzukiMeasdayRoalsvig1987'), 'W': ElementLifetime(symbol='W', z=74, name='Tungsten', tau_us=0.0765, sigma_us=0.0008, source='SuzukiMeasdayRoalsvig1987'), 'Y': ElementLifetime(symbol='Y', z=39, name='Yttrium', tau_us=0.12, sigma_us=0.001, source='SuzukiMeasdayRoalsvig1987'), 'Zn': ElementLifetime(symbol='Zn', z=30, name='Zinc', tau_us=0.161, sigma_us=0.001, source='SuzukiMeasdayRoalsvig1987'), 'Zr': ElementLifetime(symbol='Zr', z=40, name='Zirconium', tau_us=0.11, sigma_us=0.001, source='SuzukiMeasdayRoalsvig1987')}

Element symbol → ElementLifetime. Transcribed from plan.md §2. ⚠confirm rows (period-5 transition cluster and lanthanides/actinides) are included at the plan’s stated values; they have not been independently verified against the textbook table layout and are NOT test-spot-check anchors.

asymmetry.core.negmu.lifetimes.lifetime(symbol: str) ElementLifetime[source]

Return the table entry for symbol (raises KeyError if absent).

asymmetry.core.negmu.lifetimes.tau_us(symbol: str) float[source]

Return the capture lifetime (µs) for symbol.

asymmetry.core.negmu.lifetimes.has_element(symbol: str) bool[source]

Return True if symbol is in the table.

asymmetry.core.negmu.lifetimes.elements() list[str][source]

Element symbols present, ordered by atomic number Z.

Multi-exponential count model

EXPERIMENTAL — WORK IN PROGRESS. Negative-muon (μ⁻) capture-lifetime analysis.

This API is UNVALIDATED against real μ⁻ elemental-analysis data. No μ⁻ corpus exists in this project; every result here has been exercised only against synthetic histograms. The element lifetime values are literature-anchored (Suzuki, Measday & Roalsvig, Phys. Rev. C 35, 2212 (1987), via Blundell et al., Muon Spectroscopy: An Introduction, OUP 2022, Table C.1), but the fitting, capture-ratio, and background machinery have NOT been checked against an established tool (WiMDA, Mantid) on measured data. The API, parameter names, and return shapes MAY CHANGE without notice. Do not rely on results for publication without independent verification. This feature is deliberately NOT exposed in the GUI fit builders. Promotion trigger for a GUI: real ISIS μ⁻ data AND a user.

Multi-exponential μ⁻ capture count model: N(t) = Σ_i amp_i·exp(−t/τ_i) + bg. A raw-count model, NOT an asymmetry model — see the study comparison.md §3 for why the single-envelope count model used for μ⁺ cannot express this.

class asymmetry.core.negmu.model.CaptureComponent(label: str, tau_us: float)[source]

One exponential component in the capture count model.

label: str
tau_us: float
__init__(label: str, tau_us: float) None
asymmetry.core.negmu.model.build_capture_count_model(components: Sequence[CaptureComponent]) Callable[[...], ndarray[tuple[int, ...], dtype[float64]]][source]

Return f(t, **params) → raw counts for the fixed component order.

Recognised params: amp_<label> (per component), optional tau_<label> (overrides the component seed when the lifetime is freed), and background (flat). Unknown params are ignored. Vectorised over t (µs).

asymmetry.core.negmu.model.evaluate_capture_model(components: Sequence[CaptureComponent], params: dict[str, float], t: ndarray[tuple[int, ...], dtype[float64]]) ndarray[tuple[int, ...], dtype[float64]][source]

Convenience: build + call in one step (used by background.py and tests).

Fitting

EXPERIMENTAL — WORK IN PROGRESS. Negative-muon (μ⁻) capture-lifetime analysis.

This API is UNVALIDATED against real μ⁻ elemental-analysis data. No μ⁻ corpus exists in this project; every result here has been exercised only against synthetic histograms. The element lifetime values are literature-anchored (Suzuki, Measday & Roalsvig, Phys. Rev. C 35, 2212 (1987), via Blundell et al., Muon Spectroscopy: An Introduction, OUP 2022, Table C.1), but the fitting, capture-ratio, and background machinery have NOT been checked against an established tool (WiMDA, Mantid) on measured data. The API, parameter names, and return shapes MAY CHANGE without notice. Do not rely on results for publication without independent verification. This feature is deliberately NOT exposed in the GUI fit builders. Promotion trigger for a GUI: real ISIS μ⁻ data AND a user.

Fit a μ⁻ capture-lifetime histogram (raw counts) to the multi-exponential model. Reuses the shared minimiser drive (drive_minuit), FitResult, and Parameter/ParameterSet. The Poisson (Cash) and Gaussian (√N) count costs are replicated locally (the fitting engine’s helpers are private API, off-limits). Lifetimes are FIXED at the table value by default; free any via spec.free_tau.

class asymmetry.core.negmu.fit.CaptureModelSpec(elements: tuple[str, ...], include_decay_background: bool = True, free_tau: frozenset[str] = <factory>)[source]

Which elemental components are in the fit, with τ fixed by default.

elements: tuple[str, ...]
include_decay_background: bool = True
free_tau: frozenset[str]
components() tuple[CaptureComponent, ...][source]

Element components (τ from table) + optional decayBG (τ_μ).

labels() tuple[str, ...][source]

Component labels in component order.

__init__(elements: tuple[str, ...], include_decay_background: bool = True, free_tau: frozenset[str] = <factory>) None
asymmetry.core.negmu.fit.default_capture_parameters(spec: CaptureModelSpec, *, time: ndarray[tuple[int, ...], dtype[float64]], counts: ndarray[tuple[int, ...], dtype[float64]], seeds: Mapping[str, float] | None = None) ParameterSet[source]

Seeded ParameterSet for a capture fit.

amp_<label> (free, ≥0) — derived from the signal window integral using the exact telescoping normalisation so that seeds scale correctly with τ: amp_seed_i = (signal / n_comp) · (1 − exp(−Δt/τ_i)) / (1 − exp(−T/τ_i)). tau_<label> (fixed unless label in spec.free_tau; freed τ bounded to [0.5×, 2×] the table value, widened to include any explicit seed override). background (free, ≥0) — seeded from the late-time floor. seeds overrides any amp_/tau_/background value before Parameter creation.

asymmetry.core.negmu.fit.fit_capture_histogram(time: ndarray[tuple[int, ...], dtype[float64]], counts: ndarray[tuple[int, ...], dtype[float64]], spec: CaptureModelSpec, *, variance: ndarray[tuple[int, ...], dtype[float64]] | None = None, cost: str = 'poisson', parameters: ParameterSet | None = None, polarisation: str | None = None, minos: bool = False, cancel_callback: Callable[[], bool] | None = None) FitResult[source]

Fit raw counts to Σ_i amp_i·exp(−t/τ_i)+bg, with optional polarisation.

The array-level entry: testable without a dataset. variance (if given) is used by the Gaussian cost; Poisson (Cash) ignores it. Returns a FitResult with dof set so assess_fit_quality() applies.

Parameters:

polarisationNone (no polarisation; result identical to the base model), "lorgau" (Lorentzian-damped oscillation), or "diamagnetic" (undamped precession). When not None, the exponential sum is multiplied by (1 + P_pol(t)) and extra parameters pol_a0, pol_freq, pol_phase (and pol_lam for “lorgau”) are added to the fit. Pass them via parameters or rely on the conservative default seeds (pol_freq = 0.5 MHz).

asymmetry.core.negmu.fit.fit_capture_group(dataset, group_id: int, spec: CaptureModelSpec, *, t_min: float | None = None, t_max: float | None = None, cost: str = 'poisson', parameters: ParameterSet | None = None, polarisation: str | None = None, exclude: tuple[float, float] | None = None, minos: bool = False, cancel_callback: Callable[[], bool] | None = None) FitResult[source]

Build the raw (time, counts) trace for group_id and fit.

Uses build_count_group() with lifetime_corrected=False to obtain raw counts on which the Poisson cost is exact, then delegates to fit_capture_histogram(). parameters and polarisation are forwarded verbatim.

asymmetry.core.negmu.fit.fit_capture_fb_alpha(dataset, forward_group: int, backward_group: int, spec: CaptureModelSpec, *, cost: str = 'poisson', t_min: float | None = None, t_max: float | None = None, exclude: tuple[float, float] | None = None, alpha_seed: float = 1.0, minos: bool = False, cancel_callback: Callable[[], bool] | None = None) GroupedTimeDomainFitResult[source]

Simultaneously fit forward and backward capture histograms with shared per-element amplitudes amp_<label>, shared τ_i, and a free detector balance α:

N_F(t) = √α · Σ_i amp_i·exp(−t/τ_i) + bg_F N_B(t) = (1/√α) · Σ_i amp_i·exp(−t/τ_i) + bg_B

mirroring build_count_groups geometry and the √α detector-balance split. Both banks built in ONE context (build_count_groups) for a common t0. Returns a GroupedTimeDomainFitResult: group_results keyed by forward_group / backward_group; shared_parameters holds α, amplitudes, and τ.

DIVERGENCE from WiMDA: WiMDA fits independent per-side amplitudes (NF, NB); this shares amp_i (isotropic capture populations), so per-side capture ratios are identical by construction. Use fit_capture_group() on each side independently when a genuine F/B amplitude difference is wanted.

Capture-ratio report

EXPERIMENTAL — WORK IN PROGRESS. Negative-muon (μ⁻) capture-lifetime analysis.

This API is UNVALIDATED against real μ⁻ elemental-analysis data. No μ⁻ corpus exists in this project; every result here has been exercised only against synthetic histograms. The element lifetime values are literature-anchored (Suzuki, Measday & Roalsvig, Phys. Rev. C 35, 2212 (1987), via Blundell et al., Muon Spectroscopy: An Introduction, OUP 2022, Table C.1), but the fitting, capture-ratio, and background machinery have NOT been checked against an established tool (WiMDA, Mantid) on measured data. The API, parameter names, and return shapes MAY CHANGE without notice. Do not rely on results for publication without independent verification. This feature is deliberately NOT exposed in the GUI fit builders. Promotion trigger for a GUI: real ISIS μ⁻ data AND a user.

Capture-ratio report: relative capture probabilities from fitted amplitude ratios amp_i/amp_ref, with covariance-aware uncertainties. Adapts WiMDA’s RatioButtonClick (NegMuAnalyse.pas:455–620) as a derived-quantities function — no new results framework.

class asymmetry.core.negmu.ratio.CaptureRatio(numerator: str, denominator: str, ratio: float, sigma: float)[source]

One amplitude ratio and its uncertainty.

numerator: str
denominator: str
ratio: float
sigma: float
__init__(numerator: str, denominator: str, ratio: float, sigma: float) None
class asymmetry.core.negmu.ratio.CaptureRatioReport(side: str, reference: str, ratios: tuple[CaptureRatio, ...], amplitudes: dict[str, float], amplitude_uncertainties: dict[str, float])[source]

Derived capture-probability ratios from a single fit result.

side: str
reference: str
ratios: tuple[CaptureRatio, ...]
amplitudes: dict[str, float]
amplitude_uncertainties: dict[str, float]
__init__(side: str, reference: str, ratios: tuple[CaptureRatio, ...], amplitudes: dict[str, float], amplitude_uncertainties: dict[str, float]) None
asymmetry.core.negmu.ratio.capture_ratio_report(fit: FitResult, spec: CaptureModelSpec, *, reference: str, side: str = 'forward') CaptureRatioReport[source]

Compute per-element capture-probability ratios amp_label/amp_reference.

For each element label ≠ reference (and ≠ decayBG, excluded by default):

R = amp_label / amp_reference
σ_R = R · sqrt((σ_i/amp_i)² + (σ_ref/amp_ref)² − 2·cov(i,ref)/(amp_i·amp_ref))

where the covariance term is taken from fit.covariance / fit.covariance_parameters when both parameters are present in the covariance matrix, and falls back to 0 (pure quadrature) otherwise.

asymmetry.core.negmu.ratio.fb_capture_ratio_report(grouped: GroupedTimeDomainFitResult, spec: CaptureModelSpec, forward_group: int, backward_group: int, *, reference: str) dict[str, CaptureRatioReport][source]

Per-side capture-ratio reports from a fit_capture_fb_alpha() result.

Returns {'forward': CaptureRatioReport, 'backward': CaptureRatioReport}. Since amplitudes are shared in the F+B fit, the two reports will have identical ratios by construction (they differ only in their side label).

Background subtraction

EXPERIMENTAL — WORK IN PROGRESS. Negative-muon (μ⁻) capture-lifetime analysis.

This API is UNVALIDATED against real μ⁻ elemental-analysis data. No μ⁻ corpus exists in this project; every result here has been exercised only against synthetic histograms. The element lifetime values are literature-anchored (Suzuki, Measday & Roalsvig, Phys. Rev. C 35, 2212 (1987), via Blundell et al., Muon Spectroscopy: An Introduction, OUP 2022, Table C.1), but the fitting, capture-ratio, and background machinery have NOT been checked against an established tool (WiMDA, Mantid) on measured data. The API, parameter names, and return shapes MAY CHANGE without notice. Do not rely on results for publication without independent verification. This feature is deliberately NOT exposed in the GUI fit builders. Promotion trigger for a GUI: real ISIS μ⁻ data AND a user.

Set-as-BG capture-component subtraction.

Adapts WiMDA’s SetBgButtonClick (NegMuAnalyse.pas) as a histogram-level model subtraction: evaluate the unwanted components from the fitted parameters and subtract from the counts, leaving the signal of interest. The derived Run produced by capture_background_run() can be re-fitted with fit_capture_group().

asymmetry.core.negmu.background.subtract_capture_background(time: ndarray[tuple[int, ...], dtype[float64]], counts: ndarray[tuple[int, ...], dtype[float64]], fit: FitResult, spec: CaptureModelSpec, *, unwanted: Sequence[str]) ndarray[tuple[int, ...], dtype[float64]][source]

Return counts Σ_{label in unwanted} amp_label·exp(−t/tau_label).

The unwanted components are evaluated from the fitted parameters via evaluate_capture_model(). The result is the residual signal of interest. Passing an empty unwanted sequence is the identity — the counts are returned unchanged. (WiMDA SetBgButtonClick adapted as a histogram-level model subtraction, comparison.md §7.)

Note

The unwanted components are evaluated from the non-polarised model (plain amp·exp(−t/τ)). If the fit was run with a Phase-4 polarisation multiplier, that (1 + P_pol(t)) modulation is not reproduced here, so the subtraction is only exact for the unpolarised capture model.

Parameters:
  • time – Time axis (µs) — the same array used for the fit.

  • counts – Raw count histogram on that time axis.

  • fitFitResult from a preceding fit_capture_group() / fit_capture_histogram() call.

  • specCaptureModelSpec used for the fit — provides the component ordering and lifetime seeds.

  • unwanted – Labels of the components to subtract. Each must appear in spec. Duplicates are silently ignored.

asymmetry.core.negmu.background.capture_background_run(dataset: MuonDataset, group_id: int, fit: FitResult, spec: CaptureModelSpec, *, unwanted: Sequence[str], run_number: int | None = None) Run[source]

Derived Run with unwanted components subtracted.

Builds the raw grouped histogram for group_id via build_count_group() (lifetime_corrected=False), subtracts the unwanted components, and returns a minimal Run suitable for re-fitting with fit_capture_group().

The derived Run contains a single detector histogram (the grouped, subtracted counts) with group_id mapped to that detector. This preserves the time-bin structure of the original fit exactly.

Parameters:
  • dataset – Source dataset with a Run and grouping (same dataset passed to the original fit).

  • group_id – Group whose histogram is to be cleaned.

  • fitFitResult from the preceding fit of this group.

  • specCaptureModelSpec used for that fit.

  • unwanted – Labels of the components to subtract. Empty sequence → identity.

  • run_number – Run number for the derived Run. Defaults to the source run’s number.

Polarisation multipliers

EXPERIMENTAL — WORK IN PROGRESS. Negative-muon (μ⁻) capture-lifetime analysis.

This API is UNVALIDATED against real μ⁻ elemental-analysis data. No μ⁻ corpus exists in this project; every result here has been exercised only against synthetic histograms. The element lifetime values are literature-anchored (Suzuki, Measday & Roalsvig, Phys. Rev. C 35, 2212 (1987), via Blundell et al., Muon Spectroscopy: An Introduction, OUP 2022, Table C.1), but the fitting, capture-ratio, and background machinery have NOT been checked against an established tool (WiMDA, Mantid) on measured data. The API, parameter names, and return shapes MAY CHANGE without notice. Do not rely on results for publication without independent verification. This feature is deliberately NOT exposed in the GUI fit builders. Promotion trigger for a GUI: real ISIS μ⁻ data AND a user.

μ⁻SR polarisation functions and a polarisation-aware capture count model.

In μ⁻SR a surviving spin polarisation (≥5/6 is lost in the muonic cascade; Blundell et al. 2022, §22.1) gives rise to an oscillatory modulation of the muonic-atom count decay. The modulation multiplies the raw exponential sum by (1 + P_pol(t)), leaving the flat background unchanged.

Two polarisation functions are provided:

  • lorentzian_gaussian_polarisation (WiMDA “LorGau”) — damped precession

  • diamagnetic_polarisation (WiMDA “Diamagnetic”) — undamped precession

Both return P_pol(t) (the modulation depth × envelope × oscillation), not the full multiplier (1 + P_pol(t)). The composite model builder build_capture_count_model_with_polarisation wraps the multiplier.

Parameterisation conventions confirmed against WiMDA Analyse.pas Polfunc() (RadioGroup1.itemindex = 2, lines 1322–1323):

LorGau forward: (1 + a0·exp(−λ·t)·cos(2π·freq·t + phase))

where freq is in MHz, t in µs, λ in µs⁻¹, and phase in radians. Diamagnetic is the undamped (λ = 0) limit. WiMDA’s Diamagnetic RadioGroup mode (itemindex = 3) has no implementation in Polfunc (falls through with undefined result — a WiMDA bug), so the Asymmetry formula is derived from the textbook physics rather than the WiMDA source.

asymmetry.core.negmu.polarisation.POLARISATION_MODES: tuple[str, ...] = ('lorgau', 'diamagnetic')

Recognised polarisation mode names.

asymmetry.core.negmu.polarisation.lorentzian_gaussian_polarisation(t: ndarray[tuple[int, ...], dtype[float64]], a0: float, lam: float, freq: float, phase: float) ndarray[tuple[int, ...], dtype[float64]][source]

Lorentzian-damped oscillation — WiMDA “LorGau” polarisation function.

Returns a0·exp(−λ·t)·cos(2π·freq·t + phase) (the modulation part P_pol(t) only; the full multiplier is 1 + P_pol(t)).

Parameters:
  • t – Time array in µs.

  • a0 – Precession amplitude (dimensionless, typically ≤ 1/3 from muonic cascade geometry).

  • lam – Lorentzian damping rate λ in µs⁻¹.

  • freq – Precession frequency in MHz.

  • phase – Initial phase in radians.

asymmetry.core.negmu.polarisation.diamagnetic_polarisation(t: ndarray[tuple[int, ...], dtype[float64]], a0: float, freq: float, phase: float) ndarray[tuple[int, ...], dtype[float64]][source]

Undamped diamagnetic precession — WiMDA “Diamagnetic” polarisation.

Returns a0·cos(2π·freq·t + phase) (P_pol(t) only; full multiplier is 1 + P_pol(t)). This is the λ → 0 limit of lorentzian_gaussian_polarisation().

Parameters:
  • t – Time array in µs.

  • a0 – Precession amplitude (dimensionless).

  • freq – Precession frequency in MHz.

  • phase – Initial phase in radians.

asymmetry.core.negmu.polarisation.build_capture_count_model_with_polarisation(components: Sequence[CaptureComponent], polarisation: str | None = None) Callable[[...], ndarray[tuple[int, ...], dtype[float64]]][source]

Return f(t, **params) → raw counts with optional polarisation multiplier.

When polarisation is None the result is bit-identical to build_capture_count_model() on the same inputs — verified by np.array_equal.

When polarisation is "lorgau" or "diamagnetic" the model is:

N(t) = [Σ_i amp_i·exp(−t/τ_i)] · (1 + P_pol(t)) + background

where P_pol(t) is the polarisation function evaluated from the following named parameters in **params:

  • "lorgau": pol_a0, pol_lam, pol_freq, pol_phase

  • "diamagnetic": pol_a0, pol_freq, pol_phase

Missing pol params default to 0. Unknown params are ignored.

Parameters:
  • components – Capture components (same as build_capture_count_model()).

  • polarisationNone (no polarisation), "lorgau", or "diamagnetic".