Skip to content

Calculating NMR Spectra with SimpCalc

This tutorial demonstrates how to use SimpCalc to design and run NMR simulations using SIMPSON. We will cover defining spin systems, setting up the calculator, using different pulse sequences, and handling simulation outputs.

1. Defining the Spin System

The spin system defines the nuclei, their chemical shifts, and couplings. You can define this using a SIMPSON-formatted string or use soprano objects if you have them from DFT calculations.

from __future__ import annotations

import shutil

import matplotlib.pyplot as plt

from simpyson.calculator import SimpCalc

# Check if SIMPSON is installed
SIMPSON_INSTALLED = shutil.which('simpson') is not None
if not SIMPSON_INSTALLED:
    print("WARNING: 'simpson' executable not found in PATH. You can generate input files but cannot run simulations directly.")
else:
    print("SIMPSON detected. Ready to run simulations.")
SIMPSON detected. Ready to run simulations.

# Define a simple 2-spin system
spinsys_str = """
channels 1H
nuclei 1H 1H
shift 1 2.5p 0 0 0 0 0
shift 2 7.0p 0 0 0 0 0
"""

2. Setting up the Calculator

SimpCalc is the main interface. You initialize it with your spin system and simulation parameters.

Note: SimpCalc requires several parameters to be explicitly set, even if standard defaults are desired.

sim = SimpCalc(
    spinsys=spinsys_str,
    proton_frequency='400MHz',
    sw=2000,          # Spectral width in Hz
    np=4096,          # Number of points
    variable_ref=0,   # Center frequency
    # Creating standard static simulation parameters
    spin_rate='10e3', # MAS rate in Hz
    start_operator="I1x",
    detect_operator="I1p",
    method="direct",
    crystal_file="rep100", # Powder averaging
    gamma_angles=10,
    verbose=0
)

# Save file
sim.save('../../examples/calculator/my_simulation.in')

3. Pulse Sequences

SimpCalc supports various pulse sequences. The default is 'no_pulse' (just acquire). You can specify others like 'pulse90' or 'cpmas'.

3.1 Standard Pulse-Acquire (Pulse90)

This uses a simple 90-degree pulse before acquisition.

from simpyson.templates import Pulse90

pulse90 = Pulse90()

spinsys_13C = """
channels 13C
nuclei 13C 13C
shift 1 50p 0 0 0 0 0
shift 2 20p 0 0 0 0 0
"""

sim_90 = SimpCalc(
    spinsys=spinsys_13C,
    proton_frequency='400e6',
    sw=20000,
    np=4096,
    # Standard Parameters
    spin_rate='10e3', # MAS rate in Hz
    start_operator="Inz", # Start with z, then pulse90 flips it
    detect_operator="Inp",
    method="direct",
    crystal_file="rep100",
    gamma_angles=10,
    verbose=0,
    pulse_sequence=pulse90,
    lb=10,                     # Add line broadening
    zerofill=4096              # Add zerofill
)

4. Running the Simulation

If simpson is available, you can run the simulation explicitly. The run method returns a Simpy object containing the result.

output = sim_90.run()

fig, ax = plt.subplots(nrows=1, ncols=3, figsize=(15,5), constrained_layout=True)
ax[0].plot(output.fid['time'], output.fid['real'])
ax[0].set_title('FID')
ax[0].set_xlabel('Time (ms)')
ax[1].plot(output.spe['ppm'], output.spe['real'])
ax[1].set_title('Spectrum in ppm')
ax[1].set_xlim(100, 0)
ax[1].set_xlabel('Chemical Shift (ppm)')
ax[2].plot(output.spe['hz'], output.spe['real'])
ax[2].set_xlim(7500, 0)
ax[2].set_title('Spectrum in Hz')
ax[2].set_xlabel('Frequency (Hz)')
plt.show()
No description has been provided for this image

5. Advanced: Cross Polarization (CPMAS)

In a cross-polarization (CP) experiment, magnetization is typically transfered from an abundant nucleus (I) to a dilute via through-space (dipolar) coupling

Dipolar coupling and internuclear distance

The dipolar coupling constant \(b_{IS}\) between two spins \(I\) and \(S\) depends on their distance \(r\):

\[b_{IS} = -\frac{\mu_0 \gamma_I \gamma_S \hbar}{4\pi r^3}\]

It falls off as \(1/r^3\), so nearby atoms have a much stronger coupling than distant ones.

In the spin system below, one 1H is coupled to two 13C nuclei with very different coupling strengths:

Pair \(b_{IS}\) (Hz) Relative distance
H–C2 −20 000 closer
H–C3 −5 000 further

The role of contact time

During the CP spin-lock magnetization flows from 1H to 13C at a rate governed by \(b_{IS}\), and thus a short contact time only allows magnetization to reach the strongly coupled (nearby) carbon, while a longer contact time gives enough time for the magnetization to travel further, also polarizing the weakly coupled (distant) carbon.

from simpyson.templates import CPMAS

# Spin system: one 1H coupled to two 13C at different distances.
# C2 has a strong dipolar coupling (-20000 Hz, closer to H).
# C3 has a weak dipolar coupling (-5000 Hz, further from H).
cp_spinsys = """
channels 1H 13C
nuclei 1H 13C 13C
shift 1 2.0p 0 0 0 0 0
shift 2 10p 0 0 0 0 0
shift 3 15p 0 0 0 0 0
dipole 1 2 -20000 0 0 0
dipole 1 3 -5000 0 0 0
"""

cp_calc = SimpCalc(
    spinsys=cp_spinsys,
    proton_frequency='400e6',
    sw=20000,
    spin_rate=10000,
    start_operator="I1x",
    detect_operator="I2p+I3p",
    method="direct",
    crystal_file="rep100",
    gamma_angles=10,
    verbose=0,
    np=4096,
    pulse_sequence='cp_mas',
    lb=20,
)
# Short contact time: 500 μs — only the strongly coupled C2 (nearby) is polarized
cp_calc.pulse_sequence.update_parameters(pcp=500)
sim_short = cp_calc.run()

# Long contact time: 5000 μs — magnetization reaches the weakly coupled C3 (further away)
cp_calc.pulse_sequence.update_parameters(pcp=5000)
sim_long = cp_calc.run()
fig, ax = plt.subplots(figsize=(6, 4), constrained_layout=True)

ax.plot(sim_short.spe['ppm'], sim_short.spe['real'] / sim_short.spe['real'].max(), label='Short contact time (500 μs)')
ax.plot(sim_long.spe['ppm'],  sim_long.spe['real']  / sim_long.spe['real'].max()+ 1, label='Long contact time (5000 μs)')

ax.set_xlabel('Chemical Shift (ppm)')
ax.set_ylabel('Normalised Intensity')
ax.set_xlim(20, 5)
ax.set_yticks([])
ax.legend()
ax.set_title('Effect of CP contact time on $^{13}C$ spectrum')

# Annotate peaks
ax.axvline(10, color='gray', linestyle='--', linewidth=0.8, alpha=0.5)
ax.axvline(15, color='gray', linestyle='--', linewidth=0.8, alpha=0.5)
ax.text(8.6, 0.15, 'C$_2$\n(10 ppm, close)', ha='center', fontsize=8, color='gray')
ax.text(14, 0.15, 'C$_3$\n(15 ppm, distant)', ha='center', fontsize=8, color='gray')

plt.show()
No description has been provided for this image