Phase correction is a frequency-domain operation, but the data you have in hand is often a time-domain FID. Classically that forces you to remember the ritual:
spectrum = fid.xmr.to_spectrum().xmr.autophase() # you must FFT first, by handautophase now removes that ceremony — hand it a FID and it Fourier-transforms
into the frequency domain for you, then phases:
spectrum = fid.xmr.autophase() # auto-FFT happens under the hoodThis is powered by the domain-contract taxonomy: a gate decorator plus two domain decorators sharing one engine, each declaring a function’s contract at the definition site.
| Tier | Decorator | Contract | Cost |
|---|---|---|---|
| gate | @requires_attrs(...) | raises if metadata is missing | |
| domain — funnel | @ensures_domain(...) | transforms into the home domain, result stays there | |
| domain — preserving | @computes_in(...) | round-trips through the home domain, representation restored |
Both domain decorators also resolve the working axis: a dim=None argument is
filled with the spectral dimension actually present (frequency [Hz] or
chemical_shift [ppm]) — an explicit dim is never overridden.
autophase is a funnel operation — you phase in order to inspect the
spectrum, so the result lands there:
A FID and its spectrum are a Fourier pair, , and phase correction acts on the spectrum,
@ensures_domain supplies the so you can start from either side.
Imports & a small plotting helper
import numpy as np
import matplotlib.pyplot as plt
import xmris # registers the .xmr accessor
from xmris.fitting.simulation import simulate_fid
def plot_real(spectra, title):
"""Plot the real part of one or more spectra against the ppm axis."""
fig, axes = plt.subplots(len(spectra), 1, figsize=(7, 2.4 * len(spectra)), sharex=True)
axes = np.atleast_1d(axes)
for ax, (da, label) in zip(axes, spectra):
ppm = da.xmr.to_ppm()
ax.plot(ppm.coords["chemical_shift"], np.real(ppm.values), lw=1.5)
ax.axhline(0, color="red", ls="--", alpha=0.4)
ax.set_ylabel("Re{S}")
ax.legend([label], loc="upper right")
ax.grid(True, alpha=0.3)
axes[-1].set_xlabel("Chemical shift (ppm)")
axes[-1].invert_xaxis()
fig.suptitle(title)
plt.tight_layout()
plt.show()Hand it a raw FID¶
We simulate a noisy, time-domain FID with a 65° zero-order phase error baked in. Note the dimension: this is a FID, not a spectrum.
fid = simulate_fid(
amplitudes=[100, 70, 45],
chemical_shifts=[2.0, 3.5, 5.0],
reference_frequency=123.2,
carrier_ppm=3.0,
dampings=[25, 25, 30],
phases=np.deg2rad(65), # zero-order phase distortion, baked into the FID
target_snr=250,
n_points=2048,
)
fid.dims # -> ('time',)('time',)Now call autophase directly on the FID. There is no to_spectrum() in the
chain — @ensures_domain performs the FFT, and the output comes back as a spectrum.
raw_spectrum = fid.xmr.to_spectrum() # for comparison: the distorted spectrum
phased = fid.xmr.autophase() # auto-FFT + autophase, straight from the FID
print("input FID :", fid.dims)
print("phased :", phased.dims, "| p0 = %.1f°, p1 = %.1f°"
% (phased.attrs["phase_p0"], phased.attrs["phase_p1"]))input FID : ('time',)
phased : ('frequency',) | p0 = -60.2°, p1 = 107.2°
The distorted spectrum has its signal smeared between the real (absorptive) and imaginary (dispersive) channels; after autophasing the peaks stand up cleanly in the real part.
plot_real(
[(raw_spectrum, "Before — distorted (Re)"), (phased, "After — autophased (Re)")],
"autophase() applied directly to a time-domain FID",
)
Already a spectrum? No extra FFT.¶
The same call on data that is already spectral is a no-op on the domain — the
ensures tier sees a spectral dimension and passes the array straight through,
so nothing is transformed twice.
spectrum = fid.xmr.to_spectrum() # already frequency-domain
phased_again = spectrum.xmr.autophase()
print("input :", spectrum.dims, "-> output:", phased_again.dims) # frequency in, frequency outinput : ('frequency',) -> output: ('frequency',)
Whether you start from the FID or the spectrum, autophase does the right thing —
the domain plumbing is handled for you, and the output honestly reports where it
landed.