Asymmetry Logo

Contents

  • Getting started
  • Analysis workflows
  • Reference
    • Find a feature
    • Scripting
    • Data and projects
    • Reduction and calibration
    • Setting up and running fits
    • Fit wizards
    • Fit-function library
    • Frequency-domain analysis
    • Trending and downstream models
      • Parameter trending
      • Suggest next point
      • Muonium reaction kinetics at a pulsed source (scripting)
      • Field-dependent transport models (DiffusionLF / BallisticLF)
      • Superconducting penetration depth models
    • Specialised modes
    • Simulation
    • The graphical interface
  • Background and design
  • API reference
  • Contributing
⚠️ Alpha software. Descriptions of µSR data, fitting models, and workflows here may be incomplete or in error — verify the physics against trusted sources.
Asymmetry
  • Reference
  • Parameter trending
  • View page source

Parameter trending

Fit Parameters trending panel showing MgB₂ σ(T) points with the fitted two-gap SC_TwoGap_SS curve

The Fit Parameters trending panel with synthetic MgB₂ σ(T) data (Tc = 36 K) loaded as the σ(T) — MgB₂ series and σ (µs⁻¹) selected on the y-axis. The smooth trend curve is a SC_TwoGap_SS two-gap fit (Model Fit* button), an MgB₂ alpha-model decomposition (small and large gap ratios; Niedermayer et al. Phys. Rev. B 65, 094512, 2002); the fit recovers σ₀ ≈ 1.24 µs⁻¹ and Tc ≈ 35.8 K. The σ(T) → λ(T) inversion that follows from this fit is discussed in Superconducting penetration depth models.

Parameter trending is the second stage of any temperature or field scan analysis. Once a series of runs has been individually (or globally) fitted in the time domain, the extracted parameters themselves carry the physically interesting information: an order parameter as a function of temperature, a relaxation rate as a function of field, a precession frequency tracking magnetic ordering. The parameter-trending framework fits those derived curves with parametric models — power laws, Arrhenius expressions, Redfield denominators, superconducting gap models, transport forms — and reports the resulting physical quantities (critical temperature, activation energy, gap ratios, dimensionality of motion) with uncertainties.

Frequency-domain global fits use the same panel. Peak centres and widths from Fourier spectra appear as nu0 and fwhm in MHz, with derived field equivalents B0 and Bwid for plotting or fitting spectral shifts and broadening directly against field, temperature, or run number.

The trending data model

Everything trendable is a FitSeries. Whatever produced the numbers — a batch of single-run fits, a grouped multi-detector fit, a global cross-fit, a frequency-domain peak fit — the result lands in one container type, stored in the project’s batch collection. A FitSeries holds the per-run fit summaries (Assessing a Fit: success, χ², reduced χ², parameter values and uncertainties) keyed by run, and that is exactly the structure the parameter-trending tools consume. There is no separate “trend object”: to trend a quantity is to read a parameter out of a FitSeries across its runs.

Two distinct paths write into this one container, by design:

  • Results recursion records the per-group rows of a model fit — each fitted group’s parameters become a row, so the series carries a parameter-versus-run (or versus-group) curve ready to trend.

  • The global summary accumulates one row per fit from a cross-group or global fit — the shared (global) parameters of each fit, gathered across a run series.

Both write FitSeries instances into the same collection; they differ only in what each row represents (per-group values versus per-fit global values), not in the container. This is why a grouped fit and a global fit both appear as selectable series in the panel and trend through identical machinery.

The Global Parameter Fit window is a view onto persisted studies, not a third container. Each cross-group fit is a named study (a GlobalFitStudy) kept in a project-level registry, so several can sit side by side — rename, duplicate, or delete them from the window’s studies sidebar, and select one to display it. A study records the group snapshot it was fit against and detects when the live trend data has drifted from it (a stale badge, with a Refit action). The window’s decorations (the per-parameter local model fits and free-floating plot annotations you add in the window) are stored inside the FitSeries it is viewing — keyed by batch id, in the series’ extra — rather than under a separate project key. They therefore travel with the data they annotate and reappear whenever the window shows that fit again, including across a project save/reload, and can no longer be orphaned when the fit is re-run. View preferences (log axes, plot mode, show-components) are not decorations: they are remembered per window, not per fit. Legacy projects that stored decorations under the old window-state key still load, and migrate to the new home on the next save.

Group-bound series and staleness

A run-membered FitSeries produced by a batch or global fit belongs to a Data Browser group: either the group the fit was launched from (via Fit this group…), or a group auto-created from the ad-hoc run selection. The group is the batch vehicle — its membership drives the series, not the other way round. A series’ effective membership is derived live as the owning group’s members minus any runs the series itself has excluded (untick a run in the Batch tab’s member list without removing it from the group), so the same group can carry more than one analysis — a field scan fit with one model and, separately, a subset of it re-fit with another — without duplicating the run collection.

Because membership is live-derived, adding a run to the group or excluding one from the series does not retroactively change already-recorded results: those stay a snapshot of what was actually fit. When the group’s current effective membership no longer matches that snapshot, the series is stale, and its button in the panel’s series row grows a ⚠ with the tooltip “Membership changed since last fit — re-run to refresh.”; re-running the series (from the Batch tab, still bound to the same group) refreshes the snapshot and clears the marker. Re-running a group-bound series always replaces its previous results in place rather than accumulating a second series for the same group and model. A series with no owning group (a legacy analysis, or one kept as a standalone record after its group was deleted) is never stale — its membership is a fixed snapshot, exactly as before this distinction existed. See Project files for the persisted group_id / excluded_run_numbers / last_fitted_members fields and GUI usage for the Data Browser and Batch tab controls that drive them.

How the T / B abscissa is sourced

When a series is recorded, each member run’s temperature and field are stamped into its summary so the trend plots every point at the right coordinate even after the dataset leaves the browser or the project is saved and reopened. That coordinate is the value the Data Browser displays for the run — not, in general, the raw header scalar.

This matters for parked-setpoint series. If a scan was run with the cryostat setpoint left at a single value (e.g. every run logged as 1 K) while the true sample temperature drifted, the header temperature is identical for every run and a temperature trend would collapse onto one abscissa point. Enabling Options → Use temperature from log switches the T column to the logged sample temperature, and the trend X-axis and TSV export now follow: each point is placed at its logged temperature, so a \(T\)-trend (or Arrhenius fit) becomes possible. The field axis behaves the same way under the analogous use field from log option, and per-dataset overrides (set from a single run’s Get Info) are honoured too.

With both toggles off (the default) the abscissa is the header setpoint exactly as before. A run with no recorded temperature or field stays off that axis (plotted as NaN), never planted at 0. Series recorded before this behaviour existed re-plot against the browser’s currently displayed value when their runs are still loaded.

Beyond temperature, field, and run number, the X: selector also offers any fitted parameter (parameter-vs-parameter trending) and any custom data-browser column (Columns: metadata and custom). Custom columns hold free-form text, so when one is the x-axis each value is coerced to a number and runs whose value is empty or non-numeric are dropped, with a note reporting how many were skipped.

Axis transforms

Some of the most common µSR presentations are linearisations of a curved trend: the Redfield analysis plots \(1/\lambda\) against \((\mu_0 H)^2\) so a straight line’s slope and intercept give the fluctuation rate and field width; the Arrhenius analysis plots \(\ln\lambda\) against \(1/T\) so the slope is an activation energy. The collapsible Axis transforms section (below the Y-parameter list) applies such a transform to either axis independently.

The trending panel showing a Redfield linearisation — 1/λ versus B² with a straight-line Linear fit

A Redfield linearisation of a longitudinal-field \(\lambda(B)\) scan: the Y axis transformed to 1/x  (reciprocal) and the X axis to x²  (square) turn the three-regime \(\lambda(B)\) falloff into a straight line, and a Linear model fit on the transformed plateau gives the Redfield slope and intercept. The high-field saturated point is excluded from the trend, so it sits off the line.

Each axis has its own chooser — X: and Y: — offering None, 1/x  (reciprocal), x²  (square), ln x, log₁₀ x, √x and Custom…. Choosing Custom… opens a small Custom X transform / Custom Y transform dialog with one field, Expression in x: (placeholder e.g. 1000/x); the expression is validated live and previewed on a representative data value. The accepted expression then labels the combo item itself, and the last-used custom expression is remembered per axis.

The transform is applied at the point where the panel assembles its data, so it governs the plotted points, the propagated error bars and the trend fit: fitting a Linear model with the axes transformed to \(1/\lambda\) versus \((\mu_0 H)^2\) is the Redfield line, and its slope/intercept are read straight from the model-fit dialog. A point whose transform is undefined (1/0, ln of a non-positive value) is dropped, and the count surfaces on the trend-provenance line beneath the plot (”⚠ 1 dropped by transform”). Changing a transform strands an existing trend fit in the previous coordinate: its curve is hidden and its Model Fit button changes to Model Fit ⚠ until you re-fit under the new axes.

