Continuous Wave Saturation Experiments

The following code analyzes data from a steady-state experiment similar to the original work of Henkelman et al. In this experiment, the magnetization of the coupled spin system is saturated with off-resonant continuous waves of the exponentially spaced frequencies:

Δ = exp.(range(log(0.01e3), log(100e3), length=20)) * 2π # rad/s
20-element Vector{Float64}:
     62.83185307179588
    102.02490149810555
    165.66566187064444
    269.00404822785634
    436.8025162599944
    709.2697655220819
   1151.6957470645957
   1870.0967647060686
   3036.619626561797
   4930.79231537499
   8006.5058674764105
  13000.777989786218
  21110.360891171575
  34278.51297096368
  55660.65201622967
  90380.47203203167
 146757.70816250978
 238301.75281093005
 386948.8431222893
 628318.5307179587

and the amplitudes:

ω1_dB = -60:5:-5 # dB
ω1 = @. 10^(ω1_dB / 20) * π / 2 / 11.4e-6 # rad/s
12-element Vector{Float64}:
   137.78915147323656
   245.02761099159085
   435.7275555173729
   774.845340363136
  1377.8915147323655
  2450.276109915909
  4357.275555173728
  7748.453403631359
 13778.915147323654
 24502.76109915909
 43572.75555173729
 77484.53403631358

The waves were applied for 7 seconds to ensure a steady state. Thereafter, the magnetization was excited with a π/2-pulse and an FID was acquired. The repetition times was 30s to ensure full recovery to thermal equilibrium.

We fit the data with Henkelman's closed form solution to this steady-state problem while assuming a Lorentzian lineshape for the free spin pool, and different lineshapes for the semi-solid spin pool:

g_Lorentzian(Δ, T2) = T2 / π / (1 + (T2 * Δ)^2)
g_Gaussian(Δ, T2) = T2 / sqrt(2π) * exp(-(T2 * Δ)^2 / 2)
g_superLorentzian(Δ, T2) = T2 * sqrt(2 / π) * quadgk(ct -> exp(- 2 *  (T2 * Δ / (3 * ct^2 - 1))^2) / abs(3 * ct^2 - 1), 0, sqrt(1 / 3), 1)[1];

For this data analysis we need the following packages:

using MRIgeneralizedBloch
using QuadGK
using LsqFit
using LinearAlgebra
using Statistics
using Printf
using Plots

The raw data is stored in a separate github repository and the following functions return the URL to the individual files:

MnCl2_data(ω1_dB) = string("https://github.com/JakobAsslaender/MRIgeneralizedBloch_NMRData/blob/main/20210419_1mM_MnCl2/ja_PreSat_v2%20(", ω1_dB, ")/1/data.2d?raw=true")
BSA_data(ω1_dB)   = string("https://github.com/JakobAsslaender/MRIgeneralizedBloch_NMRData/blob/main/20210416_15%25BSA_2ndBatch/ja_PreSat_v2%20(", ω1_dB, ")/1/data.2d?raw=true");

which can be loaded with functions implemented in this file:

include(string(pathof(MRIgeneralizedBloch), "/../../docs/src/load_NMR_data.jl"));

We store the off-resonance frequencies and wave amplitudes in a single matrix for convenience:

