title: Automated Phase Correction¶
Automated Phase Correction¶
Manual phase correction is often tedious. The .xmr.autophase() method automates this process by utilizing global optimization algorithms to find the ideal zero-order () and first-order () phase angles.
Because Magnetic Resonance Spectra vary wildly in density and Signal-to-Noise Ratio (SNR), xmris provides three distinct scoring methods:
acme: Minimizes spectral entropy. Excellent for dense, high-SNR spectra with multiple overlapping peaks.positivity: Maximizes positive real signal and heavily penalizes negative signal within a defined Region of Interest (ROI). Excellent for sparse, noisy spectra.peak_minima: Minimizes the difference between the minima on either side of a target peak. Good for isolated peaks on a rolling baseline.
Imports & plotting function to compare spectra
import numpy as np
import matplotlib.pyplot as plt
import xarray as xr
# Import xmris simulation tools and ensure accessors are registered
import xmris
from xmris.fitting.simulation import simulate_fid
def plot_spectra(spectra_list, title, target_coord=None, peak_width=None, xlim=None):
"""Dynamically plots an arbitrary number of spectra for visual inspection."""
n_plots = len(spectra_list)
fig, axes = plt.subplots(n_plots, 1, figsize=(10, 2.2 * n_plots), sharex=True)
if n_plots == 1:
axes = [axes]
fig.suptitle(title, fontsize=14, fontweight="bold")
for ax, (da, label) in zip(axes, spectra_list):
if "Distorted" in label:
ax.plot(da.coords["chemical_shift"], np.real(da.values), color="gray", lw=2)
elif "Corrected" in label:
ax.plot(da.coords["chemical_shift"], np.real(da.values), color="blue", lw=2)
else:
ax.plot(da.coords["chemical_shift"], np.real(da.values), color="black", lw=1.2)
ax.set_ylabel("Amplitude")
ax.legend([label], loc="upper right")
ax.grid(True, alpha=0.3)
ax.axhline(0, color="red", linestyle="--", alpha=0.5)
if target_coord is not None and peak_width is not None:
roi_start = target_coord + (peak_width / 2.0)
roi_end = target_coord - (peak_width / 2.0)
ax.axvspan(roi_start, roi_end, color='green', alpha=0.1)
if xlim is not None:
ax.set_xlim(*xlim)
axes[-1].set_xlabel("Chemical Shift (ppm)")
axes[-1].invert_xaxis()
plt.tight_layout()
plt.show()1. Complex Multi-Peak Spectra (ACME)¶
The ACME algorithm works best when there is plenty of signal to evaluate across the entire spectrum. Here, we simulate a dense spectrum, intentionally ruin the phase, and compare the automated solvers.
As expected, acme reconstructs the ground truth perfectly, whereas local ROI solvers struggle because they ignore the wider spectral context.
Make a dummy 1H spectrum
# 1. Simulate an ideal, perfectly phased FID and convert to spectrum
fid_ideal = simulate_fid(
amplitudes=[100, 60, 40, 20], chemical_shifts=[2.0, 3.0, 3.2, 1.3],
reference_frequency=123.2, carrier_ppm=0.0, dampings=[30, 25, 25, 40],
dead_time=0.0, phases=0.0, target_snr=50, n_points=2048
)
spec_ideal = fid_ideal.xmr.to_spectrum().xmr.to_ppm()
# 2. Distort it natively in the frequency domain
p0_distort, p1_distort, pivot_val = 60.0, -800.0, 0.0
spec_distorted = spec_ideal.xmr.phase(
dim="chemical_shift", p0=p0_distort, p1=p1_distort, pivot=pivot_val
)
# 3. Apply manual correction (double apply with inverted signs)
spec_corrected = spec_distorted.xmr.phase(
dim="chemical_shift", p0=-p0_distort, p1=-p1_distort, pivot=pivot_val
)# Step 1-3.
# --> Simulation of the spectrum is hidden in the collpased cell above.
# 4. Run Autophasing
acme_res = spec_distorted.xmr.autophase(method="acme", dim="chemical_shift")
pkmin_res = spec_distorted.xmr.autophase(method="peak_minima", peak_width=5.0, dim="chemical_shift")
posit_res = spec_distorted.xmr.autophase(method="positivity", peak_width=5.0, dim="chemical_shift")
# 5. Plot
plot_spectra([
(spec_distorted, "Distorted Spectrum (xmr.phase)"),
(spec_corrected, "Corrected Spectrum (Manual Inverse)"),
(acme_res, "ACME Phased"),
(pkmin_res, "Peak Minima Phased"),
(posit_res, "Positivity Phased")
], "TEST 1: Dense Spectrum (High SNR)", xlim=(-4, 9))
2. Sparse, Noisy Spectra (Positivity)¶
When dealing with sparse spectra (e.g., hyperpolarized or isolated peaks) hidden in high noise, entropy-based methods like ACME often fail because the noise dominates the objective function.
In these cases, utilizing the positivity or peak_minima methods over a localized peak_width around the highest signal is significantly more robust.
Make a dummy 13C spectrum
# 1. Simulate an ideal, but NOISY FID
fid_noisy = simulate_fid(
amplitudes=[100, 20], chemical_shifts=[171.0, 183.0],
reference_frequency=32.1, carrier_ppm=175.0, spectral_width=5000,
dampings=[15, 15], dead_time=0.0, phases=0.0, target_snr=4, n_points=1024
)
# Exponential apodization helps smooth the noise floor
spec_noisy = fid_noisy.xmr.apodize_exp(lb=10.0).xmr.to_spectrum().xmr.to_ppm()
# 2. Distort it
p0_distort_s, p1_distort_s, pivot_s = -45.0, 500.0, 175.0
spec_distorted_s = spec_noisy.xmr.phase(
dim="chemical_shift", p0=p0_distort_s, p1=p1_distort_s, pivot=pivot_s
)
# 3. Manual correction
spec_corrected_s = spec_distorted_s.xmr.phase(
dim="chemical_shift", p0=-p0_distort_s, p1=-p1_distort_s, pivot=pivot_s
)# Step 1-3.
# --> Simulation of the spectrum is hidden in the collpased cell above.
# 4. Autophase
acme_s = spec_distorted_s.xmr.autophase(dim="chemical_shift", method="acme")
pkmin_s = spec_distorted_s.xmr.autophase(dim="chemical_shift", method="peak_minima", peak_width=10.0)
posit_s = spec_distorted_s.xmr.autophase(dim="chemical_shift", method="positivity", peak_width=10.0)
# 5. Plot
plot_spectra([
(spec_distorted_s, "Distorted Spectrum (xmr.phase)"),
(spec_corrected_s, "Corrected Spectrum (Manual Inverse)"),
(acme_s, "ACME Phased"),
(pkmin_s, "Peak Minima Phased"),
(posit_s, "Positivity Phased")
], "TEST 2: Sparse Spectrum (Low SNR)")
3. Ultra-Low SNR & Parameter Locking¶
In extremely challenging SNR environments, fitting both and simultaneously can lead to the optimizer escaping into local minima by applying massive, unrealistic first-order twists.
You can restrict the solver by locking the first-order phase (p0_only=True) and forcing it to evaluate an explicit coordinate (target_coord) rather than guessing the peak location based on the absolute maximum (which might just be a noise spike).
Make another dummy 13C spectrum (even lower SNR)
# 1. Simulate an ideal, ULTRA-NOISY FID
fid_ultra = simulate_fid(
amplitudes=[100], chemical_shifts=[171.0],
reference_frequency=32.1, carrier_ppm=171.0, spectral_width=5000,
dampings=[15], dead_time=0.0, phases=0.0, target_snr=1.5, n_points=1024
)
spec_ultra = fid_ultra.xmr.apodize_exp(lb=10.0).xmr.to_spectrum().xmr.to_ppm()
# 2. Distort it (Heavy p0 twist, no p1 roll for this test)
p0_distort_u, p1_distort_u, pivot_u = 90.0, 0.0, 171.0
spec_distorted_u = spec_ultra.xmr.phase(
dim="chemical_shift", p0=p0_distort_u, p1=p1_distort_u, pivot=pivot_u
)
spec_corrected_u = spec_distorted_u.xmr.phase(
dim="chemical_shift", p0=-p0_distort_u, p1=-p1_distort_u, pivot=pivot_u
)# Step 1-3.
# --> Simulation of the spectrum is hidden in the collpased cell above.
# 4. Autophase (Testing Positivity configurations)
target = 171.0
roi_width = 8.0
pos_def = spec_distorted_u.xmr.autophase(
dim="chemical_shift", method="positivity", peak_width=roi_width
)
pos_tgt = spec_distorted_u.xmr.autophase(
dim="chemical_shift", method="positivity", peak_width=roi_width, target_coord=target
)
# Locking p1 to 0.0 stabilizes the solver in extreme noise
pos_tgt_p0 = spec_distorted_u.xmr.autophase(
dim="chemical_shift", method="positivity", peak_width=roi_width, target_coord=target, p0_only=True
)
plot_spectra(
[
(spec_distorted_u, "Distorted Spectrum (xmr.phase)"),
(spec_corrected_u, "Corrected Spectrum (Manual Inverse)"),
(pos_def, "Positivity (Default/Argmax)"),
(pos_tgt, f"Positivity (target_coord={target})"),
(pos_tgt_p0, f"Positivity (target_coord={target} + p0_only)")
],
"TEST 3: Ultra-Low SNR Configs",
target_coord=target,
peak_width=roi_width
)
- Chen, L., Weng, Z., Goh, L., & Garland, M. (2002). An efficient algorithm for automatic phase correction of NMR spectra based on entropy minimization. Journal of Magnetic Resonance, 158(1–2), 164–168. 10.1016/s1090-7807(02)00069-1