The field axis is labelled in gauss (the loaders’ native field unit), so a transform squares or inverts that unit accordingly (B² (G²)); a dataset held in tesla should be trended via a custom column carrying its own unit.

A transform is distinct from the log axis-scale checkbox next to the X: selector (and the per-parameter log checkbox in the Y-parameter list): those change the axis tick spacing while leaving the numbers alone, whereas the transform changes the plotted values (which is what a straight-line Arrhenius fit needs). To keep the two from compounding, selecting ln x / log₁₀ x on an axis disables that axis’s log checkbox until the transform is cleared.

Note

Arrhenius on a plateau needs the baseline subtracted first. A rate that plateaus at low \(T\) and only activates above some threshold — e.g. a hop rate \(\nu(T) = \nu_0\,e^{-E_a/k_BT} + c\) with a residual \(c\) — is not linearised by the plain ln preset, because \(\ln(\nu_0 e^{-E_a/k_BT}+c)\) is not linear in \(1/T\); fitting it returns an activation energy biased low. Subtract the plateau first with a Custom… expression, e.g. log(x - 0.274) with the plateau value read off the low-\(T\) end, so \(\ln(\nu-c)\) is straight. (The plainer route is to skip the linearisation and fit an Arrhenius model in raw coordinates directly.)

Note

