Process raw NMR free-induction decay (FID) data through apodization, zero-filling,
Fourier transformation, and phase correction to obtain interpretable 1D and 2D
spectra. Automate peak picking, chemical shift referencing, and J-coupling constant
extraction using nmrglue and scipy.
When to Use This Skill
You have raw NMR data in Bruker, Varian/Agilent, or JCAMP-DX format and need to
convert it to a frequency-domain spectrum.
You want to apply apodization (window functions) and zero-filling before
Fourier transformation to improve resolution or sensitivity.
You need automated peak picking on 1D or 2D NMR spectra and want results as a
pandas DataFrame.
You are performing chemical shift referencing against an internal standard
(TMS, DSS, TSP) or a known solvent peak.
You need to extract J-coupling constants from well-resolved multiplets using
line shape fitting.
You want to overlay, compare, or subtract spectra from different samples or
experiments (titration, temperature series).
Background & Key Concepts
Free Induction Decay (FID)
The FID is the time-domain NMR signal recorded after a radiofrequency pulse. It is a
sum of exponentially decaying sinusoids; each frequency corresponds to a resonance.
The complex FID is Fourier-transformed to yield the frequency-domain spectrum.
Apodization (Window Functions)
Before Fourier transformation, a window function is multiplied with the FID to:
Improve sensitivity (exponential multiplication — line broadening).
Improve resolution (Lorentz-to-Gauss transformation — line narrowing).
Reduce truncation artifacts (cosine/sine bells for 2D data).
Common window functions:
Function
Effect
Use Case
Exponential (LB)
Sensitivity
1D 13C, 15N
Gaussian (GM)
Resolution
1D 1H
Cosine bell
Balanced
2D indirect dimension
Sine bell
Resolution
2D direct dimension
Zero-Filling
Appending zeros to the FID before Fourier transformation increases the number of
spectral points and therefore digital resolution. Zero-filling by a factor of 2 is
standard; zero-filling by 4–8 is used for high-resolution work.
Phase Correction
The spectrum has real (absorption) and imaginary (dispersion) components. Phase
correction (zeroth-order P0 and first-order P1) ensures all peaks have pure
absorption lineshapes, which are symmetric and integrable.
Chemical Shift Referencing
Chemical shifts in ppm are calculated relative to a reference compound:
1H / 13C: TMS (tetramethylsilane) at 0.00 ppm
Aqueous solutions: DSS or TSP at 0.00 ppm
31P: H3PO4 at 0.00 ppm
J-Coupling Constants
Spin-spin coupling splits resonances into multiplets. The coupling constant J (in Hz)
equals the frequency separation between adjacent lines of a first-order multiplet.
Accurate J values require fitting each line of the multiplet with a Lorentzian.
import numpy as np
import pandas as pd
from scipy.integrate import trapezoid
# Integrate spectral regions (ppm intervals)
regions = {
"CH3 (TMS)" : (0.0, 0.1),
"Aliphatic CH3": (0.8, 1.0),
"Aliphatic CH2": (1.2, 1.6),
"CH alpha" : (3.4, 3.7),
"Aromatic" : (7.0, 8.0),
}
integrals = {}
for label, (lo, hi) in regions.items():
# ppm_axis_ref decreases, so lo and hi may need swapping depending on direction
mask = (ppm_axis_ref >= lo) & (ppm_axis_ref <= hi)
integrals[label] = trapezoid(spectrum[mask], ppm_axis_ref[mask])
# Normalize to reference region (e.g., TMS 9H)
ref_label = "CH3 (TMS)"
ref_protons = 9if integrals[ref_label] != 0:
scale = ref_protons / abs(integrals[ref_label])
normalized = {k: abs(v) * scale for k, v in integrals.items()}
else:
normalized = integrals
df_integrals = pd.DataFrame({
"Region" : list(regions.keys()),
"ppm Low" : [v[0] for v in regions.values()],
"ppm High" : [v[1] for v in regions.values()],
"Raw Integral" : list(integrals.values()),
"Normalized (Hcount)": list(normalized.values()),
})
print(df_integrals.to_string(index=False))
df_integrals.to_csv("integrations.csv", index=False)
Spectral Comparison / Overlay
import numpy as np
import matplotlib.pyplot as plt
# Suppose you have two spectra from Step 2, stored as arrays# spectrum_A, spectrum_B and ppm_axis_A, ppm_axis_B# Scale both spectra to unit max intensity for visual comparisondefnormalize_spectrum(s):
s_norm = s - s.min()
return s_norm / s_norm.max()
fig, ax = plt.subplots(figsize=(14, 5))
ax.plot(ppm_axis_ref, normalize_spectrum(spectrum), lw=0.9,
color="steelblue", label="Sample A", alpha=0.85)
# ax.plot(ppm_axis_ref_B, normalize_spectrum(spectrum_B), lw=0.9,# color="coral", label="Sample B", alpha=0.85)
ax.invert_xaxis()
ax.set_xlabel("Chemical Shift (ppm)", fontsize=12)
ax.set_ylabel("Normalized Intensity", fontsize=12)
ax.set_title("Spectral Overlay Comparison", fontsize=13)
ax.legend()
ax.grid(alpha=0.3)
plt.tight_layout()
plt.savefig("spectral_overlay.png", dpi=150)
plt.show()
NMRPipe Format Export
import nmrglue as ng
import numpy as np
# Convert processed spectrum to NMRPipe format for further analysis
udic_pipe = ng.bruker.guess_udic(dic, data_final)
# Write NMRPipe file
ng.pipe.write("spectrum.ft2", ng.pipe.create_dic(udic_pipe), data_final.real)
print("Exported spectrum.ft2 in NMRPipe format")
Troubleshooting
FileNotFoundError: acqus or fid not found
# Ensure you point to the numbered experiment directory, not the sample directoryls /path/to/bruker/sample/1/ # should contain: fid, acqus, procs/