diff --git a/KomaMRIBase/src/datatypes/Phantom.jl b/KomaMRIBase/src/datatypes/Phantom.jl index 5f155f28e..c825b33b8 100644 --- a/KomaMRIBase/src/datatypes/Phantom.jl +++ b/KomaMRIBase/src/datatypes/Phantom.jl @@ -1,5 +1,5 @@ """ - obj = Phantom(name, x, y, z, ρ, T1, T2, T2s, Δw, Dλ1, Dλ2, Dθ, motion) + obj = Phantom(name, x, y, z, ρ, T1, T2, T2s, Δw, Dλ1, Dλ2, Dθ, motion, B1) The Phantom struct. Most of its field names are vectors, with each element associated with a property value representing a spin. This struct serves as an input for the simulation. @@ -18,6 +18,7 @@ a property value representing a spin. This struct serves as an input for the sim - `Dλ2`: (`::AbstractVector{T<:Real}`) spin Dλ2 (diffusion) parameter vector - `Dθ`: (`::AbstractVector{T<:Real}`) spin Dθ (diffusion) parameter vector - `motion`: (`::AbstractMotion{T<:Real}`) motion +- `B1`: (`::AbstractVector{Complex{T<:Real}}`) spin transmit B1 parameter vector # Returns - `obj`: (`::Phantom`) Phantom struct @@ -48,6 +49,7 @@ julia> obj.ρ #Diff::Vector{DiffusionModel} #Diffusion map #Motion motion::AbstractMotion{T} = NoMotion{eltype(x)}() + B1::AbstractVector{Complex{T}} = Complex.(ones(eltype(x), size(x))) end const NON_STRING_PHANTOM_FIELDS = Iterators.filter(x -> fieldtype(Phantom, x) != String, fieldnames(Phantom)) diff --git a/KomaMRIBase/test/runtests.jl b/KomaMRIBase/test/runtests.jl index 238e74b88..0e4857b1b 100644 --- a/KomaMRIBase/test/runtests.jl +++ b/KomaMRIBase/test/runtests.jl @@ -381,8 +381,9 @@ end Dλ1 = [-4e-6; -2e-6; 0.0; 2e-6; 4e-6] Dλ2 = [-6e-6; -3e-6; 0.0; 3e-6; 6e-6] Dθ = [-8e-6; -4e-6; 0.0; 4e-6; 8e-6] - obj1 = Phantom(name=name, x=x, y=y, z=z, ρ=ρ, T1=T1, T2=T2, T2s=T2s, Δw=Δw, Dλ1=Dλ1, Dλ2=Dλ2, Dθ=Dθ) - obj2 = Phantom(name=name, x=x, y=y, z=z, ρ=ρ, T1=T1, T2=T2, T2s=T2s, Δw=Δw, Dλ1=Dλ1, Dλ2=Dλ2, Dθ=Dθ) + B1 = Complex.([0.8; 0.9; 1.0; 1.1; 1.2]) + obj1 = Phantom(name=name, x=x, y=y, z=z, ρ=ρ, T1=T1, T2=T2, T2s=T2s, Δw=Δw, Dλ1=Dλ1, Dλ2=Dλ2, Dθ=Dθ, B1=B1) + obj2 = Phantom(name=name, x=x, y=y, z=z, ρ=ρ, T1=T1, T2=T2, T2s=T2s, Δw=Δw, Dλ1=Dλ1, Dλ2=Dλ2, Dθ=Dθ, B1=B1) @test obj1 == obj2 # Test size and length definitions of a phantom @@ -578,7 +579,8 @@ end Dλ1, Dλ2, Dθ, - simplemotion + simplemotion, + B1 ) rng = 1:2:5 obs2 = Phantom( @@ -595,6 +597,7 @@ end Dλ2[rng], Dθ[rng], simplemotion[rng], + B1[rng] ) @test obs1[rng] == obs2 @test @view(obs1[rng]) == obs2 @@ -617,13 +620,14 @@ end [Dλ1; Dλ1[rng]], [Dλ2; Dλ2[rng]], [Dθ; Dθ[rng]], - vcat(obs1.motion, obs2.motion, length(obs1), length(obs2)) + vcat(obs1.motion, obs2.motion, length(obs1), length(obs2)), + [B1; B1[rng]] ) @test obs1 + obs2 == oba # Test scalar multiplication of a phantom c = 7 - obc = Phantom(name=name, x=x, y=y, z=z, ρ=c*ρ, T1=T1, T2=T2, T2s=T2s, Δw=Δw, Dλ1=Dλ1, Dλ2=Dλ2, Dθ=Dθ) + obc = Phantom(name=name, x=x, y=y, z=z, ρ=c*ρ, T1=T1, T2=T2, T2s=T2s, Δw=Δw, Dλ1=Dλ1, Dλ2=Dλ2, Dθ=Dθ, B1=B1) @test c * obj1 == obc #Test brain phantom 2D diff --git a/KomaMRICore/src/datatypes/Spinor.jl b/KomaMRICore/src/datatypes/Spinor.jl index 8d8977bbb..a7aae0ad5 100644 --- a/KomaMRICore/src/datatypes/Spinor.jl +++ b/KomaMRICore/src/datatypes/Spinor.jl @@ -149,7 +149,7 @@ IEEE Transactions on Medical Imaging, 10(1), 53-65. doi:10.1109/42.75611 # Arguments - `φ`: (`::Real`, `[rad]`) φ angle -- `nxy`: (`::Real`) nxy factor +- `nxy`: (`::Complex`) nxy factor - `nz`: (`::Real`) nz factor # Returns diff --git a/KomaMRICore/src/simulation/SimMethods/Bloch/BlochCPU.jl b/KomaMRICore/src/simulation/SimMethods/Bloch/BlochCPU.jl index ddd74ae7a..da3c6eb8c 100644 --- a/KomaMRICore/src/simulation/SimMethods/Bloch/BlochCPU.jl +++ b/KomaMRICore/src/simulation/SimMethods/Bloch/BlochCPU.jl @@ -3,6 +3,7 @@ struct BlochCPUPrealloc{T} <: PreallocResult{T} M::Mag{T} # Mag{T} Bz_old::AbstractVector{T} # Vector{T}(Nspins x 1) Bz_new::AbstractVector{T} # Vector{T}(Nspins x 1) + B1::AbstractVector{Complex{T}} # Vector{Complex{T}}(Nspins x 1) ϕ::AbstractVector{T} # Vector{T}(Nspins x 1) Rot::Spinor{T} # Spinor{T} ΔBz::AbstractVector{T} # Vector{T}(Nspins x 1) @@ -13,6 +14,7 @@ Base.view(p::BlochCPUPrealloc, i::UnitRange) = begin p.M[i], p.Bz_old[i], p.Bz_new[i], + p.B1[i], p.ϕ[i], p.Rot[i], p.ΔBz[i] @@ -28,12 +30,13 @@ function prealloc(sim_method::Bloch, backend::KA.CPU, obj::Phantom{T}, M::Mag{T} ), similar(obj.x), similar(obj.x), + similar(obj.x, Complex{eltype(obj.x)}), similar(obj.x), Spinor( similar(M.xy), similar(M.xy) ), - obj.Δw ./ T(2π .* γ) + obj.Δw ./ T(2π .* γ) # Why is this not similar(obj.x)? ) end @@ -126,6 +129,7 @@ function run_spin_excitation!( ) where {T<:Real} Bz = prealloc.Bz_old B = prealloc.Bz_new + B1 = prealloc.B1 φ = prealloc.ϕ α = prealloc.Rot.α β = prealloc.Rot.β @@ -139,12 +143,13 @@ function run_spin_excitation!( x, y, z = get_spin_coords(p.motion, p.x, p.y, p.z, s.t) #Effective field @. Bz = (s.Gx * x + s.Gy * y + s.Gz * z) + ΔBz - s.Δf / T(γ) # ΔB_0 = (B_0 - ω_rf/γ), Need to add a component here to model scanner's dB0(x,y,z) - @. B = sqrt(abs(s.B1)^2 + abs(Bz)^2) + @. B1 = s.B1 * p.B1 # Take B1+ transmit RF field map into account. This is a vector of complex numbers + @. B = sqrt(abs(B1)^2 + abs(Bz)^2) @. B[B == 0] = eps(T) #Spinor Rotation @. φ = T(-π * γ) * (B * s.Δt) # TODO: Use trapezoidal integration here (?), this is just Forward Euler @. α = cos(φ) - Complex{T}(im) * (Bz / B) * sin(φ) - @. β = -Complex{T}(im) * (s.B1 / B) * sin(φ) + @. β = -Complex{T}(im) * (B1 / B) * sin(φ) mul!(Spinor(α, β), M, Maux_xy, Maux_z) #Relaxation @. M.xy = M.xy * exp(-s.Δt / p.T2) diff --git a/KomaMRICore/src/simulation/SimMethods/Bloch/BlochGPU.jl b/KomaMRICore/src/simulation/SimMethods/Bloch/BlochGPU.jl index 8f85dfed6..86a2437f1 100644 --- a/KomaMRICore/src/simulation/SimMethods/Bloch/BlochGPU.jl +++ b/KomaMRICore/src/simulation/SimMethods/Bloch/BlochGPU.jl @@ -67,6 +67,7 @@ struct BlochGPUPrealloc{T} <: PreallocResult{T} seq_properties::AbstractVector{SeqBlockProperties{T}} Bz::AbstractMatrix{T} B::AbstractMatrix{T} + B1::AbstractMatrix{Complex{T}} φ::AbstractMatrix{T} ΔT1::AbstractMatrix{T} ΔT2::AbstractMatrix{T} @@ -84,6 +85,7 @@ function prealloc_block(p::BlochGPUPrealloc{T}, i::Integer) where {T<:Real} [seq_block], view(p.Bz,:,1:seq_block.length), view(p.B,:,1:seq_block.length), + view(p.B1,:,1:seq_block.length), view(p.φ,:,1:seq_block.length-1), view(p.ΔT1,:,1:seq_block.length-1), view(p.ΔT2,:,1:seq_block.length-1), @@ -102,6 +104,7 @@ function prealloc(sim_method::Bloch, backend::KA.GPU, obj::Phantom{T}, M::Mag{T} precalc.seq_properties, KA.zeros(backend, T, (size(obj.x, 1), max_block_length)), KA.zeros(backend, T, (size(obj.x, 1), max_block_length)), + KA.zeros(backend, Complex{T}, (size(obj.x, 1), max_block_length)), KA.zeros(backend, T, (size(obj.x, 1), max_block_length-1)), KA.zeros(backend, T, (size(obj.x, 1), max_block_length-1)), KA.zeros(backend, T, (size(obj.x, 1), max_block_length-1)), @@ -186,7 +189,8 @@ function run_spin_excitation!( #Effective Field pre.Bz .= (x .* seq.Gx' .+ y .* seq.Gy' .+ z .* seq.Gz') .+ pre.ΔBz .- seq.Δf' ./ T(γ) # ΔB_0 = (B_0 - ω_rf/γ), Need to add a component here to model scanner's dB0(x,y,z) - pre.B .= sqrt.(abs.(seq.B1') .^ 2 .+ abs.(pre.Bz) .^ 2) + pre.B1 .= transpose(seq.B1) .* p.B1 + pre.B .= sqrt.(abs.(pre.B1) .^ 2 .+ abs.(pre.Bz) .^ 2) #Spinor Rotation pre.φ .= T(-π .* γ) .* (pre.B[:,1:end-1] .* seq.Δt') # TODO: Use trapezoidal integration here (?), this is just Forward Euler @@ -196,7 +200,7 @@ function run_spin_excitation!( pre.ΔT2 .= exp.(-seq.Δt' ./ p.T2) #Excitation - apply_excitation!(backend, 512)(M.xy, M.z, pre.φ, seq.B1, pre.Bz, pre.B, pre.ΔT1, pre.ΔT2, p.ρ, ndrange=size(M.xy,1)) + apply_excitation!(backend, 512)(M.xy, M.z, pre.φ, pre.B1, pre.Bz, pre.B, pre.ΔT1, pre.ΔT2, p.ρ, ndrange=size(M.xy,1)) KA.synchronize(backend) #Reset Spin-State (Magnetization). Only for FlowPath diff --git a/KomaMRICore/src/simulation/SimMethods/Bloch/KernelFunctions.jl b/KomaMRICore/src/simulation/SimMethods/Bloch/KernelFunctions.jl index 1f425ba38..b51fe3583 100644 --- a/KomaMRICore/src/simulation/SimMethods/Bloch/KernelFunctions.jl +++ b/KomaMRICore/src/simulation/SimMethods/Bloch/KernelFunctions.jl @@ -32,13 +32,13 @@ using KernelAbstractions: @kernel, @Const, @index, @uniform, @groupsize, @localm cos_φ = cos(φ[i_g, t]) s_α_r[i_l] = cos_φ if (iszero(B[i_g, t])) - s_α_i[i_l] = -(Bz[i_g, t] / (B[i_g, t] + eps(T))) * sin_φ - s_β_r[i_l] = (imag(B1[t]) / (B[i_g, t] + eps(T))) * sin_φ - s_β_i[i_l] = -(real(B1[t]) / (B[i_g, t] + eps(T))) * sin_φ + s_α_i[i_l] = -(Bz[i_g, t] / (B[i_g, t] + eps(T))) * sin_φ # why not zero? + s_β_r[i_l] = (imag(B1[i_g, t]) / (B[i_g, t] + eps(T))) * sin_φ + s_β_i[i_l] = -(real(B1[i_g, t]) / (B[i_g, t] + eps(T))) * sin_φ else s_α_i[i_l] = -(Bz[i_g, t] / B[i_g, t]) * sin_φ - s_β_r[i_l] = (imag(B1[t]) / B[i_g, t]) * sin_φ - s_β_i[i_l] = -(real(B1[t]) / B[i_g, t]) * sin_φ + s_β_r[i_l] = (imag(B1[i_g, t]) / B[i_g, t]) * sin_φ + s_β_i[i_l] = -(real(B1[i_g, t]) / B[i_g, t]) * sin_φ end s_Mxy_new_r[i_l] = 2 * (s_Mxy_i[i_l] * (s_α_r[i_l] * s_α_i[i_l] - s_β_r[i_l] * s_β_i[i_l]) + s_Mz[i_l] * (s_α_i[i_l] * s_β_i[i_l] + s_α_r[i_l] * s_β_r[i_l])) + diff --git a/KomaMRICore/src/simulation/SimMethods/BlochSimple/BlochSimple.jl b/KomaMRICore/src/simulation/SimMethods/BlochSimple/BlochSimple.jl index d754d389a..ec0ac3d31 100644 --- a/KomaMRICore/src/simulation/SimMethods/BlochSimple/BlochSimple.jl +++ b/KomaMRICore/src/simulation/SimMethods/BlochSimple/BlochSimple.jl @@ -86,11 +86,12 @@ function run_spin_excitation!( #Effective field ΔBz = p.Δw ./ T(2π .* γ) .- s.Δf ./ T(γ) # ΔB_0 = (B_0 - ω_rf/γ), Need to add a component here to model scanner's dB0(x,y,z) Bz = (s.Gx .* x .+ s.Gy .* y .+ s.Gz .* z) .+ ΔBz - B = sqrt.(abs.(s.B1) .^ 2 .+ abs.(Bz) .^ 2) + B1 = s.B1 .* p.B1 # Take B1+ transmit RF field map into account. This is complex + B = sqrt.(abs.(B1) .^ 2 .+ abs.(Bz) .^ 2) B[B .== 0] .= eps(T) #Spinor Rotation φ = T(-2π .* γ) .* (B .* s.Δt) # TODO: Use trapezoidal integration here (?), this is just Forward Euler - mul!(Q(φ, s.B1 ./ B, Bz ./ B), M) + mul!(Q(φ, B1 ./ B, Bz ./ B), M) #Relaxation M.xy .= M.xy .* exp.(-s.Δt ./ p.T2) M.z .= M.z .* exp.(-s.Δt ./ p.T1) .+ p.ρ .* (1 .- exp.(-s.Δt ./ p.T1)) diff --git a/KomaMRIPlots/src/ui/DisplayFunctions.jl b/KomaMRIPlots/src/ui/DisplayFunctions.jl index 81e0bb198..6d6811339 100644 --- a/KomaMRIPlots/src/ui/DisplayFunctions.jl +++ b/KomaMRIPlots/src/ui/DisplayFunctions.jl @@ -992,8 +992,8 @@ Plots a phantom map for a specific spin parameter given by `key`. # Arguments - `obj`: (`::Phantom`) Phantom struct -- `key`: (`::Symbol`, opts: [`:ρ`, `:T1`, `:T2`, `:T2s`, `:x`, `:y`, `:z`]) symbol for - displaying different parameters of the phantom spins +- `key`: (`::Symbol`, opts: [`:ρ`, `:T1`, `:T2`, `:T2s`, `:x`, `:y`, `:z`, `:Δw`, `:B1`]) + symbol for displaying different parameters of the phantom spins # Keywords - `height`: (`::Integer`, `=600`) plot height @@ -1064,8 +1064,9 @@ function plot_phantom_map( end path = @__DIR__ - cmin_key = minimum(getproperty(obj, key)) - cmax_key = maximum(getproperty(obj, key)) + this_map = getproperty(obj, key) + cmin_key = minimum(real.(this_map)) # allow for complex maps + cmax_key = maximum(real.(this_map)) if key == :T1 || key == :T2 || key == :T2s cmin_key = 0 factor = 1e3 @@ -1113,6 +1114,11 @@ function plot_phantom_map( factor = 1 / (2π) unit = " Hz" colormap = "Greys" + elseif key == :B1 + factor = 1 + this_map = real.(this_map) + unit = "" + colormap = "Greys" else factor = 1 cmin_key = 0 @@ -1161,7 +1167,7 @@ function plot_phantom_map( x=(x[:,i])*1e2, y=(y[:,i])*1e2, mode="markers", - marker=attr(color=getproperty(obj,key)*factor, + marker=attr(color=this_map*factor, showscale=colorbar, colorscale=colormap, colorbar=attr(ticksuffix=unit, title=string(key)), @@ -1171,7 +1177,7 @@ function plot_phantom_map( ), visible=i==1, showlegend=false, - text=round.(getproperty(obj,key)*factor,digits=4), + text=round.(this_map*factor,digits=4), hovertemplate="x: %{x:.1f} cm
y: %{y:.1f} cm
$(string(key)): %{text}$unit")) end else # 3D @@ -1214,7 +1220,7 @@ function plot_phantom_map( y=(y[:,i])*1e2, z=(z[:,i])*1e2, mode="markers", - marker=attr(color=getproperty(obj,key)*factor, + marker=attr(color=this_map*factor, showscale=colorbar, colorscale=colormap, colorbar=attr(ticksuffix=unit, title=string(key)), @@ -1224,7 +1230,7 @@ function plot_phantom_map( ), visible=i==1, showlegend=false, - text=round.(getproperty(obj,key)*factor,digits=4), + text=round.(this_map*factor,digits=4), hovertemplate="x: %{x:.1f} cm
y: %{y:.1f} cm
z: %{z:.1f} cm
$(string(key)): %{text}$unit")) end end diff --git a/KomaMRIPlots/test/GUI_PlotlyJS_backend_test.jl b/KomaMRIPlots/test/GUI_PlotlyJS_backend_test.jl index 58ae36aa6..ed5382229 100644 --- a/KomaMRIPlots/test/GUI_PlotlyJS_backend_test.jl +++ b/KomaMRIPlots/test/GUI_PlotlyJS_backend_test.jl @@ -30,6 +30,11 @@ @test true #If the previous line fails the test will fail end + @testset "plot_phantom_map_B1" begin + plot_phantom_map(ph, :B1) #Plotting the phantom's rho map + @test true #If the previous line fails the test will fail + end + @testset "plot_phantom_map_2dview" begin plot_phantom_map(ph, :ρ, view_2d=true) #Plotting the phantom's rho map @test true #If the previous line fails the test will fail @@ -65,6 +70,11 @@ @test true #If the previous line fails the test will fail end + @testset "plot_motion_phantom_map_B1" begin + plot_phantom_map(ph, :B1) #Plotting the phantom's rho map + @test true #If the previous line fails the test will fail + end + @testset "plot_motion_phantom_map_2dview" begin plot_phantom_map(ph, :ρ, view_2d=true) #Plotting the phantom's rho map @test true #If the previous line fails the test will fail diff --git a/src/KomaUI.jl b/src/KomaUI.jl index 72ec17e29..f098a3bb7 100644 --- a/src/KomaUI.jl +++ b/src/KomaUI.jl @@ -40,7 +40,7 @@ function KomaUI(; darkmode=true, frame=true, phantom_mode="2D", sim=Dict{String, Observables.clear(img_ui) # For phantom sub-buttons - fieldnames_obj = [fieldnames(Phantom)[5:end-3]...] + fieldnames_obj = [fieldnames(Phantom)[[5:end-5;end]]...] # TODO should this be more explicit on which symbols to include rather than using indices? widgets_button_obj = button.(string.(fieldnames_obj)) # Setup the Blink window diff --git a/src/ui/ExportMATFunctions.jl b/src/ui/ExportMATFunctions.jl index e9d6b4d23..adf74c4d0 100644 --- a/src/ui/ExportMATFunctions.jl +++ b/src/ui/ExportMATFunctions.jl @@ -40,8 +40,8 @@ end function export_2_mat_phantom(phantom, matfolder; matfilename="phantom.mat") phantom_dict = Dict("name" => phantom.name, - "columns" => ["x", "y", "z", "rho", "T1", "T2", "T2s", "delta_omega"], - "data" => hcat(phantom.x, phantom.y, phantom.z, phantom.ρ, phantom.T1, phantom.T2, phantom.T2s, phantom.Δw)) + "columns" => ["x", "y", "z", "rho", "T1", "T2", "T2s", "delta_omega", "B1"], + "data" => hcat(phantom.x, phantom.y, phantom.z, phantom.ρ, phantom.T1, phantom.T2, phantom.T2s, phantom.Δw, phantom.B1)) matwrite(joinpath(matfolder, matfilename), Dict("phantom" => phantom_dict)) end