Working with Spin objects directly¶
SpinPlots makes it easy to create plots with a few lines of code using the .plot() method, but advanced users might want more control over data visualization. This tutorial shows how to work directly with Spin and SpinCollection objects to access raw data, process it, and create custom plots.
Spin and SpinCollection objects¶
SpinPlots provides two main classes for handling NMR data:
Spin: Represents a single NMR dataset (spectrum data, provider, dimensionality, and tag).SpinCollection: A dictionary-like container ofSpinobjects.read_nmr()always returns aSpinCollection, even for a single path.
When a SpinCollection contains exactly one spectrum, convenience properties like .spectrum, .tag, .ndim, and .provider work directly on it — so you can use it just like a Spin in most cases.
Working with a single spectrum¶
Loading a single path returns a SpinCollection with one element. You can access .spectrum, .ndim, .provider, and .tag directly:
# 1D data
glycine = read_nmr("../../data/1D/glycine/pdata/1", provider="bruker")
print(f"Object type: {type(glycine).__name__}")
print(f"Number of spectra: {glycine.size}")
print(f"Dimensions: {glycine.ndim}D")
print(f"Provider: {glycine.provider}")
print(f"Path to file: {glycine.spectrum['path']}")
print("\nAvailable keys in spectrum dict:")
for key in glycine.spectrum.keys():
print(f" - {key}")
# Load a 2D spectrum
spectrum_2d = read_nmr("../../data/2D/16/pdata/1", provider="bruker")
print(f"Object type: {type(spectrum_2d).__name__}")
print(f"Dimensions: {spectrum_2d.ndim}D")
print(f"Provider: {spectrum_2d.provider}")
print(f"Path to file: {spectrum_2d.spectrum['path']}")
print("\nAvailable keys in spectrum dict:")
for key in spectrum_2d.spectrum.keys():
print(f" - {key}")
Working with SpinCollection¶
A SpinCollection behaves like a dictionary of Spin objects, where each spectrum can be accessed by its tag:
# Load multiple spectra into a collection
collection = read_nmr(
["../../data/1D/glycine/pdata/1", "../../data/1D/alanine/pdata/1"],
provider="bruker",
tags=["Glycine", "Alanine"] # Optional custom tags
)
print(f"Type: {type(collection).__name__}")
print(f"Number of spectra: {collection.size}")
print(f"Dimensions: {collection.ndim}D")
print(f"Provider: {collection.provider}")
print(f"Path to file 1: {collection[0].spectrum['path']}")
print(f"Path to file 2: {collection[1].spectrum['path']}")
# Access individual Spin objects by tag
glycine_spin = collection["Glycine"]
alanine_spin = collection["Alanine"]
print(f"Glycine Spin object: {glycine_spin}")
print(f"Alanine Spin object: {alanine_spin}")
# Can also be accessed by index
first_spin = collection[0]
print(f"\nFirst Spin in collection: {first_spin}")
# Get all tags
all_tags = list(collection.spins.keys())
print(f"\nAvailable spectra: {all_tags}")
Spin objects can be easily added or removed from a SpinCollection. Since read_nmr() returns a SpinCollection, use indexing ([0]) to extract the inner Spin before appending:
tyrosine = read_nmr("../../data/1D/tyrosine/pdata/1", provider="bruker")
tyrosine.tag = "Tyrosine" # Works on single-element SpinCollection
collection.append(tyrosine[0]) # Extract the Spin to append
print(f"Collection size after adding: {collection.size}")
print(f"Updated tags: {list(collection.spins.keys())}\n")
collection.remove("Alanine")
print(f"Collection size after removal: {collection.size}")
print(f"Remaining tags: {list(collection.spins.keys())}")
Create custom plots from spectrum data¶
The .spectrum dictionary contains all the data needed to build a plot from scratch with matplotlib. This is useful when you need full control over the figure layout.
fig, ax = plt.subplots(figsize=(6, 5))
# Load glycine spectrum
glycine = read_nmr("../../data/1D/glycine/pdata/1", provider="bruker")
ppm = glycine.spectrum["ppm_scale"]
data = glycine.spectrum["data"]
nuclei = glycine.spectrum["nuclei"]
# Extract the number and nucleus from the nuclei string
number = "".join(filter(str.isdigit, nuclei))
nucleus = "".join(filter(str.isalpha, nuclei))
ax.plot(ppm, data, linewidth=1.5, color='darkblue')
ax.set_xlabel(f"$^{{{number}}}${nucleus} (ppm)", fontsize=14)
ax.set_ylabel("Intensity (a.u.)", fontsize=14)
ax.set_xlim(180, 0)
plt.tight_layout()
plt.show()
You can also iterate over a SpinCollection to build custom multi-spectrum plots:
collection = read_nmr(
["../../data/1D/glycine/pdata/1", "../../data/1D/alanine/pdata/1"],
provider="bruker",
tags=["Glycine", "Alanine"]
)
fig, ax = plt.subplots(figsize=(6, 5))
colors = ['#1f77b4', '#ff7f0e']
for i, (tag, spin) in enumerate(collection):
ppm = spin.spectrum["ppm_scale"]
data = spin.spectrum["norm_max"] # Use normalized data by maximum intensity
color = colors[i % len(colors)]
ax.plot(ppm, data, linewidth=1.5, label=tag, color=color)
ax.set_xlabel("$^{13}$C (ppm)", fontsize=12)
ax.set_ylabel("Normalized Intensity", fontsize=12)
ax.set_xlim(180, 0)
ax.legend()
ax.grid(alpha=0.3, linestyle='--')
plt.tight_layout()
plt.show()
2D spectrum data can also be used to build custom contour plots with projections:
fig = plt.figure(figsize=(8, 6))
gs = fig.add_gridspec(2, 2,
width_ratios=[0.8, 6],
height_ratios=[0.8, 6],
wspace=0.05, hspace=0.05)
ax_main = fig.add_subplot(gs[1, 1]) # Main 2D spectrum
ax_top = fig.add_subplot(gs[0, 1], sharex=ax_main) # Top projection (F2)
ax_left = fig.add_subplot(gs[1, 0], sharey=ax_main) # Left projection (F1)
spectrum_2d = read_nmr("../../data/2D/16/pdata/1", provider="bruker")
data_2d = spectrum_2d.spectrum["data"]
ppm_f1 = spectrum_2d.spectrum["ppm_scale"][0]
ppm_f2 = spectrum_2d.spectrum["ppm_scale"][1]
proj_f1 = spectrum_2d.spectrum["projections"]["f1"]['F1 projection']
proj_f2 = spectrum_2d.spectrum["projections"]["f2"]['F2 projection']
contour_start = 6e6
contour_factor = 1.5
contour_num = 25
contour_levels = contour_start * contour_factor ** np.arange(contour_num)
X, Y = np.meshgrid(ppm_f2, ppm_f1)
contour = ax_main.contour(
X, Y, data_2d,
levels=contour_levels,
colors='black',
linewidths=0.5
)
ax_top.plot(ppm_f2, proj_f2, 'k-', linewidth=1)
ax_left.plot(-proj_f1, ppm_f1, 'k-', linewidth=1)
ax_top.axis('off')
ax_left.axis('off')
ax_main.yaxis.tick_right()
ax_main.yaxis.set_label_position("right")
ax_main.set_xlim(200, 30)
ax_main.set_ylim(400, 60)
ax_left.set_ylim(400, 60)
ax_top.set_xlim(200, 30)
nuclei = spectrum_2d.spectrum["nuclei"]
number_f1, nucleus_f1 = "".join(filter(str.isdigit, nuclei[0])), "".join(filter(str.isalpha, nuclei[0]))
number_f2, nucleus_f2 = "".join(filter(str.isdigit, nuclei[1])), "".join(filter(str.isalpha, nuclei[1]))
ax_main.set_xlabel(f"$^{{{number_f2}}}${nucleus_f2} (ppm)", fontsize=14)
ax_main.set_ylabel(f"$^{{{number_f2}}}${nucleus_f2} (ppm)", fontsize=14)
Find peaks¶
from scipy.signal import find_peaks
alanine = collection["Alanine"]
ppm = alanine.spectrum["ppm_scale"]
data = alanine.spectrum["norm_max"]
# Find peaks
peaks, _ = find_peaks(data, height=0.1, distance=50)
peak_ppm = ppm[peaks]
peak_heights = data[peaks]
fig, ax = plt.subplots(figsize=(7, 6))
ax.plot(ppm, data, 'b-', linewidth=1.5)
ax.plot(peak_ppm, peak_heights, 'ro', label='Detected Peaks')
for i, (x, y) in enumerate(zip(peak_ppm, peak_heights)):
ax.annotate(f'{x:.1f}', xy=(x, y), xytext=(0, 10),
textcoords='offset points', ha='center', fontsize=10)
ax.set_xlabel('$^{13}$C (ppm)', fontsize=14)
ax.set_ylabel('Normalized Intensity', fontsize=14)
ax.legend()
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)
plt.tight_layout()
plt.show()