Error bars in transformed space are propagated to first order (\(\sigma_{f(x)} \approx |f'(x)|\,\sigma_x\)) and drawn symmetric. This is standard and accurate when the relative error is small; where it is large (a low-signal point under ln or 1/x) the true interval is asymmetric, so read such error bars as indicative.

Exports under a transform. Export TSV keeps every raw column (the durable record) and appends the transformed columns that match the on-screen axes, with # X transform / # Y transform provenance comments; with several series overlaid it gains a leading Series column and writes every selected series. Export to GLE likewise appends transformed columns (and points its plot and fitted curve at them, so the figure matches the screen), but currently writes only the active series — it warns and names that series; export the others separately, or use TSV.

Overlaying several series

The series buttons above the plot are multi-select: Shift+click a second series to overlay it on the first (the same selection also arms a joint Cross-Group Fitting). Overlaid series are distinguished by colour, with a legend of their names — the natural way to compare, say, \(\sigma(T)\) measured at two applied fields, or the same observable across two samples. When more than one parameter is also selected, each parameter takes a distinct marker shape so colour stays free to encode the series; the twin-axis layout is used only for a single series. Model-fit trend curves are drawn for the active (last-clicked) series — flagged (active) in the legend — which retains ownership of the parameter table, composites, and the model-fit controls.

The trending panel overlaying σ(T) at 400 G and 200 G as two colour-coded series

Two \(\sigma(T)\) series — a 400 G and a 200 G transverse-field scan — overlaid on one axis. Colour encodes the series (legend top-right, the active series flagged), and the 200 G plateau sitting below the 400 G one is the pancake-vortex field dependence of the London second moment.

Representation-aware trending

The Fit Parameters panel automatically tracks which representation is active in the central workspace. The Showing: row at the top of the panel indicates the current representation — F-B Asymmetry, Detector Groups, FFT, or MaxEnt — and the panel’s series buttons below show only the fit series that belong to that representation.

Switching the central workspace view (e.g. from F-B asymmetry to Individual Groups) refreshes the panel instantly to show the grouped-fit series instead of the asymmetry ones. Switching back restores the asymmetry series. Each representation maintains its own independent set of series, x-axis selection, and derived composite parameters, so there is no risk of cross- contamination between a time-domain and a frequency-domain analysis.

When a series button is selected, the Data Browser highlights (in amber) the runs that belong to that series, making it easy to trace which runs contributed to the current trend.

Typical use cases include extracting the ordering temperature \(T_c\) and the critical exponent \(\beta\) of a magnetic order parameter by fitting a spontaneous precession frequency \(\nu(T)\) (or internal field) from a zero-field (ZF) series with an OrderParameter form (see Magnetic Order Parameter below and Temperature scan through a magnetic transition); locating a critical temperature where a relaxation rate diverges with a CriticalDivergence form; inverting the transverse-field (TF) second-moment \(\sigma(T)\) into a London penetration depth using one of the SC_* gap models (Superconductor penetration depth from σ(T) and Superconducting penetration depth models); fitting an Arrhenius form to \(\lambda(T)\) across a temperature scan for motional narrowing; and field sweeps in either longitudinal-field (LF) or TF geometry, where the trending panel handles parameter-vs-\(B\) identically to parameter-vs-\(T\). The GUI panel is the right tool for interactive exploration and quick model selection; exporting to a Python script (the snippets below) is the right tool for reproducible, publication-grade analyses and for unusual parametric models that are not in the built-in registry.

Fitting a trend model

Selecting a model, seeding it sensibly, and deciding which points it should see is the slow, error-prone part of trend analysis: a power law seeded far from the data walks into a bad minimum, and a critical divergence contaminated by the very region it cannot describe reports a meaningless \(T_c\). The model-fit dialog exists to make those decisions visible while you make them. It fits one parametric model through the selected “parameter versus X” points and shows the candidate curve against the data as you set it up, so a converging fit is apparent before you commit to it.

Open it from the Fit Parameters panel: choose the abscissa in the X: selector (temperature, field, run number, angle, a fitted parameter, or a custom logbook column), then click the Model Fit button beside the trended quantity in the Y-parameter list. (The button reads Model Fit* once a fit is active, and becomes a joint Global fit action when two or more group series are selected — see Cross-Group Fitting.) The panel hands the dialog the included trend points for that parameter — a point excluded from the trend (via its context menu or the include checkbox) stays visible on the plot but does not pull the fit — together with their propagated errors and, when the abscissa is a fitted parameter, its per-point x-uncertainty.

The trend model-fit dialog fitting an OrderParameter form to an EuO ν(T) trend

The model-fit dialog fitting the OrderParameter form to a synthetic EuO spontaneous-precession-frequency trend \(\nu(T)\) through the Curie point. The left pane carries the controls — error mode, the range card, the fit-region editor, the seed table and the fit result; the right pane is the live preview, with the candidate curve tracing the in-range points. The converged fit returns \(T_c = 69.2(6)\) K and \(\beta = 0.41(8)\), with a good χ² verdict.

The model catalogue

The dialog offers only the basis models that make sense for the current abscissa: the pool is drawn from component_names_for_x and filtered by each component’s declared scope, so a temperature axis never lists a field-only form and vice versa. The registry (grouped by the context that offers them) is:

  • Any axis (common) — Constant, Linear, Quadratic (\(c_0 + c_1 x + c_2 x^2\), the plain parabola — e.g. a steering-curve minimum), and the general fifth-order Polynomial; PowerLaw and its quadrature-background variant PowerLawQuadBG; and ExponentialDecay.

  • Temperature axis — Arrhenius (thermal activation), OrderParameter (a magnetic order parameter vanishing at \(T_c\)), CriticalDivergence (a rate diverging at \(T_c\)), and the superconducting gap models SC_SWave, SC_DWave, the anisotropic and non-monotonic forms (SC_AnisotropicS_Cos4, SC_NonmonotonicD, SC_PWaveAxial, SC_ExtendedS, SC_SPlusG), the phenomenological SC_AlphaModel, the two-gap SC_TwoGap_SS / SC_TwoGap_SD, and their \(q\)-weighted _Q counterparts. These feed the \(\sigma(T) \to \lambda(T)\) inversion of Superconducting penetration depth models.

  • Field axis — degree-fixed baselines Cubic / Quartic / Quintic / Sextic (each a fixed-order restriction of Polynomial, for resonance backgrounds of increasing curvature); Redfield; the resonance lineshapes Lorentzian, GaussianLCR, LorentzianLCR; the muonium repolarisation curve MuRepolarisation and the exact-diagonalisation RF resonance RFResonanceMuP; the longitudinal-field diffusion and ballistic transport forms DiffusionLF_1D/2D/3D and BallisticLF_1D/2D/3D with the Lambda_bg background (Field-dependent transport models (DiffusionLF / BallisticLF)); and the Brandt vortex-lattice second-moment models SC_Brandt_VortexLattice and its powder average.

  • Angle axis — KnightAnisotropy, AngularCos2, and AngularFourier2 for a Knight shift mapped out by rotating a single crystal (Knight shift).

Any of these can be combined into a composite via Edit Model, which opens the same two-panel builder used for time-domain functions (see Build a Parameter Composite Model and the quadrature combinator ⊕). The dialog picks a sensible default for a fresh range — Linear in general, but OrderParameter automatically when the trended quantity is a magnetic order-parameter observable (a precession frequency or internal field) versus temperature, so the common critical-behaviour fit converges out of the box.

The live preview

The right-hand pane redraws as you edit: the data points (with error bars scaled to the current error mode), the in-range points against the greyed-out excluded ones, and the candidate model curve. The curve is sampled off the GUI thread — debounced, with stale results discarded — so dragging an edge or retyping a seed never freezes the dialog. The Show residuals toggle adds a \((\text{data} - \text{model})/\sigma\) strip with a \(\pm 1\sigma\) guide band beneath the plot. On a narrow window the preview auto-collapses behind a Show preview toggle so the controls stay usable.

The interactive fit region

Every fit is restricted to a fit region — the set of x-intervals a point must fall inside to enter the fit. A range is defined by a range card (a colour swatch matching its span on the plot, a χ²ᵣ status chip, and its compact bounds) with Run Fit and Edit Model actions; Add Range adds another. The region is sculpted, not assembled: it starts as one interval spanning the data and you carve gaps out of it with Exclude region… (or by right-dragging across the plot), leaving a list of included intervals shown in the Selected range editor. This is the natural way to exclude a contaminated critical region — fit CriticalDivergence on both sides of a transition while dropping the points nearest \(T_c\) — while keeping one consistent model across the gap (the fitted curve is drawn continuously through it). Directly on the preview you can drag an interval edge, drag on empty space to add a new range, click a span to select it, and right-drag to exclude a region.

Adding a second range is different from excluding a gap: separate ranges fit independent models to different parts of the abscissa (piecewise modelling), whereas the intervals of one range OR-combine into the mask of a single fit. Internally the two-or-more-interval case is stored as fit windows and the single-interval case as a plain range, so a fit region that carves down to one interval collapses back automatically and saved projects are unaffected.

Seeding and running

The seed (starting-value) table lists each model parameter with its start value, bounds and a Fixed toggle; you can also type bounds to constrain the fit. Starting values are chosen data-aware where it matters: the critical-temperature components take \(T_c\) and amplitude seeds derived from the actual x/y data, so an OrderParameter default lands close to the transition rather than at a generic guess. Guess seeds re-derives data-aware starts for the selected range’s free parameters on demand (fixed parameters are never touched), running off the GUI thread. The fit itself is a bounded least-squares minimisation via iminuit, and the dialog runs it from four extra deterministic starts in addition to your own seed, keeping the run reproducible while making a bad local minimum much less likely; when a parameter converges hard against a bound, or a data-aware restart beats your seed, the dialog says so inline.

The Errors selector (Weighting and Error Modes) controls how each point is weighted, and after a successful fit the result box tints green and reports \(\chi^2\), \(\chi^2_r\), and the two-sided quality verdict (χ² Quality Verdict); the same verdict appears as the range card’s status chip.

Where the fit lands

Accepting the dialog with OK writes the fit back into the panel: the fitted curve overlays the trend plot (and any GLE/TSV export of it), the fitted parameters and uncertainties become readable, and the whole fit is stored with the project’s trend series — the dialog remembers the last-used model per (parameter, x-axis) within that project, so reopening it restores your choice. The fit’s own outputs are also recorded as a new, trendable results series (Model fit (single) · <parameter> vs <x>): each fit range contributes one row of its fitted parameters and \(\chi^2_r\), indexed by the centre of its fitted x-window, so a multi-range fit is immediately trendable in turn (see Recursive Trending (Model-Fit Results as a Series)). Remove Fit discards the fit and drops that series.

For jointly fitting several detector groups or samples with shared and local parameters, the panel routes two or more selected group series to a cross-group variant of the same dialog — same catalogue, live preview, error modes, and fit region, described under Cross-Group Fitting.

Trending one parameter against another

The x-axis need not be a run-level quantity. Below the fixed Auto / B / T / Run entries, the X axis selector lists every fitted parameter in the active series, so any parameter can be trended against any other — for example a relaxation rate \(\lambda\) against a precession frequency \(\nu\), both extracted per run. Internally the choice is the key param:<name>; the core fit functions are x-agnostic, so no API change is needed — pass the chosen parameter’s values as x:

result = fit_parameter_model(nu_values, lambda_values, lambda_errors,
                             model, params)

When the x-axis is a fitted parameter the field/temperature scope no longer applies (the abscissa is no longer a sample condition), so the component picker degrades to the common basis set — the same components offered for a run-index x. Field- and temperature-specific forms (penetration-depth gap models, order parameters) are hidden, because they describe a dependence on a physical control variable, not on another fitted quantity.

When to use this. Reach for parameter-vs-parameter trending when you suspect a relationship between two fitted quantities rather than a dependence on the external control — correlating a rate with an amplitude across a series, or plotting an internal field against a measured frequency to check a linear gyromagnetic relation. For the ordinary “property versus temperature or field” analysis, keep the B / T axes.

Accounting for x uncertainty

When the abscissa is a fitted parameter it carries its own per-point uncertainty, which an ordinary least-squares fit (x exact) ignores. Tick Account for x uncertainty in the model-fit dialog — offered only for a parameter-vs-parameter x — to weight the fit by the effective variance

\[\sigma^2_{\mathrm{eff},i} = \sigma_{y,i}^2 + \left(\left.\frac{\partial f}{\partial x}\right|_{x_i}\right)^2 \sigma_{x,i}^2 ,\]

the first-order errors-in-variables treatment of Orear and York. The slope is evaluated by a central finite difference, so it needs no analytic derivative, and because the minimiser re-evaluates the weighting at every step it is self-consistent at the minimum with no outer iteration. With \(\sigma_x = 0\) it reduces exactly to ordinary least squares, so the toggle is off by default and never changes a field/temperature/run fit. Programmatically, pass xerr:

result = fit_parameter_model(nu_values, lambda_values, lambda_errors,
                             model, params, xerr=nu_errors)

The trend plot draws horizontal error bars from \(\sigma_x\) whenever the x-axis is a fitted parameter, independent of the toggle, so the spread is visible even when the fit treats x as exact.

When to use this. Switch it on when the x-parameter’s relative error is not small compared with the y-error divided by the local slope; otherwise the correction is negligible and the simpler exact-x fit suffices. The estimator is a first-order approximation: it inflates the parameter errors (correctly) and shifts the estimates slightly — the intended errors-in-variables behaviour.

Note

WiMDA’s Model layer treats the x-column as exact and has no x-error concept; the effective-variance option is an Asymmetry addition for physical correctness in parameter-vs-parameter fits. Total least squares (orthogonal distance regression) was considered and rejected: it offers no box constraints, and Asymmetry’s parameter models rely on bounds.

Available basis components

from asymmetry.core.fitting import component_names_for_x

print(component_names_for_x("field"))
print(component_names_for_x("temperature"))

Build a parameter composite model

In the GUI parameter-trending workflow, the Edit Model action opens the same two-panel builder used for time-domain composite functions: a searchable basis-model library on the left and the model as structured, reorderable rows on the right. Each basis model becomes a single row, joined by +, -, *, /, or the quadrature combinator ⊕ (below); parentheses group sub-expressions. The library’s ⓘ Info button reports the documentation for the currently selected basis model, including superconducting gap-model details when relevant. Fraction groups are a time-domain-only concept and are not offered here — ⊕ is the parameter-grammar’s dedicated combinator for sharing a width-like budget between terms.

The builder validates the expression in real time, and the Model Fit dialog shows the expanded y(x) preview above the parameter table as you edit.

from asymmetry.core.fitting import ParameterCompositeModel

model = ParameterCompositeModel(
    component_names=["DiffusionLF_2D", "Redfield", "Lambda_bg"],
    operators=["+", "+"],
)
print(model.formula_string())

Grouped expressions are also supported programmatically and in the GUI, for example Linear + (Arrhenius * Constant).

Quadrature combinator (⊕)

The parameter-vs-x builder adds one operator beyond ordinary arithmetic: the quadrature combinator

\[f \oplus g = \sqrt{f^2 + g^2},\]

which composes two components as the square root of the sum of their squares. It is the natural composition rule for width-like quantities — relaxation rates, linewidths, second moments — where independent broadening channels add in quadrature rather than linearly. ⊕ is binary, commutative, and associative (a ⊕ b ⊕ c \(= \sqrt{a^2 + b^2 + c^2}\)); it binds at the same precedence as + and -, so a ⊕ b * c evaluates the product first and a ⊕ b + c reads as \(\sqrt{a^2 + b^2} + c\). Use parentheses to override the default grouping.

from asymmetry.core.fitting import ParameterCompositeModel

model = ParameterCompositeModel.from_expression("PowerLaw ⊕ Constant")
print(model.formula_string())

The identity PowerLaw ⊕ Constant \(\equiv\) PowerLawQuadBG is exact — the operator generalises the fixed quadrature-background component to any pair of basis models. The parameter mapping is direct: the composite’s dedicated parameters are a, n, c_1 (the power law’s own additive constant) and c_2 (the constant term), so the monolith’s BG maps to c_2 while c_1 must be fixed at 0 for the equivalence to hold — the PowerLawQuadBG inner term carries no additive offset.

When to use this. Reach for ⊕ whenever a fitted quantity is the quadrature sum of two contributions — most often a signal of interest riding on an incoherent background floor (a residual linewidth, an instrumental second moment). When the background is instead a genuine additive offset of the observable, use + with a Constant.

Note

⊕ is a parameter-grammar operator only; the time-domain composite function builder does not offer it (quadrature of two time-domain muon components has no established meaning). In displayed formulae and GLE exports the operator is shown as the ⊕ glyph rather than expanded to a square root. WiMDA has no general quadrature operator — only the fixed PowerLawQuadBG-style model — so this is an Asymmetry generalisation.

Single-Series Fit

import numpy as np
from asymmetry.core.fitting import Parameter, ParameterSet, fit_parameter_model

field = np.array([20.0, 50.0, 100.0, 200.0, 400.0, 800.0])
values = np.array([2.7, 2.2, 1.6, 1.1, 0.7, 0.5])
errors = np.full_like(values, 0.1)

params = ParameterSet([
    Parameter("A", value=3.0, min=0.0),
    Parameter("D_2D", value=0.7, min=0.0),
    Parameter("D_perp", value=0.0, min=0.0),
    Parameter("D", value=10.0, min=0.0),
    Parameter("nu", value=10.0, min=0.0),
    Parameter("m", value=2.0, min=0.0),
    Parameter("lambda_BG", value=0.0, min=0.0),
])

result = fit_parameter_model(field, values, errors, model, params)
print(result.success, result.reduced_chi_squared)

Weighting and error modes

By default a model fit weights each point by the propagated error of the trended parameter (Column mode). The Errors selector in the model-fit dialog (and the error_mode argument of fit_parameter_model) offers four alternatives for when those errors are absent or untrustworthy:

  • Percent of y — \(\sigma_i = (p/100)\,|y_i|\). Points with \(y_i = 0\) carry no error information and are excluded from the fit.

  • Absolute — one constant \(\sigma\) for every point.

  • None — unit weights. The fitted parameters are the unweighted least-squares solution; \(\chi^2\) loses its absolute meaning.

  • Estimate from scatter — an unweighted fit whose parameter uncertainties are rescaled by \(\sqrt{\chi^2/\nu}\) afterwards. This is the standard way to quote errors when the only information about the noise is the scatter of the points themselves. It is exactly the converged limit of WiMDA’s Estimate mode (which rescales its constant \(\sigma\) by \(\sqrt{\chi^2_r}\) after each fit until \(\chi^2_r = 1\)): because a uniform error rescale never moves the minimum, the iteration lands on the same answer in one step.

In Column mode (only), errors are floored at half the median positive error before fitting, so a single near-zero propagated error cannot dominate the fit; explicit Percent/Absolute choices are honoured verbatim. With unit weights or scatter-estimated errors, \(\chi^2_r\) is forced toward 1 by construction and carries no goodness-of-fit information — the quality verdict (below) is suppressed in those modes.

result = fit_parameter_model(
    field, values, None, model, params, error_mode="scatter"
)

Fit windows (union multi-range)

A fit range may be restricted to a union of (min, max) windows: a point enters the fit if it falls in any window, and a single model is fitted across all of them. Use + Window on a range row in the model-fit dialog, or pass windows to fit_parameter_model. The fitted curve is drawn continuously through the excluded gaps.

The canonical use is excluding a critical region: a relaxation rate \(\lambda(T)\) diverging at \(T_c\) is well described by CriticalDivergence on both sides of the transition, but the points nearest \(T_c\) are dominated by physics the power law does not capture (and by the singularity itself). Fitting the union \([T_{\min}, T_c - \delta] \cup [T_c + \delta, T_{\max}]\) keeps one consistent model with the contaminated region excluded:

result = fit_parameter_model(
    temperature, rate, rate_errors, model, params,
    windows=[(40.0, 64.0), (74.0, 100.0)],
)

This differs from adding a second range in the dialog: separate ranges fit independent models (piecewise modelling), whereas windows OR-combine into the mask of one model.

χ² quality verdict

After a successful fit the dialog reports a quality verdict alongside \(\chi^2_r\). For a correct model with correct error bars, \(\chi^2\) follows the chi-squared distribution with \(\nu = N - N_{\mathrm{free}}\) degrees of freedom, so at 95 % confidence a good fit’s \(\chi^2_r\) falls inside a band around 1 that tightens as \(\nu\) grows (for \(\nu = 10\) the band is 0.32–2.05; for \(\nu = 50\), 0.65–1.43). The verdict is two-sided:

  • good — \(\chi^2_r\) inside the band.

  • poor — above the band: the model misses real structure, or the errors are underestimated.

  • overdone — below the band: the fit reproduces the data better than the errors allow, which usually means overestimated errors or too many free parameters soaking up noise.

The verdict assumes real (Column-mode) errors and is suppressed for unit-weight and scatter-estimated fits. Programmatic access is via asymmetry.core.fitting.assess_fit_quality(chi_squared, dof), which returns the verdict and the band.

Cross-group fitting

For jointly fitting multiple groups with shared and local parameters, use global_fit_parameter_model with ParameterGroupData entries.

from asymmetry.core.fitting.parameter_models import (
    ParameterGroupData,
    global_fit_parameter_model,
)

groups = [
    ParameterGroupData(
        group_id="g1",
        group_name="Sample A",
        x=field,
        y=values,
        yerr=errors,
        group_variable_value=5.0,
    ),
    ParameterGroupData(
        group_id="g2",
        group_name="Sample B",
        x=field,
        y=values * 1.1,
        yerr=errors,
        group_variable_value=15.0,
    ),
]

result = global_fit_parameter_model(
    groups=groups,
    model=model,
    global_params=["D_2D", "D", "nu", "m"],
    local_params=["A", "lambda_BG"],
    fixed_params={"D_perp": 0.0},
)
print(result.success)

The cross-group fit honours the same error modes and fit windows as a single-series fit. The cross-group dialog now shows the Errors selector and + Window controls, and global_fit_parameter_model accepts the matching error_mode, error_value, and windows arguments; the chosen mode and window union apply to every group. In Estimate from scatter mode the \(\sqrt{\chi^2/\nu}\) rescale is applied to both the shared global uncertainties and every group’s local uncertainties.

result = global_fit_parameter_model(
    groups=groups, model=model,
    global_params=["m"], local_params=["b"], fixed_params={},
    error_mode="scatter", windows=[(0.0, 40.0), (60.0, 100.0)],
)

When the cross-group abscissa is itself a fitted parameter, the same effective-variance x-uncertainty treatment used for single-series fits (Accounting for x Uncertainty) is available here too. Tick Account for x uncertainty in the cross-group dialog, or pass per-group \(\sigma_x\) arrays as xerr (keyed by group_id); each group’s points are weighted by \(\sigma_{y}^2 + (\partial f/\partial x)^2\,\sigma_x^2\) with the same estimator as the single-series path. It is off by default, reduces exactly to ordinary least squares when \(\sigma_x = 0\), and is ignored under the None / Estimate from scatter modes (whose unit weights carry no scale to combine with \(\sigma_x\)).

result = global_fit_parameter_model(
    groups=groups, model=model,
    global_params=["m"], local_params=["b"], fixed_params={},
    xerr={"g1": nu_errors_g1, "g2": nu_errors_g2},
)

Recursive trending (model-fit results as a series)

A successful cross-group fit is itself recorded as a new trend series, named Model fit: <parameter> vs <x>, alongside the original fit series in the Fit Parameters panel. Each selected group contributes one row carrying that group’s local parameters (with the shared global parameters as constant columns), indexed by the group’s orthogonal coordinate — temperature for a field fit, and vice versa; a final globals row carries the shared parameters and \(\chi^2_r\). Because this results series is an ordinary trend series, it can be trended again — fit the per-group local rate against temperature, for instance — so the analysis recurses naturally. This supersedes WiMDA’s separate second-level Model Fit Table, which only stored the rows for inspection.

Re-running the same cross-group fit (same parameter, x-axis, and group set) replaces its results series rather than accumulating duplicates.

Single-fit ranges. A single-series fit is recorded the same way: each fit range contributes one row to a Model fit (single): <parameter> vs <x> series, carrying that range’s fitted parameters and \(\chi^2_r\), indexed by the centre of the range’s fitted x-window (in the trend’s own x-units, also kept as a range_center column). A multi-range fit — for example one model below a transition and another above — is therefore immediately trendable: plot a fitted coefficient against the window centre to see how it drifts across the data.

Trending shared globals across fits. Every successful cross-group fit also appends one row to a persistent Global summary series, carrying that fit’s shared global parameters, \(\chi^2_r\), and a monotonic fit_index. Because the globals from successive fits accumulate in one series, they can be trended against each other or against fit_index — the natural way to follow a shared parameter as you sweep a model choice, a fixed value, or a group set across a sequence of joint fits. Re-running a given fit updates its row in place (keyed by the same parameter / x-axis / group set), so the series records one row per distinct fit. These rows sit off the physical field/temperature axes; select fit_index or a global parameter as the x-axis (see Trending One Parameter Against Another) to trend them.

Magnetic order parameter

For a second-order magnetic transition, the spontaneous muon precession frequency, the internal field, or an ordered-moment-like asymmetry follows an order-parameter temperature dependence that rises continuously from zero at the ordering temperature \(T_c\) to a saturated value \(y_0\) at \(T = 0\). The OrderParameter basis model captures this with a generalised two-exponent form:

\[y(T) = y_0 \left[1 - \left(\frac{T}{T_c}\right)^{\alpha}\right]^{\beta} \quad (0 \le T < T_c), \qquad y(T) = 0 \quad (T \ge T_c).\]

Here \(y_0\) carries the unit of the trended observable (MHz for \(\nu(T)\), G or T for an internal field, % for an asymmetry), \(\beta\) is the critical exponent that dominates the near-\(T_c\) shape (typically 0.33–0.37 for 3D Heisenberg/Ising magnets, 0.5 for mean field), and the shape exponent \(\alpha\) controls the departure from a pure power law away from \(T_c\). Fixing \(\alpha = 1\) recovers the simple near-\(T_c\) power law \(y_0 (1 - T/T_c)^{\beta}\). The model is exactly zero above \(T_c\), so include the ordered-phase points and let \(T_c\) fall inside the fitted range. Use CriticalDivergence instead for quantities that diverge at \(T_c\) (such as a relaxation rate), not for an order parameter that vanishes there.

import numpy as np
from asymmetry.core.fitting import (
    Parameter,
    ParameterSet,
    ParameterCompositeModel,
    fit_parameter_model,
)

# Spontaneous precession frequency nu(T) through Tc.
temperature = np.array([1.5, 20.0, 40.0, 55.0, 62.0, 66.0, 68.0])
nu = np.array([29.9, 29.1, 26.0, 20.6, 15.4, 9.6, 5.7])
errors = np.full_like(nu, 0.3)

model = ParameterCompositeModel(["OrderParameter"], operators=[])
params = ParameterSet([
    Parameter("y0", value=30.0, min=0.0),
    Parameter("Tc", value=69.0, min=0.0),
    Parameter("beta", value=0.36, min=0.0),
    Parameter("alpha", value=1.0, min=0.0),  # fix to fit the near-Tc power law
])

result = fit_parameter_model(temperature, nu, errors, model, params)
print(result.success, result.reduced_chi_squared)
for p in result.parameters:
    print(p.name, p.value, result.uncertainties.get(p.name))

Polynomial trends

Polynomial fits an empirical trend or background up to fifth order,

\[y(x) = c_0 + c_1 x + c_2 x^2 + c_3 x^3 + c_4 x^4 + c_5 x^5,\]

with all six coefficients available as fit parameters. Fix the unused high-order coefficients at 0 to fit lower orders — for a quadratic background, free \(c_0\)–\(c_2\) and fix \(c_3\)–\(c_5\). Use it when no physical model is available, or as the smooth background under resonance lineshapes (see the LCR recipe below). Two cautions apply to any polynomial fit: the coefficients are only meaningful inside the fitted window (polynomials extrapolate badly), and the order should be the lowest the residuals support — a freed high-order term that the data does not constrain will soak up noise and inflate the uncertainties of every other coefficient.

Power law with quadrature background

PowerLawQuadBG combines a power law with a constant background in quadrature,

\[y(x) = \sqrt{\left(a\,|x|^{n}\right)^2 + \mathrm{BG}^2},\]

which is the natural composition rule for width-like quantities — relaxation rates, linewidths, second moments — where independent broadening channels add as squares. At small \(x\) the curve saturates smoothly at \(\mathrm{BG}\) instead of falling linearly onto an offset; at large \(x\) it approaches the bare power law. Use the plain PowerLaw (with its additive constant) when the background is a genuine offset of the observable itself rather than an independent broadening channel.

This fixed component is the special case PowerLaw ⊕ Constant of the general quadrature combinator (above); reach for ⊕ when either side of the quadrature sum is a richer model than a bare power law or constant.

Muonium repolarisation

MuRepolarisation measures a muonium hyperfine constant from a longitudinal-field scan, without resolving any precession. In an applied field \(B\) along the initial muon polarisation, the only muonium transition that mixes the muon spin states is suppressed as the field decouples the muon and electron spins; time-averaging the unresolved fast oscillation leaves the repolarisation curve

\[y(B) = a_{\mathrm{Mu}}\, \frac{\tfrac{1}{2} + (B/B_0)^2}{1 + (B/B_0)^2} + a_{\mathrm{Dia}}, \qquad B_0 = \frac{A_{\mathrm{hf}}}{\gamma_e + \gamma_\mu},\]

rising from half the muonium amplitude at \(B = 0\) (the other half is lost to the unobserved oscillation) to the full amplitude once \(B \gg B_0\), on top of a field-independent diamagnetic baseline \(a_{\mathrm{Dia}}\). The component is parameterised directly by the hyperfine constant \(A_{\mathrm{hf}}\) (MHz) — the quantity the experiment is designed to extract — with \(B_0\) derived internally from CODATA gyromagnetic ratios. For vacuum muonium (\(A_{\mathrm{hf}} = 4463\;\mathrm{MHz}\)), \(B_0 \approx 1585\) G, and the curve reaches three quarters of the muonium amplitude at exactly \(B = B_0\).

Fit it to an initial-asymmetry or integral-asymmetry LF scan (built with the integral-asymmetry observable, Integral scan mode (avoided-level-crossing field scans), x-axis in G). It is the standard method when the hyperfine coupling is too large for the precession to be resolved directly. The model assumes an isotropic (vacuum-like) hyperfine interaction observed in time average: anisotropic muonium, rapid chemical reaction, or spin exchange distort the curve, and any missing fraction appears as a reduced \(a_{\mathrm{Mu}}\).

import numpy as np
from asymmetry.core.fitting import (
    Parameter,
    ParameterSet,
    ParameterCompositeModel,
    fit_parameter_model,
)

field_G = np.array([0.0, 50.0, 150.0, 400.0, 1000.0, 2500.0, 6000.0, 15000.0])
asym = np.array([12.6, 12.7, 13.1, 14.2, 16.4, 19.0, 21.0, 21.9])
errors = np.full_like(asym, 0.2)

model = ParameterCompositeModel(["MuRepolarisation"])
params = ParameterSet([
    Parameter("a_Mu", value=10.0),
    Parameter("A_hf", value=4000.0, min=1.0),
    Parameter("a_Dia", value=5.0),
])

result = fit_parameter_model(field_G, asym, errors, model, params)
print({p.name: p.value for p in result.parameters})

References

  1. S. J. Blundell, R. De Renzi, T. Lancaster, and F. L. Pratt, Muon Spectroscopy: An Introduction (Oxford University Press, Oxford, 2022).

      1. Patterson, Rev. Mod. Phys. 60, 69 (1988).

    1. Orear, Am. J. Phys. 50, 912 (1982).

  2. D. York, N. M. Evensen, M. L. Martínez, and J. De Basabe Delgado, Am. J. Phys. 72, 367 (2004).

RF-μSR resonance (muon + electron + proton)

RFResonanceMuP fits a field-swept RF-μSR resonance scan of a muoniated radical — muon plus electron plus one dominant proton, such as the cyclohexadienyl radical C₆H₆Mu formed in benzene — and extracts the muon and proton hyperfine couplings \(A_\mu\) and \(A_p\) simultaneously. This is the Asymmetry counterpart of WiMDA’s RigiWorkshopFit exact-diagonalisation RF model.

At a fixed applied RF frequency \(\nu_{\mathrm{RF}}\), the static field is swept and the (Red − Green) integral asymmetry shows two resonance features. A resonance occurs where an RF-driven muon-spin-flip transition of the three-spin Hamiltonian

\[H = A_\mu\,\mathbf{S}_e\!\cdot\!\mathbf{S}_\mu + A_p\,\mathbf{S}_e\!\cdot\!\mathbf{S}_p + (\gamma_e S_{e,z} - \gamma_\mu S_{\mu,z} - \gamma_p S_{p,z})\,B\]

matches \(\nu_{\mathrm{RF}}\). The two resonance fields \(B_1, B_2\) are found by exact diagonalisation of the 8×8 Hamiltonian (the high-field-limit linear relation \(B_\mathrm{res} = (\nu_{\mathrm{RF}} \pm \tfrac{1}{2}A_\mu)/\gamma_\mu\) is inaccurate at the few-hundred-G fields these scans use), and the model is two Lorentzians on a flat background:

\[y(B) = \mathrm{BG} + \sum_{i=1,2} \mathrm{ampl}_i\, \frac{\mathrm{wid}_i^2}{\mathrm{wid}_i^2 + (B - B_i)^2}.\]

The resonance mean tracks \(A_\mu\) and the splitting tracks \(A_p\). Set the amplitudes negative to fit resonance dips (the usual Red − Green observable); hold \(\nu_{\mathrm{RF}}\) fixed at the applied frequency and give \(A_\mu, A_p\) starting values near the expected couplings — the resonance condition is nonlinear, so the field-swept curve only constrains the fit once the trial resonances fall inside the scanned window. \(A_\mu\) (the mean) is well determined; \(A_p\) (the splitting) is the weaker axis and benefits from a complementary avoided-level-crossing (ALC) measurement.

import numpy as np
from asymmetry.core.fitting import (
    Parameter,
    ParameterSet,
    ParameterCompositeModel,
    fit_parameter_model,
)

field_G = np.array([580.0, 700.0, 780.0, 820.0, 870.0, 950.0, 1060.0])
asym = np.array([-1.6, -3.0, -16.0, -10.5, -17.0, -3.2, -1.5])  # Red-Green ×10⁻³
errors = np.full_like(asym, 0.3)

model = ParameterCompositeModel(["RFResonanceMuP"])
params = ParameterSet([
    Parameter("A_mu", value=515.0, min=300.0, max=700.0),
    Parameter("A_p", value=124.0, min=40.0, max=250.0),
    Parameter("nu_RF", value=218.5, fixed=True),
    Parameter("ampl1", value=-18.0), Parameter("wid1", value=25.0, min=1.0),
    Parameter("ampl2", value=-18.0), Parameter("wid2", value=25.0, min=1.0),
    Parameter("BG", value=-1.5),
])

result = fit_parameter_model(field_G, asym, errors, model, params)
print({p.name: p.value for p in result.parameters})

References

  1. I. McKenzie, R. Scheuermann, S. P. Cottrell, J. S. Lord, and I. M. Tucker, J. Phys. Chem. B 117, 13614 (2013).

  2. E. Roduner, The Positive Muon as a Probe in Free Radical Chemistry, Lecture Notes in Chemistry Vol. 40 (Springer, Berlin, 1988).

Migrating WiMDA model functions

Every function in WiMDA’s Model-layer library (“Standard fit models”) has a direct counterpart or a composite recipe. Parameter names map as follows:

WiMDA function

Asymmetry equivalent

Notes

Polynomial fit up to fifth order

Polynomial

Identical (coefficients c₀–c₅).

Power law

PowerLaw

Identical on \(x > 0\); Asymmetry evaluates \(|x|^n\) so negative-\(x\) points cannot fault the fit.

Power law (BG quad)

PowerLawQuadBG

Identical.

2 Lorentzians + cubic BG

LorentzianLCR + LorentzianLCR + Polynomial

Peak terms map exactly (Ampl → f, Pos → B₀, Wid → B_wid; fix \(c_4, c_5\) at 0 for the cubic). Background coefficients do not transfer: WiMDA’s cubic is in powers of \((x - \mathrm{Pos}\,1)\), Asymmetry’s in absolute \(x\) — refit rather than copying values.

Thermal activation (2 component)

Arrhenius + Arrhenius

WiMDA uses eV, Asymmetry meV: \(E_a[\mathrm{meV}] = 1000 \times E_a[\mathrm{eV}]\). WiMDA’s hard-coded \(e/k_B\) is 0.089 % below the CODATA value, so refitted activation energies are expected to come out very slightly higher than WiMDA’s.

Internal field vs T for ordered magnet

OrderParameter + Constant

Identical on the physical domain, including the clamp to the background value above \(T_c\) (B₀ → y₀, B_bg → the Constant).

Divergence of relaxation rate

CriticalDivergence

Identical away from \(T = T_c\) (Tc → Tc, alpha → ν, Min rate → c, scaling → a). Exclude the critical region from the fit window rather than relying on the singular point.

Repolarisation of isotropic Mu

MuRepolarisation

Same curve; WiMDA fits \(B_0\) in G, Asymmetry fits \(A_{\mathrm{hf}}\) in MHz with \(B_0 = A_{\mathrm{hf}}/(\gamma_e + \gamma_\mu)\) derived (\(A_{\mathrm{hf}}[\mathrm{MHz}] = 2.816 \times B_0[\mathrm{G}]\)).

RF resonance Mu+p (exact diag.)

RFResonanceMuP

Port of RigiWorkshopFit’s RFresonanceMuPlusProtonExact (the analytic RFresonanceMuPlusProton variant is intentionally not a separate component — it is inaccurate at low field). Parameters map A → A_mu, Ap → A_p, RF → nu_RF, ampl1/wid1/ampl2/wid2/BG identical. WiMDA’s bespoke Eigenuni.pas Hermitian eigensolver is replaced by numpy.linalg.eigvalsh() (basis-independent spectrum, identical level differences).

Composite parameters in the Fit Parameters panel

In the GUI Fit Parameters panel, you can define a derived parameter from existing fitted parameters using Create Composite Parameter.

After selecting a derived parameter in the Y-parameter list, you can also use Edit Selected Composite and Remove Selected Composite to manage saved definitions.

The expression builder supports:

  • Arithmetic: +, -, *, /, ^

  • Parentheses and numeric constants

  • Built-in constants: pi, e

  • Built-in functions: sin, cos, tan, asin, acos, atan, sinh, cosh, tanh, exp, log, log10, sqrt, abs

Expressions are parsed safely (no arbitrary code execution) and validated in real time against available fitted parameter names.

Derived parameters are integrated into the same workflow as ordinary fit parameters:

  • They appear in the Y-parameter selector and fitted-parameter table.

  • They can be plotted and used with parameter-model fitting.

  • They are saved/restored with project state.

  • They are recomputed automatically when source fit parameters change.

Uncertainties are propagated using first-order error propagation:

\[\sigma_f^2 = J\,\Sigma\,J^T\]

where \(J\) is the gradient of the expression with respect to source parameters and \(\Sigma\) is the parameter covariance matrix when available (falling back to diagonal variances otherwise).

Knight shift

A muon in a metal or paramagnet precesses not at the bare Larmor frequency of the applied field but at a frequency shifted by the local hyperfine field — the field-induced polarisation of the conduction electrons (Fermi contact) together with the dipolar and RKKY-mediated contact fields of any local moments. That fractional shift is the muon Knight shift, the muon analogue of the NMR Knight shift and a direct, site-resolved probe of the local magnetic response. The panel converts a fitted precession frequency \(\nu\) to

\[K = \frac{\nu - \nu_{\mathrm{ref}}}{\nu_{\mathrm{ref}}}\]

against one of two references.

The Knight shift analysis window is the recommended way to set this up: it is a non-modal window dedicated to the conversion, so the reference, unit, and component choices, the resulting branches, and the scan’s crossings are all visible together while you edit them, and nothing is written back to the trend table until you ask for it. Analysis → Knight shift analysis… is the unconditional entry point, always available regardless of the active series. The Knight shift window… shortcut in the Derived parameters section of the Fit Parameters panel is narrower: it appears only when the active series’ fitted model has at least one Knight-convertible component (a local precession frequency or field, as opposed to an applied-field muonium term) — opening it on an unrelated fit would have nothing to convert. Its sidebar reads top to bottom as the pipeline:

  • Source — the fitted series supplying the frequencies (run count, component count, and scan axis), with a Refresh from trend button that rebuilds the snapshot from the trend panel’s current rows after a refit or a series change.

  • Conversion — the reference (Applied field (γ_µ·B) or Designated component, with a combo box for the chosen component when the latter is selected), the display Unit (Auto (ppm / %), ppm, percent, or fraction), a checkbox per convertible component to include or exclude it from the conversion, and the Lorentz/demag correction checkbox with its Shape combo, N field, and χ (SI) field (described below) that turns the measured shift into the intrinsic \(K_\mu\).

  • Branches — one converted \(K\) trace per included component, each named \(K_n\) and coloured to match its curve on the plot, together with a count of the crossings flagged along the scan (see Component identity and crossings below).

  • Model fit — the joint \(K(\theta)\) fit that resolves those crossings (see Component identity and crossings, below). The Info… button beside the model selector opens the shared component-information dialog for the selected \(K(\theta)\) model — its formula, parameters, and applicability. Fitted parameters are presented per branch in a table, each value quoted to the precision its uncertainty supports, with a Scale errors by √χ²ᵣ checkbox that inflates the quoted uncertainties when a fit’s reduced \(\chi^2\) exceeds one.

  • Suggest next angle — an optional section, collapsed by default, that plans the next scan angle from the fit above: see Suggest next angle, below.

The plot area has two view toggles, Fold 180° (overlay symmetry-equivalent orientations onto one period, for an angle scan — a display choice local to the window, independent of the trend panel’s own Fold control described below) and Crossing markers (draw a dashed vertical line at each flagged crossing; on by default). Every control change re-derives the branches and redraws immediately, so a bad reference choice or an accidentally excluded component is visible before anything is published. The footer’s Send K columns to trend table button writes the current configuration back to the trend panel as \(K[\ldots]\) columns so they can be plotted and exported alongside the fitted parameters. Until that button is pressed the trend table is untouched.

The Knight shift analysis window, with the Applied field reference selected, two frequency branches converted, a completed joint K(theta) fit in the Model fit section, the Suggest next angle section expanded with a computed D-optimal refine suggestion, and the fitted curves plus the suggestion's utility band overlaid on the K(theta) plot.

The Knight shift analysis window on a two-site angle scan: Source, Conversion, Branches, Model fit, and Suggest next angle in the sidebar — Model fit showing a completed joint \(K(\theta)\) fit and Suggest next angle a computed Refine parameters / All parameters (D-optimal) suggestion — and the converted branches with their fitted curves, Crossing markers, and the suggestion’s utility band overlaid in the plot area.

The window’s configuration and view-toggle state — including any joint fit — persist with the project under the knight_shift_analysis_state key (the point snapshot itself is always rebuilt from the source series on load, so a saved project can never carry stale fitted values). Projects saved before the window existed stored the conversion, and any joint fit, under the trend panel’s own fit_parameters_state block; these migrate automatically the first time the project is opened, so the window reopens already configured and, where a legacy joint fit is present, with its run-keyed branch assignment carried over (the migrated fit’s curves render as stale — “re-run to refresh” — until the fit is re-run, since the unit they were originally fitted in is not always recoverable).

  • Applied field — the precession is referenced to the bare applied field. For a frequency-parameterised component (MHz) the reference is the free-muon Larmor frequency \(\nu_{\mathrm{ref}} = \gamma_\mu B\) (\(\gamma_\mu/2\pi = 135.5\ \mathrm{MHz\,T^{-1}}\)); for a component parameterised directly by the local field \(B_\mu\) (Gauss, as in the OscillatoryField family) the reference is the applied field itself, giving the most direct form \(K = (B_\mu - B)/B\). This needs no reference line and is the default; use it whenever a separate unshifted signal is absent (a low-background sample) and the applied field is known.

  • Designated component — \(\nu_{\mathrm{ref}}\) is the fitted frequency of a chosen oscillation component, such as a co-measured diamagnetic line. Because \(\nu\) and \(\nu_{\mathrm{ref}}\) then come from the same fit, their covariance is carried through the error propagation; the applied-field reference treats \(B\) as exact.

K is dimensionless. It is stored internally as a fraction and shown in a unit you select, with an Auto mode that reads parts per million for a diamagnet (typically tens of ppm) and per cent for a paramagnet (up to a few per cent). The conversion itself yields the directly measured shift \(K_{\mathrm{exp}}\); the window’s Lorentz/demag correction (below) recovers the intrinsic \(K_\mu\) from it given the sample geometry and bulk susceptibility.

Lorentz/demagnetising correction. The measured shift \(K_{\mathrm{exp}}\) includes the Lorentz cavity field and the sample’s own demagnetising field, both of which depend on the sample shape rather than the local hyperfine coupling. The Lorentz/demag correction checkbox in the Conversion section removes them,

\[K_\mu = K_{\mathrm{exp}} - \left(\frac{1}{3} - N\right)\chi \qquad\text{(Amato \& Morenzoni Eq. 5.60)}\]

with \(N\) the demagnetisation factor along the applied field (SI convention, \(\sum N = 1\) over the three principal axes) and \(\chi\) the sample’s volume susceptibility, also SI dimensionless — multiply a CGS value in emu cm-3 by \(4\pi\) before entering it. The Shape combo sets \(N\) from the sample geometry — Sphere (N = 1/3), for which the correction vanishes exactly (the Lorentz and demagnetising fields cancel); Thin plate, B ∥ plane (N = 0); Thin plate, B ⊥ plane (N = 1); Long cylinder, B ∥ axis (N = 0); Long cylinder, B ⊥ axis (N = 1/2); or Custom N, which enables the N field for a value outside these standard shapes — and χ (SI) takes the volume susceptibility. The correction is a constant offset applied equally to every branch, so it shifts K but never reorders branches or moves a crossing. It is exact for an ellipsoidal sample whose orientation relative to the field is fixed; for a rotating non-spheroidal sample \(N\) itself varies with angle, which this scalar form does not capture — a caveat worth remembering for an angle-scan correction with a non-sphere shape. The K uncertainties reported by the window do not include a \(\chi\) uncertainty.

Angle dependence. Rotating a single crystal in the applied field maps out \(K(\theta)\). The contact term is isotropic, while the dipolar term carries the full angular dependence through the dipolar coupling tensor, so \(K(\theta)\) constrains the muon stopping site. Add an Angle (°) column (Columns: metadata and custom), fit each orientation, and select Angle (°) as the trend x-axis to plot each component’s shift against orientation. Because the dependence is periodic (the dipolar term goes as \(\cos^2\theta\), period 180°), the Fold control next to the x-axis selector folds the angle into one period (180° or 360°) so equivalent orientations from a wide or wrap-around scan overlay — doubling the effective angular sampling and exposing the periodic shape for a model fit. Folding is a display/analysis choice; the stored angles are untouched.

Component identity and crossings. Across an orientation (or temperature, or field) scan, each oscillation component keeps its stable label (frequency, frequency_2, …) and is converted independently, so the trend follows one component through the scan. Where two component frequencies approach or cross, that identification becomes ambiguous — the fit is free to interchange the two — and the converted trace can jump. The panel detects these points (a frequency crossing, or a relabelling that better preserves continuity than the raw label order) and marks them on the trend; it does not silently reassign the components, leaving the judgement to you. Seeding each fit from its neighbour (chained batch seeding) keeps the labelling stable through an ordered scan and minimises such crossings in the first place.

To resolve a crossing rather than just flag it, open the Knight shift analysis window (with at least two branches and Angle as the scan axis) and use the Model fit section of its sidebar: choose a Model (KnightAnisotropy, AngularCos2, or AngularFourier2 — the same angular basis models described below) and a Max iterations bound, then press the footer’s Run joint K(θ) fit button. The fit runs off-thread — the button reports progress and re-enables when it finishes — and fits one K(θ) curve per branch simultaneously, assigning each angle’s component points one-to-one to the curve they best fit (a Hungarian matching) and iterating fit ↔ reassignment to convergence. The plotted branches are realigned so each follows one physical curve continuously through the crossings, the fitted model curves overlay in the branch colours, and dashed vertical markers flag the angles where the assignment swaps (the crossings the fit actually resolved, a firmer signal than the raw proximity flags). The sidebar reports each curve’s fitted parameters and reduced \(\chi^2\) beneath the controls; Clear fit discards the fit and returns the branches to their raw component labels. Changing the display unit or the Lorentz/demag correction only marks the fitted curves stale (their parameters shift with either — the assignment does not, since a common offset or scale cannot reorder branches) — re-run the fit to refresh them. The Scale errors by √χ²ᵣ checkbox inflates every quoted parameter uncertainty by \(\sqrt{\chi^2_r}\) when that curve’s reduced \(\chi^2\) exceeds one (a fit that is already better than the noise is left alone); it is display-only — the stored fit itself is unchanged, and toggling it appends or removes an “(errors ×√χ²ᵣ)” note next to the affected curve. The fit (assignment and per-curve parameters) is saved with the project and restored on reload, and stays applicable across a Refresh from trend as long as the branch count is unchanged; a different component selection invalidates it.

In the trend panel, select one or more K[...] traces and use Remove to delete them: the backing component is dropped from the conversion (via set_knight_shift_config, so the trace does not regenerate). Removing every component turns the conversion off.

Fitting the anisotropy \(K(\theta)\). With Angle (°) as the trend x-axis, the model-fit builder offers three angle-only basis models alongside the usual ones:

\[K(\theta) = K_{\mathrm{iso}} + K_{\mathrm{ax}}\,\frac{3\cos^2(\theta - \theta_0) - 1}{2} \qquad\text{(}\texttt{KnightAnisotropy}\text{)}\]

for the axial dipolar form — the isotropic contact term \(K_{\mathrm{iso}}\) plus the traceless dipolar anisotropy \(K_{\mathrm{ax}}\) — the general two-fold modulation

\[K(\theta) = K_{\mathrm{avg}} + K_{\mathrm{amp}}\cos 2(\theta - \theta_0) \qquad\text{(}\texttt{AngularCos2}\text{)}\]

for a crystal rotated in a plane — algebraically the same curve family as KnightAnisotropy (\(K_{\mathrm{iso}} + K_{\mathrm{ax}}/4 = K_{\mathrm{avg}}\), \(3K_{\mathrm{ax}}/4 = K_{\mathrm{amp}}\), sharing \(\theta_0\)) — and a misalignment-sensitive extension

\[K(\theta) = K_{\mathrm{avg}} + K_1\cos(\theta - \theta_1) + K_{\mathrm{amp}}\cos 2(\theta - \theta_2) \qquad\text{(}\texttt{AngularFourier2}\text{)}\]

which adds a first-harmonic term to AngularCos2. A perfectly aligned rotation axis gives a pure second harmonic; a tilted axis leaks a first harmonic whose amplitude grows with the tilt, so a fitted \(K_1\) significantly different from zero is fit-level evidence that the rotation axis is not exactly where the goniometer says it is. This is a phenomenological Fourier form — it does not itself solve for the physical tilt angle from \(K_1\) and \(\theta_1\) — and, with five free parameters, needs at least five shared angles per curve to fit. See Suggest next angle, below, for using it to test for misalignment directly rather than assuming an aligned axis.

KnightAnisotropy and AngularCos2 both carry a \(\theta_0\) term — the goniometer/mount misalignment between the zero of the angle scale and the crystal’s principal axis, since a real mount is never perfectly aligned; without it, that misalignment would otherwise bias \(K_{\mathrm{iso}}\) and \(K_{\mathrm{ax}}\) (or \(K_{\mathrm{avg}}\) and \(K_{\mathrm{amp}}\)) directly. Both forms are invariant under a \(90^\circ\) shift of \(\theta_0\) together with a sign flip of the anisotropic amplitude, so the joint fit canonicalises the fitted \(\theta_0\) into \((-45^\circ, 45^\circ]\) — folding to the small-\(|\theta_0|\) representation so a mount that is nearly aligned reads with a small offset and a physically sensible amplitude sign, rather than an equally valid but larger-offset relabelling of the same curve. AngularFourier2’s second-harmonic phase \(\theta_2\) folds the same way; its first-harmonic phase \(\theta_1\) folds independently, over the full \(360^\circ\) period a first harmonic repeats on, into \((-90^\circ, 90^\circ]\) with a sign flip of \(K_1\). Every fold is an exact linear reparameterisation, so the fitted covariance is carried through it exactly (\(\Sigma' = J\Sigma J^{\mathsf{T}}\)) rather than approximated — the quoted uncertainties, and anything downstream that consumes the covariance (such as Suggest next angle, below), see the same figure a refit started directly in the canonical branch would have produced. All three models take \(\theta\) in degrees, so they fit directly against the Angle axis (folded or not). The diagonal dipolar-tensor components recovered for rotations about the crystal axes constrain the muon stopping site. For a worked example end to end, see Angle-dependent Knight shift.

Clogston–Jaccarino (\(K\) vs \(\chi\)). Plotting the Knight shift against the bulk susceptibility with temperature as the implicit parameter gives a straight line whose slope \(\mathrm{d}K/\mathrm{d}\chi\) measures the muon hyperfine coupling and whose intercept is the \(\chi\)-independent shift. The muon experiment does not itself measure \(\chi\), so this is API-only — pair the exported Knight shift with an independent susceptibility:

from asymmetry.core.fitting.knight_shift import clogston_jaccarino_fit

result = clogston_jaccarino_fit(chi, knight, sigma_knight)  # χ, K, σ_K per T
print(result.slope, result.slope_err)   # dK/dχ ∝ hyperfine coupling
print(result.intercept)                 # χ-independent (orbital) shift

When to use this. Convert to the Knight shift whenever the quantity of interest is the shift of a precession frequency from the applied-field value — tracking a local susceptibility, determining a muon site from \(K(\theta)\), or building a Clogston–Jaccarino \(K\)–\(\chi\) plot. Keep the raw frequency trend when the absolute precession frequency itself is the observable (an internal field, a magnetic order parameter).

Suggest next angle

Once the joint \(K(\theta)\) fit above has converged, the collapsible Suggest next angle section beneath Model fit answers the question Suggest next point already answers for a scalar trend — where should the next measurement go? — with three angle-specific flavours, chosen from a Mode selector: Refine parameters, Test misalignment, and Resolve assignment. All three share one Candidate range (seeded from the measured angle span; widen it to allow an extrapolated suggestion) and one Suggest button, and reuse the Laplace/Fisher acquisition machinery described on that page rather than a separate derivation — what is genuinely new here is specific to a rotation scan: one new run yields a value for every curve at once, and which measured value belongs to which physical curve is itself something the joint fit had to work out.

The section stays inactive, with a muted hint explaining why, until every prerequisite is met: no joint fit (“Run the joint K(θ) fit first.”), one that no longer matches the branches or predates the current unit/correction (“Re-run the joint K(θ) fit — it no longer matches the branches.” / “…the display unit or correction changed.”), or — for a fit saved before this feature shipped — a missing stored covariance (“Re-run the joint K(θ) fit to store fit covariance.”): the run-keyed assignment of a legacy fit survives a project reload, but its per-curve covariance was never recorded, so a next-angle suggestion needs one re-run before it becomes available.

Refine parameters sums the expected information gain over every curve — one new run adds a datum to each of them, so their information gains add. Target chooses what to refine: All parameters (D-optimal) shrinks every curve’s whole parameter covariance at once, or pick one curve’s parameter from the list for a c-optimal solve, which enables Precision goal and the Typical run (Mevents) / Rate (Mevents/h) conversion — exactly as in Suggest next point, down to the result line’s phrasing:

Measure at θ = 48.2° × 0.6 of a typical run's statistics
→ σ ≈ 0.018 (approximate)

Test misalignment asks a different question: is the scan actually consistent with a perfectly aligned rotation axis, or is there evidence of a tilt? On Suggest it fits the AngularFourier2 alternative (above) automatically, off-thread — cached against the joint fit’s model, unit, and Lorentz/demag correction, so repeat clicks do not refit — and ranks candidate angles by the disagreement between the current model and that alternative, summed over every branch (the same model-discrimination utility Suggest next point’s Compare against uses for a scalar trend). The result line names the currently preferred model and its Akaike weight, e.g.

Measure at θ = 71.5°; prefers AngularFourier2 (Akaike weight 0.86, evidence ratio 6.1)

and when the two models already agree within noise everywhere in the candidate range, the section reports that instead of pointing at an arbitrary angle.

Resolve assignment targets the labelling itself rather than either model’s parameters. Near a crossing, the joint fit’s classification-EM step can settle on more than one near-equally-good assignment of which measured component belongs to which physical curve; this mode ranks candidate angles by how well a new, unlabelled run there would separate the winning assignment from its near-degenerate runners-up — a minimum-cost matching between the two labellings’ predicted value sets, zero exactly at a crossing (where the competing labellings coincide by construction) and largest where they imply genuinely different curves. The runners-up are kept only in memory from the last fit run, so if the stored fit came from a project load, Suggest re-runs the joint fit off-thread first to recover them; with none to compare against, the section reports “No near-degenerate assignments to discriminate.”

Overlay and risk shading. A computed suggestion draws the same utility band used by Suggest next point, anchored beneath the \(K(\theta)\) plot with the best angle marked and any extrapolated candidates shown at reduced opacity. Refine and Test-misalignment suggestions can additionally shade a candidate span with a muted hatched band where two curves’ predicted values sit close enough (within about \(2\sigma\) of each other) that a new, unlabelled run there risks being fitted onto the wrong curve — exactly the crossing risk the joint fit’s own Hungarian assignment exists to resolve. A suggested angle that falls inside a shaded span should be read with that caveat in mind rather than acted on blindly.

When to use this. Reach for Refine parameters while extending an otherwise routine scan; reach for Test misalignment once a fit already looks good but you want to actively rule out a tilted mount rather than assume alignment; reach for Resolve assignment specifically when a crossing has left two labellings within reach of each other and the next run should settle which is real.

References

  1. A. Amato and E. Morenzoni, Introduction to Muon Spin Spectroscopy: Applications to Solid State and Material Sciences, Lecture Notes in Physics Vol. 961 (Springer, Cham, 2024).

  2. S. J. Blundell, R. De Renzi, T. Lancaster, and F. L. Pratt, Muon Spectroscopy: An Introduction (Oxford University Press, Oxford, 2022).

      1. Knight, Phys. Rev. 76, 1259 (1949).

      1. Clogston, V. Jaccarino, and Y. Yafet, Phys. Rev. 134, A650 (1964).

Runnable example

See examples/parameter_trending.py for a complete executable script.

Downstream trend models

The trend framework feeds several physical models that consume a fitted parameter series: for TF-μSR superconducting penetration-depth analysis via a temperature-dependent \(\sigma(T)\), see Superconducting penetration depth models; for muonium reaction kinetics from relaxation rates linear in reactant concentration, see Muonium reaction kinetics at a pulsed source (scripting); and for the field-dependent transport models fitted to \(\lambda(B_\mathrm{LF})\), see Field-dependent transport models (DiffusionLF / BallisticLF).

Previous Next

© Copyright 2026, Asymmetry Contributors.

Built with Sphinx using a theme provided by Read the Docs.