Fitting
The fitting subsystem is split into four parts: the iminuit-backed
FitEngine that minimises any
f(t, **params) -> array callable; the
MODELS registry of standalone
time-domain models with explicit baselines (one-shot Python use); the
CompositeModel builder that
parses arithmetic expressions over the
COMPONENTS registry into a
compiled callable (the canonical path for any non-trivial muSR model,
mirroring the GUI Edit Function… dialog); and the
parameter_models package that fits
extracted fit parameters as a function of field, temperature, or run
number with the same machinery. Superconducting models live under
asymmetry.core.fitting.sc and are surfaced through both the
composite registry and the parameter-trending registry — the physics
context is in Superconducting penetration depth models. Symbols, units,
and physical descriptions for every fit parameter are sourced from the
PARAM_INFO_REGISTRY, which
is canonical: GUI labels, GLE export labels, and the autodoc parameter
tables all derive from it.
Frequency-domain peak fitting reuses the same engine and composite-model path. The spectral helper module supplies the default Fourier-spectrum peak model, MHz/G conversion, and derived field parameters for trend tables.
Fit engine
- class asymmetry.core.fitting.engine.FitEngine[source]
Bases:
objectFit μSR asymmetry data to a model function using iminuit.
Example
from asymmetry.core.fitting import FitEngine, ParameterSet, Parameter from asymmetry.core.fitting.models import MODELS engine = FitEngine() model = MODELS["ExponentialRelaxation"] # Set up parameters params = ParameterSet() params.add(Parameter(name="A0", value=0.2, min=0, max=1)) params.add(Parameter(name="lambda", value=0.5, min=0)) result = engine.fit(dataset, model.function, params) print(f"χ²ᵣ = {result.reduced_chi_squared:.3f}")
- fit(dataset: MuonDataset, model_fn: Callable[[...], ndarray[tuple[int, ...], dtype[_ScalarType_co]]], parameters: ParameterSet, t_min: float | None = None, t_max: float | None = None, method: str = 'migrad', minos: bool = False, cancel_callback: Callable[[], bool] | None = None, frequency_offsets: dict[str, float] | None = None, cost_factory: CostFactory | None = None, migrad_kwargs: dict | None = None, *, error_oversampling: float = 1.0) FitResult[source]
Run a single-dataset fit.
- Parameters:
dataset (MuonDataset) – The data to fit. The engine uses the dataset object’s
time,asymmetry, anderrorarrays as provided, optionally clipped only byt_min/t_max.model_fn (callable) –
f(t, **params) -> array.parameters (ParameterSet) – Initial parameter values and constraints.
t_min (float, optional) – Restrict fit range.
t_max (float, optional) – Restrict fit range.
method (str) – Minimization method (
"migrad"for gradient-based,"simplex"for Nelder-Mead).cost_factory (CostFactory, optional) – Selectable fit objective (
GAUSSIAN_COST/POISSON_COST).Nonekeeps the historical √-weighted least squares, byte-for-byte. WithPOISSON_COSTthechi_squared/reduced_chi_squaredreport the Cash statistic (asymptotically χ²-distributed), and the data must be raw Poisson counts with a count-expectation model.migrad_kwargs (dict, optional) – Forwarded to
m.migrad/m.simplexvia the shareddrive_minuit()seam (e.g.ncallto cap iterations for a cheap screening fit).Nonekeeps the historical unbounded drive.frequency_offsets (dict[str, float], optional) – Rotating-reference-frame offsets
{param: ν₀}resolved byasymmetry.core.fitting.rrf_offset.rrf_frequency_offsets(). When given, each named parameter is shifted by its offset before the model is evaluated, so the raw lab-frame data is fitted while the fitted values read as rotating-frame offsets δν (lab = δν + ν₀ viaapply_rrf_offsets()). This is the engine-level home of the RRF offset wrapper — fitting through it is bit-for-bit identical to wrappingmodel_fnwithrrf_offset_model().error_oversampling (float, optional) –
Correction for correlated samples, keyword-only, default
1.0(no correction). A zero-padded FFT spectrum is sinc-interpolated: padding by a factorsdensifies the sampled points but does not add independent information, so only~1/sof the points are statistically independent. Fitting every sample as if independent overcounts the χ² sum by~sand underestimates parameter uncertainties by~√s. Pass the spectrum’s zero-padding factor (metadata["fourier_padding"]fromasymmetry.core.fourier.spectrum.compute_average_group_spectrum()) here; for time-domain or count fits this correction is meaningless and callers pass the default1.0.WiMDA’s precedent (
Analyse.pas:5228) is to divide the reported dof by the zero-pad factor (dof := n div zpad - nv) but leaves χ² and the parameter errors untouched. This implementation is the fully consistent version: whenerror_oversampling(s) is greater than 1,the effective point count is
ndata_eff = max(round(ndata / s), 1);dof = max(ndata_eff - nfree, 1);the minimised χ² summed
s×too many correlated terms, sochi_squared = m.fval / sandreduced_chi_squared = chi_squared / dof;Minuit’s HESSE/MINOS errors come from the curvature of the overcounted χ², so every entry of
uncertaintiesis scaled by√s, thecovariancematrix bys, and everyminos_errorsbound by√s;the fitted parameter values are untouched — the overcounting scales the curvature, not the location of the minimum.
A
FitResult.warningsadvisory records the applied correction so it stays visible wherever the fit result is surfaced.
- Returns:
Container with fit results including χ², parameters, and uncertainties.
- Return type:
- global_fit(datasets: list[MuonDataset], model_fn: Callable[[...], ndarray[tuple[int, ...], dtype[_ScalarType_co]]], global_params: list[str], local_params: list[str], initial_params: dict[int, ParameterSet], t_min: float | None = None, t_max: float | None = None, method: str = 'migrad', max_calls: int = 10000, migrad_iterations: int = 5, use_simplex_rescue: bool = True, minuit_strategy: int | None = None, minuit_tol: float | None = None, initial_step_sizes: dict[str, float] | None = None, minos: bool = False, screening: bool = False, strategy: str = 'joint', use_varpro: bool = False, cancel_callback: Callable[[], bool] | None = None, cost_factory: CostFactory | None = None, local_param_groups: dict[str, dict[int, Hashable]] | None = None, *, error_oversampling: float = 1.0) tuple[dict[int, FitResult], ParameterSet][source]
Simultaneous fit of multiple datasets with shared and local parameters.
- Parameters:
datasets – List of datasets to fit simultaneously.
model_fn – Model function applied to each dataset.
global_params – Names of parameters shared across all datasets (e.g., [“A0”]).
local_params – Names of parameters that vary per dataset (e.g., [“lambda”]).
initial_params – Dictionary mapping dataset run_number to initial ParameterSet. Global parameters should have the same value in all sets.
t_min – Optional time range restriction applied to all datasets.
t_max – Optional time range restriction applied to all datasets.
method – Minimization method (“migrad” or “simplex”).
max_calls – Maximum function evaluations for minimization. Limits runtime for large global fits.
strategy – Minimiser architecture for the shared-parameter problem.
"joint"(default) builds one Minuit problem over the globals plus every dataset’s locals — the historical, byte-for-byte path."profiled"runs an outer Minuit over the free globals only and, for each candidate global vector, solves each dataset’s locals independently (viafit()with the globals held fixed). The two share the same objective, so at the optimum they agree on values and χ²; profiled drops the per-fit Hessian from(n_global+n_local·G)²ton_global²plusGsmall block Hessians, so cost scales ~linearly inGrather than super-linearly. Profiled requires at least one free global (with none, the joint problem is already block-separable and the fast path below handles it).use_varpro – Deferred — not implemented. Passing
use_varpro=Truecurrently raisesNotImplementedError; the defaultFalseis the only supported value. Variable projection (technique M) is planned as a follow-up: it would solve the parameters flagged linear in the model metadata (amplitudes, constant backgrounds; seeasymmetry.core.fitting.models.default_linear_params()) by weighted linear least-squares inside each residual and remove them from the nonlinear Minuit vector, still counting them indof/ the ICk. It is deferred because, once the"profiled"strategy has already separated the per-dataset locals, VarPro is only a constant-factor per-fit win (it does not change the G-exponent), and reproducing the current engine’s marginal parameter errors needs a final full Hessian over all parameters. The role-based linear-parameter metadata is in place for that follow-up.
- Returns:
results (dict[int, FitResult]) – Dictionary mapping run_number to individual FitResult for each dataset.
global_result (ParameterSet) – The fitted global parameters with uncertainties.
local_param_groups – Optional per-local-parameter sharing.
local_param_groups[pname]maps a dataset run number to a group key; datasets with the same key share one fitted value forpname(a third scope between fully global and fully per-dataset). Absent →pnameis per-dataset.error_oversampling (float, optional) – Correction for correlated samples (zero-padded FFT spectra), applied to every dataset’s reported
chi_squared/reduced_chi_squared/dof/uncertainties/covariance/minos_errorsthe same way asfit()— see that method’s docstring for the exact relations, the WiMDA precedent, and why fitted parameter values are unaffected. Keyword-only, default1.0(no correction); pass the common zero-padding factor of the fitted spectra (metadata["fourier_padding"]), or1.0for time-domain/count fits.
Notes
Fixed parameters (where param.fixed=True) are held constant during fitting.
Models
Built-in μSR fit functions.
Each model is a callable f(t, **params) -> array plus metadata describing
its parameters. Models are collected in the MODELS registry.
- asymmetry.core.fitting.models.LINEAR_PARAM_ROLE_NAMES: frozenset[str] = frozenset({'A', 'A0', 'A_bg', 'BG', 'a_Dia', 'baseline', 'bg', 'c0', 'c1', 'c2', 'c3', 'c4', 'c5', 'c6'})
Groundwork for the deferred variable-projection (VarPro) follow-up. VarPro is not wired into a fit path yet (see the PR body: it is a constant-factor win once profiled locals already separate the per-dataset solves, and its marginal errors need a final full Hessian), so this metadata is presently consumed only by tests and the follow-up plan — but it is the generic, name-role marking the study asks for, kept here so the follow-up does not re-derive it.
Parameter role names that enter a μSR model linearly — amplitudes and additive constant backgrounds. These are roles, not component names: an amplitude
Ascales its term and a constant baselinebaselineadds to the model, so both appear to first order asmodel = a·term + .... Variable projection (VarPro) solves such parameters by linear least-squares inside the objective instead of by Minuit. The set is deliberately generic (matched by name, never by which component a parameter belongs to); anf_<Component>fraction weight is not here because a normalised fraction group is not a free linear scale. VarPro always verifies affineness numerically at runtime and falls back to the nonlinear treatment when a flagged name is not actually affine in a given model, so this list only has to be a safe over-approximation of “usually linear”.
- asymmetry.core.fitting.models.default_linear_params(param_names: list[str]) list[str][source]
Role-based linear-parameter guess for a model’s parameter list.
Returns the subset of
param_nameswhose (index-stripped) base name is a known linear role (LINEAR_PARAM_ROLE_NAMES) — i.e. an amplitude or an additive constant background. This is only a candidate set: VarPro verifies affineness numerically before using it, so a false positive is corrected at runtime, never silently trusted.
- class asymmetry.core.fitting.models.ModelDefinition(name: str, description: str, function: Callable[[...], ndarray[tuple[int, ...], dtype[float64]]], param_names: list[str], param_defaults: dict[str, float], param_info: dict[str, ParamInfo], domain: str = 'time', linear_params: list[str] | None = None)[source]
Descriptor for a built-in fit function.
- linear_params: list[str] | None = None
Names of parameters that enter the model linearly (amplitudes, constant backgrounds).
Nonemeans “derive from role names viadefault_linear_params()”. Variable projection solves these by linear least-squares, but only after a numeric affineness check, so an entry here is a hint, not a guarantee.
- asymmetry.core.fitting.models.exponential_relaxation(t: ndarray[tuple[int, ...], dtype[_ScalarType_co]], A0: float, Lambda: float, baseline: float = 0.0) ndarray[tuple[int, ...], dtype[_ScalarType_co]][source]
Simple exponential: A(t) = A0 exp(−Λt) + baseline.
- asymmetry.core.fitting.models.gaussian_relaxation(t: ndarray[tuple[int, ...], dtype[_ScalarType_co]], A0: float, sigma: float, baseline: float = 0.0) ndarray[tuple[int, ...], dtype[_ScalarType_co]][source]
Gaussian relaxation: A(t) = A0 exp(−σ²t²) + baseline.
- asymmetry.core.fitting.models.oscillatory(t: ndarray[tuple[int, ...], dtype[_ScalarType_co]], A0: float, frequency: float, phase: float = 0.0, Lambda: float = 0.0, baseline: float = 0.0) ndarray[tuple[int, ...], dtype[_ScalarType_co]][source]
Damped oscillation: A0 cos(2πft + φ) exp(−Λt) + baseline.
- asymmetry.core.fitting.models.stretched_exponential(t: ndarray[tuple[int, ...], dtype[_ScalarType_co]], A0: float, Lambda: float, beta: float = 1.0, baseline: float = 0.0) ndarray[tuple[int, ...], dtype[_ScalarType_co]][source]
Stretched exponential: A0 exp(−(Λt)^β) + baseline.
- asymmetry.core.fitting.models.static_gkt_zf(t: ndarray[tuple[int, ...], dtype[_ScalarType_co]], A0: float, Delta: float, baseline: float = 0.0) ndarray[tuple[int, ...], dtype[_ScalarType_co]][source]
Static Gaussian Kubo-Toyabe (zero field).
GKT(t) = A0 [1/3 + 2/3 (1 − Δ²t²) exp(−Δ²t²/2)] + baseline
- asymmetry.core.fitting.models.longitudinal_field_kubo_toyabe(t: ndarray[tuple[int, ...], dtype[_ScalarType_co]], A0: float, Delta: float, B_L: float, baseline: float = 0.0) ndarray[tuple[int, ...], dtype[_ScalarType_co]][source]
Static Gaussian Kubo-Toyabe relaxation in a longitudinal field.
The longitudinal-field depolarisation function for a muon in a static, isotropic Gaussian distribution of local fields of width
Deltawith an applied longitudinal decoupling fieldB_L. AsB_Lis swept through the decoupling crossover the polarisation recovers toward unity, which is the experimental signature of a static (rather than dynamic) local field.\[G_z(t) = 1 - \frac{2\Delta^2}{\omega_0^2} \left[1 - e^{-\Delta^2 t^2/2}\cos(\omega_0 t)\right] + \frac{2\Delta^4}{\omega_0^3} \int_0^t e^{-\Delta^2\tau^2/2}\sin(\omega_0\tau)\,d\tau ,\]with \(\omega_0 = \gamma_\mu B_L\) (
B_Lin Gauss, converted to Tesla internally). The returned asymmetry is \(A(t) = A_0\,G_z(t) + baseline\). ForB_L = 0it reduces exactly to the zero-field Gaussian Kubo-Toyabe function (static_gkt_zf()); for largeB_Lit tends to 1 (decoupling).- Parameters:
t (NDArray) – Time values in microseconds.
A0 (float) – Initial asymmetry amplitude at
t = 0.Delta (float) – Static Gaussian field-distribution width in us^-1 (Delta = gamma_mu * sqrt(<B^2>)).
B_L (float) – Applied longitudinal magnetic field in Gauss.
baseline (float, optional) – Constant additive baseline.
Notes
The oscillatory-decaying integral term is evaluated for all requested times at once by cumulative trapezoidal integration on a shared fine grid whose step is sized from
omega0andDelta. This is both faster and smoother than per-point adaptive quadrature (which left the integral noisy), so it speeds up the static component and the dynamic Kubo-Toyabe that builds on it.References
R. S. Hayano, Y. J. Uemura, J. Imazato, N. Nishida, T. Yamazaki and R. Kubo, “Zero- and low-field spin relaxation studied by positive muons”, Phys. Rev. B 20, 850 (1979).
- asymmetry.core.fitting.models.abragam(t: ndarray[tuple[int, ...], dtype[_ScalarType_co]], A0: float, Delta: float, nu: float, baseline: float = 0.0) ndarray[tuple[int, ...], dtype[_ScalarType_co]][source]
Abragam relaxation function (Abragam, Principles of Nuclear Magnetism, 1961).
G(t) = A0 exp[ -(Delta^2 / nu^2) (e^{-nu t} - 1 + nu t) ] + baseline
A single-component relaxation that interpolates between the static Gaussian line shape and the motionally-narrowed exponential:
nu -> 0: G -> A0 exp(-Delta^2 t^2 / 2) (Gaussian)nu >> Delta: G -> A0 exp(-(Delta^2 / nu) t) (exponential)
Notation and form follow Blundell, De Renzi, Lancaster & Pratt, Muon Spectroscopy: An Introduction (OUP, 2022), eqn 5.52 (the damping factor of the transverse-field Abragam function), with the same Gaussian width symbol
Deltaas the Kubo-Toyabe family.- Parameters:
t (NDArray) – Time values in microseconds.
A0 (float) – Initial asymmetry amplitude.
Delta (float) – Static Gaussian field-distribution width in us^-1.
nu (float) – Field fluctuation (hop) rate in MHz (== us^-1).
nu<= 0 gives the static Gaussian limit.baseline (float, optional) – Constant baseline offset.
- asymmetry.core.fitting.models.keren(t: ndarray[tuple[int, ...], dtype[_ScalarType_co]], A0: float, Delta: float, nu: float, B_L: float, baseline: float = 0.0) ndarray[tuple[int, ...], dtype[_ScalarType_co]][source]
Keren dynamic Gaussian relaxation in a longitudinal field (Keren, PRB 50, 10039 (1994)).
P(t) = A0 exp[-Gamma(t)] + baseline, with omega0 = gamma_mu * B_L and:
Gamma(t) = (2 Delta^2 / (omega0^2 + nu^2)^2) * [ (omega0^2 + nu^2) nu t + (omega0^2 - nu^2) (1 - e^{-nu t} cos(omega0 t)) - 2 nu omega0 e^{-nu t} sin(omega0 t) ]
Keren’s analytic generalisation of the Abragam function to a longitudinal field. At
B_L = 0it reduces to the Abragam exponent (x2, for the two transverse zero-field components): Gamma = (2 Delta^2 / nu^2)(e^{-nu t} - 1 + nu t).- Parameters:
t (NDArray) – Time values in microseconds.
A0 (float) – Initial asymmetry amplitude.
Delta (float) – Static Gaussian field-distribution width in us^-1.
nu (float) – Field fluctuation rate in MHz (== us^-1).
B_L (float) – Longitudinal magnetic field in Gauss (omega0 = gamma_mu * B_L).
baseline (float, optional) – Constant baseline offset.
- asymmetry.core.fitting.models.static_lorentzian_kt_zf(t: ndarray[tuple[int, ...], dtype[_ScalarType_co]], A0: float, a_L: float, baseline: float = 0.0) ndarray[tuple[int, ...], dtype[_ScalarType_co]][source]
Static Lorentzian Kubo-Toyabe, zero field (Uemura et al. PRB 31, 546 (1985)).
G(t) = A0 [1/3 + 2/3 (1 - a t) exp(-a t)] + baseline
For a dilute / Lorentzian distribution of local fields (e.g. spin glasses), where
a_Lis the half-width of the field distribution expressed as a rate in us^-1.
- asymmetry.core.fitting.models.static_lorentzian_kt_lf(t: ndarray[tuple[int, ...], dtype[_ScalarType_co]], A0: float, a_L: float, B_L: float, baseline: float = 0.0) ndarray[tuple[int, ...], dtype[_ScalarType_co]][source]
Static Lorentzian Kubo-Toyabe in a longitudinal field (computed numerically).
Blundell, De Renzi, Lancaster & Pratt, Muon Spectroscopy (OUP, 2022), sec 5.3, note that the Kubo-Toyabe function “becomes modified in applied field … [and] must be computed numerically”. This evaluates the stochastic field average (eqn 5.3) over an isotropic Lorentzian local-field distribution of half-width
a_L(us^-1) with applied longitudinal fieldB_L(Gauss), via the analytic angular + time reduction in_lorentzian_lf_lineshape().B_L -> 0: reduces to the zero-field Lorentzian KT (eqn 5.47), 1/3 + 2/3 (1 - a_L t) e^{-a_L t}.B_L -> inf: decoupling, G -> 1.
Accurate to better than ~0.1-0.3 % for B_L >~ 20 G (finer at higher field); very small fields (omega0 < 0.05 a_L) are treated as zero field, where the eqn 5.47 form is exact. The line shape is computed once on a grid and interpolated, and cached per (a_L, B_L, tmax).
- asymmetry.core.fitting.models.risch_kehr(t: ndarray[tuple[int, ...], dtype[_ScalarType_co]], Gamma: float) ndarray[tuple[int, ...], dtype[_ScalarType_co]][source]
Risch-Kehr relaxation for spin transport by 1D diffusion.
G(t) = e^{Gamma t} erfc(sqrt(Gamma t))
Relaxation of the muon (or muonium) polarisation when the depolarising agent diffuses in one dimension (e.g. a polaron on a conducting-polymer chain): the return probability of a 1D random walk gives a
(pi Gamma t)^{-1/2}long-time tail instead of an exponential.Evaluated as
erfcx(sqrt(Gamma t))(the scaled complementary error function), which is numerically stable for allGamma t— no asymptotic branch switch is needed (WiMDA switches forms atGamma t = 20).Gammais used as|Gamma|; WiMDA’s mirrored2 - Gbranch for negative rates is an unphysical fitting convenience and is not ported.References
Risch and K. W. Kehr, Phys. Rev. B 46, 5246 (1992).
- asymmetry.core.fitting.models.bessel_oscillation(t: ndarray[tuple[int, ...], dtype[_ScalarType_co]], frequency: float, phase: float = 0.0) ndarray[tuple[int, ...], dtype[_ScalarType_co]][source]
Zeroth-order Bessel oscillation J0(2 pi f t + phase).
The polarisation produced by an incommensurate (spin-density-wave) field distribution: an Overhauser distribution of local fields between -B_1 and +B_1 gives P(t) = J0(gamma_mu B_1 t) (Blundell, De Renzi, Lancaster & Pratt, Muon Spectroscopy, OUP 2022, eqn 6.47), which at late times looks like a damped cosine with a -45 degree phase shift (eqn 6.48). Here the field-distribution edge is parameterised as a frequency
f = gamma_mu B_1 / 2 piin MHz.
- asymmetry.core.fitting.models.gaussian_broadened_kt(t: ndarray[tuple[int, ...], dtype[_ScalarType_co]], Delta: float, B_L: float, w_rel: float) ndarray[tuple[int, ...], dtype[_ScalarType_co]][source]
Static Gaussian Kubo-Toyabe averaged over a Gaussian distribution of Delta.
G(t) = integral dDelta’ p(Delta’) G_KT(t; Delta’, B_L), with
pa Gaussian of meanDeltaand standard deviationw_rel * Delta(w_relis the fractional width). Models disordered hosts where a single Kubo-Toyabe width is too sharp — the distribution of static widths fills in the dip and softens the 1/3-tail recovery (cf. the Gaussian-broadened Gaussian of Noakes & Kalvius, Phys. Rev. B 56, 2352 (1997); WiMDA’sGau broad KT).Evaluated by Gauss-Hermite quadrature (21 nodes) over the Delta distribution, with negative quadrature widths reflected to
|Delta'|(as in WiMDA). The quadrature is evaluated directly at the requested times (vectorised over the nodes, with the Hayano longitudinal-field integral computed once on a shared fine grid for all nodes), so the model is smooth inw_relandw_rel = 0reduces exactly and continuously to the static (LF) Kubo-Toyabe.Note: WiMDA’s
rel widthparameter enters as weightexp(-(i/7)^2)overDelta(1 + w i/7), i.e. a Gaussian of fractional standard deviationw/sqrt(2);w_relhere is the fractional standard deviation (w_rel = w_WiMDA / sqrt(2)).
- asymmetry.core.fitting.models.dynamic_gaussian_kt(t: ndarray[tuple[int, ...], dtype[_ScalarType_co]], A0: float, Delta: float, nu: float, B_L: float = 0.0, baseline: float = 0.0) ndarray[tuple[int, ...], dtype[_ScalarType_co]][source]
Dynamic Gaussian Kubo-Toyabe (strong collision; Hayano et al. PRB 20, 850 (1979)).
Strong-collision generalisation of the static Gaussian KT: a Gaussian local field of width
Delta(us^-1) fluctuating at ratenu(MHz), with optional longitudinal fieldB_L(Gauss).nu -> 0: recovers the static (LF) Gaussian Kubo-Toyabe.nu >> Delta: motional narrowing, G -> exp(-2 Delta^2 t / nu) (B_L = 0).B_L -> inf: decoupling, G -> 1.
- asymmetry.core.fitting.models.dynamic_lorentzian_kt(t: ndarray[tuple[int, ...], dtype[_ScalarType_co]], A0: float, a_L: float, nu: float, B_L: float = 0.0, baseline: float = 0.0) ndarray[tuple[int, ...], dtype[_ScalarType_co]][source]
Dynamic Lorentzian Kubo-Toyabe (strong collision; Uemura et al. PRB 31, 546 (1985)).
Strong-collision generalisation of the static Lorentzian KT for a dilute / Lorentzian local-field distribution of half-width
a_L(us^-1) fluctuating at ratenu(MHz), with optional longitudinal fieldB_L(Gauss).nu -> 0: recovers the static Lorentzian KT (zero-field analytic eqn 5.47; longitudinal field computed numerically per Blundell et al. 2022).B_L -> inf: decoupling, G -> 1.
Composite models
- class asymmetry.core.fitting.composite.ComponentDefinition(name: str, description: str, function: Callable[[...], ndarray[tuple[int, ...], dtype[float64]]], param_names: list[str], param_defaults: dict[str, float], param_info: dict[str, ParamInfo], formula_template: str, latex_equation: str = '', category: str = 'General', domain: str = 'time', fixed_params: tuple[str, ...] = (), user: bool = False, missing: bool = False, knight_observable: str | None = None, field_geometries: frozenset[FieldGeometry] = frozenset({FieldGeometry.LF, FieldGeometry.TF, FieldGeometry.ZF}), physics_classes: frozenset[PhysicsClass] = frozenset({PhysicsClass.CUSTOM}), cost: ComputationalCost = ComputationalCost.MODERATE)[source]
Bases:
objectDescriptor for a baseline-free component function.
- fixed_params: tuple[str, ...] = ()
Parameters that should start fixed in a fit (e.g. a nuclear spin the model is piecewise-constant in, or a hyperfine constant that is known). The GUI pre-checks the fix box; the user can always free them.
- user: bool = False
Truefor components registered through the user-function facade (asymmetry.core.fitting.user_functions). Provenance is keyed off this flag — picker badges and the docs-enforcement exemptions — never off name lists.
- missing: bool = False
Truefor the per-instance placeholder definitions that stand in for a user component referenced by a project but not currently registered. Placeholders evaluate to zero and are never inserted intoCOMPONENTS.
- knight_observable: str | None = None
For a precession component whose fitted value is the local field/frequency at the muon (so it can be converted to a Knight shift), the name of that parameter (
"frequency"or"field").Nonefor everything else — crucially including muonium components, whosefieldparameter is the applied field fed into the Breit–Rabi levels, not a local field.
- field_geometries: frozenset[FieldGeometry] = frozenset({FieldGeometry.LF, FieldGeometry.TF, FieldGeometry.ZF})
Applied-field geometries the component is physically meaningful for. The fit wizard uses this to drop components that cannot apply to a run’s geometry. Defaults to all three (no restriction).
- physics_classes: frozenset[PhysicsClass] = frozenset({PhysicsClass.CUSTOM})
Physics-class tags for wizard scoping.
CUSTOMis the untagged sentinel default for user functions; every built-in must override it with real class(es) — enforced bytests/test_component_tags.py.
- cost: ComputationalCost = 'moderate'
Relative evaluation-cost hint for tiered wizard screening (the wizard trials cheap components before expensive ones).
- __init__(name: str, description: str, function: Callable[[...], ndarray[tuple[int, ...], dtype[float64]]], param_names: list[str], param_defaults: dict[str, float], param_info: dict[str, ParamInfo], formula_template: str, latex_equation: str = '', category: str = 'General', domain: str = 'time', fixed_params: tuple[str, ...] = (), user: bool = False, missing: bool = False, knight_observable: str | None = None, field_geometries: frozenset[FieldGeometry] = frozenset({FieldGeometry.LF, FieldGeometry.TF, FieldGeometry.ZF}), physics_classes: frozenset[PhysicsClass] = frozenset({PhysicsClass.CUSTOM}), cost: ComputationalCost = ComputationalCost.MODERATE) None
- class asymmetry.core.fitting.composite.CompositeModel(component_names: list[str], operators: list[str] | None = None, open_parentheses: list[int] | None = None, close_parentheses: list[int] | None = None, fraction_groups: list[tuple[int, int]] | None = None, *, allow_missing: bool = False)[source]
Bases:
objectA flat composite model built from baseline-free components.
allow_missinglets a model materialise even when some component names are not registered (seeplaceholder_component_definition()); callers that fit or edit the model must checkmissing_component_names.- __init__(component_names: list[str], operators: list[str] | None = None, open_parentheses: list[int] | None = None, close_parentheses: list[int] | None = None, fraction_groups: list[tuple[int, int]] | None = None, *, allow_missing: bool = False) None[source]
- classmethod from_expression(expression: str) CompositeModel[source]
Construct a CompositeModel from a component-name expression.
- parameter_mapping() list[dict[str, str]][source]
Return per-component maps of local parameter name → unique fit name.
One dict per entry of
components, in the same order. Copies are returned so callers (e.g. the RRF frequency-offset wrapper) cannot mutate the model’s internal mapping.
- knight_observable_params() dict[str, str][source]
Map fitted parameter name → kind for Knight-convertible components.
For each component whose value is a local precession field/frequency (
ComponentDefinition.knight_observableset — Oscillatory/Bessel give"frequency", OscillatoryField gives"field"), return its unique fitted parameter name pointing at that kind. Components whosefieldis the applied field (muonium TF) are excluded, so a Knight-shift conversion never mistakes the applied field for a local one.
- component_expression_string() str[source]
Return the builder-facing expression using component names.
- fraction_weights(values: dict[str, float]) dict[str, float][source]
Return
{name: weight}for every fraction term across all groups.Both the n-1 free parameters (keyed by their real names, carrying the clamped [0, 1] value) and the derived remainder of each group (keyed by its synthesized display name, carrying
1 - Σ free) appear. A group is skipped entirely when any of its free parameters is missing fromvalues, so callers never receive a partial partition.
- normalized_parameter_values(values: dict[str, float]) dict[str, float][source]
Return a copy with free fraction parameters clamped into [0, 1].
Under the n-1 scheme the fitted values are already the physical free weights, so this only clamps each free fraction into its [0, 1] range (leaving every other entry untouched). Kept because callers rely on it to present display-ready values.
- fraction_parameter_groups() list[list[str]][source]
Return the free fraction-parameter names grouped by fraction group.
Each group contributes its n-1 free parameter names; the remainder term has no parameter and is not listed here (see
derived_fraction_names()for its display label).
- derived_fraction_names() list[str][source]
Return one synthesized display name per group for its remainder term.
These are the
f_<Component>labels of each group’s last additive term — display-only names (no fitted parameter) that never collide with a real parameter name. Ordered to matchfraction_groups.
- derived_fraction_terms() list[tuple[str, tuple[int, int]]][source]
Return
(display_name, group)for each group’s remainder term.Pairs each derived remainder name with the fraction group it labels, so a caller can place the remainder row and know which group it belongs to.
- with_default_fraction_groups() CompositeModel[source]
Return a copy with a top-level additive fraction group when suitable.
- domains() set[str][source]
Return the set of analysis domains of the model’s components.
A well-formed model has a single domain (
{"time"}or{"frequency"}); a mixed set indicates a model that combines time- and frequency-domain components (e.g. restored from a project saved before domain filtering existed) and should be surfaced to the user rather than silently fitted.Missing-component placeholders are skipped: their domain is unknowable, and the missing-ness itself is surfaced separately (fit blocking via
missing_component_names).
- fixed_by_default_params() set[str][source]
Unique parameter names that should start fixed in a fit.
Collected from each component’s
ComponentDefinition.fixed_paramsthrough the model’s parameter mapping (so duplicated components yield their indexed names, e.g.J_spin_2).
- function(t: ndarray[tuple[int, ...], dtype[_ScalarType_co]], **kwargs: float) ndarray[tuple[int, ...], dtype[float64]][source]
Evaluate the composite function with standard arithmetic precedence.
- additive_component_indices() list[int][source]
Return component indices that contribute in additive (+) form.
This includes the first component and any component joined with a
+operator. Components joined with-,*, or/are excluded because their visual contribution is not an additive area.
- evaluate_components(t: ndarray[tuple[int, ...], dtype[_ScalarType_co]], *, additive_only: bool = False, **kwargs: float) list[tuple[str, ndarray[tuple[int, ...], dtype[float64]]]][source]
Evaluate individual component curves.
- latex_terms() list[LatexTerm][source]
Return the model as a list of typeset (mathtext) additive terms.
Splits at top-level
+/-operators; each term carries itsseparator(”” for the first, “ + “/” - “ otherwise) and the sole fractiongroupits component range intersects (orNone). Never raises and never returns an empty list for a valid model — a single multiplicative chain yields one term.
- latex_string() str[source]
Return the whole model as one mathtext string.
The concatenation
"".join(sep + latex)overlatex_terms().
- to_model_definition(name: str = 'Composite') ModelDefinition[source]
Create a ModelDefinition-compatible wrapper for the fit engine.
- classmethod from_dict(data: dict, *, allow_missing: bool = False) CompositeModel[source]
Construct a CompositeModel from serialized data.
With
allow_missing=True, component names that are not registered materialise as named zero-valued placeholders instead of raising — the degrade path for projects referencing user functions that are not installed in this session (the original names round-trip unchanged throughto_dict()).
Note
The available time-domain component registry is exposed as
asymmetry.core.fitting.composite.COMPONENTS.
Diffusive longitudinal-field relaxation model.
Implements field-dependent relaxation rates for diffusive spin excitations:
lambda(B_LF) = lambda_diff(B_LF) + lambda_0D
lambda_diff(B_LF) = (C^2 / 4) * J(omega)
omega = gamma_e * B_LF
The diffusion autocorrelation uses the nD form from Pratt (J. Phys.: Conf. Ser. 2462 012038, 2023), with n in {1, 2, 3}:
- S_nD(t) = [exp(-2 D_nD t) I0(2 D_nD t)]^n
[exp(-2 D_perp t) I0(2 D_perp t)]^(3-n)
Numerics
The spectral density is a one-sided cosine transform:
J(omega) = 2 * integral_0^inf S(t) cos(omega t) dt
Two evaluation strategies are available:
A fast, vectorized Filon transform (the default): S(t) is sampled once on a shared log-spaced grid up to a finite
t_maxchosen from the diffusion rates, and J(omega) is evaluated for the whole omega vector at once. Within each grid segment S(t) is treated as piecewise linear and the oscillatorycos(omega t)factor is integrated analytically. Because the oscillation is handled in closed form per segment, the grid only has to resolve the smooth envelope S(t) – not the (arbitrarily fast) cosine – so a modest grid stays accurate even whenomegais very large. See_spectral_density_fastfor the design notes and accuracy bounds.The original scalar
scipy.integrate.quadcosine-weighted integrator, retained as_spectral_density_quad. It is used as a parity reference in the tests, as an automatic fall-back for the very-slow-diffusion corner where the fixed grid would need an impractically larget_max(see_DIFFUSION_MIN_RATE_FLOOR), and as a global escape hatch when the environment variableASYMMETRY_DIFFUSION_QUADis set to a truthy value.
A finite upper limit t_max is chosen adaptively from the diffusion rates to
provide stable and reproducible behavior suitable for GUI fitting workflows.
- asymmetry.core.fitting.diffusion.autocorrelation_nD(t: ndarray[tuple[int, ...], dtype[float64]] | list[float], D_nD: float, D_perp: float = 0.0, n: int = 2) ndarray[tuple[int, ...], dtype[float64]][source]
Return S_nD(t) for n-dimensional diffusion.
- Parameters:
t – Time in microseconds.
D_nD – In-plane diffusion rate in us^-1.
D_perp – Perpendicular diffusion rate in us^-1.
n – Dimensionality, one of 1, 2, or 3.
- asymmetry.core.fitting.diffusion.spectral_density(omega: float, D_nD: float, D_perp: float = 0.0, n: int = 2) float[source]
Return one-sided cosine spectral density J(omega).
Uses the convention J(omega) = 2 * integral_0^inf S(t) cos(omega t) dt. For omega = 0, this function returns +inf because the integral diverges for algebraic long-time tails relevant to low-dimensional diffusion.
- asymmetry.core.fitting.diffusion.lambda_diff(B_LF: ndarray[tuple[int, ...], dtype[float64]] | list[float] | float, C: float, D_nD: float, D_perp: float = 0.0, n: int = 2) ndarray[tuple[int, ...], dtype[float64]][source]
Return field-dependent diffusive relaxation rate lambda_diff(B_LF).
- asymmetry.core.fitting.diffusion.lambda_total(B_LF: ndarray[tuple[int, ...], dtype[float64]] | list[float] | float, C: float, D_nD: float, lambda_0D: float = 0.0, D_perp: float = 0.0, n: int = 2) ndarray[tuple[int, ...], dtype[float64]][source]
Return total LF relaxation lambda(B_LF) including field-independent term.
Muon-fluorine models
Dipolar Hamiltonian helpers for muon-fluorine spin systems.
- asymmetry.core.fitting.muon_fluorine.dipolar.omega_d_mu_f_rad_per_us(distance_angstrom: float) float[source]
Return mu-F dipolar angular frequency in rad/us.
- asymmetry.core.fitting.muon_fluorine.dipolar.omega_d_f_f_rad_per_us(distance_angstrom: float) float[source]
Return F-F dipolar angular frequency in rad/us.
- asymmetry.core.fitting.muon_fluorine.dipolar.omega_dipolar_rad_per_us(distance_angstrom: float, gamma_i_mhz_per_t: float, gamma_j_mhz_per_t: float) float[source]
Return dipolar angular frequency in rad/us for a spin pair.
- asymmetry.core.fitting.muon_fluorine.dipolar.r_mu_f_from_omega_d(omega_d_rad_per_us: float) float[source]
Invert
omega_d_mu_f_rad_per_us(): mu-F distance in angstrom.omega_dscales asr^-3, sor = (omega_d(1 A) / omega_d)^(1/3).
- asymmetry.core.fitting.muon_fluorine.dipolar.three_spin_hamiltonian_rad_per_us(coupling_mu_f1: float, coupling_mu_f2: float, coupling_f1_f2: float, n_mu_f1: ndarray[tuple[int, ...], dtype[float64]], n_mu_f2: ndarray[tuple[int, ...], dtype[float64]], n_f1_f2: ndarray[tuple[int, ...], dtype[float64]]) ndarray[tuple[int, ...], dtype[complex128]][source]
Construct the full 8x8 dipolar Hamiltonian (angular-frequency units).
Muon-fluorine polarization functions for entangled spin states.
- asymmetry.core.fitting.muon_fluorine.polarization.mu_f_polarization(t: ndarray[tuple[int, ...], dtype[float64]], r_muF: float) ndarray[tuple[int, ...], dtype[float64]][source]
Analytical mu-F longitudinal polarization, D_z(t), for one fluorine.
- asymmetry.core.fitting.muon_fluorine.polarization.linear_fmuf_polarization(t: ndarray[tuple[int, ...], dtype[float64]], r_muF: float) ndarray[tuple[int, ...], dtype[float64]][source]
Analytical collinear F-mu-F polarization from the classic ionic-crystal model.
- asymmetry.core.fitting.muon_fluorine.polarization.dynamic_fmuf_polarization(t: ndarray[tuple[int, ...], dtype[float64]], r_muF: float, nu: float) ndarray[tuple[int, ...], dtype[float64]][source]
Strong-collision dynamicized linear F-mu-F polarization (WiMDA
dyn F-u-F).The static collinear F-mu-F polarization
G_s(eqn 4.81 of Blundell, De Renzi, Lancaster & Pratt, Muon Spectroscopy, OUP 2022) dynamicized by the strong-collision integral equation (eqn 5.30):G_d(t) = e^{-nu t} G_s(t) + nu * integral_0^t G_d(t - t’) e^{-nu t’} G_s(t’) dt’
modelling muon hopping away from the F-mu-F site (or fluctuation of the coupling) at rate
nu(µs⁻¹).nu = 0reduces exactly to the staticlinear_fmuf_polarization(); largenugives motional narrowing towardexp(-2 omega_d^2 t / nu). Unlike WiMDA, the integration horizon is derived from the requested time range rather than a user-visibletmaxparameter, the solution is cached per(r_muF, nu, tmax), and the solver is used over its full accuracy range with an Abragam-form interpolation beyond it (WiMDA jumps to the bare narrowing exponential at a fixed rate, leaving a discontinuity in the model).
- asymmetry.core.fitting.muon_fluorine.polarization.general_fmuf_polarization(t: ndarray[tuple[int, ...], dtype[float64]], r1: float, r2: float, theta: float) ndarray[tuple[int, ...], dtype[float64]][source]
Numerical powder-averaged polarization for a general F-mu-F geometry.
The geometry is parameterized by two mu-F distances (r1, r2) in Angstrom and a bond angle theta in degrees. The eigenspectrum for each geometry is cached to keep fitting workloads feasible when the same geometry is re-evaluated.
- asymmetry.core.fitting.muon_fluorine.polarization.fmuf_triangle_polarization(t: ndarray[tuple[int, ...], dtype[float64]], r_muF: float, r3: float, phi3: float) ndarray[tuple[int, ...], dtype[float64]][source]
Powder-averaged polarization for F-mu-F plus a third fluorine (16-dim).
A collinear F-mu-F pair (both fluorines at
r_muF, as inlinear_fmuf_polarization()) plus a third fluorine at distancer3and anglephi3(degrees) from the F-mu-F axis, solved exactly in the 16-dimensional muon + 3F Hilbert space with all pairwise dipolar couplings (mu-F and F-F) and a proper powder average.This supersedes WiMDA’s
F-u-F-F ZF PCR, which neglects the F-F couplings and approximates the powder average by (P_z + 2 P_x)/3 for a single crystal orientation pair; seedocs/porting/wimda-fit-function-parity/comparison.md. Asr3 -> infinitythe result approaches the linear F-mu-F polarization.
Parameter trend models
- class asymmetry.core.fitting.parameter_models.ParameterModelComponentDefinition(name: str, description: str, function: Callable[[...], ndarray[tuple[int, ...], dtype[float64]]], param_names: list[str], param_defaults: dict[str, float], param_info: dict[str, ParamInfo], formula_template: str, latex_equation: str = '', scopes: tuple[str, ...] = ('common',), fwhm_factor: float | None = None, user: bool = False, category: str = 'General')[source]
Bases:
objectDescriptor for a parameter-vs-x basis function.
- fwhm_factor: float | None = None
For centred-peak components, FWHM =
fwhm_factor * Bwid(None otherwise).
- user: bool = False
Truefor components registered through the user-function facade (asymmetry.core.fitting.user_functions); seeComponentDefinition.user.
- category: str = 'General'
Library-panel grouping (mirrors
ComponentDefinition.category). Assigned in bulk via_PARAMETER_MODEL_CATEGORIESbelow rather than per-definition, so the taxonomy is reviewable in one place.
- __init__(name: str, description: str, function: Callable[[...], ndarray[tuple[int, ...], dtype[float64]]], param_names: list[str], param_defaults: dict[str, float], param_info: dict[str, ParamInfo], formula_template: str, latex_equation: str = '', scopes: tuple[str, ...] = ('common',), fwhm_factor: float | None = None, user: bool = False, category: str = 'General') None
- class asymmetry.core.fitting.parameter_models.ParameterCompositeModel(component_names: list[str], operators: list[str] | None = None, open_parentheses: list[int] | None = None, close_parentheses: list[int] | None = None)[source]
Bases:
objectFlat composite model for parameter-vs-x fitting.
- __init__(component_names: list[str], operators: list[str] | None = None, open_parentheses: list[int] | None = None, close_parentheses: list[int] | None = None) None[source]
- classmethod from_expression(expression: str) ParameterCompositeModel[source]
Construct a ParameterCompositeModel from a component-name expression.
- component_expression_string() str[source]
Return the builder-facing expression using component names.
- component_param_name(component_index: int, local_name: str) str[source]
Global (possibly index-suffixed) param name for a component’s param.
For
["GaussianLCR", "LorentzianLCR"]the second component’s"B0"is"B0_2"; for a single component it is the bare"B0". Lets callers address a component’s parameters by name instead of positional arithmetic overparam_names.
- latex_terms() list[LatexTerm][source]
Return the trend model as typeset (mathtext) additive terms.
groupis alwaysNone(parameter-vs-x models have no fraction groups). The quadrature operator⊕renders a flat chain as\sqrt{a^{2} + b^{2}}. Parenthesised or⊕-mixed expressions that are ambiguous to lay out fall back to a single\mathrm{...}term. Never raises and never returns an empty list for a valid model.
- function(x: ndarray[tuple[int, ...], dtype[_ScalarType_co]], **kwargs: float) ndarray[tuple[int, ...], dtype[float64]][source]
Evaluate the composite model with arithmetic precedence.
- additive_component_indices() list[int][source]
Return indices for additive components (first term, ‘+’ and ‘⊕’ terms).
Quadrature (
⊕) combines whole components like+does, so each operand is a distinct curve worth plotting on its own; subtractive and multiplicative terms are not standalone contributions.
- evaluate_components(x: ndarray[tuple[int, ...], dtype[_ScalarType_co]], *, additive_only: bool = False, **kwargs: float) list[tuple[str, ndarray[tuple[int, ...], dtype[float64]]]][source]
Evaluate individual component curves for plotting/export.
- to_dict() dict[str, list[str] | list[int]][source]
Return a JSON-serializable representation of the model.
- classmethod from_dict(data: dict) ParameterCompositeModel[source]
Construct a ParameterCompositeModel from serialized data.
- class asymmetry.core.fitting.parameter_models.ParameterModelFitResult(success: bool, chi_squared: float = 0.0, reduced_chi_squared: float = 0.0, parameters: ~asymmetry.core.fitting.parameters.ParameterSet = <factory>, uncertainties: dict[str, float] = <factory>, message: str = '', error_mode: str = 'column', n_points: int = 0, n_starts_tried: int = 0, seed_beat_user_start: bool = False, params_at_bound: tuple[str, ...] = (), covariance: tuple[list[str], list[list[float]]] | None = None)[source]
Bases:
objectResult of fitting a parameter-vs-x model.
- parameters: ParameterSet
- error_mode: str = 'column'
Error mode the fit was run with (χ²ᵣ carries no goodness information for
"none"/"scatter"— quality verdicts should be suppressed).
- n_starts_tried: int = 0
How many initial-value candidates the multi-start machinery actually tried (1 for the default single-start path; more with
extra_starts).
- seed_beat_user_start: bool = False
Trueiff the winning fit came from a non-user start (a data-aware or perturbed seed beat the user’s provided parameters). Advisory only.
- params_at_bound: tuple[str, ...] = ()
Names of free parameters whose fitted value sits within tolerance of a finite
min/maxbound — a sign the data did not determine them.
- covariance: tuple[list[str], list[list[float]]] | None = None
(names, matrix)of the free-parameter covariance from Minuit, in the same order asnames(free parameters only, fixed parameters excluded).Nonewhen unavailable: the fit failed/invalid or Minuit produced no covariance (e.g. HESSE did not run/converge).
- __init__(success: bool, chi_squared: float = 0.0, reduced_chi_squared: float = 0.0, parameters: ~asymmetry.core.fitting.parameters.ParameterSet = <factory>, uncertainties: dict[str, float] = <factory>, message: str = '', error_mode: str = 'column', n_points: int = 0, n_starts_tried: int = 0, seed_beat_user_start: bool = False, params_at_bound: tuple[str, ...] = (), covariance: tuple[list[str], list[list[float]]] | None = None) None
- class asymmetry.core.fitting.parameter_models.ModelFitRange(x_min: float | None, x_max: float | None, model: ParameterCompositeModel, parameters: ParameterSet, result: ParameterModelFitResult | None = None, windows: list[tuple[float, float]] | None = None)[source]
Bases:
objectA model and fit results over a specific x-range.
windowsoptionally restricts the range to a union of (min, max) intervals: a point enters the fit if it falls in any window (OR-combination), with one model fitted across the union. When absent, the plainx_min/x_maxbounds apply.- model: ParameterCompositeModel
- parameters: ParameterSet
- result: ParameterModelFitResult | None = None
- __init__(x_min: float | None, x_max: float | None, model: ParameterCompositeModel, parameters: ParameterSet, result: ParameterModelFitResult | None = None, windows: list[tuple[float, float]] | None = None) None
- class asymmetry.core.fitting.parameter_models.ParameterModelFit(parameter_name: str, x_key: str, ranges: list[ModelFitRange] = <factory>, active: bool = True, use_x_errors: bool = False)[source]
Bases:
objectModel fits attached to a single parameter trace.
- ranges: list[ModelFitRange]
- class asymmetry.core.fitting.parameter_models.ParameterModelFitExecution(range_index: int, x: ndarray[tuple[int, ...], dtype[float64]], y: ndarray[tuple[int, ...], dtype[float64]], result: ParameterModelFitResult)[source]
Bases:
objectFitted model with sampled curve ready for plotting/export.
- result: ParameterModelFitResult
- class asymmetry.core.fitting.parameter_models.ParameterGroupData(group_id: str, group_name: str, x: ndarray[tuple[int, ...], dtype[float64]], y: ndarray[tuple[int, ...], dtype[float64]], yerr: ndarray[tuple[int, ...], dtype[float64]], group_variable_value: float, xerr: ndarray[tuple[int, ...], dtype[float64]] | None = None)[source]
Bases:
objectInput data for one selected group in a cross-group parameter fit.
- xerr: ndarray[tuple[int, ...], dtype[float64]] | None = None
Optional per-point x-uncertainty (aligned to
x), used only for an effective-variance fit when the abscissa is itself a fitted parameter.
- class asymmetry.core.fitting.parameter_models.CrossGroupFitResult(success: bool, chi_squared: float, reduced_chi_squared: float, global_parameters: ~asymmetry.core.fitting.parameters.ParameterSet = <factory>, local_parameters: dict[str, ~asymmetry.core.fitting.parameters.ParameterSet] = <factory>, fixed_parameters: ~asymmetry.core.fitting.parameters.ParameterSet = <factory>, global_uncertainties: dict[str, float] = <factory>, local_uncertainties: dict[str, dict[str, float]] = <factory>, message: str = '', error_mode: str = 'column', n_points: int = 0, per_group_chi_squared: dict[str, float] = <factory>, per_group_n_points: dict[str, int] = <factory>, global_correlations: tuple[list[str], list[list[float]]] | None = None)[source]
Bases:
objectResult container for cross-group parameter fits.
- global_parameters: ParameterSet
- local_parameters: dict[str, ParameterSet]
- fixed_parameters: ParameterSet
- error_mode: str = 'column'
Error mode the fit was run with (χ²ᵣ carries no goodness information for
"none"/"scatter"— quality verdicts should be suppressed).
- per_group_chi_squared: dict[str, float]
Per-group χ², keyed by
group_id. Computed from the same masked/ windowed residuals (and xerr effective-variance term, when active) that the joint cost function used, sosum(per_group_chi_squared.values()) == chi_squared. Under SCATTER error mode this is reported on the pre-rescale basis — the same basischi_squareditself uses (the rescale only touches parameter uncertainties, never the fitted χ²).
- per_group_n_points: dict[str, int]
Number of points that entered the fit for each group (post mask/window).
- global_correlations: tuple[list[str], list[list[float]]] | None = None
(names, matrix)of free GLOBAL parameter correlations derived from the Minuit covariance (corr_ij = cov_ij / sqrt(cov_ii * cov_jj)).Nonewhen unavailable: fewer than two free global parameters, a failed fit, or no covariance from Minuit.
- to_dict() dict[source]
Canonical, JSON-safe serialization.
ParameterSets serialize as lists of{name, value, min, max, fixed}dicts (see_parameter_set_to_dicts()), matching the shape already used by the GUI’s project-state serializers.
- classmethod from_dict(data: Mapping[str, object]) CrossGroupFitResult[source]
Tolerant deserialization: missing/malformed keys fall back to empty defaults rather than raising, so a legacy payload (recorded before these fields existed) still loads.
- __init__(success: bool, chi_squared: float, reduced_chi_squared: float, global_parameters: ~asymmetry.core.fitting.parameters.ParameterSet = <factory>, local_parameters: dict[str, ~asymmetry.core.fitting.parameters.ParameterSet] = <factory>, fixed_parameters: ~asymmetry.core.fitting.parameters.ParameterSet = <factory>, global_uncertainties: dict[str, float] = <factory>, local_uncertainties: dict[str, dict[str, float]] = <factory>, message: str = '', error_mode: str = 'column', n_points: int = 0, per_group_chi_squared: dict[str, float] = <factory>, per_group_n_points: dict[str, int] = <factory>, global_correlations: tuple[list[str], list[list[float]]] | None = None) None
- asymmetry.core.fitting.parameter_models.component_names_for_x(x_key: str) list[str][source]
Return available basis functions for a given x-variable.
- asymmetry.core.fitting.parameter_models.fit_parameter_model(x: ndarray[tuple[int, ...], dtype[_ScalarType_co]], y: ndarray[tuple[int, ...], dtype[_ScalarType_co]], yerr: ndarray[tuple[int, ...], dtype[_ScalarType_co]] | None, model: ParameterCompositeModel, parameters: ParameterSet, x_min: float | None = None, x_max: float | None = None, method: str = 'migrad', error_mode: ErrorMode | str = ErrorMode.COLUMN, error_value: float | None = None, windows: Sequence[tuple[float, float]] | None = None, xerr: ndarray[tuple[int, ...], dtype[_ScalarType_co]] | None = None, extra_starts: int = 0, seed: int = 0) ParameterModelFitResult[source]
Fit a parameter-vs-x model using iminuit.
error_mode/error_valueselect the per-point σ assignment (seeErrorMode);windowsoptionally restricts the fit to a union of (min, max) intervals, overridingx_min/x_max.xerroptionally supplies per-point x-uncertainties for an errors-in-variables (Orear/York effective-variance) fit — used for parameter-vs-parameter trending where the abscissa is itself a fitted quantity. WhenxerrisNoneor all zero/non-finite the fit is ordinary least squares (the abscissa is treated as exact). It is ignored under theNONE/SCATTERerror modes, whose unit y-weights have no physical scale to combine with the x-variance term.extra_startsadds multi-start robustness: withextra_starts > 0the fit additionally tries the generic data-aware seed (suggest_model_seeds()) andextra_startsdeterministic perturbed starts around the user’s parameters, keeping the best converged candidate by the existing success-then-χ² rule. The perturbation is driven solely bynumpy.random.default_rng(seed), so a givenseedis fully reproducible.extra_starts = 0(the default) is byte-identical to the single-start behaviour — no RNG is constructed and the candidate list is unchanged.
- asymmetry.core.fitting.parameter_models.global_fit_parameter_model(groups: list[ParameterGroupData], model: ParameterCompositeModel, global_params: list[str], local_params: list[str], fixed_params: dict[str, float], initial_params: dict[str, float] | None = None, parameter_bounds: dict[str, tuple[float, float]] | None = None, method: str = 'migrad', error_mode: ErrorMode | str = ErrorMode.COLUMN, error_value: float | None = None, windows: Sequence[tuple[float, float]] | None = None, xerr: Mapping[str, ndarray[tuple[int, ...], dtype[_ScalarType_co]]] | None = None) CrossGroupFitResult[source]
Jointly fit a parameter model across multiple groups.
Parameters are classified as: - global: one shared value across all groups - local: independent value per group - fixed: fixed constant
error_mode/error_valueselect the per-point σ assignment for every group (seeErrorMode);windowsoptionally restricts each group to a union of (min, max) intervals (OR-combined, one model across the union).SCATTERrescales all parameter errors (global and local) by √(χ²/ν) after the fit — the fixed point of WiMDA’s Estimate iteration.xerroptionally mapsgroup_id→ per-point x-uncertainties (aligned to that group’s storedx) for an errors-in-variables (Orear/York effective-variance) fit, used for parameter-vs-parameter trending where the abscissa is itself a fitted quantity. It uses the same central-difference estimator as the single-seriesfit_parameter_model(). A group with no entry, or all-zero/non-finite σ_x, keeps ordinary least squares. Like the single-series path,xerris ignored underNONE/SCATTER, whose unit y-weights carry no physical scale to combine with the x-variance term.
- asymmetry.core.fitting.parameter_models.evaluate_parameter_model_fit(fit: ParameterModelFit, num_points: int = 200) list[ParameterModelFitExecution][source]
Return sampled curves for each successful active range.
Note
The available parameter-trend component registry is exposed as
asymmetry.core.fitting.parameter_models.PARAMETER_MODEL_COMPONENTS.
The angle-only \(K(\theta)\) basis models KnightAnisotropy,
AngularCos2, and AngularFourier2 are registered alongside the
others (scope="angle").
Experiment design
The Laplace/Fisher “where to measure next” acquisition shared by Suggest next point (a scalar trend) and the Knight-shift window’s Suggest next angle (see Knight shift and Parameter trending) — expected information gain, model discrimination, and the Hungarian set-matching utility used when a candidate’s components are unlabelled.
Bayesian experimental design for parameter trending.
Given a converged trend-model fit (fit_parameter_model), suggest where
the next measurement should go — and how many events it should collect —
to most constrain the model’s parameters. The acquisition is the Laplace
(Gaussian-posterior) expected information gain for a single new datum:
IG(x) = 1/2 log[1 + g(x)^T Sigma g(x) / sigma_new^2(x)] (D-optimal)
- DeltaVar_k(x) = (Sigma g(x))_k^2 / (sigma_new^2(x) + g(x)^T Sigma g(x))
(c-optimal)
where g(x) is the model’s parameter sensitivity at the fitted values and
Sigma the fit covariance. The rank-one c-optimal update is exact for a
linear model; for strongly nonlinear models (critical exponents) it ranks
candidates correctly but underestimates the realised post-fit variance —
see docs/studies/prototype-results.md — so the counting recommendation
must be calibrated with calibrate_suggestion before being shown to a
user.
Everything here is GUI-free and pure numpy; refits happen only inside
calibrate_suggestion.
- class asymmetry.core.fitting.experiment_design.NextPointSuggestion(x_candidates: ndarray[tuple[int, ...], dtype[float64]], utility: ndarray[tuple[int, ...], dtype[float64]], extrapolated: ndarray[tuple[int, ...], dtype[bool]], best_x: float, target: str | None, sigma_new: ndarray[tuple[int, ...], dtype[float64]], predicted_post_sigma: float | None = None, events_factor_to_target: float | None = None, target_unreachable: bool = False, floor_sigma: float | None = None, risk_mask: ndarray[tuple[int, ...], dtype[bool]] | None = None, warnings: tuple[str, ...] = ())[source]
Bases:
objectUtility curve and recommendation for the next measurement.
utilityisDeltaVar_k(variance reduction of the target parameter) in c-optimal mode, or the information gainIGin D-optimal mode (target is None). Two degenerate shapes exist, both explained bywarnings: unusable inputs (bad covariance, no noise model, …) yield empty arrays withbest_xNaN, while a valid setup where no candidate carries information keeps the full curve (all-zeroutility) withbest_xNaN.- predicted_post_sigma: float | None = None
Predicted post-measurement sigma of the target at
best_xfor a reference-statistics point (c-optimal mode only). Rank-one estimate — optimistic near critical points; calibrate before display.
- events_factor_to_target: float | None = None
N/N_ref needed at
best_xto reachsigma_goal(rank-one estimate).0.0means the goal is already met without a new point.
- floor_sigma: float | None = None
sigma of the target in the N -> infinity limit of a single new point at
best_x— the floor set by the other parameters’ uncertainty.
- risk_mask: ndarray[tuple[int, ...], dtype[bool]] | None = None
Per-candidate assignment-risk flag (multi-series acquisition only).
Truewhere two series’ predicted values lie within_ASSIGNMENT_RISK_Ksigma of each other, so a new datum there may attach to the wrong curve.Nonefor single-series suggestions, which have no crossing to worry about.
- __init__(x_candidates: ndarray[tuple[int, ...], dtype[float64]], utility: ndarray[tuple[int, ...], dtype[float64]], extrapolated: ndarray[tuple[int, ...], dtype[bool]], best_x: float, target: str | None, sigma_new: ndarray[tuple[int, ...], dtype[float64]], predicted_post_sigma: float | None = None, events_factor_to_target: float | None = None, target_unreachable: bool = False, floor_sigma: float | None = None, risk_mask: ndarray[tuple[int, ...], dtype[bool]] | None = None, warnings: tuple[str, ...] = ()) None
- class asymmetry.core.fitting.experiment_design.SuggestionCalibration(x_new: float, events_factor: float, target: str, n_trials: int, n_failed: int, realized_post_sigma: float, realized_post_sigma_p16: float, realized_post_sigma_p84: float, predicted_post_sigma: float | None = None, calibration_ratio: float | None = None, warnings: tuple[str, ...] = ())[source]
Bases:
objectMonte-Carlo check of a suggestion: simulate the point, refit, measure.
realized_post_sigmais the median post-fit sigma of the target over the successful trials; the p16/p84 band shows its spread.calibration_ratiois realised variance / rank-one predicted variance (> 1 means the analytic figure was optimistic).- __init__(x_new: float, events_factor: float, target: str, n_trials: int, n_failed: int, realized_post_sigma: float, realized_post_sigma_p16: float, realized_post_sigma_p84: float, predicted_post_sigma: float | None = None, calibration_ratio: float | None = None, warnings: tuple[str, ...] = ()) None
- asymmetry.core.fitting.experiment_design.suggest_next_point(model: ParameterCompositeModel, parameters: ParameterSet, covariance: tuple[list[str], list[list[float]]] | None, x_data: ndarray[tuple[int, ...], dtype[float64]], y_err: ndarray[tuple[int, ...], dtype[float64]], x_min: float, x_max: float, *, target: str | None = None, sigma_goal: float | None = None, n_candidates: int = 257) NextPointSuggestion[source]
Rank candidate x values by how much a new point there constrains the fit.
covarianceis the(names, matrix)pair fromParameterModelFitResult.covariance;x_data/y_errare the measured series (the per-point errors define the empirical noise model for the hypothetical new point at reference statistics).targetselects c-optimality for that parameter;Nonemeans D-optimal.sigma_goal(c-optimal only) additionally solves for the event-count factor N/N_ref needed at the best x — a rank-one planning figure that should be calibrated withcalibrate_suggestion()before display.Never raises on degenerate inputs: returns an empty suggestion with an explanatory warning instead.
- class asymmetry.core.fitting.experiment_design.SeriesSpec(model: ParameterCompositeModel, parameters: ParameterSet, covariance: tuple[list[str], list[list[float]]] | None, x_data: ndarray[tuple[int, ...], dtype[float64]], y_err: ndarray[tuple[int, ...], dtype[float64]])[source]
Bases:
objectOne series’ inputs to a multi-series acquisition.
Mirrors
suggest_next_point()’s per-series argument types: a fittedmodel/parameterspair, the(names, matrix)fitcovariance(Nonewhen the fit produced none), and the measuredx_data/y_errthat define the empirical new-point noise model. One physical measurement at a candidatexis assumed to yield one datum for every series (§3.1).- model: ParameterCompositeModel
- parameters: ParameterSet
- asymmetry.core.fitting.experiment_design.suggest_next_point_multi(series: Sequence[SeriesSpec], x_min: float, x_max: float, *, target: tuple[int, str] | None = None, sigma_goal: float | None = None, n_candidates: int = 257, labels: Sequence[str] | None = None) NextPointSuggestion[source]
Rank candidate x by how much a shared new point constrains several series.
Each entry of
seriesis a fittedSeriesSpec; one measurement atxadds one datum to every series. Withtarget is Nonethe utility is the D-optimal information-gain sum over series (§3.1):U(x) = Σ_m ½ log[1 + g_m(x)ᵀ Σ_m g_m(x) / σ_m²(x)]
With
target = (series_index, param_name)the objective is that one series’ c-optimal variance reduction forparam_name— the other series still receive the datum but do not enter the objective — and thesigma_goal/event-count solve applies to the target exactly as insuggest_next_point(). Either way the returned suggestion carries a per-candidateNextPointSuggestion.risk_mask(§3.1): the rank-one update assumes the new datum attaches to the right curve, so candidates near a predicted crossing are flagged.A series whose covariance is invalid (
_validated_covariance()) contributes nothing to the utility and adds a warning naming it (bylabelswhen supplied, else by index); the suggestion still succeeds if at least one series is usable. All-unusable inputs degrade to an empty suggestion. Never raises.
- asymmetry.core.fitting.experiment_design.calibrate_suggestion(model: ParameterCompositeModel, parameters: ParameterSet, x_data: ndarray[tuple[int, ...], dtype[float64]], y_data: ndarray[tuple[int, ...], dtype[float64]], y_err: ndarray[tuple[int, ...], dtype[float64]], x_new: float, *, target: str, events_factor: float = 1.0, n_trials: int = 50, seed: int = 0, predicted_post_sigma: float | None = None) SuggestionCalibration[source]
Monte-Carlo-calibrate a suggestion by simulating the point and refitting.
Each trial draws the hypothetical new datum from the fitted model plus Gaussian noise at the empirical sigma for
x_new(scaled by1/sqrt(events_factor)), appends it to the measured series, refits from the fitted values, and records the post-fit sigma oftarget. The median over trials is the calibrated post-measurement sigma — the honest counterpart of the rank-onepredicted_post_sigma, which is known to be optimistic near critical points (seedocs/studies/prototype-results.md).y_errmust be the effective per-point sigmas the trend fit was weighted with (already resolved through the fit’s error mode).
- asymmetry.core.fitting.experiment_design.calibrate_events_for_goal(model: ParameterCompositeModel, parameters: ParameterSet, x_data: ndarray[tuple[int, ...], dtype[float64]], y_data: ndarray[tuple[int, ...], dtype[float64]], y_err: ndarray[tuple[int, ...], dtype[float64]], x_new: float, *, target: str, sigma_goal: float, initial_events_factor: float = 1.0, n_trials: int = 30, seed: int = 0, max_events_factor: float = 1024.0, max_iterations: int = 6) tuple[float | None, SuggestionCalibration][source]
Find the event-count factor whose calibrated post-sigma meets the goal.
Multiplicative search starting from
initial_events_factor(use the rank-oneevents_factor_to_targetwhen available): each step scales the factor by the shortfall(median/goal)^2— exact if the new point’s variance dominates, conservative otherwise. Returns(factor, last_calibration); factor isNonewhen the goal is not met bymax_events_factor(the single-point floor applies).
- asymmetry.core.fitting.experiment_design.suggest_discriminating_point(lead_model: ParameterCompositeModel, lead_parameters: ParameterSet, alternatives: Sequence[tuple[ParameterCompositeModel, ParameterSet]], x_data: ndarray[tuple[int, ...], dtype[float64]], y_err: ndarray[tuple[int, ...], dtype[float64]], x_min: float, x_max: float, *, n_candidates: int = 257) NextPointSuggestion[source]
Rank candidate x values by how well they discriminate between models.
Scores the
lead_model(the currently favoured fit) against every entry inalternatives(each a fitted(model, parameters)pair over the same series), taking the elementwise max over alternatives — per the TAS-AI lock-in lesson (§4/§8.1 of the study), a leader must be tested against all live competitors, not just the runner-up:U_disc(x) = max_i [f_lead(x) - f_i(x)]^2 / (2 sigma_new^2(x))
Returns a
NextPointSuggestionwithtarget=None(this mode has no single target parameter) and no event-count/goal fields set. Never raises: degenerate inputs (no alternatives, no noise model, bad range) degrade to an empty suggestion with an explanatory warning, and a utility that is everywhere within noise of zero is flagged rather than silently returning a meaningless argmax.
- asymmetry.core.fitting.experiment_design.set_matching_divergence(hypothesis_a: Sequence[tuple[ParameterCompositeModel, ParameterSet]], hypothesis_b: Sequence[tuple[ParameterCompositeModel, ParameterSet]], sigma: Sequence[ndarray[tuple[int, ...], dtype[float64]]], x_candidates: ndarray[tuple[int, ...], dtype[float64]]) ndarray[tuple[int, ...], dtype[float64]][source]
Per-candidate min-cost set-matching divergence between two hypotheses (§3.3).
Each hypothesis is an equal-length list of
(model, parameters)curves. At every candidatexthe two hypotheses’ predictedN-vectors are matched one-to-one (Hungarian) to minimise the total weighted cost, with per-pair cost[f_i^A(x) − f_j^B(x)]² / (2 σ_i²(x))
(
sigma[i]is seriesiof hypothesis A’s per-candidate noise). Because a new run’s components are unlabelled, two assignment hypotheses are only distinguishable through the sets of values they predict — this is that set-level discrepancy. It is zero where the hypotheses predict the same value set (e.g. at a crossing where the labellings coincide) and peaks where they imply genuinely different curve sets. ForN = 1it reduces exactly to the labelled discrimination integrand ofsuggest_discriminating_point().Returns
U(x)overx_candidates. Non-finite predictions or zero sigmas are treated as a large finite cost so the matching stays well-posed.
- asymmetry.core.fitting.experiment_design.aic_weights(chi_squareds: Sequence[float], n_free_params: Sequence[int]) list[float][source]
Akaike-weight a set of candidate models from their fit chi-squareds.
AIC_i = chi2_i + 2 p_i; weights areexp(-(AIC_i - min AIC)/2)normalised to sum to 1 (the minimum is subtracted before exponentiating for numerical safety, per Burnham & Anderson). BIC is deliberately not used here — itsp * ln(n)penalty drifts under the sequential-n growth of an in-progress trend series (§4 of the study).A model with non-finite chi-squared gets weight
0.0and is excluded from the normalisation; if every model is non-finite, returns a list of0.0(there is nothing to rank). RaisesValueErrorif the two input lists differ in length — a mismatch here is a programming error, not a user-data problem.
- asymmetry.core.fitting.experiment_design.cost_weighted_utility(x_candidates: ndarray[tuple[int, ...], dtype[float64]], utility: ndarray[tuple[int, ...], dtype[float64]], x_current: float, *, count_time: float, up_rate: float, down_rate: float, gamma: float = 0.7) ndarray[tuple[int, ...], dtype[float64]][source]
Weight a utility curve by movement + counting cost (TAS-AI’s IG^gamma/t).
move_time(x) = up_rate * (x - x_current)forx > x_current, elsedown_rate * (x_current - x); all rates/times share a unit (hours). Returnsclip(utility, 0)**gamma / (count_time + move_time).Pure array-in/array-out with defensive guards rather than raising: an invalid cost model (
count_time <= 0, a negative rate, or a non-finitex_current) must not silently distort the ranking, so the originalutilityis returned unchanged in that case. The GUI is responsible for validating these inputs before calling; this function does not emit warnings.
Knight shift
Convert fitted oscillation components to the muon Knight shift and fit its angular dependence. See Knight shift for the physics and the GUI workflow.
Muon Knight-shift conversion from fitted precession frequencies.
The Knight shift is the relative frequency shift of the muon precession away from the free-muon Larmor frequency of the applied field:
K = (ν_µ − ν_ref) / ν_ref (dimensionless)
with two reference conventions (both supported here):
Applied field —
ν_ref = γ_µ · B_ext, the free-muon Larmor frequency of the applied field. This needs no measured reference line, so it is the default for low-background systems. (Amato, Intro to MuSR, Eq. 5.54; Blundell, Muon Spectroscopy, Eq. 16.14:K = (B_µ − B_ext)·B_ext / B_ext².)Designated component —
ν_refis the fitted frequency of a reference oscillation component measured in the same fit (e.g. a diamagnetic line). The two frequencies are then correlated, so the covariance is carried through the error propagation.
This module computes the directly-measured K_exp. The Lorentz/demagnetizing
correction to the intrinsic K_µ (Amato Eqs. 5.59–5.60) needs sample geometry
and susceptibility and is intentionally out of scope.
Knight shifts span a wide range — tens of ppm for diamagnets up to a few percent
for paramagnets (Amato p.194) — so resolve_auto_unit() picks a sensible
display unit from the data.
- asymmetry.core.fitting.knight_shift.MUON_LARMOR_MHZ_PER_G = 0.013553881700000001
Free-muon Larmor frequency per unit applied field, γ_µ/(2π) in MHz/G (≈ 0.013554 MHz/G). Derived from the single γ_µ·B conversion in
asymmetry.core.fitting.spectralso the Larmor slope cannot drift from the frequency-domain field axis.
- asymmetry.core.fitting.knight_shift.larmor_frequency_mhz(field_gauss: float) float[source]
Free-muon Larmor frequency (MHz) for an applied field in Gauss.
- asymmetry.core.fitting.knight_shift.knight_shift(nu: float, nu_ref: float, *, sigma_nu: float = 0.0, sigma_ref: float = 0.0, cov: float = 0.0) tuple[float, float][source]
Knight shift
K = ν/ν_ref − 1and its 1σ uncertainty.covis the covariance betweennuandnu_ref— non-zero only when both are fitted in the same fit (the designated-component reference). For the applied-field referencenu_ref = γ_µ·Bis treated as exact, so the caller leavessigma_refandcovat zero.Returns
(nan, nan)whennu_refis zero (no reference to shift against). The error propagation is the standard first-order expansion of the ratio:σ_K² = σ_ν²/ν_ref² + ν²·σ_ref²/ν_ref⁴ − 2·ν·cov/ν_ref³
- class asymmetry.core.fitting.knight_shift.KnightShiftUnit(*values)[source]
Bases:
EnumDisplay unit for a Knight shift (stored internally as a fraction).
- FRACTION = 'fraction'
- PPM = 'ppm'
- PERCENT = 'percent'
- AUTO = 'auto'
- asymmetry.core.fitting.knight_shift.resolve_auto_unit(values: Iterable[float]) KnightShiftUnit[source]
Pick ppm vs percent for a set of Knight shifts (fractions).
Returns
KnightShiftUnit.PPMwhen the largest finite \(|K|\) is below_AUTO_PPM_MAX, elseKnightShiftUnit.PERCENT. An empty or all-non-finite set defaults to ppm.
- asymmetry.core.fitting.knight_shift.concrete_unit(unit: KnightShiftUnit, values: Iterable[float]) KnightShiftUnit[source]
Resolve
AUTOagainst the data; pass concrete units through unchanged.
- asymmetry.core.fitting.knight_shift.scale_for_unit(unit: KnightShiftUnit) float[source]
Factor to multiply an internal fraction by for display in
unit.
- asymmetry.core.fitting.knight_shift.label_for_unit(unit: KnightShiftUnit) str[source]
Short unit suffix for an axis/legend label (empty for a bare fraction).
- asymmetry.core.fitting.knight_shift.REFERENCE_APPLIED_FIELD = 'applied_field'
Reference conventions for the Knight shift.
- class asymmetry.core.fitting.knight_shift.KnightShiftConfig(enabled: bool = False, reference_mode: str = 'applied_field', reference_component: str | None = None, unit: KnightShiftUnit = KnightShiftUnit.AUTO, components: tuple[str, ...]=<factory>)[source]
Bases:
objectUser configuration for converting fitted frequencies to Knight shifts.
reference_modeis"applied_field"(ν_ref = γ_µ·B, needs no reference line) or"component"(ν_ref isreference_component’s fitted frequency).componentslists the oscillation-frequency parameter names to convert; an empty tuple means “all discovered components”.unitis the display unit (the conversion is stored internally as a dimensionless fraction).- unit: KnightShiftUnit = 'auto'
- classmethod from_dict(data: object) KnightShiftConfig[source]
- class asymmetry.core.fitting.knight_shift.ClogstonJaccarinoResult(slope: float, intercept: float, slope_err: float, intercept_err: float, n: int)[source]
Bases:
objectLinear K-vs-χ fit (Clogston–Jaccarino), with temperature implicit.
slopeisdK/dχ— proportional to the muon hyperfine coupling at the site — andinterceptis the χ-independent (orbital/diamagnetic) shift K0.*_errare 1σ uncertainties;nis the number of points used.
- asymmetry.core.fitting.knight_shift.clogston_jaccarino_fit(chi: Sequence[float], knight: Sequence[float], sigma_knight: Sequence[float] | None = None) ClogstonJaccarinoResult[source]
Weighted linear fit of Knight shift vs susceptibility (API-only).
Fits
K = slope·χ + interceptwith temperature as the implicit parameter — the Clogston–Jaccarino construction — where the slopedK/dχmeasures the muon hyperfine coupling and the intercept the χ-independent shift. Points with a non-finite χ, K, or σ are dropped;sigma_knight(if given) weights the fit by1/σ²(else unit weights). RaisesValueErrorwith fewer than two points or no spread in χ (the slope is then undetermined).There is no GUI entry point because the muon experiment does not measure χ; pair the trended Knight shift (exported per Knight shift) with an independent susceptibility and call this directly.
Joint K(θ) fit with per-angle component assignment.
Fitting each raw-labelled Knight-shift component trace independently against angle
breaks at crossings, where the underlying grouped fit swaps which physical site a
label refers to. This module fits N angle-dependent curves jointly and, at
each angle, assigns that angle’s N measured component values to the curves
they best match (a one-to-one optimal assignment). Each physical site is then
followed continuously through crossings, and the assignment realigns the
component identity.
The procedure is a classification-EM / alternating least squares: hold the assignment, fit each curve to its assigned points; then hold the curves, reassign each angle’s points to curves by minimum weighted residual (Hungarian). Because the objective is non-convex (label switching), a few seeds are tried and the lowest-χ² solution kept.
Pure/deterministic — no Qt. Reuses the K(θ) basis models and fit_parameter_model.
- asymmetry.core.fitting.angular_assignment.ANGULAR_MODELS: tuple[str, ...] = ('KnightAnisotropy', 'AngularCos2', 'AngularFourier2')
K(θ) basis models eligible for the joint fit (angle-scoped, Phase 5/6).
- class asymmetry.core.fitting.angular_assignment.AngularAssignmentAlternative(assignment: list[tuple[int, ...]], curves: list[ParameterModelFitResult], total_chi_squared: float, converged: bool)[source]
Bases:
objectA near-degenerate runner-up assignment kept for discrimination (§3.3).
Carries the competing per-point
assignment(distinct from the winner’s), its converged per-curvecurves(canonicalised like the winner), the seed’stotal_chi_squaredand whether that seedconverged. These are the labellings within a Δχ² window of the winner — the ones a new angle could resolve.- curves: list[ParameterModelFitResult]
- class asymmetry.core.fitting.angular_assignment.AngularAssignmentResult(success: bool, converged: bool, model_name: str, angles: list[float], curves: list[~asymmetry.core.fitting.parameter_models.ParameterModelFitResult], assignment: list[tuple[int, ...]], curve_values: list[list[float]], curve_errors: list[list[float]], total_chi_squared: float = 0.0, dof: int = 0, message: str = '', alternatives: list[~asymmetry.core.fitting.angular_assignment.AngularAssignmentAlternative] = <factory>)[source]
Bases:
objectOutcome of a joint K(θ) fit with per-angle component assignment.
curves[m]is the fit of curvem;curve_values[m]/curve_errors[m]are the realigned per-point values assigned to curvem(so curvemis a continuous trace through crossings).assignment[p][c]gives the curve index that componentcat scan pointpwas assigned to.alternativesholds the distinct near-degenerate runner-up labellings (empty unlesskeep_alternatives > 0was requested).- curves: list[ParameterModelFitResult]
- alternatives: list[AngularAssignmentAlternative]
- __init__(success: bool, converged: bool, model_name: str, angles: list[float], curves: list[~asymmetry.core.fitting.parameter_models.ParameterModelFitResult], assignment: list[tuple[int, ...]], curve_values: list[list[float]], curve_errors: list[list[float]], total_chi_squared: float = 0.0, dof: int = 0, message: str = '', alternatives: list[~asymmetry.core.fitting.angular_assignment.AngularAssignmentAlternative] = <factory>) None
- asymmetry.core.fitting.angular_assignment.fit_assigned_angular_curves(angles: Sequence[float], component_values: Sequence[Sequence[float]], component_errors: Sequence[Sequence[float]] | None = None, *, model_name: str = 'KnightAnisotropy', max_iter: int = 25, keep_alternatives: int = 0, alternative_delta_chi2: float = 9.0) AngularAssignmentResult[source]
Jointly fit
NK(θ) curves, assigning each angle’s components one-to-one.component_valuesis[scan_point][component];Ncurves are fitted (one per component). Returns the per-curve fits, the per-point assignment, and the realigned per-curve traces. The fit is seeded from both the identity (raw-label) and value-continuity assignments and the lowest-χ² result kept.When
keep_alternatives > 0, each seed’s converged outcome is collected and the runners-up whose assignment differs from the winner (and each other) and whosetotal_chi_squaredlies withinalternative_delta_chi2of the winner are exposed, χ²-ordered and capped atkeep_alternatives, onAngularAssignmentResult.alternatives(canonicalised like the winner). The default (keep_alternatives = 0) leaves behaviour unchanged.
Knight-shift analysis session: a GUI-free pipeline from fitted frequencies to K.
This module is the core model behind the Knight shift analysis window. It owns the derivation pipeline that previously lived inline in the trend panel:
- fitted components (ν or B_µ per run) → K per component per run
→ crossing detection along the scan
The three pieces are deliberately separate:
KnightAnalysisInput— an immutable snapshot of the measured quantities: oneKnightPointper source run (abscissa, applied field, fitted component values/errors/covariance). The GUI builds it from the live trend rows; scripts can build it directly.KnightShiftConfig— the user’s conversion choices (reference, unit, component subset), reused unchanged.evaluate()— the pure derivation producing aKnightAnalysisResultof per-componentKnightBranchtraces plus detected crossings. Nothing upstream is mutated.
Only the configuration and the source binding (which series, which axis) are
persisted — see KnightAnalysisState. The point snapshot is rebuilt from
the source series on load, so saved projects cannot carry a stale copy of the
fitted values.
- asymmetry.core.fitting.knight_analysis.KNIGHT_COMPONENT_KINDS = ('frequency', 'field')
Oscillation components convertible to a Knight shift, by base parameter name.
frequencycomponents are in MHz (referenced to γ_µ·B);fieldcomponents are the fitted local field in Gauss (referenced to B itself).
- class asymmetry.core.fitting.knight_analysis.KnightPoint(run_number: int, run_label: str, x: float, field_gauss: float, values: Mapping[str, float], errors: Mapping[str, float], covariance: Mapping[str, Mapping[str, float]] | None = None, include: bool = True)[source]
Bases:
objectOne source run’s contribution to the analysis snapshot.
xis the scan abscissa (already resolved by the caller — for a rotation scan the unfolded angle in degrees; NaN drops the point).values/errorscarry the fitted component parameters;covarianceis the (optional) nested parameter-covariance map used by the designated-component reference.includemirrors the trend include-gate so excluded runs stay visible-but-skipped rather than silently vanishing.
- class asymmetry.core.fitting.knight_analysis.KnightAnalysisInput(x_key: str, x_label: str, components: tuple[tuple[str, str], ...], points: tuple[KnightPoint, ...], source_label: str = '', batch_id: str | None = None, group_id: str | None = None)[source]
Bases:
objectImmutable snapshot of everything
evaluate()reads.componentslists the convertible components as(name, kind)in model order (kind perKNIGHT_COMPONENT_KINDS); it is the source series’ model-derived observable map when available, so muonium components whosefieldis the applied field are already excluded.x_key/x_labelidentify the scan axis for display and persistence;source_labelnames the originating series/batch for the window header.- points: tuple[KnightPoint, ...]
- class asymmetry.core.fitting.knight_analysis.KnightBranch(name: str, component: str, kind: str, subscript: str, x: tuple[float, ...], k: tuple[float, ...], k_err: tuple[float, ...], run_numbers: tuple[int, ...], included: tuple[bool, ...])[source]
Bases:
objectOne component’s Knight-shift trace along the scan.
Values are dimensionless fractions; the result-level unit applies a display scale. Entries align with
run_numbers(points with a NaN abscissa or a missing component are dropped and counted at the result level; excluded points are kept, flagged byincluded, so the GUI can grey them out).
- class asymmetry.core.fitting.knight_analysis.KnightAnalysisResult(unit: KnightShiftUnit, unit_label: str, scale: float, branches: tuple[KnightBranch, ...], crossings: tuple[CrossingEvent, ...], skipped_points: int = 0)[source]
Bases:
objectOutput of
evaluate(): branches, crossings, and the resolved unit.- unit: KnightShiftUnit
- branches: tuple[KnightBranch, ...]
- skipped_points: int = 0
non-finite abscissa or missing component.
- Type:
Points dropped from every branch
- branch(name: str) KnightBranch | None[source]
- __init__(unit: KnightShiftUnit, unit_label: str, scale: float, branches: tuple[KnightBranch, ...], crossings: tuple[CrossingEvent, ...], skipped_points: int = 0) None
- asymmetry.core.fitting.knight_analysis.SAMPLE_SHAPE_DEMAG_FACTORS: dict[str, float | None] = {'custom': None, 'cylinder_axial': 0.0, 'cylinder_transverse': 0.5, 'plate_parallel': 0.0, 'plate_perpendicular': 1.0, 'sphere': 0.3333333333333333}
Demagnetization factor N along the applied field for standard sample shapes (SI convention, ΣN = 1).
customdefers to the user-entered value.
- class asymmetry.core.fitting.knight_analysis.KnightCorrection(enabled: bool = False, shape: str = 'sphere', custom_n: float = 0.3333333333333333, chi_volume_si: float = 0.0)[source]
Bases:
objectLorentz/demagnetizing-field correction to the measured shift.
Applies Amato & Morenzoni Eq. 5.60:
K_µ = K_exp − (1/3 − N)·χ, withNthe demagnetization factor along the applied field (SI convention, sphere = 1/3 — for which Lorentz and demagnetizing fields cancel and the correction vanishes) andchithe volume susceptibility in SI dimensionless units (multiply a CGS emu/cm³ value by 4π). The correction is a constant offset per branch, exact for an ellipsoidal sample whose shape is fixed relative to the field; for a rotating non-spheroidal sample N itself varies with angle, which this scalar form does not capture.- classmethod from_dict(data: object) KnightCorrection[source]
- asymmetry.core.fitting.knight_analysis.branch_name(component: str) str[source]
Trace name for a component’s Knight shift (
K[<component>]).
- asymmetry.core.fitting.knight_analysis.selected_components(analysis_input: KnightAnalysisInput, config: KnightShiftConfig) tuple[tuple[str, str], ...][source]
The
(name, kind)components the config actually converts.Applies the config’s component subset (empty = all) and, for the designated-component reference, restricts to components of the same kind as the reference — dividing a frequency (MHz) by a field (Gauss) is meaningless — excluding the reference itself. Returns an empty tuple when the designated reference is not among the snapshot’s components (emit nothing rather than guess).
- asymmetry.core.fitting.knight_analysis.evaluate(analysis_input: KnightAnalysisInput, config: KnightShiftConfig, correction: KnightCorrection | None = None) KnightAnalysisResult[source]
Derive the Knight-shift branches and crossings for a snapshot + config.
Pure: the snapshot is not mutated, and calling twice with the same inputs gives the same result. A disabled config yields an empty result (no branches, no crossings) with the unit still resolved so axis labels stay stable while the user toggles the conversion.
correction(optional) applies the Lorentz/demagnetizing offset of Eq. 5.60 to every shift; the offset is common to all branches, so crossings and branch ordering are unaffected. K uncertainties do not include a χ uncertainty.
- class asymmetry.core.fitting.knight_analysis.KnightJointCurve(branch_name: str, parameters: tuple[tuple[str, float, float], ...], chi_squared: float, reduced_chi_squared: float, n_points: int, success: bool, covariance: tuple[tuple[str, ...], tuple[tuple[float, ...], ...]] | None = None)[source]
Bases:
objectOne fitted physical curve of a joint K(θ) fit.
branch_nameis theK[...]trace this curve occupies after realignment.parametersare(name, value, error)triples in the fit unit (seeKnightJointFitState.unit).covariancemirrorsParameterModelFitResult.covariance—(names, matrix)for the curve’s free parameters, in the same order asnames. It is already in the display unit:run_joint_fit()scales the values into the display unit before fitting, so the Minuit covariance it reads back is already unit² in that display unit — no extra scaling is applied here.Nonewhen the underlying fit produced no covariance (HESSE failed/didn’t run) or the curve predates this field (legacy project).- classmethod from_dict(data: object) KnightJointCurve | None[source]
- class asymmetry.core.fitting.knight_analysis.KnightJointFitState(model_name: str = 'KnightAnisotropy', max_iter: int = 25, unit: str = 'percent', correction_offset: float = 0.0, converged: bool = False, total_chi_squared: float = 0.0, dof: int = 0, message: str = '', assignment: dict[int, tuple[int, ...]]=<factory>, curves: tuple[~asymmetry.core.fitting.knight_analysis.KnightJointCurve, ...]=())[source]
Bases:
objectPersisted joint K(θ) fit: model, per-run assignment, per-curve parameters.
assignment[run_number][component]is the curve index that component was assigned to at that run — the durable representation (run numbers survive refits and reordering; scan-point indices would not).unitrecords the concrete display unit the fit ran in: the assignment is unit-independent, but the curve parameters are not, so a unit change marks the curves stale until the fit is re-run.- correction_offset: float = 0.0
Lorentz/demag offset (fraction units) active when the fit ran — like
unit, a bookkeeping value: a changed correction shifts every K, so the fitted curves go stale (the assignment does not — a common offset cannot reorder branches).
- curves: tuple[KnightJointCurve, ...] = ()
- classmethod from_dict(data: object) KnightJointFitState | None[source]
- __init__(model_name: str = 'KnightAnisotropy', max_iter: int = 25, unit: str = 'percent', correction_offset: float = 0.0, converged: bool = False, total_chi_squared: float = 0.0, dof: int = 0, message: str = '', assignment: dict[int, tuple[int, ...]]=<factory>, curves: tuple[~asymmetry.core.fitting.knight_analysis.KnightJointCurve, ...]=()) None
- asymmetry.core.fitting.knight_analysis.run_joint_fit_outcome(result: KnightAnalysisResult, *, model_name: str = 'KnightAnisotropy', max_iter: int = 25, correction_offset: float = 0.0, keep_alternatives: int = 0) tuple[KnightJointFitState, AngularAssignmentResult][source]
Run the joint K(θ) fit and return both the persisted state and the outcome.
The persisted
KnightJointFitStateis exactly whatrun_joint_fit()returns; the second element is the rawAngularAssignmentResultfrom the classification-EM fit. The outcome carries the near-degenerate runner-up labellings onAngularAssignmentResult.alternativeswhenkeep_alternatives > 0— the input the assignment-discrimination bridge (suggest_assignment_discriminating_angle()) needs. It is deliberately not folded into the state: the state is persisted, but the alternatives are in-memory only (per the study), so a project load never carries them.keep_alternativesis forwarded tofit_assigned_angular_curves(). RaisesValueErrorwith fewer than two branches or two shared points.
- asymmetry.core.fitting.knight_analysis.run_joint_fit(result: KnightAnalysisResult, *, model_name: str = 'KnightAnisotropy', max_iter: int = 25, correction_offset: float = 0.0) KnightJointFitState[source]
Jointly fit all branches’ K(θ) with per-angle component assignment.
A thin bridge to
asymmetry.core.fitting.angular_assignment. fit_assigned_angular_curves(): builds the aligned matrices in the result’s display unit (so curve parameters read in the plotted unit), runs the classification-EM fit, and re-keys the assignment by run number. Delegates torun_joint_fit_outcome()and returns only the persisted state, so its public behaviour is unchanged (the EM runner-up alternatives are discarded). RaisesValueErrorwith fewer than two branches or two shared points.
- asymmetry.core.fitting.knight_analysis.apply_assignment(result: KnightAnalysisResult, joint: KnightJointFitState) KnightAnalysisResult[source]
Realign the branches so each follows its physical curve through crossings.
For every run in the joint fit’s assignment, the component values are permuted onto the branch of the curve they were assigned to (
perm[component] = curve). Runs outside the assignment (new points, or points not shared by all branches at fit time) keep their raw labels. The input result is not mutated.
- asymmetry.core.fitting.knight_analysis.assignment_swap_positions(result: KnightAnalysisResult, joint: KnightJointFitState) tuple[float, ...][source]
Scan positions (midpoints) where the fitted assignment swaps curves.
These are the crossings the joint fit actually resolved — a firmer signal than the raw proximity flags, so the window marks these when a fit is active. Points not covered by the assignment are skipped.
- asymmetry.core.fitting.knight_analysis.suggest_next_angle(result: KnightAnalysisResult, joint: KnightJointFitState, *, x_min: float, x_max: float, target: tuple[str, str] | None = None, sigma_goal: float | None = None, n_candidates: int = 257) NextPointSuggestion[source]
Suggest the next scan angle that best constrains the joint K(θ) fit (§3.1).
Builds one series per fitted curve (model from
joint.model_name, parameters/covariance from the curve, empirical noise from its realigned trace) and delegates tosuggest_next_point_multi().targetnames(branch_name, param_name)for a c-optimal solve (withsigma_goal);Nonegives the D-optimal information-gain sum. Curves without stored covariance degrade with a warning naming the branch; when every curve lacks covariance the suggestion is empty with a “re-run the joint fit” warning (legacy fits predate stored covariance).
- asymmetry.core.fitting.knight_analysis.joint_fit_aic_inputs(joint: KnightJointFitState) tuple[float, int][source]
(total_chi_squared, n_curves·n_params)for AIC-weighting a joint fit.Feeds
aic_weights()so the GUI can rank an aligned (lead) fit against a misalignment (alternative) fit (§3.2).n_paramsis per-curve free-parameter count (all curves share the model), multiplied by the number of curves.
- asymmetry.core.fitting.knight_analysis.suggest_model_discriminating_angle(result: KnightAnalysisResult, joint_lead: KnightJointFitState, joint_alt: KnightJointFitState, *, x_min: float, x_max: float, n_candidates: int = 257) NextPointSuggestion[source]
Suggest the angle best distinguishing an aligned vs misaligned fit (§3.2).
Curve identity is preserved: curves are matched across the two joint states by
branch_name(an error if the branch sets differ), so the utility is the labelled disagreement sumU(θ) = Σ_m [f_m^lead(θ) − f_m^alt(θ)]² / 2σ_m²(θ)
with no matching step. σ_m is the lead fit’s realigned per-curve noise (display unit). When the two fits agree within noise everywhere the standard “agree within noise” warning is carried (as in
suggest_discriminating_point()).
- asymmetry.core.fitting.knight_analysis.suggest_assignment_discriminating_angle(result: KnightAnalysisResult, outcome: AngularAssignmentResult, *, x_min: float, x_max: float, n_candidates: int = 257) NextPointSuggestion[source]
Suggest the angle best resolving competing per-angle assignments (§3.3).
outcomeis anAngularAssignmentResultcarrying near-degenerate runner-up labellings (keep_alternatives > 0). The utility is the elementwise max over alternatives of the Hungarian set-matching divergence between the winner’s and each alternative’s predicted value sets (leader-vs-all — the TAS-AI lock-in lesson): it is ~0 where the labellings coincide (e.g. at a crossing) and peaks where they imply genuinely different curve sets. With no alternatives the suggestion is empty (“no near-degenerate assignments to discriminate”).Per-curve noise comes from the winner’s realigned trace errors (
outcome.curve_errors); because the utility is a value²/σ² ratio it is invariant to the common display-unit scale, soresultis used only to confirm the curve/branch counts agree.
- class asymmetry.core.fitting.knight_analysis.KnightAnalysisState(config: KnightShiftConfig = <factory>, correction: KnightCorrection = <factory>, source_batch_id: str | None = None, source_group_id: str | None = None, x_key: str = 'angle', fold_180: bool = False, show_markers: bool = True, rescale_errors: bool = False, joint: KnightJointFitState | None = None)[source]
Bases:
objectPersisted window state: conversion config + source binding + view hints.
Deliberately excludes the point snapshot (rebuilt from the source series on open — a saved copy could go stale against a refit).
source_batch_id/source_group_idre-bind the window to its series;x_keypins the scan axis.fold_180andshow_markersare plot-view preferences.jointcarries the (optional) joint K(θ) fit; its run-keyed assignment stays valid across snapshot rebuilds.- config: KnightShiftConfig
- correction: KnightCorrection
- joint: KnightJointFitState | None = None
- classmethod from_dict(data: object) KnightAnalysisState[source]
- __init__(config: KnightShiftConfig = <factory>, correction: KnightCorrection = <factory>, source_batch_id: str | None = None, source_group_id: str | None = None, x_key: str = 'angle', fold_180: bool = False, show_markers: bool = True, rescale_errors: bool = False, joint: KnightJointFitState | None = None) None
- asymmetry.core.fitting.knight_analysis.migrate_legacy_state(fit_parameters_state: object) KnightAnalysisState | None[source]
Lift a pre-window trend-panel Knight-shift block into the new state.
Projects saved before the analysis window stored the conversion config under
fit_parameters_state["knight_shift"](with the scan axis inx_axis_key). ReturnsNonewhen there is nothing to migrate (missing block or conversion never enabled) so callers can skip writing an empty state; the legacy block itself is left untouched for older app versions.
- asymmetry.core.fitting.knight_analysis.snapshot_from_rows(rows: Sequence[object], *, x_values: Sequence[float], x_key: str, x_label: str, components: Sequence[tuple[str, str]], source_label: str = '', batch_id: str | None = None, group_id: str | None = None) KnightAnalysisInput[source]
Build a snapshot from trend-row-shaped objects.
rowsare duck-typed against the trend panel’s row records (attributesrun_number,run_label,field,values,errors,covariance,include_in_trend);x_valuessupplies the resolved (unfolded) abscissa for each row, letting the caller keep ownership of the axis resolution. Rows and abscissae must align.
Frequency-domain helpers
Frequency-domain fitting helpers.
- asymmetry.core.fitting.spectral.append_frequency_field_derived_parameters(parameters: ParameterSet, uncertainties: dict[str, float] | None = None) tuple[ParameterSet, dict[str, float]][source]
Return a copy with derived centre/width field parameters appended.
The canonical fitted quantities remain
nu0andfwhmin MHz. The derivedB0andBwidvalues are appended for trend tables and exports.
- asymmetry.core.fitting.spectral.default_frequency_model() CompositeModel[source]
Return the default V1 frequency-domain peak model.
- asymmetry.core.fitting.spectral.field_gauss_to_frequency_mhz(value_gauss: float) float[source]
Convert a magnetic field or width in Gauss to MHz.
- asymmetry.core.fitting.spectral.frequency_mhz_to_field_gauss(value_mhz: float) float[source]
Convert a muon precession frequency or width in MHz to Gauss.
- asymmetry.core.fitting.spectral.seed_peak_parameters_from_dataset(dataset, model: CompositeModel, *, guard_bins: int = 3, guard_freq_mhz: float = 2.0) dict[str, float][source]
Return simple peak/background seeds for one displayed Fourier spectrum.
The dominant-magnitude bin of a (Power)^1/2 spectrum is usually the DC/apodisation spike, not the physical precession peak, so the peak search excludes a low-frequency guard band before taking the argmax. The guard width is
max(guard_bins * df, guard_freq_mhz), wheredfis the spectrum’s bin spacing (a proxy for1/T_obs); it falls back to the unguarded global argmax if the guard would empty the search array.When the model declares two or more peak components, seeding delegates to
_seed_multiple_peaks(), which assigns the strongest detected peaks to each component (see there for the under-detection fallback).
Superconductivity models
The superconductivity API is organised around normalised superfluid density
with measured \(\sigma(T)\) provided in additive and quadrature forms.
Kernel and gap helpers
Thermal kernel and angular averaging:
- asymmetry.core.fitting.sc.kernel.energy_integral(delta_reduced: ndarray[tuple[int, ...], dtype[float64]] | list[float] | float, t_reduced: float, *, n_energy: int = 96) ndarray[tuple[int, ...], dtype[float64]][source]
Evaluate the reduced thermal integral for one temperature.
\[I(\Delta,t)=\int_{\Delta}^{\infty} \frac{\partial f}{\partial E} \frac{E\,dE}{\sqrt{E^2-\Delta^2}}\]where energies are expressed in units of \(k_B T_c\).
The integral is transformed to x in [0, 1) via E = delta + x/(1-x).
- asymmetry.core.fitting.sc.kernel.superfluid_density_2d(t_values: ndarray[tuple[int, ...], dtype[float64]] | list[float] | float, *, tc: float, gap_ratio: float, gap_function: Callable[[...], ndarray[tuple[int, ...], dtype[float64]]], gap_params: dict[str, float] | None = None, n_phi: int = 128, n_energy: int = 96, reduced_gap_function: Callable[[ndarray[tuple[int, ...], dtype[float64]]], ndarray[tuple[int, ...], dtype[float64]]] | None = None, normalize_g: bool = False, normalization: str = 'rms') ndarray[tuple[int, ...], dtype[float64]][source]
Compute \(\rho_s(T)\) for a quasi-2D gap function
g(phi).- Parameters:
t_values – Temperature values in K.
tc – Critical temperature in K.
gap_ratio – Dimensionless gap ratio \(\Delta_0/(k_B T_c)\).
gap_function – Callable returning angular form factor
g(phi, **gap_params).reduced_gap_function – Optional helper returning the reduced temperature dependence \(\delta(T/T_c)\). Defaults to
asymmetry.core.fitting.sc.bcs.delta_bcs().normalize_g – If True, normalize
|g|before integration.normalization –
"rms"or"mean_abs"normalization mode.
- asymmetry.core.fitting.sc.kernel.superfluid_density_3d(t_values: ndarray[tuple[int, ...], dtype[float64]] | list[float] | float, *, tc: float, gap_ratio: float, gap_function: Callable[[...], ndarray[tuple[int, ...], dtype[float64]]], gap_params: dict[str, float] | None = None, n_theta: int = 32, n_phi: int = 64, n_energy: int = 96, reduced_gap_function: Callable[[ndarray[tuple[int, ...], dtype[float64]]], ndarray[tuple[int, ...], dtype[float64]]] | None = None, normalize_g: bool = False, normalization: str = 'rms') ndarray[tuple[int, ...], dtype[float64]][source]
Compute \(\rho_s(T)\) for a 3D gap function
g(theta, phi).- Parameters:
t_values – Temperature values in K.
tc – Critical temperature in K.
gap_ratio – Dimensionless gap ratio \(\Delta_0/(k_B T_c)\).
gap_function – Callable returning angular form factor
g(theta, phi, **gap_params).gap_params – Optional keyword arguments forwarded to
gap_function.n_theta – Number of Gauss-Legendre nodes in polar angle.
n_phi – Number of Gauss-Legendre nodes in azimuthal angle.
n_energy – Number of Gauss-Legendre nodes used in the thermal energy integral.
reduced_gap_function – Optional helper returning the reduced temperature dependence \(\delta(T/T_c)\). Defaults to
asymmetry.core.fitting.sc.bcs.delta_bcs().normalize_g – If
True, normalize \(|g|\) before integration.normalization – Either
"rms"or"mean_abs".
- Returns:
Normalized superfluid density clipped to
[0, 1]with limiting valuesrho_s(0)=1andrho_s(T>=Tc)=0imposed for numerical stability.- Return type:
Gap-amplitude approximations and convention helpers:
- asymmetry.core.fitting.sc.bcs.delta_bcs(t_reduced: ndarray[tuple[int, ...], dtype[float64]] | list[float] | float) ndarray[tuple[int, ...], dtype[float64]][source]
Return reduced BCS gap from the Carrington-Manzano interpolation.
This uses the Carrington-Manzano approximation:
delta(T) = tanh(1.82 * [1.018 * (1/t - 1)]^0.51), for 0 < t < 1
with t = T / Tc.
The function is pinned to: - delta = 1 at t <= 0 - delta = 0 at t >= 1
Notes
A frequently cited alternative is a Gross-type form [1],
\[\Delta_0(T)=\Delta_0(0)\tanh\left[\frac{\pi T_c}{\Delta_0(0)} \sqrt{a\left(\frac{T_c}{T}-1\right)}\right],\]where
ais symmetry-dependent. Usedelta_generalized()when a symmetry-specific weak-coupling shape factor is available.
- asymmetry.core.fitting.sc.bcs.delta_generalized(t_reduced: ndarray[tuple[int, ...], dtype[float64]] | list[float] | float, *, gap_ratio: float, shape_factor: float) ndarray[tuple[int, ...], dtype[float64]][source]
Return reduced gap from the generalized Gross-style expression.
In reduced units with \(t = T/T_c\) and \(\Delta_0(0)/(k_B T_c)=\text{gap_ratio}\) this becomes
\[\delta(t)=\tanh\left[\frac{\pi}{\text{gap_ratio}} \sqrt{a\left(\frac{1}{t}-1\right)}\right],\]where
shape_factorcorresponds to the literature parameter \(a\). The function is pinned to1fort <= 0and0fort >= 1.
- asymmetry.core.fitting.sc.bcs.gap_ratio_from_mev(gap_mev: float, tc: float) float[source]
Convert
Delta0in meV toDelta0/(k_B Tc).- Parameters:
gap_mev – Zero-temperature gap magnitude in meV.
tc – Critical temperature in K.
- asymmetry.core.fitting.sc.bcs.resolve_gap_ratio(*, tc: float, gap_ratio: float | None = None, gap_mev: float | None = None) float[source]
Resolve gap ratio from either explicit ratio or meV input.
If both are provided,
gap_mevtakes precedence.- Returns:
Dimensionless ratio \(\Delta_0/(k_B T_c)\).
- Return type:
Angular gap form factors:
- asymmetry.core.fitting.sc.gaps.isotropic_s(phi: ndarray[tuple[int, ...], dtype[float64]] | list[float] | float) ndarray[tuple[int, ...], dtype[float64]][source]
Return isotropic s-wave form factor.
\[g(\phi) = 1.\]
- asymmetry.core.fitting.sc.gaps.d_wave(phi: ndarray[tuple[int, ...], dtype[float64]] | list[float] | float) ndarray[tuple[int, ...], dtype[float64]][source]
Return 2D d-wave form factor with line nodes.
\[g(\phi) = \cos(2\phi).\]
- asymmetry.core.fitting.sc.gaps.anisotropic_s_cos4(phi: ndarray[tuple[int, ...], dtype[float64]] | list[float] | float, a_anis: float) ndarray[tuple[int, ...], dtype[float64]][source]
Return fourfold-anisotropic s-wave form factor.
\[g(\phi) = 1 + a\cos(4\phi).\]For abs(a) < 1 this form is nodeless. For abs(a) >= 1 accidental nodes can occur.
- asymmetry.core.fitting.sc.gaps.nonmonotonic_d_wave(phi: ndarray[tuple[int, ...], dtype[float64]] | list[float] | float, beta_nm: float) ndarray[tuple[int, ...], dtype[float64]][source]
Return nonmonotonic d-wave form factor.
\[g(\phi) = \beta\cos(2\phi) + (1-\beta)\cos(6\phi).\]
- asymmetry.core.fitting.sc.gaps.s_plus_g(theta: ndarray[tuple[int, ...], dtype[float64]] | list[float] | float, phi: ndarray[tuple[int, ...], dtype[float64]] | list[float] | float) ndarray[tuple[int, ...], dtype[float64]][source]
Return s+g form factor used in anisotropic singlet models.
\[g(\theta,\phi) = \frac{1 - \sin^4\theta\cos(4\phi)}{2}.\]This representation follows the weak-coupling form tabulated in Ref. [1] for s+g-wave phenomenology.
Single-gap and anisotropic models
Isotropic and nodal baselines:
- asymmetry.core.fitting.sc.models.rho_s_wave(T: ndarray[tuple[int, ...], dtype[float64]] | list[float] | float, *, Tc: float, gap_ratio: float = 1.764, gap_mev: float | None = None, n_phi: int = 64, n_energy: int = 48) ndarray[tuple[int, ...], dtype[float64]][source]
Return \(\rho_s(T)\) for isotropic s-wave gap, \(g(\phi)=1\).
- Parameters:
T – Temperature in K.
Tc – Critical temperature in K.
gap_ratio – Dimensionless ratio \(\Delta_0/(k_B T_c)\).
gap_mev – Optional \(\Delta_0\) in meV. Overrides
gap_ratiowhen given.n_phi – Number of angular quadrature points for Fermi-surface averaging.
n_energy – Number of Gauss-Legendre nodes for the energy integral.
- Returns:
Normalized superfluid density \(\rho_s(T)\).
- Return type:
- asymmetry.core.fitting.sc.models.sc_s_wave(T: ndarray[tuple[int, ...], dtype[float64]] | list[float] | float, sigma_0: float, Tc: float, gap_ratio: float = 1.764, sigma_bg: float = 0.0, gap_mev: float | None = None) ndarray[tuple[int, ...], dtype[float64]][source]
Additive isotropic s-wave model for measured \(\sigma(T)\).
\[\sigma(T) = \sigma_0\,\rho_s(T) + \sigma_{bg}\]
- asymmetry.core.fitting.sc.models.rho_d_wave(T: ndarray[tuple[int, ...], dtype[float64]] | list[float] | float, *, Tc: float, gap_ratio: float = 2.14, gap_mev: float | None = None, n_phi: int = 64, n_energy: int = 48) ndarray[tuple[int, ...], dtype[float64]][source]
Return \(\rho_s(T)\) for d-wave \(g(\phi)=\cos(2\phi)\).
This model has line nodes and therefore stronger low-temperature variation than isotropic s-wave, typically close to linear-in-T in clean limits [1]. The reduced gap amplitude uses the generalized weak-coupling form with the d-wave shape factor \(a=4/3\).
- asymmetry.core.fitting.sc.models.sc_d_wave(T: ndarray[tuple[int, ...], dtype[float64]] | list[float] | float, sigma_0: float, Tc: float, gap_ratio: float = 2.14, sigma_bg: float = 0.0, gap_mev: float | None = None) ndarray[tuple[int, ...], dtype[float64]][source]
Additive d-wave model for measured \(\sigma(T)\).
Anisotropic variants:
- asymmetry.core.fitting.sc.models.rho_anisotropic_s_cos4(T: ndarray[tuple[int, ...], dtype[float64]] | list[float] | float, *, Tc: float, gap_ratio: float = 1.764, a_anis: float = 0.2, shape_factor_a: float = 0.0, gap_mev: float | None = None, n_phi: int = 64, n_energy: int = 48) ndarray[tuple[int, ...], dtype[float64]][source]
Return \(\rho_s(T)\) for anisotropic s-wave
1 + a*cos(4phi).a_aniscontrols anisotropy. Forabs(a_anis) < 1the gap remains nodeless; accidental nodes may appear whenabs(a_anis) >= 1. Ifshape_factor_a > 0, use the generalized weak-coupling reduced-gap law with that value; otherwise fall back to the Carrington-Manzano interpolation.
- asymmetry.core.fitting.sc.models.sc_anisotropic_s_cos4(T: ndarray[tuple[int, ...], dtype[float64]] | list[float] | float, sigma_0: float, Tc: float, gap_ratio: float = 1.764, a_anis: float = 0.2, shape_factor_a: float = 0.0, sigma_bg: float = 0.0, gap_mev: float | None = None) ndarray[tuple[int, ...], dtype[float64]][source]
Additive anisotropic-s model for measured \(\sigma(T)\).
- asymmetry.core.fitting.sc.models.rho_nonmonotonic_d(T: ndarray[tuple[int, ...], dtype[float64]] | list[float] | float, *, Tc: float, gap_ratio: float = 2.14, beta_nm: float = 0.8, gap_mev: float | None = None, n_phi: int = 64, n_energy: int = 48) ndarray[tuple[int, ...], dtype[float64]][source]
Return \(\rho_s(T)\) for nonmonotonic d-wave.
\[g(\phi)=\beta\cos(2\phi)+(1-\beta)\cos(6\phi).\]This form is commonly used as a phenomenological extension when a monotonic \(\cos(2\phi)\) d-wave is insufficient [1]. The temperature-dependent gap amplitude uses the same d-wave weak-coupling shape factor \(a=4/3\) as
rho_d_wave().
- asymmetry.core.fitting.sc.models.sc_nonmonotonic_d(T: ndarray[tuple[int, ...], dtype[float64]] | list[float] | float, sigma_0: float, Tc: float, gap_ratio: float = 2.14, beta_nm: float = 0.8, sigma_bg: float = 0.0, gap_mev: float | None = None) ndarray[tuple[int, ...], dtype[float64]][source]
Additive nonmonotonic-d model for measured \(\sigma(T)\).
- asymmetry.core.fitting.sc.models.rho_s_plus_g(T: ndarray[tuple[int, ...], dtype[float64]] | list[float] | float, *, Tc: float, gap_ratio: float = 2.77, gap_mev: float | None = None, n_theta: int = 24, n_phi: int = 48, n_energy: int = 48) ndarray[tuple[int, ...], dtype[float64]][source]
Return \(\rho_s(T)\) for s+g-wave anisotropic singlet gap.
\[g(\theta,\phi)=\frac{1-\sin^4\theta\cos(4\phi)}{2}.\]The default
gap_ratio=2.77follows the weak-coupling tabulation used in Ref. [1]. The reduced gap amplitude uses the generalized weak-coupling form with the s+g shape factor \(a=2\).- Parameters:
T – Temperature in K.
Tc – Critical temperature in K.
gap_ratio – Dimensionless ratio \(\Delta_0/(k_B T_c)\).
gap_mev – Optional \(\Delta_0\) in meV. Overrides
gap_ratiowhen given.n_theta – Number of polar-angle quadrature points.
n_phi – Number of azimuthal-angle quadrature points.
n_energy – Number of Gauss-Legendre nodes for the energy integral.
- asymmetry.core.fitting.sc.models.sc_s_plus_g(T: ndarray[tuple[int, ...], dtype[float64]] | list[float] | float, sigma_0: float, Tc: float, gap_ratio: float = 2.77, sigma_bg: float = 0.0, gap_mev: float | None = None) ndarray[tuple[int, ...], dtype[float64]][source]
Additive s+g model for measured \(\sigma(T)\).
\[\sigma(T) = \sigma_0\,\rho_{s+g}(T) + \sigma_{bg}.\]
Additional unconventional examples:
- asymmetry.core.fitting.sc.models.rho_extended_s(T: ndarray[tuple[int, ...], dtype[float64]] | list[float] | float, *, Tc: float, gap_ratio: float = 2.14, signed: bool = False, gap_mev: float | None = None, n_phi: int = 64, n_energy: int = 48) ndarray[tuple[int, ...], dtype[float64]][source]
Return \(\rho_s(T)\) for extended s-wave based on
cos(2phi).Set
signed=Trueto preserve sign of \(\cos(2\phi)\). The default uses magnitude because the quasiparticle excitation energy depends on \(|\Delta|\). The reduced gap amplitude uses the generalized weak-coupling form with \(a=4/3\), consistent with the \(\cos(2\phi)\) basis tabulated in Ref. [1].
- asymmetry.core.fitting.sc.models.sc_extended_s(T: ndarray[tuple[int, ...], dtype[float64]] | list[float] | float, sigma_0: float, Tc: float, gap_ratio: float = 2.14, signed_gap: float = 0.0, sigma_bg: float = 0.0, gap_mev: float | None = None) ndarray[tuple[int, ...], dtype[float64]][source]
Additive extended-s model for measured \(\sigma(T)\).
- asymmetry.core.fitting.sc.models.rho_p_wave_axial(T: ndarray[tuple[int, ...], dtype[float64]] | list[float] | float, *, Tc: float, gap_ratio: float = 2.0, shape_factor_a: float = 0.0, gap_mev: float | None = None, n_phi: int = 64, n_energy: int = 48) ndarray[tuple[int, ...], dtype[float64]][source]
Return \(\rho_s(T)\) for 2D axial p-wave, \(g(\phi)=\cos\phi\).
If
shape_factor_a > 0, use the generalized weak-coupling reduced-gap law with that value; otherwise fall back to the Carrington-Manzano interpolation.
- asymmetry.core.fitting.sc.models.sc_p_wave_axial(T: ndarray[tuple[int, ...], dtype[float64]] | list[float] | float, sigma_0: float, Tc: float, gap_ratio: float = 2.0, shape_factor_a: float = 0.0, sigma_bg: float = 0.0, gap_mev: float | None = None) ndarray[tuple[int, ...], dtype[float64]][source]
Additive 2D axial p-wave model for measured \(\sigma(T)\).
- asymmetry.core.fitting.sc.models.rho_p_wave_polar_3d(T: ndarray[tuple[int, ...], dtype[float64]] | list[float] | float, *, Tc: float, gap_ratio: float = 2.0, shape_factor_a: float = 0.0, gap_mev: float | None = None, n_theta: int = 24, n_phi: int = 48, n_energy: int = 48) ndarray[tuple[int, ...], dtype[float64]][source]
Return \(\rho_s(T)\) for 3D polar p-wave line-node example.
Uses \(g(\theta)=\sin\theta\) with spherical angular averaging. If
shape_factor_a > 0, use the generalized weak-coupling reduced-gap law with that value; otherwise fall back to the Carrington-Manzano interpolation.
Note
rho_extended_s uses the generalized weak-coupling reduced-gap law with
a = 4/3 by default. For rho_anisotropic_s_cos4,
sc_anisotropic_s_cos4, rho_p_wave_axial, sc_p_wave_axial, and
rho_p_wave_polar_3d, the optional shape_factor_a parameter can be
supplied when a symmetry-specific weak-coupling shape factor is known or is
to be fitted. If shape_factor_a is omitted or left at 0, these
models fall back to the Carrington-Manzano interpolation.
Two-gap and alpha models
Weighted-sum two-gap models (including MgB2-style s+s decomposition):
- asymmetry.core.fitting.sc.models.sc_two_gap_ss(T: ndarray[tuple[int, ...], dtype[float64]] | list[float] | float, sigma_0: float, Tc: float, gap_ratio_1: float = 1.2, gap_ratio_2: float = 2.2, weight: float = 0.5, sigma_bg: float = 0.0) ndarray[tuple[int, ...], dtype[float64]][source]
Two-gap isotropic model, weighted superfluid density sum.
\[ \begin{align}\begin{aligned} \rho_s(T) = w\rho_1(T) + (1-w)\rho_2(T),\quad 0\le w\le 1.\\This is the standard MgB2-style alpha-model decomposition for multiband superconductors [2].\end{aligned}\end{align} \]
- asymmetry.core.fitting.sc.models.sc_two_gap_sd(T: ndarray[tuple[int, ...], dtype[float64]] | list[float] | float, sigma_0: float, Tc: float, gap_ratio_s: float = 1.764, gap_ratio_d: float = 2.14, weight: float = 0.5, sigma_bg: float = 0.0) ndarray[tuple[int, ...], dtype[float64]][source]
Two-gap mixed-symmetry model (s + d weighted sum).
Alpha-model scaling:
- asymmetry.core.fitting.sc.models.sc_alpha_model(T: ndarray[tuple[int, ...], dtype[float64]] | list[float] | float, sigma_0: float, Tc: float, alpha_sc: float = 1.0, sigma_bg: float = 0.0) ndarray[tuple[int, ...], dtype[float64]][source]
Single-gap alpha model using isotropic BCS kernel.
alpha_sc rescales the weak-coupling s-wave ratio 1.764.
Quadrature sigma models
These variants are intended for workflows where superconducting and non-superconducting linewidth channels are modelled as independent Gaussian contributions added in quadrature.
- asymmetry.core.fitting.sc.models.sc_s_wave_q(T: ndarray[tuple[int, ...], dtype[float64]] | list[float] | float, sigma_sc: float, sigma_nm: float, Tc: float, gap_ratio: float = 1.764, gap_mev: float | None = None) ndarray[tuple[int, ...], dtype[float64]][source]
Quadrature s-wave model for measured \(\sigma(T)\).
Use this convention when independent Gaussian broadening channels combine at the second-moment level, motivating \(\sigma^2=(\sigma_{sc}\rho_s)^2+\sigma_{nm}^2\).
- asymmetry.core.fitting.sc.models.sc_d_wave_q(T: ndarray[tuple[int, ...], dtype[float64]] | list[float] | float, sigma_sc: float, sigma_nm: float, Tc: float, gap_ratio: float = 2.14, gap_mev: float | None = None) ndarray[tuple[int, ...], dtype[float64]][source]
Quadrature d-wave model for measured \(\sigma(T)\).
- asymmetry.core.fitting.sc.models.sc_s_plus_g_q(T: ndarray[tuple[int, ...], dtype[float64]] | list[float] | float, sigma_sc: float, sigma_nm: float, Tc: float, gap_ratio: float = 2.77, gap_mev: float | None = None) ndarray[tuple[int, ...], dtype[float64]][source]
Quadrature s+g model for measured \(\sigma(T)\).
This is useful when superconducting and non-superconducting linewidth channels are treated as independent Gaussian contributions that add in quadrature.
lambda and sigma conversion helpers:
- asymmetry.core.fitting.sc.constants.sigma_to_lambda_nm(sigma_us: ndarray[tuple[int, ...], dtype[float64]] | list[float] | float, *, brandt_coefficient: float = 0.0609) ndarray[tuple[int, ...], dtype[float64]][source]
Convert superconducting sigma (us^-1) to penetration depth lambda (nm).
For sigma <= 0, the returned lambda is +inf.
- asymmetry.core.fitting.sc.constants.lambda_nm_to_sigma_us(lambda_nm: ndarray[tuple[int, ...], dtype[float64]] | list[float] | float, *, brandt_coefficient: float = 0.0609) ndarray[tuple[int, ...], dtype[float64]][source]
Convert penetration depth lambda (nm) to superconducting sigma (us^-1).
- asymmetry.core.fitting.sc.models.rho_to_lambda_inv_sq(rho: ndarray[tuple[int, ...], dtype[float64]] | list[float] | float, *, lambda_0_nm: float) ndarray[tuple[int, ...], dtype[float64]][source]
Convert normalized \(\rho_s(T)\) to \(\lambda^{-2}(T)\) in nm^-2.
- asymmetry.core.fitting.sc.models.rho_to_lambda(rho: ndarray[tuple[int, ...], dtype[float64]] | list[float] | float, *, lambda_0_nm: float) ndarray[tuple[int, ...], dtype[float64]][source]
Convert normalized \(\rho_s(T)\) to \(\lambda(T)\) in nm.
- asymmetry.core.fitting.sc.models.lambda_inv_sq_from_model(T: ndarray[tuple[int, ...], dtype[float64]] | list[float] | float, *, rho_function: Callable[[...], ndarray[tuple[int, ...], dtype[float64]]], lambda_0_nm: float, **kwargs: float) ndarray[tuple[int, ...], dtype[float64]][source]
Evaluate \(\lambda^{-2}(T)\) from any
rho_functioncallable.
- asymmetry.core.fitting.sc.models.lambda_from_model(T: ndarray[tuple[int, ...], dtype[float64]] | list[float] | float, *, rho_function: Callable[[...], ndarray[tuple[int, ...], dtype[float64]]], lambda_0_nm: float, **kwargs: float) ndarray[tuple[int, ...], dtype[float64]][source]
Evaluate \(\lambda(T)\) from any
rho_functioncallable.
Parameters
- class asymmetry.core.fitting.parameters.Parameter(name: str, value: float = 0.0, min: float = -inf, max: float = inf, fixed: bool = False, expr: str | None = None, link_group: int | None = None, tie: AffineTie | None = None)[source]
Bases:
objectA single fit parameter.
- class asymmetry.core.fitting.parameters.ParameterSet(params: list[Parameter] | None = None)[source]
Bases:
objectOrdered collection of
Parameterobjects.- link_groups() dict[int, list[Parameter]][source]
Return members of each equality link group, keyed by group id.
Singletons (a group with one member) are dropped — linking one parameter to nothing is a no-op.
- link_main(group: int) Parameter[source]
Return the “main” parameter of a link group.
Mirrors WiMDA: the first member, unless a later member is free (non-fixed) — then the first free member is the main, so the free-fit set always contains the group main.
- link_followers() dict[str, str][source]
Map each non-main linked parameter name to its group main’s name.
- tie_followers() dict[str, AffineTie][source]
Map each affinely-tied parameter name to its
AffineTie.These followers are derived from other parameters at fit time and so drop out of the free set (via
Parameter.is_constrained).
Fit results
- class asymmetry.core.fitting.engine.FitResult(success: bool, chi_squared: float = 0.0, reduced_chi_squared: float = 0.0, parameters: ~asymmetry.core.fitting.parameters.ParameterSet = <factory>, uncertainties: dict[str, float] = <factory>, covariance: ~numpy.ndarray[tuple[int, ...], ~numpy.dtype[~numpy.float64]] | None = None, covariance_parameters: list[str] = <factory>, residuals: ~numpy.ndarray[tuple[int, ...], ~numpy.dtype[~numpy.float64]] | None = None, message: str = '', function_calls: int = 0, gradient_calls: int = 0, hessian_calls: int = 0, edm: float | None = None, covariance_accurate: bool = False, dof: int = 0, minos_errors: dict[str, tuple[float, float]] | None = None, warnings: list[str] = <factory>)[source]
Container for the outcome of a fit.
- parameters: ParameterSet
- dof: int = 0
Degrees of freedom ν = N_data − N_free for this (sub)fit. Used by the χ² quality verdict; 0 means “unknown” and callers fall back to inference.
- minos_errors: dict[str, tuple[float, float]] | None = None
Opt-in MINOS asymmetric 1σ intervals,
{param: (lower, upper)}withlower < 0 < upper(iminuit’s signed offsets).Nonewhen MINOS was not requested or every scan failed. A display-only overlay — the symmetric HESSEuncertaintiesare unchanged and remain the value every downstream surface (trends, export, propagation, promote) consumes.
- warnings: list[str]
Advisory warning messages emitted while fitting (the percent/fraction scale trap and the fixed-frequency σ-inflation trap — see
AsymmetryScaleWarning/FixedFrequencyFieldMismatchWarning). Captured off the Pythonwarningssystem so the GUI can surface them in the fit panel; they are also re-emitted, so stderr/logging and any outercatch_warningsstill observe them exactly as before.
- __init__(success: bool, chi_squared: float = 0.0, reduced_chi_squared: float = 0.0, parameters: ~asymmetry.core.fitting.parameters.ParameterSet = <factory>, uncertainties: dict[str, float] = <factory>, covariance: ~numpy.ndarray[tuple[int, ...], ~numpy.dtype[~numpy.float64]] | None = None, covariance_parameters: list[str] = <factory>, residuals: ~numpy.ndarray[tuple[int, ...], ~numpy.dtype[~numpy.float64]] | None = None, message: str = '', function_calls: int = 0, gradient_calls: int = 0, hessian_calls: int = 0, edm: float | None = None, covariance_accurate: bool = False, dof: int = 0, minos_errors: dict[str, tuple[float, float]] | None = None, warnings: list[str] = <factory>) None
Fit wizards
Single-spectrum fit-wizard helpers:
- class asymmetry.core.fitting.CandidateTemplate(key: str, title: str, category: str, rationale: str, model: CompositeModel, is_current_model_baseline: bool = False)[source]
One curated candidate model considered by the fit wizard.
- model: CompositeModel
- class asymmetry.core.fitting.CandidateAssessment(template: CandidateTemplate, fit_result: FitResult, aic: float, aicc: float | None, bic: float, selected_score: float, residual_rms: float, runs_z_score: float, max_abs_autocorrelation: float, residual_fft_peak_snr: float, residual_gate_passed: bool, residual_gate_reasons: tuple[str, ...], bound_hits: tuple[str, ...], fitted_time: ndarray[tuple[int, ...], dtype[float64]], fitted_curve: ndarray[tuple[int, ...], dtype[float64]], component_curves: tuple[tuple[str, ndarray[tuple[int, ...], dtype[float64]]], ...], repair_attempted: bool = False, stage: int = 2, disqualification_reasons: tuple[str, ...] = (), is_null_baseline: bool = False)[source]
Fit and comparison data for one candidate model.
- template: CandidateTemplate
- stage: int = 2
1 = Stage-1 family representative (cheap first-pass fit), 2 = full assessment.
- Type:
Screening stage that produced this assessment
- disqualification_reasons: tuple[str, ...] = ()
Targeted-disqualifier reasons that make this candidate unfit to recommend even when it wins by metric (frequency at the resolution floor / pinned at a bound, oscillation amplitude consistent with zero). Empty when the candidate is eligible. Additive — old payloads deserialize to
().
- is_null_baseline: bool = False
True for the flat/plain-exponential null baselines fitted unconditionally as a “no significant structure” reference; these never win a normal recommendation and are excluded from the ranked candidate pool.
- __init__(template: CandidateTemplate, fit_result: FitResult, aic: float, aicc: float | None, bic: float, selected_score: float, residual_rms: float, runs_z_score: float, max_abs_autocorrelation: float, residual_fft_peak_snr: float, residual_gate_passed: bool, residual_gate_reasons: tuple[str, ...], bound_hits: tuple[str, ...], fitted_time: ndarray[tuple[int, ...], dtype[float64]], fitted_curve: ndarray[tuple[int, ...], dtype[float64]], component_curves: tuple[tuple[str, ndarray[tuple[int, ...], dtype[float64]]], ...], repair_attempted: bool = False, stage: int = 2, disqualification_reasons: tuple[str, ...] = (), is_null_baseline: bool = False) None
- class asymmetry.core.fitting.FitWizardRecommendation(fingerprint: SpectrumFingerprint, templates: tuple[CandidateTemplate, ...], assessments: tuple[CandidateAssessment, ...], metric: SelectionMetric, recommended_key: str | None, comparable_keys: tuple[str, ...], summary: str, peak_analysis: PeakAnalysis | None = None, multiplet_matches: tuple[MultipletMatch, ...] = (), family_reports: tuple[FamilyScreeningReport, ...] = (), confidence: ConfidenceTier = ConfidenceTier.NONE, verdict: RecommendationVerdict = RecommendationVerdict.NONE, caveat: str = '', scope_note: str = '')[source]
Full fit-wizard analysis payload plus the current recommendation.
- fingerprint: SpectrumFingerprint
- templates: tuple[CandidateTemplate, ...]
- assessments: tuple[CandidateAssessment, ...]
- metric: SelectionMetric
- confidence: ConfidenceTier = 'none'
Confidence tier of
recommended_key(High = gates passed, Medium = metric winner with structured residuals, None = no recommendation). Additive — old payloads default toNONE.
- verdict: RecommendationVerdict = 'none'
Whether the recommendation describes real structure or the data are consistent with a null baseline. Additive — old payloads default to
NONE(they predate the null-baseline test).
- caveat: str = ''
Human-readable caveat carried when
confidenceis Medium (the residual gate reasons for the recommended template) or when the null-baseline verdict fires. Empty for a clean High recommendation. GUI-facing.
- scope_note: str = ''
Human-readable note on how the candidate scope was resolved (run geometry, near-zero-field widening, a fluorine sample-name prior — see
wizard_scope.ScopeResolution.inference_note). Empty when no scope was resolved (scope=Noneinbuild_fit_wizard_recommendation()). Additive — old payloads default to"". GUI-facing.
- property recommended_assessment: CandidateAssessment | None
- sorted_assessments(metric: SelectionMetric | None = None) list[CandidateAssessment][source]
- __init__(fingerprint: SpectrumFingerprint, templates: tuple[CandidateTemplate, ...], assessments: tuple[CandidateAssessment, ...], metric: SelectionMetric, recommended_key: str | None, comparable_keys: tuple[str, ...], summary: str, peak_analysis: PeakAnalysis | None = None, multiplet_matches: tuple[MultipletMatch, ...] = (), family_reports: tuple[FamilyScreeningReport, ...] = (), confidence: ConfidenceTier = ConfidenceTier.NONE, verdict: RecommendationVerdict = RecommendationVerdict.NONE, caveat: str = '', scope_note: str = '') None
- asymmetry.core.fitting.build_fit_wizard_recommendation(dataset: MuonDataset, current_model: CompositeModel | None = None, *, metric: SelectionMetric = SelectionMetric.AICC, scope: WizardScope | None = None, user_frequencies_mhz: Sequence[float] | None = None, max_workers: int | None = None, progress_callback: Callable[[str], None] | None = None, cancel_callback: Callable[[], bool] | None = None) FitWizardRecommendation[source]
Analyze one asymmetry spectrum and recommend a fit candidate.
Tiered screening: every in-scope candidate family gets its cheap Stage-1 representative fitted (fingerprint hints only prioritise — they no longer exclude); families that pass the residual gates, score within
_STAGE1_PROMOTE_DELTAof the best, or are named by a multiplet pattern match expand to their full Stage-2 portfolios.scoperestricts the families physically (Nonescreens the default superset);user_frequencies_mhzadds trusted peak seeds;max_workers=1gives a deterministic serial path.cancel_callbackis polled between and inside fits when running serially or on the thread-pool fallback (engine in-fit abort); when the resolved worker count allows a shared process pool, cancellation instead takes effect only between fits (a cancel_callback cannot cross a process boundary) — either way, cancellation raisesFitCancelledError.
- asymmetry.core.fitting.rerank_fit_wizard_recommendation(recommendation: FitWizardRecommendation, metric: SelectionMetric) FitWizardRecommendation[source]
Apply the recommendation policy against the (already fitted) assessments.
Policy (gates classify, they no longer veto):
Recommend the best-by-metric successful, non-null, non-disqualified candidate. Targeted disqualifiers (frequency at the resolution floor / pinned at a bound, zero-consistent oscillation amplitude) drop a candidate to the next survivor.
Confidence tier: High when the winner’s residuals pass every gate, Medium when it is the clear metric winner but leaves structured residuals (its gate reasons ride along as a
caveat).Null-baseline verdict: if the winner does not beat the better strictly-simpler null baseline by
_NULL_BASELINE_MIN_DELTA_AICC, the verdict becomesNO_SIGNIFICANT_STRUCTUREand the recommendation points at the winning null (fixes pure-noise over-confidence). Tolerates missing nulls (old payloads, the explicit-template path): the test is skipped.
Global fit-wizard helpers:
- class asymmetry.core.fitting.GlobalParameterRecommendation(name: str, recommended_role: str, global_score: float, local_score: float, score_delta: float, total_variation: float, roughness: float, rationale: str)[source]
Recommended role for one parameter in a global fit.
- class asymmetry.core.fitting.GlobalCandidateAssessment(template: CandidateTemplate, fit_results_by_run: dict[int, FitResult], global_parameters: ParameterSet, global_param_names: tuple[str, ...], local_param_names: tuple[str, ...], fixed_param_names: tuple[str, ...], parameter_recommendations: tuple[GlobalParameterRecommendation, ...], run_diagnostics: tuple[RunResidualDiagnostic, ...], series_warnings: tuple[str, ...], aic: float, aicc: float | None, bic: float, selected_score: float, fitted_curves_by_run: dict[int, tuple[ndarray[tuple[int, ...], dtype[float64]], ndarray[tuple[int, ...], dtype[float64]]]], component_curves_by_run: dict[int, tuple[tuple[str, ndarray[tuple[int, ...], dtype[float64]]], ...]], prescreen_only: bool = False, assessment_key: str | None = None)[source]
Fit and comparison data for one global-fit candidate.
- template: CandidateTemplate
- global_parameters: ParameterSet
- parameter_recommendations: tuple[GlobalParameterRecommendation, ...]
- fitted_curves_by_run: dict[int, tuple[ndarray[tuple[int, ...], dtype[float64]], ndarray[tuple[int, ...], dtype[float64]]]]
- component_curves_by_run: dict[int, tuple[tuple[str, ndarray[tuple[int, ...], dtype[float64]]], ...]]
- __init__(template: CandidateTemplate, fit_results_by_run: dict[int, FitResult], global_parameters: ParameterSet, global_param_names: tuple[str, ...], local_param_names: tuple[str, ...], fixed_param_names: tuple[str, ...], parameter_recommendations: tuple[GlobalParameterRecommendation, ...], run_diagnostics: tuple[RunResidualDiagnostic, ...], series_warnings: tuple[str, ...], aic: float, aicc: float | None, bic: float, selected_score: float, fitted_curves_by_run: dict[int, tuple[ndarray[tuple[int, ...], dtype[float64]], ndarray[tuple[int, ...], dtype[float64]]]], component_curves_by_run: dict[int, tuple[tuple[str, ndarray[tuple[int, ...], dtype[float64]]], ...]], prescreen_only: bool = False, assessment_key: str | None = None) None
- class asymmetry.core.fitting.GlobalFitWizardRecommendation(series_axis_key: str, series_axis_label: str, mixed_axes_warning: str | None, fingerprints_by_run: dict[int, SpectrumFingerprint], dataset_order: tuple[int, ...], templates: tuple[CandidateTemplate, ...], assessments: tuple[GlobalCandidateAssessment, ...], metric: SelectionMetric, recommended_key: str | None, comparable_keys: tuple[str, ...], summary: str)[source]
Global-fit analysis payload plus the current recommendation.
- templates: tuple[CandidateTemplate, ...]
- assessments: tuple[GlobalCandidateAssessment, ...]
- metric: SelectionMetric
- property recommended_assessment: GlobalCandidateAssessment | None
- assessments_for_template_key(template_key: str) tuple[GlobalCandidateAssessment, ...][source]
- sorted_assessments(metric: SelectionMetric | None = None) list[GlobalCandidateAssessment][source]
- sorted_prescreen_assessments(metric: SelectionMetric | None = None) list[GlobalCandidateAssessment][source]
- optimized_assessments() tuple[GlobalCandidateAssessment, ...][source]
- sorted_optimized_assessments(metric: SelectionMetric | None = None) list[GlobalCandidateAssessment][source]
- __init__(series_axis_key: str, series_axis_label: str, mixed_axes_warning: str | None, fingerprints_by_run: dict[int, SpectrumFingerprint], dataset_order: tuple[int, ...], templates: tuple[CandidateTemplate, ...], assessments: tuple[GlobalCandidateAssessment, ...], metric: SelectionMetric, recommended_key: str | None, comparable_keys: tuple[str, ...], summary: str) None
- asymmetry.core.fitting.build_global_fit_wizard_recommendation(datasets: list[MuonDataset], current_model: CompositeModel | None = None, *, current_parameter_types: dict[str, str] | None = None, current_values: dict[str, float] | None = None, parameter_bounds: dict[str, tuple[float, float]] | None = None, single_fit_recommendations_by_run: dict[int, FitWizardRecommendation] | None = None, metric: SelectionMetric = SelectionMetric.AICC, progress_callback: Callable[[str], None] | None = None, instrumentation: dict[str, object] | None = None, selected_template_keys: tuple[str, ...] | None = None, scope: WizardScope | None = None, user_frequencies_mhz: Sequence[float] | None = None, search_engine: str | None = None, effort_tier: EffortTier = EffortTier.EXHAUSTIVE, cancel_callback: Callable[[], bool] | None = None) GlobalFitWizardRecommendation[source]
Analyze one ordered dataset series and recommend a global-fit candidate.
effort_tieris the user-facing effort slider (PR 5):LOW,BALANCED,THOROUGH,EXHAUSTIVE(defaultEXHAUSTIVE). As of the PR 5 rework, every tier resolves to the exact bounded-wavefront engine (see_EFFORT_TIER_SEARCH_ENGINE): PR 2’s exact bounds already made the exhaustive path near-minimal and 12-way parallel, so the heuristic Low/ Balanced engines were empirically slower with no fit-count headroom, and the user-facing slider now collapses to one honest mode. The enum and payload are retained so a future scope-based quick-look tier can be added without a schema/UI change.search_engineremains available as a lower-level override for existing callers/tests — when given explicitly it takes precedence overeffort_tierfor engine selection."exhaustive"and"thorough"run the exact bounded wavefront (byte-for-byte the current call);"low"and"balanced"run the retained non-exhaustive heuristic engines (techniques E/F/G/H and the I/J/K knobs), reachable only through this seam for future large-P use and regression coverage — never througheffort_tier.
- asymmetry.core.fitting.rerank_global_fit_wizard_recommendation(recommendation: GlobalFitWizardRecommendation, metric: SelectionMetric) GlobalFitWizardRecommendation[source]
Reuse existing global-fit assessments and recompute the recommendation.