1D diffusion analysis

Let's analyse a 15N-edited XSTE measurement of translational diffusion. This experiment was acquired as a single pseudo-2D measurement, with gradient strengths ranging from 2 to 98% of the maximum strength, 0.55 T/m.

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.

using NMRTools
using Plots
using LsqFit
using Measurements
using Statistics

spec = exampledata("pseudo2D_XSTE")
2048×10 NMRData{Float64,2} with dimensions: 
  F1Dim Sampled{Float64} LinRange{Float64}(14.6974, -5.29882, 2048) ReverseOrdered Regular Points,
  X2Dim Sampled{Int64} 1:10 ForwardOrdered Regular Points
             1          28          9         10
 14.6974   167.844   -134.474        -7.54529  -25.4966     9.46338
 14.6876   179.224   -107.113        -8.37372  -39.5615     5.87933
 14.6779   170.19     -90.1916      -28.2758   -41.7948    11.9211
 14.6681   155.913    -90.0144      -65.6379   -28.3213     7.94275
 14.6583   151.127    -97.3782  …  -105.439    -12.8307   -11.4384
  ⋮                             ⋱                           ⋮
 -5.24998  130.441    -13.8422     -126.371    -11.2571     2.41998
 -5.25975  100.636    -16.1934     -105.532    -18.4735    48.7333
 -5.26952   78.3331   -48.7836      -75.8578   -23.2347    85.418
 -5.27929   79.6031   -98.3458  …   -49.7943   -18.4113    91.8344
 -5.28905  107.111   -136.581       -30.7485    -9.7998    66.6335
 -5.29882  147.385   -143.411       -16.2812    -9.23572   29.9128

Set up parameters

The file we have just loaded has an UnknownDimension as the non-frequency dimension. We need to replace this with a GradientDimension and set the gradient strengths that were used. We do this with the setgradientlist function:

gradients = LinRange(0.02, 0.98, size(spec, X2Dim))
Gmax = 0.55 # T/m

spec = setgradientlist(spec, gradients, Gmax)
2048×10 NMRData{Float64,2} with dimensions: 
  F1Dim Sampled{Float64} LinRange{Float64}(14.6974, -5.29882, 2048) ReverseOrdered Regular Points,
  G2Dim Sampled{Float64} LinRange{Float64}(0.011, 0.539, 10) ForwardOrdered Regular Points
             0.011      0.06966670.421667    0.480333    0.539
 14.6974   167.844   -134.474           -7.54529   -25.4966      9.46338
 14.6876   179.224   -107.113           -8.37372   -39.5615      5.87933
 14.6779   170.19     -90.1916         -28.2758    -41.7948     11.9211
 14.6681   155.913    -90.0144         -65.6379    -28.3213      7.94275
 14.6583   151.127    -97.3782     …  -105.439     -12.8307    -11.4384
  ⋮                                ⋱                             ⋮
 -5.24998  130.441    -13.8422        -126.371     -11.2571      2.41998
 -5.25975  100.636    -16.1934        -105.532     -18.4735     48.7333
 -5.26952   78.3331   -48.7836         -75.8578    -23.2347     85.418
 -5.27929   79.6031   -98.3458     …   -49.7943    -18.4113     91.8344
 -5.28905  107.111   -136.581          -30.7485     -9.7998     66.6335
 -5.29882  147.385   -143.411          -16.2812     -9.23572    29.9128

Next, we extract or set other acquisition parameters required for analysis. In particular, we extract the diffusion pulse length, δ, and the diffusion delay, Δ, from the acqus file. We also specify the chemical shift ranges used for plotting, fitting, and for determination of the noise level.

δ = acqus(spec, :p, 30) * 2e-6 # gradient pulse length = p30/2
Δ = acqus(spec, :d, 20)        # diffusion delay = d20
σ = 0.9                        # gradient pulse shape factor (for SMSQ10)

coherence = SQ(H1)             # coherence for diffusion encoding
γ = gyromagneticratio(coherence)            # calculate effective gyromagnetic ratio

g = data(spec, G2Dim)          # list of gradient strengths

# select chemical shift ranges for plotting and fitting
plotrange = 6 .. 10 # ppm
datarange = 7.7 .. 8.6 # ppm
noiseposition = 10.5 # 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 the Stejskal-Tanner equation using the LsqFit package.

# model parameters are (I0, D) - scale D by 1e-10 for a nicer numerical value
model(g, p) = p[1] * exp.(-(γ*δ*σ*g).^2 .* (Δ - δ/3) .* p[2] .* 1e-10)

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

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

# extract the fit parameters and standard errors
pfit = coef(fit)
err = stderror(fit)
D = (pfit[2] ± err[2]) * 1e-10

\[4.275e-11 \pm 3.6e-13\]

Plot the results

Finally, plot the results:

x = LinRange(0, maximum(g)*1.1, 100)
yfit = model(x, pfit)

p1 = scatter(g, integrals .± noise, label="observed",
        frame=:box,
        xlabel="G (T m⁻¹)",
        ylabel="Integrated signal",
        title="",
        ylims=(0,Inf), # make sure y axis starts at zero
        widen=true,
        grid=nothing)
plot!(p1, x, yfit, label="fit")

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))

Estimating the hydrodynamic radius

We can use the known viscosity of water as a function of temperature to estimate the hydrodynamic radius from the measured diffusion coefficient. First, we extract the temperature from the spectrum metadata:

T = acqus(spec, :te)
283.0556

Next, we can create a little function to calculate viscosity for H2O or D2O solvents as a function of temperature:

function viscosity(solvent, T)
	if solvent==:h2o
        A = 802.25336
        a = 3.4741e-3
        b = -1.7413e-5
        c = 2.7719e-8
        gamma = 1.53026
        T0 = 225.334
    elseif solvent==:d2o
        A = 885.60402
        a = 2.799e-3
        b = -1.6342e-5
        c = 2.9067e-8
        gamma = 1.55255
        T0 = 231.832
    else
        @error "solvent not recognised (should be :h2o or :d2o)"
    end

    DT = T - T0
    k = 1.38e-23

    return A * (DT + a*DT^2 + b*DT^3 + c*DT^4)^(-gamma)
end
η = viscosity(:h2o, T)
1.3102945185540058

Finally, we can use the Stokes-Einstein equation to calculate the hydrodynamic radius:

k = 1.38e-23
rH = k*T / (6π * η * 0.001 * D) * 1e9 # in nm

\[3.7 \pm 0.031\]