Skip to content

Commit cb6cbc1

Browse files
authored
Magnus-based methods, RF reference frame, and block splitting (#612)
* Port simulation block timing fixes * Handle RF frequency modulation frame * Bump package compat for RF frame changes * Update simulation docs and benchmarks * Refine RF frame phase display * Restore legacy Pulseq RF use fallback * Fix get_samples doc attachment
1 parent 2cd4411 commit cb6cbc1

37 files changed

Lines changed: 914 additions & 415 deletions

KomaMRIBase/Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "KomaMRIBase"
22
uuid = "d0bc0b20-b151-4d03-b2a4-6ca51751cb9c"
33

4-
version = "0.10.1"
4+
version = "0.11.0"
55
authors = ["Carlos Castillo Passi <cncastillo@uc.cl>"]
66

77
[workspace]

KomaMRIBase/src/datatypes/Sequence.jl

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -706,9 +706,11 @@ function get_samples(seq::Sequence, range; events=[:rf, :gr, :adc], freq_in_phas
706706
t_Δf = reduce(vcat, [T0[i] .+ times(seq.RF[1,i], :Δf) for i in range])
707707
A_rf = reduce(vcat, [ampls(seq.RF[1,i]; freq_in_phase) for i in range])
708708
A_Δf = reduce(vcat, [freqs(seq.RF[1,i]) for i in range])
709+
A_ψ = reduce(vcat, [rf_frame_phase(seq.RF[1,i]) for i in range])
709710
rf_samples = (
710711
rf = fill_if_empty((t = t_rf, A = A_rf)),
711-
Δf = fill_if_empty((t = t_Δf, A = A_Δf))
712+
Δf = fill_if_empty((t = t_Δf, A = A_Δf)),
713+
ψ = fill_if_empty((t = t_Δf, A = A_ψ))
712714
)
713715
end
714716
# Gradients
@@ -742,6 +744,8 @@ function get_samples(seq::Sequence, range; events=[:rf, :gr, :adc], freq_in_phas
742744
end
743745
get_samples(seq::Sequence; kwargs...) = get_samples(seq, 1:length(seq); kwargs...)
744746

747+
wrap_to_pi(x) = mod(x + π, 2π) - π
748+
745749
"""
746750
Gx, Gy, Gz = get_grads(seq, t::Vector)
747751
Gx, Gy, Gz = get_grads(seq, t::Matrix)
@@ -769,9 +773,9 @@ function get_grads(seq, t::Union{Vector, Matrix})
769773
end
770774

771775
"""
772-
B1, Δf_rf = get_rfs(seq::Sequence, t)
776+
B1, Δf_rf, ψ = get_rfs(seq::Sequence, t)
773777
774-
Returns the RF pulses and the delta frequency.
778+
Returns the RF pulses, delta frequency, and RF rotating-frame phase.
775779
776780
# Arguments
777781
- `seq`: (`::Sequence`) Sequence struct
@@ -780,13 +784,15 @@ Returns the RF pulses and the delta frequency.
780784
# Returns
781785
- `B1`: (`1-row ::Matrix{ComplexF64}`, `[T]`) vector of RF pulses
782786
- `Δf_rf`: (`1-row ::Matrix{Float64}`, `[Hz]`) delta frequency vector
787+
- `ψ`: (`1-row ::Matrix{Float64}`, `[rad]`) RF rotating-frame phase vector
783788
"""
784789
function get_rfs(seq, t::Union{Vector, Matrix})
785790
rf_samples = get_samples(seq; events=[:rf])
786791
for event in rf_samples
787792
Interpolations.deduplicate_knots!(event.t; move_knots=true)
788793
end
789-
return Tuple(linear_interpolation(event..., extrapolation_bc=0.0).(t) for event in rf_samples)
794+
B1, Δf, ψ = Tuple(linear_interpolation(event..., extrapolation_bc=0.0).(t) for event in rf_samples)
795+
return B1, Δf, wrap_to_pi.(ψ)
790796
end
791797

792798
"""

KomaMRIBase/src/datatypes/simulation/DiscreteSequence.jl

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"""
2-
seqd = DiscreteSequence(Gx, Gy, Gz, B1, Δf, ADC, t, Δt)
2+
seqd = DiscreteSequence(Gx, Gy, Gz, B1, Δf, ψ, ADC, t, Δt)
33
44
A sampled version of a Sequence struct, containing vectors for event amplitudes at specified
55
times. DiscreteSequence is the struct used for simulation.
@@ -10,6 +10,7 @@ times. DiscreteSequence is the struct used for simulation.
1010
- `Gz`: (`::AbstractVector{T<:Real}`, `[T/m]`) z-gradient vector
1111
- `B1`: (`::AbstractVector{Complex{T<:Real}}`, `[T]`) RF amplitude vector
1212
- `Δf`: (`::AbstractVector{T<:Real}`, `[Hz]`) RF carrier frequency displacement vector
13+
- `ψ`: (`::AbstractVector{T<:Real}`, `[rad]`) RF rotating-frame phase vector
1314
- `ADC`: (`::AbstractVector{Bool}`) ADC sample vector
1415
- `t`: (`::AbstractVector{T<:Real}`, `[s]`) time vector
1516
- `Δt`: (`::AbstractVector{T<:Real}`, `[s]`) delta time vector
@@ -23,6 +24,7 @@ struct DiscreteSequence{T<:Real}
2324
Gz::AbstractVector{T}
2425
B1::AbstractVector{Complex{T}}
2526
Δf::AbstractVector{T}
27+
ψ::AbstractVector{T}
2628
ADC::AbstractVector{Bool}
2729
t::AbstractVector{T}
2830
Δt::AbstractVector{T}
@@ -35,36 +37,39 @@ Base.getindex(seq::DiscreteSequence, i::Integer) = begin
3537
seq.Gz[i, :],
3638
seq.B1[i, :],
3739
seq.Δf[i, :],
40+
seq.ψ[i, :],
3841
seq.ADC[i, :],
3942
seq.t[i, :],
4043
seq.Δt[i, :])
4144
end
4245
Base.getindex(seq::DiscreteSequence, i::UnitRange) = begin
43-
DiscreteSequence(seq.Gx[i.start:i.stop+1],
44-
seq.Gy[i.start:i.stop+1],
45-
seq.Gz[i.start:i.stop+1],
46-
seq.B1[i.start:i.stop+1],
47-
seq.Δf[i.start:i.stop+1],
46+
DiscreteSequence(seq.Gx[i],
47+
seq.Gy[i],
48+
seq.Gz[i],
49+
seq.B1[i],
50+
seq.Δf[i],
51+
seq.ψ[i],
4852
seq.ADC[i],
49-
seq.t[i.start:i.stop+1],
50-
seq.Δt[i])
53+
seq.t[i],
54+
seq.Δt[i.start:i.stop-1])
5155
end
5256
Base.view(seq::DiscreteSequence, i::UnitRange) = begin
53-
@views DiscreteSequence(seq.Gx[i.start:i.stop+1],
54-
seq.Gy[i.start:i.stop+1],
55-
seq.Gz[i.start:i.stop+1],
56-
seq.B1[i.start:i.stop+1],
57-
seq.Δf[i.start:i.stop+1],
57+
@views DiscreteSequence(seq.Gx[i],
58+
seq.Gy[i],
59+
seq.Gz[i],
60+
seq.B1[i],
61+
seq.Δf[i],
62+
seq.ψ[i],
5863
seq.ADC[i],
59-
seq.t[i.start:i.stop+1],
60-
seq.Δt[i])
64+
seq.t[i],
65+
seq.Δt[i.start:i.stop-1])
6166
end
6267
Base.iterate(seq::DiscreteSequence) = (seq[1], 2)
6368
Base.iterate(seq::DiscreteSequence, i) = (i <= length(seq)) ? (seq[i], i+1) : nothing
6469

65-
is_GR_on(seq::DiscreteSequence) = sum(abs.([seq.Gx[1:end-1]; seq.Gy[1:end-1]; seq.Gz[1:end-1]])) != 0
66-
is_RF_on(seq::DiscreteSequence) = sum(abs.(seq.B1[1:end-1])) != 0
67-
is_ADC_on(seq::DiscreteSequence) = sum(abs.(seq.ADC[1:end-1])) != 0
70+
is_GR_on(seq::DiscreteSequence) = sum(abs.([seq.Gx; seq.Gy; seq.Gz])) != 0
71+
is_RF_on(seq::DiscreteSequence) = sum(abs.(seq.B1)) != 0
72+
is_ADC_on(seq::DiscreteSequence) = sum(abs.(seq.ADC)) != 0
6873
is_GR_off(seq::DiscreteSequence) = !is_GR_on(seq)
6974
is_RF_off(seq::DiscreteSequence) = !is_RF_on(seq)
7075
is_ADC_off(seq::DiscreteSequence) = !is_ADC_on(seq)
@@ -87,12 +92,12 @@ based on simulation parameters.
8792
"""
8893
function discretize(seq::Sequence; sampling_params=default_sampling_params(), motion=NoMotion())
8994
t, Δt = get_variable_times(seq; Δt=sampling_params["Δt"], Δt_rf=sampling_params["Δt_rf"], motion=motion)
90-
B1, Δf = get_rfs(seq, t)
95+
B1, Δf, ψ = get_rfs(seq, t)
9196
Gx, Gy, Gz = get_grads(seq, t)
9297
tadc = get_adc_sampling_times(seq)
9398
tadc_set = Set(tadc)
9499
ADCflag = [tt in tadc_set for tt in t] # Displaced 1 dt, sig[i]=S(ti+dt)
95-
seqd = DiscreteSequence(Gx, Gy, Gz, complex.(B1), Δf, ADCflag, t, Δt)
100+
seqd = DiscreteSequence(Gx, Gy, Gz, complex.(B1), Δf, ψ, ADCflag, t, Δt)
96101
return seqd
97102
end
98103

KomaMRIBase/src/timing/KeyValuesCalculation.jl

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,28 @@ function freqs(rf::RF)
5353
end
5454
return _shape_samples(rf.Δf)
5555
end
56+
57+
"""
58+
ψ = rf_frame_phase(rf::RF)
59+
60+
RF rotating-frame phase samples from frequency modulation, with `ψ` zeroed at the RF center.
61+
"""
62+
function rf_frame_phase(rf::RF)
63+
f = freqs(rf)
64+
isempty(f) && return Float64[]
65+
t = times(rf, :Δf)
66+
Interpolations.deduplicate_knots!(t; move_knots=true)
67+
center_time = rf.delay + get_RF_center(rf)
68+
t_aug = sort!([t; center_time])
69+
f_aug = linear_interpolation(t, f, extrapolation_bc=Interpolations.Flat()).(t_aug)
70+
ψ_aug = -2π .* [0.0; cumtrapz(diff(t_aug), f_aug)]
71+
ψ = ψ_aug[searchsortedfirst.(Ref(t_aug), t)]
72+
ψ_center = ψ_aug[searchsortedfirst(t_aug, center_time)]
73+
ψ .-= ψ_center
74+
ψ[1] = zero(eltype(ψ))
75+
ψ[end] = zero(eltype(ψ))
76+
return ψ
77+
end
5678
function ampls(adc::ADC)
5779
if !is_on(adc)
5880
return Bool[]

KomaMRIBase/src/timing/TimeStepCalculation.jl

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -99,11 +99,11 @@ function get_variable_times(seq; Δt=1e-3, Δt_rf=1e-5, motion=NoMotion())
9999
t0 = T0[i]
100100
if is_RF_on(s)
101101
y = s.RF[1]
102-
delay, T, center = y.delay, y.T, y.center
102+
delay, T = y.delay, y.T
103103
t1 = t0 + delay
104104
t2 = t1 + sum(T)
105-
rf0 = t0 + delay + center
106-
taux = points_from_key_times([t1, t1 + ϵ, rf0, t2 - ϵ, t2]; dt=Δt_rf)
105+
tc = t0 + delay + get_RF_center(y)
106+
taux = points_from_key_times(sort([t1, t1 + ϵ, tc, t2 - ϵ, t2]); dt=Δt_rf)
107107
append!(t_block, taux)
108108
end
109109
if is_GR_on(s)
@@ -112,7 +112,7 @@ function get_variable_times(seq; Δt=1e-3, Δt_rf=1e-5, motion=NoMotion())
112112
if is_Gy_on(s) append!(active_gradients, s.GR.y) end
113113
if is_Gz_on(s) append!(active_gradients, s.GR.z) end
114114
for y = active_gradients
115-
ts = _gradient_interpolation_samples(y).t .+ t0
115+
ts = times(y) .+ t0
116116
taux = points_from_key_times([ts[1] + ϵ; ts; ts[end] - ϵ]; dt=Δt)
117117
append!(t_block, taux)
118118
end

KomaMRIBase/test/runtests.jl

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,26 @@ using TestItems, TestItemRunner
539539
@test KomaMRIBase.get_char_from_RF_use(use) == char
540540
@test KomaMRIBase.get_RF_use_from_char(Val(char)) == use
541541
end
542+
543+
# Constant frequency offset: ψ is centered so the RF center has zero frame phase.
544+
rf = RF([1.0, 2.0, 1.0] .* 1e-6, 1e-3, 1000.0, 0.0; ϕ=0.3)
545+
@test KomaMRIBase.rf_frame_phase(rf) [0, π, -π, 0]
546+
547+
# Frequency-modulated RF: ψ is generated from the integrated Δf waveform and centered.
548+
rf_fm = RF([1.0, 2.0, 1.0] .* 1e-6, 1e-3, [0.0, 1000.0, 0.0], 0.0)
549+
ψ = KomaMRIBase.rf_frame_phase(rf_fm)
550+
@test ψ[argmin(abs.(times(rf_fm, :Δf) .- (rf_fm.delay + rf_fm.center)))] 0
551+
552+
# Discretization carries ψ so simulators can handle split RF blocks independently.
553+
seq = Sequence()
554+
seq += rf
555+
seqd = discretize(seq; sampling_params=Dict{String,Any}("Δt"=>1e-4, "Δt_rf"=>1e-4))
556+
center_idx = findfirst(==(rf.delay + rf.center), seqd.t)
557+
@test hasproperty(seqd, )
558+
@test !isnothing(center_idx)
559+
@test iszero(seqd.ψ[center_idx])
560+
@test length(seqd.ψ) == length(seqd.t)
561+
@test !all(iszero, seqd.ψ)
542562
end
543563

544564
@testset "Delay" begin
@@ -726,7 +746,8 @@ using TestItems, TestItemRunner
726746
seqd = KomaMRIBase.discretize(seq)
727747
i1, i2 = rand(1:Int(floor(0.5*length(seqd)))), rand(Int(ceil(0.5*length(seqd))):length(seqd))
728748
@test seqd[i1].t [t[i1]]
729-
@test seqd[i1:i2-1].t t[i1:i2]
749+
@test seqd[i1:i2].t t[i1:i2]
750+
@test seqd[i1:i2].Δt Δt[i1:i2-1]
730751

731752
T, N = 1.0, 4
732753
seq = Sequence()

KomaMRICore/Project.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "KomaMRICore"
22
uuid = "4baa4f4d-2ae9-40db-8331-a7d1080e3f4e"
33
authors = ["Carlos Castillo Passi <cncastillo@uc.cl>"]
4-
version = "0.10.0"
4+
version = "0.11.0"
55

66
[deps]
77
AcceleratedKernels = "6a4ca0a5-0e36-4168-a932-d9be78d558f1"
@@ -32,7 +32,7 @@ Adapt = "3, 4"
3232
CUDA = "3, 4, 5, 6.0"
3333
Functors = "0.4, 0.5"
3434
KernelAbstractions = "0.9.34"
35-
KomaMRIBase = "0.10"
35+
KomaMRIBase = "0.11"
3636
Metal = "1.2"
3737
ProgressMeter = "1"
3838
Reexport = "1"

KomaMRICore/src/KomaMRICore.jl

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
module KomaMRICore
1+
module KomaMRICore
22

33
# General
44
import Base.*, Base.abs
@@ -17,6 +17,7 @@ include("rawdata/ISMRMRD.jl")
1717
# Datatypes
1818
include("datatypes/Spinor.jl")
1919
include("other/DiffusionModel.jl")
20+
include("callbacks/Callback.jl")
2021
# Simulator
2122
include("simulation/GPUFunctions.jl")
2223
include("simulation/Functors.jl")
@@ -30,5 +31,7 @@ export Mag
3031
export simulate, simulate_slice_profile
3132
# Spinors
3233
export Spinor, Rx, Ry, Rz, Q, Un
34+
# Callback
35+
export Callback
3336

3437
end
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
struct Callback{F}
2+
enabled::Bool
3+
every::Int
4+
fun::F
5+
end
6+
7+
Callback(every::Int, fun::F) where {F} = Callback(every > 0, every, fun)
8+
9+
function (cb::Callback)(progress_info, simulation_blocks_info, device_data, sim_params)
10+
if cb.enabled && (progress_info.block - 1) % cb.every == 0
11+
cb.fun(progress_info, simulation_blocks_info, device_data, sim_params)
12+
end
13+
end
14+
15+
# Included Callbacks
16+
include("progress_callback.jl")
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
function progressbar_callback(Nblocks)
2+
progress_bar = Progress(Nblocks; desc="Running simulation...")
3+
function progressbar_callback_fun(progress_info, simulation_blocks_info, device_data, sim_params)
4+
next!(
5+
progress_bar;
6+
showvalues=[
7+
(:simulated_blocks, progress_info.block),
8+
(:rf_blocks, progress_info.rfs),
9+
(:acq_samples, progress_info.samples - 1)
10+
],
11+
)
12+
end
13+
return progressbar_callback_fun
14+
end

0 commit comments

Comments
 (0)