Nuclear magnetic resonance: Larmor frequencies and the field-independent chemical shift#

Bloch and Purcell (1946) showed that nuclei in a static field \(B_0\) absorb radio-frequency energy sharply at the Larmor frequency \(\nu_0=\gamma B_0/(2\pi)\). The effect became a chemical tool when it was found that a nucleus’s surrounding electrons shield it slightly, so the exact resonance frequency depends on its chemical environment. This example computes \(^1\mathrm{H}\) and \(^{13}\mathrm{C}\) Larmor frequencies against field strength. It then places three proton environments of methyl acetate-like shielding on the frequency axis at two field strengths and shows that converting to ppm with chemical_shift_ppm() makes the spectrum field-independent.

import matplotlib.pyplot as plt
import numpy as np
import scipy.constants as sc

from chemistrykit.spectro.systems.nmr import chemical_shift_ppm, larmor_frequency

gamma_h = sc.physical_constants["proton gyromag. ratio"][0]  # rad s^-1 T^-1
gamma_c13 = 6.728284e7  # rad s^-1 T^-1, carbon-13

for field in (1.41, 7.05, 9.40, 14.1, 23.5):
    print(f"B0 = {field:5.2f} T: 1H {larmor_frequency(gamma_h, field) / 1e6:7.1f} MHz, 13C {larmor_frequency(gamma_c13, field) / 1e6:6.1f} MHz")
B0 =  1.41 T: 1H    60.0 MHz, 13C   15.1 MHz
B0 =  7.05 T: 1H   300.2 MHz, 13C   75.5 MHz
B0 =  9.40 T: 1H   400.2 MHz, 13C  100.7 MHz
B0 = 14.10 T: 1H   600.3 MHz, 13C  151.0 MHz
B0 = 23.50 T: 1H  1000.6 MHz, 13C  251.6 MHz

Chemical shielding: each proton environment resonates at \(\nu=\nu_\text{ref}(1+\delta\times10^{-6})\), where \(\nu_\text{ref}\) is the TMS reference frequency. In Hz the separations grow with the field. In ppm they do not.

shifts_true = {"CH3-C(=O)": 2.05, "O-CH3": 3.67, "TMS": 0.0}
fig, axes = plt.subplots(2, 2, figsize=(11, 6))
for row, field in enumerate((7.05, 14.1)):
    ref = larmor_frequency(gamma_h, field)
    freqs = {name: ref * (1.0 + d * 1e-6) for name, d in shifts_true.items()}
    offsets_hz = np.array([f - ref for f in freqs.values()])
    ppm = chemical_shift_ppm(np.array(list(freqs.values())), ref)
    print(f"{ref / 1e6:.0f} MHz: offsets from TMS {np.round(offsets_hz, 1)} Hz -> {np.round(ppm, 3)} ppm")

    axes[row, 0].vlines(offsets_hz, 0.0, [3, 3, 1], color="tab:blue", lw=2)
    axes[row, 0].set_xlim(2500.0, -200.0)
    axes[row, 0].set_title(f"{ref / 1e6:.0f} MHz: offset in Hz")
    axes[row, 0].set_xlabel("frequency above TMS (Hz)")
    axes[row, 1].vlines(ppm, 0.0, [3, 3, 1], color="tab:green", lw=2)
    axes[row, 1].set_xlim(4.5, -0.5)
    axes[row, 1].set_title(f"{ref / 1e6:.0f} MHz: chemical shift in ppm")
    axes[row, 1].set_xlabel(r"$\delta$ (ppm)")
    for name, d in shifts_true.items():
        axes[row, 1].annotate(name, (d, 3.1 if name != "TMS" else 1.1), ha="center", fontsize=8)
    for ax in axes[row]:
        ax.set_ylim(0.0, 3.8)
        ax.set_yticks([])
fig.suptitle("NMR resonance: Hz separations scale with B0, chemical shifts do not")
fig.tight_layout()
plt.show()
NMR resonance: Hz separations scale with B0, chemical shifts do not, 300 MHz: offset in Hz, 300 MHz: chemical shift in ppm, 600 MHz: offset in Hz, 600 MHz: chemical shift in ppm
300 MHz: offsets from TMS [ 615.4 1101.6    0. ] Hz -> [2.05 3.67 0.  ] ppm
600 MHz: offsets from TMS [1230.7 2203.3    0. ] Hz -> [2.05 3.67 0.  ] ppm

Total running time of the script: (0 minutes 0.069 seconds)

Gallery generated by Sphinx-Gallery