Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Domain Contracts in Action

Every xmris operation makes a contract about what you get back: the output domain is a pure function of the operation and the input domain — never a surprise. This page proves the two contracts executable-style; the design story lives in The Two Domains.

You callon a FID (time)on a spectrum (frequency/chemical_shift)
apodize_exp(), zero_fill()FID ✅spectrum ✅ (round trip inside)
autophase(), baseline_als()spectrum (funnel ⤵)spectrum
to_spectrum(), to_fid()explicit conversionexplicit conversion
import numpy as np
import matplotlib.pyplot as plt
import xarray as xr

import xmris  # noqa: F401  (registers the .xmr accessor)
from xmris.fitting.simulation import simulate_fid

1. A distorted, time-domain FID

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 error, baked into the FID
    target_snr=250,
    n_points=2048,
)
fid.dims   # -> ('time',)
('time',)

2. Domain-preserving: same physics, either side

Multiplying an FID by eπlbte^{-\pi\,\mathrm{lb}\,t} is convolving its spectrum with a Lorentzian of width lb\mathrm{lb} Hz — one operation, two views. So apodize_exp never changes your representation:

spectrum = fid.xmr.to_spectrum()

fid_smooth = fid.xmr.apodize_exp(lb=3)         # FID in      -> FID out
spec_smooth = spectrum.xmr.apodize_exp(lb=3)   # spectrum in -> spectrum out

print("FID path      :", fid.dims, "->", fid_smooth.dims)
print("spectrum path :", spectrum.dims, "->", spec_smooth.dims)
FID path      : ('time',) -> ('time',)
spectrum path : ('frequency',) -> ('frequency',)

And the two paths are numerically the same operation — transforming the apodized FID reproduces the apodized spectrum to machine precision:

fig, ax = plt.subplots(figsize=(7, 3))
ax.plot(spec_smooth["frequency"], np.real(spec_smooth), lw=2.5, label="spectrum path")
ax.plot(
    spec_smooth["frequency"],
    np.real(fid_smooth.xmr.to_spectrum()),
    lw=1.0,
    ls="--",
    label="FID path → to_spectrum()",
)
ax.set_xlabel("Frequency (Hz)")
ax.set_ylabel("Re{S}")
ax.legend()
ax.grid(True, alpha=0.3)
ax.set_title("Two entry domains, one operation")
plt.tight_layout()
plt.show()
<Figure size 1050x450 with 1 Axes>

The same contract holds for zero_fill: zero-padding the FID is interpolating the spectrum onto a finer grid — so calling it on a spectrum hands back a spectrum with more points, not a FID:

spec_fine = spectrum.xmr.zero_fill(target_points=4096)
print(spectrum.sizes, "->", spec_fine.sizes)
Frozen({'frequency': 2048}) -> Frozen({'frequency': 4096})

3. Funnel: the canonical pipeline, one FFT

Phasing and baseline correction exist for the spectrum — their results land there. That makes the classic monotonic pipeline read naturally, with exactly one Fourier transform executing at the funnel boundary:

result = (
    fid.xmr.zero_fill(target_points=4096)   # time-domain home: no transform
       .xmr.apodize_exp(lb=3)               # time-domain home: no transform
       .xmr.autophase()                     # funnel: FID -> spectrum, stays
       .xmr.baseline_als()                  # already spectral: no transform
)
print("pipeline result:", result.dims, "| real-valued:", not np.iscomplexobj(result.values))
pipeline result: ('frequency',) | real-valued: True
ppm = result.xmr.to_ppm()
fig, ax = plt.subplots(figsize=(7, 3))
ax.plot(ppm["chemical_shift"], ppm.values, lw=1.5)
ax.axhline(0, color="red", ls="--", alpha=0.4)
ax.set_xlabel("Chemical shift (ppm)")
ax.set_ylabel("Re{S}")
ax.invert_xaxis()
ax.grid(True, alpha=0.3)
ax.set_title("zero_fill → apodize → autophase → baseline, straight from the FID")
plt.tight_layout()
plt.show()
<Figure size 1050x450 with 1 Axes>

4. Guardrails

One-way data fails loudly. baseline_als discards the imaginary component, so no valid FID exists behind its output — a time-domain op on it refuses rather than inventing one:

try:
    result.xmr.apodize_exp(lb=2)
except ValueError as err:
    print(err)
Cannot transform real-valued spectral data (dim 'frequency') into the time domain: the imaginary component is gone (e.g. discarded by `baseline_als`), so no valid FID exists behind this spectrum.

Apply time-domain operations before the step that discarded the imaginary part, or pass an explicit existing dimension to operate on.

Explicit foreign dims pass through — for domain-preserving ops. A domain-preserving op named on an axis outside its domain skips conversion entirely — naming another axis (k-space, say) leaves the data untouched. (Funnel ops have no passthrough: they always land in their home domain.)

rng = np.random.default_rng(42)
kspace = xr.DataArray(
    rng.standard_normal((8, 8)),
    dims=["kx", "ky"],
    coords={"kx": np.arange(8), "ky": np.arange(8)},
)
kfilled = kspace.xmr.zero_fill(dim="kx", target_points=16, position="symmetric")
print(dict(kspace.sizes), "->", dict(kfilled.sizes))
{'kx': 8, 'ky': 8} -> {'kx': 16, 'ky': 8}