1D relaxation analysis

Let's analyse a measurement of 1H T2 relaxation, acquired as a single pseudo-2D spectrum. First, we need to load the required packages. We will use LsqFit for the non-linear least squares fitting, Measurements to handle uncertainties, and Statistics for calculation of means and standard deviations.

Data have been processed in Topspin (using xf2), so can be loaded using the loadnmr function.

using NMRTools
using Plots
using LsqFit
using Measurements
using Statistics

spec = exampledata("pseudo2D_T2")
 Downloading artifact: pseudo2D_T2

Set up parameters

The experiment uses a vclist to encode the relaxation time. The contents of this list are automatically parsed when the spectrum is loaded, and can be accessed with the acqus command:

acqus(spec, :vclist)
16-element Vector{Int64}:
   1
   2
   4
   8
  12
  16
  24
  32
  48
  64
  96
 128
 192
 256
 320
 384

Each loop corresponds to a delay of 4 ms, so from this we can calculate a list of relaxation times. The spectrum we have just loaded has an UnknownDimension as the non-frequency dimension. We need to replace this with a TrelaxDimension that encodes the relaxation delays, and we can do this with the setrelaxtimes function:

τ = acqus(spec, :vclist) * 4e-3
spec = setrelaxtimes(spec, τ, "s")

Next, we specify the chemical shift ranges used for plotting, fitting, and for determination of the noise level.

plotrange = 0.7 .. 1.0 # ppm
datarange = 0.8 .. 0.9 # ppm
noiseposition = -2 # ppm

Plot the data

To take a quick look at the data, we can plot the experiment either as 3D lines using the plot command, or as a heatmap:

plot(
    plot(spec[plotrange,:]),
    heatmap(spec[plotrange,:])
)

Calculate noise and peak integrals

Now, we can determine the measurement noise, by taking the standard deviation of integrals across the different gradient points:

# create a selector for the noise, matching the width of the data range
noisewidth = datarange.right - datarange.left
noiserange = (noiseposition-0.5noisewidth)..(noiseposition+0.5noisewidth)

# integrate over the noise regions and take the standard deviation
# (calculate the sum over the frequency dimension F1Dim, and use
# `data` to convert from NMRData to a regular array)
noise = sum(spec[noiserange,:], dims=F1Dim) |> data |> std

# calculate the integral of the data region similarly, using vec to convert to a list
integrals = sum(spec[datarange,:], dims=F1Dim) |> data |> vec

# normalise noise and integrals by the maximum value
noise /= maximum(integrals)
integrals /= maximum(integrals)

Fitting

Now, we can fit the data to an exponential decay using the LsqFit package:

# model parameters are (I0, R2)
function model(t, p)
    I0 = p[1]
    R2 = p[2]
    @. I0 * exp(-R2 * t)
end

p0 = [1.0, 1.0] # rough guess of initial parameters

fit = curve_fit(model, τ, integrals, p0) # run the fit

# extract the fit parameters and standard errors
pfit = coef(fit)
err = stderror(fit)
R2 = (pfit[2] ± err[2])

\[0.837 \pm 0.017\]

So we see that the fitted R₂ relaxation rate is 0.844 ± 0.016 s⁻¹

Plot the results

Finally, plot the results:

# calculate the best-fit curve across 100 points so it looks nice and smooth
x = LinRange(0, maximum(τ)*1.1, 100)
yfit = model(x, pfit)

p1 = scatter(τ, integrals .± noise, label="observed",
        frame=:box,
        xlabel="Relaxation time (s)",
        ylabel="Integrated signal",
        title="",
        ylims=(0,Inf), # make sure y axis starts at zero
        widen=true,
        grid=nothing)
plot!(p1, x, yfit, label="fit (R₂ = $R2 s⁻¹)")

p2 = plot(spec[plotrange,1],linecolor=:black)
plot!(p2, spec[datarange,1], fill=(0,:orange), linecolor=:red)
hline!(p2, [0], c=:grey)
title!(p2, "")

plot(p1, p2, layout=(1,2))