DEER / PDS Distance Analysis¶
Distance-distribution analysis for pulsed-dipolar spectroscopy (DEER/PELDOR, and the closely related RIDME / DQC / SIFTER). All of these share one model: a background-corrected form factor \(F(t)\) is the integral over the distance distribution \(P(r)\) of an orientation-averaged dipolar kernel,
with the dipolar angular frequency \(\omega(r) = 2\pi\,\nu_{dd}/r^3\) (rad/µs, \(r\) in nm, \(t\) in µs) and \(\nu_{dd} = 52.04\ \text{MHz·nm}^3\). The kernel integral has a closed form in Fresnel integrals, so \(K\) is built without a per-orientation loop.
Recovering \(P(r)\) from \(F(t)\) is a Fredholm equation of the first kind (ill-posed). This module solves it three ways:
- Tikhonov regularization + non-negativity (NNLS) — the default. The
regularization weight \(\alpha\) is chosen automatically (by generalized
cross-validation (GCV) by default, or the classic L-curve corner). The
background can be removed sequentially (fit the tail, divide it out,
invert) or fit jointly with \(P(r)\) (DeerLab-style). A covariance
confidence band (
tikhonov_ci()) is returned with every inversion. - Analytic integral Mellin transform — a model-free inversion
(
deer_invert_mellin(), Matveeva, Nekrasov & Maryasov, PCCP 2017, 10.1039/C7CP04059H). No Tikhonov, no NNLS: \(P(r)\) is recovered in closed form, so it is not broadened and bimodal peaks are not merged. Noise enters \(P(r)\) additively and groups at short \(r\). - Parametric sum-of-Gaussians fit — a model-based inversion
(
deer_invert_gauss(); the DeerAnalysis "Gaussian" mode / DeerLabdd_gaussNapproach). \(P(r)\) is modelled as \(N\) Gaussians fit jointly with the background to the signal, with \(N\) chosen by an information criterion. When the distribution really is a few discrete modes this is the most robust choice and it gives genuine parametric error bars on each peak — including rigorous support-plane confidence intervals (Stein, Beth & Hustedt, Methods Enzymol. 2015, 10.1016/bs.mie.2015.07.031).
The dipolar zero-time can be fit automatically with
fit_zero_time() before any of these. The intermolecular
background is normally a stretched exponential (background_fit()),
but a flexible empirical form \(a\,e^{\,b(t + c\,d^{\,t})}\) is also available
(background_general()).
Why GCV is the default
A DEER L-curve is nearly vertical — the residual stays at the noise floor
across decades of \(\alpha\) — so the Menger-curvature "corner" is ill-defined
and tends to latch onto a tiny \(\alpha\), producing a spiky comb-like \(P(r)\).
GCV has a genuine minimum and picks a stable \(\alpha\), so it is used by
default. Switch to the L-corner with method='curvature' if you want it.
GCV still tends to under-regularize on real noisy traces; nudge it heavier
with alpha_factor=2..4, or use deer_validate() to average
over background choices for a smooth consensus \(P(r)\) with an uncertainty band.
scipy is required
DEER analysis needs scipy (the math extra: pip install -e .[math]).
scipy is imported lazily, so importing the module never fails on a minimal
install — the routines raise a RuntimeError when scipy is missing.
Conventions
Times are in microseconds, distances in nanometres. Internally \(P(r)\) is handled as discrete probability masses (sum \(= 1\)); the matching density \(P(r) = \text{masses}/dr\) is returned for plotting.
deer_invert()¶
res = deer.deer_invert(t, V, r=None, bg_start=None, bg_end=None,
dim=3.0, fit_dim=False, alpha=None, alphas=None,
reg_order=2, nu_dd=deer.NU_DD, scan_lcurve=True,
method='gcv', engine='sequential', alpha_factor=1.0)
The full one-call pipeline: background-correct \(V(t)\), build the kernel, and invert to \(P(r)\) by Tikhonov + NNLS. This is what most users want.
t,V— time axis (µs) and the real DEER trace \(V(t)\).r— distance grid (nm).Noneusesdefault_r_axis()(1.5–8 nm, 200 points).bg_start,bg_end— background-fit window (µs).bg_start=Nonedefaults to the midpoint of the trace;bg_end=Nonefits to the end. Seebackground_fit(). (Withengine='joint'they set the tail baseline window for the λ-pinned, truncated-grid joint background fit — seedeer_invert_joint().)dim,fit_dim— fractal background dimension (3 = homogeneous 3D); setfit_dim=Trueto float it.alpha— regularization weight.Noneselects it automatically bymethod.alpha_factor— multiplier applied to the auto-selected \(\alpha\) (ignored when an explicitalphais given). GCV (and AIC) tend to under-regularize the near-vertical DEER L-curve, leaving noise spikes in \(P(r)\); a factor of 2–4 reproduces the heavier hand-picked L-corner regularization used to obtain smooth distributions in inter-laboratory ring tests (Schiemann et al., JACS 2021, 143, 17875).alphas— the regularization scan grid (defaultnp.logspace(-4, 3, 36)).reg_order— derivative order of the smoothing operator \(L\) (default 2).scan_lcurve— whenTrue(default) the regularization scan is always computed for display, even if an explicitalphais given.method— automatic-\(\alpha\) criterion:'gcv'(default — generalized cross-validation, robust) or'curvature'(classic maximum-Menger-curvature L-corner). Seel_curve().engine— how the inversion is done:'sequential'(default; fit the background tail, divide it out, then invert),'joint'(fit background + modulation depth together with \(P(r)\) in one pass — seedeer_invert_joint(); more robust when the background window is short or hard to place),'mellin'(the model-free analytic transform — seedeer_invert_mellin()),'gauss'(the parametric sum-of-Gaussians fit — seedeer_invert_gauss()), or'none'(no background: \(B(t)=1\), fit only the modulation depth \(\lambda\) — for pre-corrected / simulated / full-modulation \(\lambda\!\to\!1\) data; fitting a decay there would absorb the dipolar decay and badly broaden \(P(r)\)).'general'selects the empiricalbackground_general()background with an otherwise sequential Tikhonov inversion.**kwargs— forwarded to the model-free / parametric engines:engine='mellin'takesdelta,tau_max,n_tau,bg_engine,n_mc, …;engine='gauss'takesn_gauss,max_gauss,ic,ci_mode,bg_engine, …;bg_params(thebackground_general()coefficients) is forwarded to any engine. Ignored otherwise.
Returns a dict:
| Key | Description |
|---|---|
t, r |
The time and distance axes used |
form_factor |
Background-corrected form factor \(F(t)\) |
F_fit |
Back-calculated fit \(K P\) |
residuals |
form_factor - F_fit |
P |
Raw distance masses (\(\ge 0\)) |
P_norm |
Masses normalized to sum \(= 1\) |
P_density |
Density $P(r) = $ P_norm\(/dr\) (integral \(= 1\)) — plot this |
P_lower, P_upper |
95% covariance confidence band on the density (see tikhonov_ci()) |
kernel |
The dipolar kernel matrix \(K\) |
alpha |
The regularization weight used |
l_curve |
The l_curve() result dict (or None) |
background |
The background_fit() result dict |
lambda, k, dim |
Modulation depth, background decay rate, dimension |
engine |
'sequential', 'joint', 'mellin', 'gauss', 'none', or 'general' |
import numpy as np
import atomize.math_modules.deer as deer
# synthetic 3.5 nm trace
r = deer.default_r_axis(2.0, 5.0, 150)
P = np.exp(-(r - 3.5)**2 / (2*0.15**2))
t = np.linspace(0, 3.0, 300) # us
V = deer.simulate(t, r, P, lam=0.3, k=0.1, dim=3.0, noise=0.01, seed=1)
res = deer.deer_invert(t, V, r=r, bg_start=1.0)
peak = res['r'][res['P_density'].argmax()]
print(f"lambda = {res['lambda']:.3f}, alpha = {res['alpha']:.3g}, peak r = {peak:.2f} nm")
deer_invert_joint()¶
res = deer.deer_invert_joint(t, V, r=None, bg_start=None, bg_end=None,
dim=3.0, fit_dim=False, alpha=None, alphas=None,
reg_order=2, nu_dd=deer.NU_DD, method='gcv',
scan_lcurve=True, alpha_factor=1.0)
DEER inversion with a joint fit of the background and modulation depth
together with the regularized non-negative \(P(r)\) — the strategy DeerLab uses.
More robust than the sequential deer_invert() pipeline on real
traces with short or shallow backgrounds, where the tail fit and the inversion
are coupled. Also reachable as deer.deer_invert(..., engine='joint').
Starting from the full model
the only nonlinear unknown is the background decay rate \(k\) (and \(d\) when
fit_dim=True). The background and modulation depth are fit by
joint_background(), the same fit the Mellin engine uses.
\(\lambda\) is pinned to the tail baseline of \(V/B\) over
\([\text{bg\_start}, \text{bg\_end}]\) (where the form factor has decayed and
\(V \approx (1-\lambda)\,B\)), and \(k\) is fit together with a coarse non-negative
\(P(r)\) on a distance grid truncated at the supported \(r_\text{max}\). The
full-resolution \(P(r)\) then follows from \(K P = (V/B - (1-\lambda))/\lambda\) by
Tikhonov + NNLS, with \(\alpha\) chosen by GCV as in deer_invert().
Why the rate is fit on a truncated grid
This breaks two coupled ambiguities. First, on the full \(r\) grid a gentle
background can be imitated by spurious long-distance \(P(r)\) mass, so an
unconstrained rate search collapses to \(k \to 0\) and broadens \(P(r)\); truncating
the grid at \(r_\text{max}\) removes that escape route. Second, with \(\lambda\)
free, a shallow background plus extra long-\(r\) mass can also imitate the correct
deeper background, so \(\lambda\) is pinned to the decayed-tail baseline. So
bg_start/bg_end set the baseline window, not just an initial guess.
Returns the same dict as deer_invert(), with engine='joint'.
Lightweight variant for the Mellin engine
joint_background() runs the same λ-pinned rate fit but
returns only the background (no full-resolution inversion / L-curve) on a
coarse internal grid, and is further hardened against collapse on short
traces / short bg_end. It is what deer_invert_mellin()
and Mellin validation use.
tikhonov_ci()¶
Covariance-based confidence band on the regularized \(P(r)\) — the asymptotic (curvature) CI DeerLab shows by default, returned with every Tikhonov inversion. For the linear Tikhonov estimator \(P = (K^\top K + \alpha^2 L^\top L)^{-1} K^\top F\) the form-factor noise propagates as
with \(\sigma^2\) estimated from the fit residuals (effective dof
\(= N - \operatorname{tr}(K M)\)). Returns (lower, upper) at confidence z
(default 1.96 ≈ 95%) on the same density scale as P/sum(P)/dr, clipped at 0. The
non-negativity constraint is not propagated, so the band is a slightly
conservative linear approximation (as in DeerLab's moment-based CI).
fit_zero_time()¶
t0 = deer.fit_zero_time(t, V, bg_start=None, bg_end=None,
n_grid=16, search_frac=0.15, refine=True,
method='parabola', drop=0.15, smooth_w=5,
xcheck=False, xcheck_tol_frac=0.004, **kwargs)
Find the dipolar zero-time \(t_0\) (the reference time). DEER is sensitive to
where \(t = 0\) of the dipolar evolution sits: an error of even a few tens of ns
misaligns the kernel, broadens \(P(r)\) and biases the mean distance long. This is
the equivalent of DeerLab's fitted reftime, and it matters more than the
background depth.
Two methods, selected by method:
'parabola'(default) — fit a quadratic to the echo maximum (the classic DeerAnalysis approach: \(V \approx V_\text{pk} - c\,(t-t_0)^2\) near the echo) and take its vertex. Noise-robust: the initial peak is the argmax of a smoothed \(V\) restricted to the first 30 % of the trace (so a stray noise spike cannot be mistaken for the echo), and the fit window widens symmetrically out to where the smoothed signal has fallendropof its peak-to-min amplitude — wide enough to average down noise, narrow enough to stay on the parabolic top (a too-wide window is biased by the dipolar oscillation/decay and the truncated pre-zero side). Data-only, fast, and ~3× more accurate than the residual search at high noise on traces with a clear echo maximum. Falls back to'residual'when no concave echo peak is found (e.g. the trace already starts at the zero-time).'residual'— minimize the V-space reconstruction residual. A candidate offset \(s\) shifts both the time axis and the (data-anchored) background window, so only the kernel alignment changes; the residual is smooth with a single minimum, found by a coarse grid over the firstsearch_fracof the trace plus a parabolicrefine. For speed it uses a fixed-\(\alpha\) sequential inversion on a capped distance grid;**kwargspass through todeer_invert()(r,dim,fit_dim, …). Robust when the echo maximum is ambiguous or absent.
xcheck is an opt-in cross-check, off by default: it computes the
'residual' estimate independently and, when it disagrees with the parabola by
more than xcheck_tol_frac of the trace span (~0.4 %), uses the residual instead.
It guards the parabola's one failure mode — a flat, shallow echo top at high noise,
where a late noise excursion can drag the vertex late — but does not improve the
recovered \(P(r)\), so leave it off unless an accurate \(t_0\) per se is the goal.
Returns \(t_0\) in the same units as t (µs).
t0 = deer.fit_zero_time(t, V, bg_start=1.0, r=r)
res = deer.deer_invert(t - t0, V, r=r, bg_start=1.0 - t0)
deer_validate()¶
val = deer.deer_validate(t, V, r=None, bg_start=None, bg_starts=None,
bg_end=None, dim=3.0, fit_dim=False, alpha=None,
alpha_factor=1.0, reg_order=2, nu_dd=deer.NU_DD,
method='gcv', engine='sequential',
noise=0.0, n_noise=0, seed=0, percentiles=(5, 95))
Validation by ensemble averaging, in the style of the DeerAnalysis validation
tool. The regularization weight is selected once on the central trace (honouring
alpha / alpha_factor) and then held fixed, while the inversion is re-run
over a sweep of background-start times (and, optionally, added-noise
realizations). The ensemble of \(P(r)\) is collapsed to a median consensus curve
plus a percentile uncertainty band.
A single GCV inversion of a noisy DEER trace leaves a spiky comb-like \(P(r)\); averaging across background choices suppresses those noise-driven spikes and yields the smooth, banded distribution familiar from inter-laboratory ring tests (Schiemann et al., JACS 2021, 143, 17875, Fig. 4). Holding \(\alpha\) fixed is both physically correct — validation probes background/noise sensitivity, not the regularization choice — and what keeps it fast (no per-trial L-curve scan).
bg_start— centre of the default background-start sweep (µs).Noneuses the trace midpoint.bg_starts— explicit sweep of background-start times.Nonebuilds a 9-point grid spanning \(\pm 7.5\%\) of the trace length aroundbg_start.alpha,alpha_factor— passed todeer_invert()for the one-off \(\alpha\) selection on the central trace; the result is then fixed.noise,n_noise— when both are positive, each background-start trial is repeated withn_noiseGaussian-noise realizations of standard deviationnoiseadded to \(V\) (estimatenoisefrom the trace residual).engine—'sequential','joint','mellin','gauss','none', or'general', as indeer_invert(). Extra engine parameters (Mellindelta/tau_max, Gaussiann_gauss/max_gauss, thebg_paramsgeneral-background coefficients, …) pass through via**kwargs.percentiles— the lower/upper percentiles of the band (default 5–95%).
Returns a dict:
| Key | Description |
|---|---|
r |
The distance axis |
P_density |
Median \(P(r)\) density across the ensemble (the consensus curve — plot this) |
P_mean |
Ensemble-mean density (for reference) |
P_lower, P_upper |
The percentiles band (shade between these) |
ensemble |
All n_trials × len(r) trial densities |
n_trials |
Number of successful trials |
bg_starts |
The background-start grid that was swept |
alpha |
The fixed regularization weight |
peak, r_mean |
Peak position and first moment of the consensus curve |
base |
The single central inversion (its form_factor / F_fit / background / l_curve, for display) |
val = deer.deer_validate(t, V, r=r, bg_start=1.0, alpha_factor=2.0)
print(f"peak r = {val['peak']:.2f} nm over {val['n_trials']} trials")
# plot the band: fill_between(val['r'], val['P_lower'], val['P_upper'])
# median: plot(val['r'], val['P_density'])
In the Data Treatment GUI this is the "Validate (background sweep → P(r) band)" checkbox; the distance view then shows the median curve over its shaded band.
deer_invert_mellin()¶
res = deer.deer_invert_mellin(t, V, r=None, bg_start=None, bg_end=None,
dim=3.0, fit_dim=False, nu_dd=deer.NU_DD,
delta=None, tau_max=30.0, n_tau=601,
bg_engine='joint', n_mc=0, ci_z=1.96, seed=0,
taumax_method='penalty', noise_space='V',
wiener=0.0, taumax_extend=True,
extend_short_frac=0.18, fit_rmin_frac=0.18,
signed_fit=True, taper_short=True)
Model-free DEER inversion by the analytic integral Mellin transform
(Matveeva, Nekrasov & Maryasov, PCCP 2017,
10.1039/C7CP04059H). No Tikhonov, no NNLS,
no L-curve: the distance distribution is recovered in closed form, so it is not
broadened and bimodal peaks are not merged. Also reachable as
deer.deer_invert(..., engine='mellin').
Writing the (background-corrected, normalized) form factor as a multiplicative convolution over the dipolar variable \(w = 2\pi\nu_{dd}/r^3\),
the Mellin transform separates the variables: with \(\tilde V(s)\), \(\Phi(s)\), \(P(s)\) the Mellin images of \(F\), \(\varphi\), \(p\), one has \(\tilde V(s) = P(1-s)\,\Phi(s)\), so on the critical line \(s = \tfrac12 + i\tau\) (using that \(F\), \(\varphi\) are real)
and the inverse Mellin transform gives \(p(w)\) directly; the Jacobian maps it to
\(f(r)\). The kernel image \(\Phi\) is computed in closed form
(mellin_kernel_spectrum()) and the signal image
\(\tilde V\) by the \(\delta\)-split of the paper
(mellin_signal_spectrum()).
The short-\(r\) noise spike (\"double peak near \(t=0\)\")
The chain is linear, so noise enters \(f(r)\) additively, and the \(r\)-space Jacobian (\(\sim r^{-2.5}\)) concentrates it into a spurious spike at short distances — the "double peak near \(t=0\)" in \(P(r)\). The same high-\(|\tau|\) content also makes the forward-fit echo top decay too fast. Two mechanisms, both on by default, suppress this without distorting the real peaks:
- Noise-adaptive \(\delta\). \(\delta\) splits the form factor into an analytic parabola term on \([0,\delta]\) and a numeric integral on \([\delta, T_\max]\); the noise enters through the numeric part. \(\delta\)'s floor and cap grow with the measured relative noise, so on noisy data more of the early signal goes to the clean analytic term. This fixes the spike at its source and does not touch the displayed density.
taper_short(default on). A geometric raised-cosine taper (fit_rmin_frac) sends the displayed \(P(r)\) smoothly to zero at the unreliable short-\(r\) edge. Because it depends only on distance, the mid- and long-\(r\) density is unchanged and a genuine short-\(r\) peak is attenuated rather than deleted. The tapered density also feedsF_fit. Settaper_short=Falsefor the raw signed density.
Together they remove the short-\(r\) spurious mass and restore the echo-top width while \(P(r)\) stays natural.
bg_engine—'joint'(default),'sequential', or'none', how the form factor is prepared (seejoint_background()/background_fit()). This matters a lot: the Mellin kernel \(\varphi(wT)\to0\), so the recovered density cannot represent a DC pedestal left by an imperfect background — a too-shallow background shows up as a constant gap between data and fit. The joint engine gives a clean \(F\to0\) and is the default.'none'sets \(B(t)=1\) and fits only \(\lambda\) — use it for data with no background (pre-corrected / simulated / full-modulation \(\lambda\!\to\!1\)): there the form factor decays to zero on its own, so fitting a background absorbs that dipolar decay and badly broadens \(P(r)\).delta— the Mellin split point \(\delta\) (µs): \([0,\delta]\) is integrated analytically, \([\delta, T_\max]\) numerically. The echo top is parabolic (\(F\approx F_0 + b\,T^2\)), so the analytic term keeps that quadratic and removes a systematic error in \(F_\text{fit}\) at the echo (the "thin parabola" near \(t=0\)).Noneauto-selects \(\delta\) where \(F(\delta)\approx0.85\), then clips it to a noise-adaptive window (wider on noisier data). A larger \(\delta\) moves the steep, noisy near-echo region into the clean analytic term, suppressing the short-\(r\) spike at its source. Seemellin_signal_spectrum()/mellin_delta().tau_max,n_tau— the Mellin variable runs over \([-\tau_\max, \tau_\max]\) withn_tausamples. The high-\(\tau\) cutoff is the regularizer.tau_max=Noneauto-selects it bytaumax_method(see below).taumax_method— how the auto cutoff is chosen.'penalty'(default) minimises the forward-fit RMS plus a penalty on the negative area of the signed density: the first term demands a good fit, the second stops the cutoff once it would only add high-\(\tau\) noise. It self-adapts, keeping clean data sharp and noisy data smooth.'discrepancy'picks the smallest cutoff that reaches the noise floor, then applies thetaumax_extendextension.'lcurve'is for comparison only — it under-regularizes on DEER (the residual is nearly flat in \(\tau_\max\), so the corner is ill-defined).noise_space—'V'(default) or'F': the space the noise floor and per-cutoff residual are measured in for the discrepancy selection.'V'(the whole background-normalized curve) is stationary and robust;'F'(the background-corrected form factor) is noise-amplified toward the tail.taumax_extend(default on) — a resolution-aware extension of the discrepancy cutoff. The discrepancy stops at the noise floor, but \(P(r)\) can keep sharpening past it, so the cutoff is pushed up as long as the short-\(r\) leakage (bottomextend_short_fracof the grid) keeps dropping, and stopped at the first increase. Clean data extends; noisy data stays put. Used only withtaumax_method='discrepancy'; the default'penalty'method does not need it.taper_short(defaultTrue) — smoothly taper the displayed \(P(r)\) to zero at the unreliable short-\(r\) edge with a geometric raised-cosine (fit_rmin_frac), removing the short-\(r\) noise spike while leaving the mid- and long-\(r\) density unchanged. The tapered density also feedsF_fit. See the info box above.Falsereturns the raw signed density.signed_fit(defaultTrue) — buildF_fit(and the penalty selector's RMS) from the honest signed density \(K\,P\), keeping the echo-top amplitude faithful. SetFalsefor low-\(\lambda\) data, where the short-\(r\) negative spike can otherwise double-peak the echo top; thenF_fituses the clipped, non-negative density instead. (Whentaper_short=Truethe tapered density already feedsF_fit.)wiener(default0= off) — strength of a Wiener-regularized inverse filter on the kernel-image division. The plain inverse \(1/\Phi(\tau)\) amplifies noise at high \(|\tau|\), which the \(r\)-space Jacobian concentrates into a short-\(r\) spike. The filter rolls that off, with its \(\varepsilon\) scaled by the measured tail noise so it does nothing on clean data. A value \(\approx 0.12\) removes the spike at moderate noise. Rarely needed, since the adaptive \(\delta\) andtaper_shortalready handle the spike.n_mc— number of Monte-Carlo noise realizations for the confidence band (0 = off). The band is built by additive-noise propagation: the white electrical-noise level is read from the decayed tail of \(V\) by smoothing (returned asnoise_level), added to the smooth \(V\) fit, and propagated through the fixed background to \(F\) — so \(F\) inherits the realistic \(1/(\lambda B)\) amplification toward the tail. The band is the per-distance STD across the realizations:P_lower/P_upper$= $P_density\(\mp\,\)ci_z\(\cdot\)P_std. ~100 realizations are typical.
Automatic cutoff — RMS penalized by symmetric noise (default)
The cutoff \(\tau_\max\) regularizes the inversion: the forward-fit RMS falls as
it captures the parabolic echo top, then sits on a broad noise-floor
plateau, so neither chasing its minimum (over-extends, injects the noisy
high-\(\tau\) spectrum into \(P(r)\)) nor the discrepancy floor (under-shoots before
\(P(r)\) has sharpened) is right. The injected noise enters the area-normalized
signed density as paired \(+\)bump/\(-\)dip excursions, so its \(|\)negative
area\(|\) (neg) measures it directly. The default 'penalty' method picks
\(\operatorname{argmin}\big(\text{rmsF}/\min(\text{rmsF}) + \text{neg}\big)\): the
ratio term (\(\ge 1\), large while the echo top is under-resolved) forces an
adequate fit, the neg term halts the extension the moment the cutoff would
only add symmetric noise. Self-adapting: clean data plateaus late (sharp \(P(r)\)
kept), noisy data accrues neg early (stays smooth). sigma_fit and the tail
sigma_noise are reported so the regime stays visible (\(\approx\) matched,
\(\gg\) underfit, \(\ll\) overfit), and whiteness flags a residual that is still
structured (see residual_whiteness()).
Returns the same dict shape as deer_invert() (so the GUI and
exporters are shared), with these Mellin-specific keys:
| Key | Description |
|---|---|
engine |
'mellin' |
P_density |
Recovered signed density (area-normalized; short-\(r\) noise ripples kept, can be < 0) — plot this |
P_signed_density |
Alias of P_density (kept for back-compat) |
P_lower, P_upper |
Monte-Carlo band $= $ P_density \(\mp\) ci_z·P_std (when n_mc > 0; else None) |
P_std |
Per-distance STD across the MC realizations (when n_mc > 0) |
noise_level |
White electrical-noise σ read from the decayed tail of \(V\) |
delta, tau_max |
The split point and cutoff used |
auto_taumax |
Whether tau_max was auto-selected |
sigma_fit, sigma_noise |
Forward-fit residual vs tail noise floor (the discrepancy diagnostic) |
whiteness |
Residual-whiteness goodness-of-fit dict (Durbin–Watson, lag-1 autocorrelation, ACF + white-noise band) — see residual_whiteness() |
tau, V_image, kernel_image |
The \(\tau\) grid and the Mellin spectra \(\tilde V(\tau)\), \(\Phi(\tau)\) |
alpha is NaN and l_curve is None (no Tikhonov regularization here).
res = deer.deer_invert_mellin(t, V, r=r, bg_start=1.0,
tau_max=None, n_mc=50) # auto cutoff + CI band
peak = res['r'][res['P_density'].argmax()]
print(f"peak r = {peak:.2f} nm, sigma_fit/sigma_noise = "
f"{res['sigma_fit']/res['sigma_noise']:.2f}")
deer_invert_gauss()¶
res = deer.deer_invert_gauss(t, V, r=None, bg_start=None, bg_end=None,
dim=3.0, fit_dim=False, nu_dd=deer.NU_DD,
n_gauss=None, max_gauss=4, bg_engine='joint',
bg_params=None, ic='aicc', n_mc=0, ci_z=1.96,
seed=0, sigma_min=None, sigma_max=None,
ci_mode='linear', ci_level=0.95,
prune_spurious=True, weight_min=0.02,
spike_weight_max=0.10, method='lsq',
mc_trials=30000, mc_tol=0.5)
Parametric DEER inversion: model \(P(r)\) as a sum of \(N\) Gaussians and fit
their centres, widths and amplitudes (the DeerAnalysis "Gaussian" mode / DeerLab
dd_gaussN approach). Also reachable as deer.deer_invert(..., engine='gauss').
Complements the regularized (deer_invert()) and model-free
(deer_invert_mellin()) engines: when the distribution
really is a few discrete modes this is the most robust, and — unlike a regularized
inversion — it gives genuine parametric error bars on each peak.
The 'lsq' solver fits the Gaussians, the background, and the modulation depth
\(\lambda\) together, directly to \(V(t)\) (DeerLab-style):
This is more robust than fitting a background first and dividing it out. On a compact, multi-peak \(P(r)\) the separate background step absorbs real signal, which distorts the form factor and leaves a residual the fit then covers with a spurious extra peak. Fitting everything at once avoids that, and an ideal \(N\)-Gaussian trace is recovered exactly. \(\lambda\) comes out as the total Gaussian mass \(S\), and the free amplitude \(A\) absorbs the small echo-top scaling, so no extra scale parameter is needed.
Seeding and the long-distance width floor
Two safeguards keep the fit reliable:
- Multi-start. The fit starts from two seeds — the peaks of a quick Tikhonov pass, and an even spread across the distance range — and keeps the better result. The Tikhonov peaks alone can land every component on the dominant peak, from where the fit never finds a weak long-distance mode; missing that mode leaves its slow oscillation in the residual.
- Width floor. At long distances the dipolar frequency is low, so a finite
trace length cannot resolve a narrow width. Left free, the fit collapses a
weak long mode into a near-delta spike — a tall thin peak in \(P(r)\) that adds
an oscillation to the residual. Each component's width is therefore floored at
the resolution limit for its distance,
\(\sigma_\text{res}(r)\approx r^4/(27\,\nu_{dd}\,T)\). Short, well-resolved peaks
are unaffected. Set
sigma_minto override.
n_gauss— force a fixed number of components.None(default) selects \(N\) automatically (seeic/prune_spurious).max_gauss— largest \(N\) tried during automatic selection (default 4).ic— information criterion for automatic \(N\):'aicc'(default, corrected Akaike),'aic', or'bic'(heavier penalty ⇒ fewer components).bg_engine— which background is co-fit with the Gaussians:'joint'(default, a stretched exponential \(B(t)=e^{-(k|t|)^{d/3}}\), with \(d\) floated only whenfit_dim=True),'none'(no background, \(B=1\) — for pre-corrected or full-modulation traces), or'general'(an empirical background shape held fixed while \(\lambda\) and \(P(r)\) are still co-fit; seebackground_general()).bg_params— coefficients for the'general'background (seebackground_general()).ci_mode— per-component error bars:'linear'(default) or'support'(see the confidence-interval box below).ci_levelis the confidence for'support'(default 0.95).prune_spurious/weight_min/spike_weight_max— parsimony guard against over-fitting (default on; see the box below).n_mc— when > 0, a parametric confidence band on \(P(r)\) by sampling the fit covariancen_mctimes (cheap, no re-inversion);P_lower/P_upper\(=\)P_density\(\mp\)ci_z·STD. (Ignored whenmethod='mc'.)method—'lsq'(default, gradient least-squares) or'mc'(Dzuba/Matveeva frequency-domain Monte-Carlo; see the box below).mc_trialssets the stochastic-multi-start budget andmc_tolthe ensemble MSD tolerance.sigma_min,sigma_max— component-width bounds. Settingsigma_minoverrides the automatic distance-resolution floor (above) with a flat lower bound $\max(2.5\,dr,\,\text{sigma_min})$ for every component. The upper bound defaults to half the distance range.
Confidence intervals: 'linear' vs 'support'
ci_mode='linear' (default) reports the 1σ diagonal of the linearized
covariance \((J^\top J)^{-1}\sigma^2\). It is fast, symmetric, and good for live
use.
ci_mode='support' computes rigorous support-plane / profile-likelihood
intervals (Stein, Beth & Hustedt, Methods Enzymol. 2015,
10.1016/bs.mie.2015.07.031). Each
centre and \(\sigma\) is fixed in turn while all other parameters are re-fit, and
the interval is taken where the residual sum of squares rises past an F-test
threshold. This accounts for parameter correlations and gives asymmetric
intervals (center_ci_lo/hi, sigma_ci_lo/hi), which the linearized bar can
misstate when the \(\chi^2\) surface is not parabolic. It costs a fit per grid step
(~1–5 s); opt-in.
method='mc' — frequency-domain Monte-Carlo
An alternative solver after Dzuba, JMR 269 (2016) 1 and Matveeva et al.,
Z. Phys. Chem. 231 (2017) 463. The Gaussian parameters are found by a random
search in the dipolar frequency (Pake) domain instead of gradient descent in time.
mc_trials random parameter sets are drawn, each locally polished, and the one
whose Pake spectrum best matches the data is kept. This has two advantages over
'lsq': the random restarts cannot get stuck on a floor-width spike, and the
frequency-domain comparison is naturally immune to ESEEM peaks and background
error. The data-consistent trials form an ensemble; its per-\(r\) percentiles give
a confidence band (P_lower/P_upper) and its spread sets center_err/sigma_err.
On clean synthetic data 'mc' performs about the same as 'lsq', so it is
opt-in (~seconds). Its real value is robustness to ESEEM and background artifacts
on measured data, plus the honest ensemble error band. n_mc and ci_mode are
ignored in this mode.
How \(N\) is chosen (prune_spurious)
Every \(N\) from 1 to max_gauss is fit, and the information criterion (ic)
picks the best. DEER traces are heavily oversampled, so at low noise the
criterion sometimes adds one extra Gaussian to absorb residual structure. Such a
component is recognizable: it sits at the width floor and carries little weight
(\(<\) spike_weight_max), or it carries negligible weight (\(<\) weight_min) at
any width. With prune_spurious on (default) the chosen \(N\) is the best fit that
contains no such component, so a simple bimodal is not reported as 3–4 Gaussians.
n_gauss_ic is the unpruned pick and pruned flags whether a reduction
happened; forcing n_gauss bypasses it.
Only weight, not width, condemns a component, because a real long-distance mode also sits near the width floor (the kernel constrains large-\(r\) widths weakly). A floor-width peak with substantial weight is kept; only floor-width and low-weight is removed. With the joint fit, multi-start seeding, and the width floor doing the main work, pruning is now just a light backstop: a real 3–4 Gaussian distribution is resolved, while a simple bimodal stays \(N=2\).
Returns the same dict shape as deer_invert() (shared GUI /
exporters), with these Gaussian-specific keys:
| Key | Description |
|---|---|
engine |
'gauss' |
components |
list of {amplitude, center, sigma, weight, center_err, sigma_err} per Gaussian (plus center_ci_lo/hi, sigma_ci_lo/hi when ci_mode='support') |
n_gauss |
the chosen number of components |
n_gauss_ic, pruned |
the unpruned criterion pick, and whether pruning reduced \(N\) |
aic, aicc, bic |
the information criteria of the chosen model |
ic, ci_mode, ci_level |
the criterion and CI mode used |
ic_curve |
list of (N, criterion, rss) over the \(N\) tried |
P_lower, P_upper, P_std |
parametric band on the density (when n_mc > 0; else None) |
noise_level |
white electrical-noise σ from the decayed tail |
alpha is NaN and l_curve is None (no Tikhonov regularization here).
res = deer.deer_invert_gauss(t, V, r=r, bg_start=1.0, ci_mode='support')
for c in res['components']:
print(f"r = {c['center']:.3f} (-{c['center']-c['center_ci_lo']:.3f}"
f"/+{c['center_ci_hi']-c['center']:.3f}) nm, "
f"sigma = {c['sigma']:.3f} nm, weight = {c['weight']:.2f}")
print(f"N = {res['n_gauss']} (AICc pick {res['n_gauss_ic']}, pruned={res['pruned']})")
joint_background()¶
bg = deer.joint_background(t, V, bg_start=None, bg_end=None, dim=3.0,
fit_dim=False, nu_dd=deer.NU_DD, n_r=60,
rate_alpha=1.0, lam_pin_frac=0.5)
The λ-pinned joint background, returning only the background (same dict shape
as background_fit()). The rate is fit on a coarse internal
distance grid (n_r) at a fixed regularization (rate_alpha): \(k\) and \(\lambda\)
are insensitive to the \(P(r)\) resolution, so this is ~30× faster than a full joint
inversion — fast enough to re-run per background-start during Mellin validation.
This is the shared background fit of both inversion engines:
deer_invert_joint() (Tikhonov, engine='joint') and
deer_invert_mellin() (bg_engine='joint') both call it.
Two robustness measures — both critical to either engine (the truncated-grid rate fit is exactly what stops a gentle background being absorbed as spurious long-\(r\) \(P(r)\) mass, which broadens the Tikhonov \(P(r)\) and leaves a pedestal the Mellin kernel \(\varphi\to0\) cannot represent):
- λ pinned over the later, more-decayed part of the tail (
lam_pin_frac, the last 50 % of \([\text{bg\_start}, T_\max]\) by default). \(\lambda\) is the asymptotic baseline (\(F\to0\)); pinning \(\langle F\rangle = 0\) over the whole tail biases it high when a broad/long-\(r\) component has not decayed (\(\langle F\rangle > 0\) there), underestimating \(\lambda\) and pushing \(k\) too steep — a residual tail droop the Mellin engine cannot represent. The later window is more decayed, so \(k\) returns near-true. (The rate-fit residual still spans the whole trace;bg_endonly seeds the initial guess.) - Adaptive rate-fit distance cap. \(k\) is fit on both the trace-supported tight cap \(r_\max \approx 5\,(T_\max/2)^{1/3}\) nm and a wider one, preferring the wider result unless widening collapses the background toward flat (\(k\to0\), the long-\(r\)/background degeneracy the tight cap guards against). This stops a genuine broad/long-\(r\) component being forced into the background (which would leave a Mellin-unrepresentable pedestal) while still keeping \(k\) determined on short single-peak traces.
mellin_kernel_spectrum()¶
Mellin image \(\Phi(\tfrac12 + i\tau)\) of the orientation-averaged dipolar kernel
\(\varphi(u) = \int_0^1\cos(u(1-3x^2))\,dx\), on the critical line, vectorized over
tau. Computed in closed form — \(\Phi(s) = \Gamma(s)\cos(\pi s/2)\int_0^1
|1-3x^2|^{-s}dx\), the \(x\)-integral splitting (via \(x = x_0\sin\theta\) and
\(x = x_0\cosh u\), \(x_0 = 1/\sqrt3\)) into an exact Beta-function term plus a smooth
quadrature — avoiding \({}_3F_3\) hypergeometric. Valid for
\(0 < \operatorname{Re} s < 3/2\).
mellin_signal_spectrum()¶
Vimg = deer.mellin_signal_spectrum(t, F, tau, delta, F0=1.0, du=0.02,
parabolic=True, fit_level=0.80)
Mellin image \(\tilde V(\tfrac12 + i\tau)\) of the form factor \(F(T)\) via the
\(\delta\)-split: on \([0,\delta]\) integrate analytically; on \([\delta, T_\max]\)
substitute \(u = \ln T\) so \(e^{i\tau\ln T} \to e^{i\tau u}\) has a constant
frequency \(\tau\), and integrate on a log-\(T\) grid of step du
(choose \(du \lesssim \pi/\max|\tau|\)). t in µs, only \(T>0\) used; \(F\) normalized
to $F(0) = $ F0.
The echo top is parabolic (\(F\) is even in \(T\) with negative curvature), so with
parabolic (default) the \([0,\delta]\) term keeps the quadratic
\(F\approx F_0 + b\,T^2\) instead of assuming \(F\) constant:
The curvature \(b\) is least-squares fit over a widened low-\(T\) window (out to where
\(F\) has fallen to fit_level·\(F_0\)), so a few \(\delta\)-wide samples cannot make it
noisy. This removes a systematic error in the recovered \(F_\text{fit}\) at the echo
(the "thin parabola" near \(t=0\)) and lets \(\delta\) be widened. Set
parabolic=False for the original constant-\(F\) split.
mellin_inverse()¶
Inverse Mellin transform on the line \(s = \tfrac12 + i\tau\) back to \(p(w)\):
\(\operatorname{Re}[p(w)] = \tfrac{1}{2\pi}\,w^{-1/2}\int
\operatorname{Re}[P(\tau)\,e^{-i\tau\ln w}]\,d\tau\), with P_tau \(= P(\tfrac12 +
i\tau)\) sampled on tau. Returns the real \(p(w)\) for each w.
mellin_delta()¶
Practical Mellin split point \(\delta\): the first \(T>0\) where the form factor has
fallen to level of \(F(0)\) (\(F(\delta)\approx0.95\)). Falls
back to the first positive sample if \(F\) never drops that far.
The raw level estimate is then clipped to [floor, cap] (µs; set either to
None to disable, and both are clamped to the last sample). The floor is the
key correction for sharp distributions: a fast-decaying form factor crosses
level within a couple of samples, leaving the analytic parabolic \([0,\delta]\)
echo-top anchor too narrow (the "thin parabola"), so the recovered \(F_\text{fit}\)
top comes out too steep and the short-\(r\) density is unstable — widening \(\delta\) to
\(\approx 90\) ns gives the parabolic term enough low-\(T\) support. The cap
(\(\approx 120\) ns) stops a slow-decaying (long-\(r\)) trace from over-smoothing
\(P(r)\) by integrating too much of the modulation analytically.
residual_whiteness()¶
Residual-whiteness goodness-of-fit diagnostic (DeerLab-style). An adequate DEER fit leaves a white (uncorrelated) residual; a structured, oscillating residual is the hallmark of a distance distribution that has not captured all the dipolar modulation — typically an over-smoothed (too-broad) \(P(r)\) at an over-regularized cutoff, but also missing dipolar pathways or orientation selection. Such model inadequacy shows up as autocorrelation even when the residual amplitude already matches the noise level — so the discrepancy principle alone cannot see it (Edwards & Stoll, JMR 288 (2018) 58; Fábregas Ibáñez et al., Magn. Reson. 1 (2020) 209). Returned as a dict:
| Key | Description |
|---|---|
durbin_watson |
\(\mathrm{DW}=\sum(e_i-e_{i-1})^2/\sum e_i^2\in[0,4]\); \(\approx 2\) white, \(<2\) positive autocorrelation (the oscillating-residual case), \(>2\) anti-correlation |
acf1 |
lag-1 autocorrelation \(r_1=\sum e_i e_{i-1}/\sum e_i^2\) (\(\approx 1-\mathrm{DW}/2\)); 0 = white — the headline number |
acf, lags |
the autocorrelation function vs lag (for an autocorrelogram) |
ci95 |
\(\pm 1.96/\sqrt N\), the 95 % white-noise band for the ACF |
white |
bool, \(\lvert r_1\rvert \le\) ci95 (residual consistent with white noise) |
deer_invert_mellin() runs it on the V-space fit residual and
returns it under whiteness. The standalone DEER / PDS Analysis tool surfaces it
as the Residual and Residual ACF top-plot views (autocorrelogram + white-noise
band) and the DW/\(r_1\) verdict in the info panel.
distribution_moments()¶
Shape descriptors of a distance distribution \(P(r)\) — the quantities most PDS work actually reports, computed from the non-central moments \(M_n=\int r^n P(r)\,dr\) of the clipped, area-normalized density (Nekrasov, Matveeva, Syryamina, Agarkin & Bowman, Phys. Chem. Chem. Phys. 2026, DOI 10.1039/D5CP04144A, Eqns. 6–7 & 17). Negative excursions (the signed Mellin output) are clipped before normalizing so these stay proper distribution moments. Returns a dict:
| Key | Description |
|---|---|
mean |
mean distance \(r_0=M_1\) (nm) |
width |
rms width \(\delta r=\sqrt{M_2-M_1^2}\) (nm) |
skew |
skewness \(\gamma=(M_3-3M_1\delta r^2-M_1^3)/\delta r^3\) |
m1–m4 |
the raw non-central moments \(M_1\ldots M_4\) |
Unlike a shape-overlap coefficient, the moments expose the direction of an error — a shifted mean vs. a wrong width vs. a wrong skew.
moment_error_apriori()¶
A priori rms error of the \(n\)-th moment of \(P(r)\) from random noise alone — the closed form of Nekrasov, Matveeva, Syryamina, Agarkin & Bowman, PCCP 2026 (DOI 10.1039/D5CP04144A, Eqn. 9, uniform acquisition):
with \(I(s)\) the analytic dipolar-kernel integral for \(g=2\) (their Eqns. 5–6: \(I(1/3)=4.35466\), \(I(2/3)=3.06158\), \(I(1)=2.77339\), \(I(4/3)=2.56993\)). Because the Mellin transform is additive, the noise decouples from the (unknown) distribution, so the precision of a moment is a property of the acquisition — it needs no inversion and no ground truth.
eps— per-point rms noise on the normalized form factor \(F(t)\) (\(F(0)=1\)); for a background-corrected trace of modulation depth \(\lambda\) this is the raw trace noise amplified by \(1/(\lambda B)\), i.e. \(\varepsilon\approx\sigma_\text{trace}/\lambda\).dt— time step in nanoseconds (the constants \(I(s)\) are fixed for \(g=2\) with the dipolar frequency in GHz, i.e. time in ns); passdt_us*1e3.n_points— number of dipolar-trace points (\(t\ge0\)).n— moment order (1–4). \(n=1\) is the mean distance, the robust one: its \(i^{-4/3}\) weight is dominated by the early points, so \(ME_1\) is nearly flat inn_points— extending the trace does not improve the mean distance; lowering the early-point noise does (the paper's NUA\(_1\) result).
Returns \(ME_n\) in nm\(^n\) (nm for the mean distance). Reproduces the paper's reported
uniform-acquisition \(\mathrm{std}(M_1)=0.0400\) nm for eps=0.04, dt=24, n_points=231
(\(\to 0.0407\)). The empirical scatter of \(M_1\) from a full Tikhonov / Mellin inversion
sits at or below this bound (regularization can only reduce noise-driven scatter),
so \(ME_1\) is a conservative a priori error bar — surfaced in the DEER / PDS Analysis
tool as mean r ± ME₁.
dipolar_kernel()¶
Orientation-averaged DEER kernel (no background, no modulation), shape
(len(t), len(r)) with \(K(0, r) = 1\). Evaluated in closed form via Fresnel
integrals:
dipolar_frequency()¶
Perpendicular dipolar frequency \(\nu_\perp(r) = \nu_{dd}/r^3\) [MHz], r in nm.
background_fit()¶
Fits the intermolecular background on the window \(\text{bg\_start} \le t \le \text{bg\_end}\) and returns the background-corrected form factor. \(V\) is normalized so \(V(0) = 1\) — using a quadratic-vertex estimate of the echo top (over ±5 samples around \(t=0\)) rather than the single nearest sample, so a noisy echo-max point cannot scale the whole form factor and narrow the recovered echo top at high noise. The tail window is fit to \((1-\lambda)\,e^{-(k|t|)^{d/3}}\) with modulation depth \(\lambda = 1 - A\), and
Only the fit window is bounded by [bg_start, bg_end]; \(B(t)\) and \(F(t)\) are
still evaluated over the whole trace. bg_end=None uses everything past
bg_start.
Returns a dict with lambda, k, dim, A, B, form_factor, V_norm,
t, bg_start, bg_end, and the boolean mask of the fit window.
background_general()¶
A flexible empirical intermolecular background, an alternative to the
stretched-exponential background_fit() for traces whose
decay is not \(e^{-(k|t|)^{d/3}}\). The tail baseline is modelled as
with \(d^{\,t}\) a true power. The same convention as
background_fit(): \(V\) is normalized so \(V(0)=1\), the tail
baseline \(g(t)=(1-\lambda)\,B(t)\), so the background normalized to \(B(0)=1\) is
\(B(t)=g(t)/g(0)\) with \(g(0)=a\,e^{\,bc}\) (since \(d^{\,0}=1\)), the modulation depth
\(\lambda = 1 - g(0)\), and \(F(t)=(V(t)/B(t)-(1-\lambda))/\lambda\). The amplitude
\(a\) cancels in the shape \(B(t)=\exp\!\big(b(t+c(d^{\,t}-1))\big)\) — which stays
strictly positive — so \(b,c,d\) set the background shape and \(a\) only its \(t=0\)
level (hence \(\lambda\)). Reachable as deer.deer_invert(..., engine='general')
and via bg_engine='general' in the Mellin / Gaussian engines.
fit— whenTrue(default), the four coefficients are fit on the tail window; any ofa/b/c/dsupplied are used as the initial guess. WhenFalse, they are used directly as the background (manual mode — no fitting).a,b,c,d— the coefficients (seeds when fitting, values when not). Time \(t\) is in microseconds, so they act on \(t\) in µs.
Needs a clean (well-decayed) tail
With four free parameters the model has more freedom than a stretched
exponential, so it will chase residual dipolar modulation if the fit window
still contains it — on a clean, well-decayed tail it recovers \(\lambda\) to ~1%,
but on an under-decayed tail it over-estimates the modulation depth. When
fitting, d is constrained so the \(d^{\,t}\) term keeps \(\ge 5\%\) of its \(t=0\)
amplitude across the window (\(d^{\,\text{span}}\ge 0.05\)): otherwise \(c\) and \(d\)
are unconstrained by the tail (where \(d^{\,t}\) has vanished) and the fit is
degenerate. The model can still be over-parametrized for a gentle decay, so the
fitted \(a\) / \(c\) may be large (a near-constant exponential trading against the
offset) — mathematically valid, \(\lambda\) is unaffected.
Returns the same dict shape as background_fit(), with k /
dim \(=\) NaN (not applicable) and the coefficients in params
(a, b, c, d) plus model='general'. In the DEER / PDS Analysis GUI
this is the "General (a·exp[b(t+c·dᵗ)])" background option, with an
Auto (fit) toggle and per-coefficient boxes — Auto fits and writes the fitted
values back, unchecking it uses the hand-set coefficients directly.
# fit the four coefficients on the tail
bg = deer.background_general(t, V, bg_start=2.0)
print(bg['params'], 'lambda =', round(bg['lambda'], 3))
# or set them by hand (no fitting) and invert with that fixed background
res = deer.deer_invert(t, V, r=r, bg_start=2.0, engine='general',
bg_params=dict(a=0.6, b=-0.05, c=1.0, d=0.5, fit=False))
tikhonov_nnls()¶
Non-negative Tikhonov solution of \(K P = F\): minimizes
\(\lVert K P - F \rVert^2 + \alpha^2 \lVert L P \rVert^2\) subject to \(P \ge 0\) by
solving the augmented NNLS problem \([\,K;\ \alpha L\,]\,P = [\,F;\ 0\,]\). L
defaults to the 2nd-derivative operator from
regularization_matrix(). Returns the masses
\(P \ge 0\).
regularization_matrix()¶
Discrete derivative operator \(L\) for Tikhonov smoothing. order=0 → identity,
1 → first difference, 2 → second difference (curvature, the default).
l_curve()¶
Regularization scan over alphas: for each one solves the NNLS-Tikhonov problem
and records the residual norm rho, the roughness norm eta, the Menger L-curve
curvature, and the gcv score. The optimum is chosen by method:
'gcv'(default) — minimum of the generalized cross-validation score. Robust for DEER, whose L-curve is nearly vertical (the residual stays at the noise floor across decades of \(\alpha\)), so the classic corner is ill-defined and tends to pick a tiny \(\alpha\) ⇒ spiky \(P(r)\). GCV uses the (unconstrained) Tikhonov influence-matrix trace as the effective degrees of freedom paired with the NNLS residual.'curvature'— classic maximum-Menger-curvature L-corner.
Returns a dict with alphas, rho (residual norms), eta (solution norms),
curvature, gcv, alpha_opt, index, method, and P (the solution at the
chosen \(\alpha\)).
default_r_axis()¶
Returns a linear distance grid (nm).
simulate()¶
Forward-simulates a DEER trace from a distance distribution \(P(r)\):
t in µs, r in nm; returns \(V(t)\) with \(V(0) = 1\) (noise aside). Handy for
validating the inversion round-trip and for tests.
NU_DD¶
Module constant — the perpendicular dipolar frequency constant
\(\nu_{dd} = 52.04\ \text{MHz·nm}^3\) (for \(g = 2.0023\)), so that
\(\nu_\perp(r) = \nu_{dd}/r^3\). Override it via the nu_dd argument of the
kernel / simulate / invert functions for other \(g\)-values.
References¶
The methods implemented here draw on the following papers:
- A. G. Matveeva, V. M. Nekrasov, A. G. Maryasov, Analytical solution of the
PELDOR inverse problem using the integral Mellin transform. Phys. Chem.
Chem. Phys. 2017, 19, 32381.
10.1039/C7CP04059H — the analytic
Mellin-transform inversion (
deer_invert_mellin()). - M. M. Nekrasov, A. G. Matveeva, S. A. Syryamina, D. V. Agarkin, M. K. Bowman,
Phys. Chem. Chem. Phys. 2026.
10.1039/D5CP04144A — distance-distribution
moments and a priori moment-error bars
(
distribution_moments(),moment_error_apriori()). - O. Schiemann et al., Benchmark Test and Guidelines for DEER/PELDOR Experiments on Nitroxide-Labeled Biomolecules. J. Am. Chem. Soc. 2021, 143, 17875. 10.1021/jacs.1c07371 — distance-reliability ranges and validation guidelines.
- R. A. Stein, A. H. Beth, E. J. Hustedt, A Straightforward Approach to the Analysis of Double Electron–Electron Resonance Data. Methods Enzymol. 2015, 563, 531. 10.1016/bs.mie.2015.07.031 — parametric Gaussian model and confidence intervals.
- S. A. Dzuba, J. Magn. Reson. 2016, 269, 1.
10.1016/j.jmr.2016.06.001 —
Monte-Carlo (Pake-doublet) solver for the Gaussian engine (
method='mc'). - A. G. Matveeva et al., Z. Phys. Chem. 2017, 231, 463. 10.1515/zpch-2016-0830 — Monte-Carlo Gaussian inversion.
- T. H. Edwards, S. Stoll, J. Magn. Reson. 2018, 288, 58 — optimal Tikhonov
regularization and residual-whiteness diagnostics
(
residual_whiteness()). - L. Fábregas Ibáñez, G. Jeschke, S. Stoll, DeerLab: a comprehensive software package for analyzing dipolar EPR spectroscopy data. Magn. Reson. 2020, 1, 209. 10.5194/mr-1-209-2020 — reference implementation for cross-validation (covariance CI, GCV).
- G. Jeschke et al., DeerAnalysis2006 — a comprehensive software package for analyzing pulsed ELDOR data. Appl. Magn. Reson. 2006, 30, 473. 10.1007/BF03166213 — the zero-time parabola fit, distance-reliability ranges and validation approach this module follows.