x = zeros(Float64, length(ω1) * length(Δ), 2)
x[:,1] = repeat(Δ, length(ω1))
x[:,2] = vec(repeat(ω1, 1, length(Δ))');

MnCl$_2$ Sample

We load the first data point of each FID:

M = zeros(Float64, length(Δ), length(ω1))
for i = 1:length(ω1_dB)
    M[:,i] = load_first_datapoint(MnCl2_data(ω1_dB[i]); set_phase=:abs)
end
M ./= maximum(M);

In contrast to the inversion-recovery experiment, the phase of the signal was not stable. Therefore, we took the absolute value of the signal by setting the flag set_phase=:abs.

The MnCl$_2$-data can be described with a single compartment model:

function single_compartment_model(x, p)
    (m0, R1, T2) = p

    Δ  = @view x[:,1]
    ω1 = @view x[:,2]

    Rrf = @. π * ω1^2 * g_Lorentzian(Δ, T2)
    m = @. m0 * R1 / (R1 + Rrf)
    return m
end;

(cf. Eqs. (14) and (15) in the paper).

As this model is merely a function of the relaxation times T₁ and T₂, we forgo a fitting routine and use the estimates from the Inversion Recovery Experiments instead:

R1 = 1.479 # 1/s
T2 = 0.075 # s

Visually, this model describes the data well:

p = plot(xlabel="Δ [rad/s]", ylabel="M / max(M)", xaxis=:log, legend=:none)
[scatter!(p, Δ, M[:,i], color=i) for i=1:length(ω1)]
[plot!(p, Δ, reshape(single_compartment_model(x, [1,R1,T2]), length(Δ), length(ω1))[:,i], color=i) for i=1:length(ω1)]

Bovine Serum Albumin Sample

We acquired the same data for the BSA sample, which we load:

for i = 1:length(ω1_dB)
    M[:,i] = load_first_datapoint(BSA_data(ω1_dB[i]); set_phase=:abs)
end
M ./= maximum(M);

We model the steady-state magnetization as described by Henkelman et al.:

function Henkelman_model(x, p; lineshape=:superLorentzian)
    (m0, m0s, R1f, R1s, T2f, T2s, Rx) = p

    m0s /= 1 - m0s # switch from m0s + m0f = 1 to m0f = 1 normalization

    Δ  = @view x[:,1]
    ω1 = @view x[:,2]

    Rrf_f = @. π * ω1^2 * g_Lorentzian(Δ, T2f)

    if lineshape == :Lorentzian
        Rrf_s = @. π * ω1^2 * g_Lorentzian(Δ, T2s)
    elseif lineshape == :Gaussian
        Rrf_s = @. π * ω1^2 * g_Gaussian(Δ, T2s)
    elseif lineshape == :superLorentzian
        Rrf_s = @. π * ω1^2 * g_superLorentzian(Δ, T2s)
    end

    m = @. m0 * (R1s * Rx * m0s + Rrf_s * R1f + R1f * R1s + R1f * Rx) / ((R1f + Rrf_f + Rx * m0s) * (R1s + Rrf_s + Rx) - Rx^2 * m0s)
    return m
end;

Here, we use a fitting routine to demonstrate the best possible fit with each of the three lineshapes. We define an initialization for the fitting routine p0 = [m0, m0s, R1f, R1s, T2f, T2s, Rx] and set some reasonable bounds:

p0   = [  1,0.01,   1,   5,0.052,  1e-5, 40]
pmin = [  0,   0,   0,   0,0.052,  1e-6,  1]
pmax = [Inf,   1, Inf, Inf,0.052, 10e-3,100];

Note that we fixed T₂ᶠ = 52ms to the value estimated with the Inversion Recovery Experiments as T₂ᶠ is poorly defined by this saturation experiment.

Super-Lorentzian Lineshape

Fitting the model with a super-Lorentzian lineshape to the data achieves good concordance:

fit = curve_fit((x, p) -> Henkelman_model(x, p; lineshape=:superLorentzian), x, vec(M), p0, lower=pmin, upper=pmax)
fit_std = stderror(fit)

p = plot(xlabel="Δ [rad/s]", ylabel="M / max(M)", xaxis=:log, legend=:none)
[scatter!(p, Δ, M[:,i], color=i) for i=1:length(ω1)]
[plot!(p, Δ, reshape(Henkelman_model(x, fit.param), length(Δ), length(ω1))[:,i], color=i) for i=1:length(ω1)]

Lorentzian Lineshape

The Lorentzian lineshape, on the other hand, does not fit the data well:

fit = curve_fit((x, p) -> Henkelman_model(x, p; lineshape=:Lorentzian), x, vec(M), p0, lower=pmin, upper=pmax)
fit_std = stderror(fit)

p = plot(xlabel="Δ [rad/s]", ylabel="M / max(M)", xaxis=:log, legend=:none)
[scatter!(p, Δ, M[:,i], color=i) for i=1:length(ω1)]
[plot!(p, Δ, reshape(Henkelman_model(x, fit.param), length(Δ), length(ω1))[:,i], color=i) for i=1:length(ω1)]

Gaussian Lineshape

And the Gaussian lineshape does not not fit the data well either:

fit = curve_fit((x, p) -> Henkelman_model(x, p; lineshape=:Lorentzian), x, vec(M), p0, lower=pmin, upper=pmax)
fit_std = stderror(fit)

p = plot(xlabel="Δ [rad/s]", ylabel="M / max(M)", xaxis=:log, legend=:none)
[scatter!(p, Δ, M[:,i], color=i) for i=1:length(ω1)]
[plot!(p, Δ, reshape(Henkelman_model(x, fit.param), length(Δ), length(ω1))[:,i], color=i) for i=1:length(ω1)]

This page was generated using Literate.jl.