diff --git a/src/MRIBuilder.jl b/src/MRIBuilder.jl index 06704351c5f189b286671165030e437b1bca1ee2..3da8a36cad50ffaa5a137603a672e0c196ffb160 100644 --- a/src/MRIBuilder.jl +++ b/src/MRIBuilder.jl @@ -12,6 +12,7 @@ include("gradients/gradients.jl") include("pulses/pulses.jl") include("containers/containers.jl") include("readouts/readouts.jl") +include("pathways.jl") import .BuildSequences: build_sequence export build_sequence @@ -40,6 +41,9 @@ export InstantReadout import .Containers: Sequence, FixedBlock, FixedSequence export Sequence, FixedBlock, FixedSequence +import .Pathways: Pathway, duration_transverse, duration_dephase, bval, bmat +export duration_transverse, duration_dephase, bval, bmat + import JuMP: @constraint, @objective, objective_function, optimize!, has_values, value, owner_model, Model export @constraint, @objective, objective_function, optimize!, has_values, value, owner_model, Model diff --git a/src/pathways.jl b/src/pathways.jl index 0255ebcad363c333fd77ca1a7a4d490ab514a274..c9dad816b3ca8224cc521bd931c781722f5647b1 100644 --- a/src/pathways.jl +++ b/src/pathways.jl @@ -1,9 +1,10 @@ module Pathways import LinearAlgebra: norm -import StaticArrays: SVector, SMatrix +import StaticArrays: SVector, SMatrix, MVector, MMatrix +import ..BuildingBlocks: BuildingBlock, GradientBlock, RFPulseBlock import ..Containers: Sequence -import ..Variables: qval, bval, area_under_curve -import ..PathwayWalkers: PathwayWalker +import ..Variables: qvec, qval, bmat_gradient, VariableType + """ Pathway(sequence::Sequence, pulse_effects::Vector{:Symbol/Number}, readout_index=1) @@ -48,7 +49,7 @@ You can select which gradients to consider when accessing these values. struct Pathway # user provided sequence :: Sequence - pulse_effects :: Vector{Union{:Symbol, :Number}} + pulse_effects :: Vector{Union{Symbol, Number}} readout_index :: Integer # computed @@ -59,12 +60,11 @@ end function Pathway(sequence::Sequence, pulse_effects::AbstractVector, readout_index::Integer) walker = PathwayWalker() - walk_pathway!(sequence, copy(pulse_effects), walker, Ref(readout_index)) + walk_pathway!(sequence, interpret_pulse_effects.(pulse_effects), walker, Ref(readout_index)) return Pathway( sequence, pulse_effects, readout_index, - Dict(k => SVector{4, VariableType}(v) for (k, v) in pairs(walker.duration_states)), Dict(k => SVector{3, VariableType}(v) for (k, v) in pairs(walker.qvec)), Dict(k => SMatrix{3, 3, VariableType, 9}(v) for (k, v) in pairs(walker.bmat)), @@ -84,13 +84,7 @@ The requested state can be set using `transverse` and `positive` as follows: - `transverse=true`, `positive=false`: -transverse """ function duration_state(pathway::Pathway, transverse, positive) - index = Dict([ - (false, true) => 1, - (true, true) => 2, - (false, false) => 3, - (true, false) => 4, - ])[(Bool(transverse), Bool(positive))] - return pathway.duration_states[index] + return pathway.duration_states[duration_state_index(transverse, positive)] end """ @@ -179,4 +173,233 @@ You can set `scale` and/or `rotate` to specific symbols to only consider gradien """ bval(pathway::Pathway; scale=nothing, rotate=nothing) = tr(bmat(pathway; scale, rotate)) + +""" + interpret_pulse_effects(number_or_symbol) + +Interpret the various numbers and symbols that can be passed on to a `Pathway`. + +The result will be one of: +- :ignore (if input is 0, :ignore, or :skip). +- :excite (if input is 90 or :excite). +- :refocus (if input is 180, :refocus, or :excite). +- :neg_excite (if input is -90, 270, or :negexcite). +""" +function interpret_pulse_effects(number::Number) + normed = Int(number % 360) + if normed == 0 + return :ignore + elseif normed == 90 + return :excite + elseif normed == 180 + return :refocus + elseif normed == 270 + return :neg_excite + else + error("The pulse effect along a pathway should be divisible by 90, not $number.") + end +end + +function interpret_pulse_effects(sym::Symbol) + mapping = Dict{Symbol, Symbol}( + :ignore => :ignore, + :skip => :ignore, + :excite => :excite, + :net_excite => :net_excite, + :refocus => :refocus, + :invert => :refocus, + ) + if sym in keys(mapping) + return mapping[sym] + else + all_symbols = join(keys(mapping), ", ") + error("The pulse effect along a pathway should be one of ($all_symbols), not $sym.") + end +end + + +""" +Helper structure for [`PathwayWalker`](@ref), which is itself a helper for `Pathway`. You are deep down the rabit hole now... + +For documentation, see that structure and [`walk_pathway`](@ref) and [`update_walker_gradient!`](@ref). +""" +mutable struct GradientTracker + last_gradient_time :: VariableType + qvec :: MVector{3, VariableType} + bmat :: MMatrix{3, 3, VariableType, 9} +end + +GradientTracker() = GradientTracker(0., zeros(3), zeros(3, 3)) + + +""" +Helper structure for `Pathway`. + +For documentation, see that structure and [`walk_pathway`](@ref). +""" +mutable struct PathwayWalker + last_pulse_time :: VariableType + is_transverse :: Bool + is_positive :: Bool + duration_states :: MVector{4, VariableType} + gradient_trackers :: Dict{Any, GradientTracker} +end + +PathwayWalker() = PathwayWalker( + 0., false, true, + zeros(4), + Dict{Any, GradientTracker}() +) + +""" + walk_pathway!(bb::BuildingBlock, walker::PathwayWalker, pulse_effects::Vector, nreadout::Ref{Int}, start_time) + +Computes the effect of a specific [`BuildingBlock`](@ref) (starting at `start_time`) on the [`PathwayWalker`](@ref). + +This needs to be defined for any [`ContainerBlock`](@ref) that the walker may encounter. + +This might be as simpla as calling [`walk_pathway!`] on all the children (i.e., for a `Sequence`). + +For individual pulses and gradients, the following behaviour is implemented: +- If a pulse is encountered, call [`update_walker_pulse!`](@ref)`(walker, pulse_effects, pulse_effective_time)` +- If a gradient is encountered, call [`update_walker_gradient!`](@ref)(gradient, walker, gradient_start_time) + +For overlapping gradients/pulses, one should first encounter the part of the gradient before the [`effective_time`](@ref) of the pulse, +then apply the pulse, and then the rest of the gradient. +This effective time can be passed on to [`update_walker_gradient!`](@ref) to allow part of the gradient waveform to be applied. + +The function should return `true` if the `Pathway` has reached its end (i.e., the final readout) and `false` otherwise. +""" +function walk_pathway!(grad::GradientBlock, walker::PathwayWalker, pulse_effects::Vector{Symbol}, nreadout::Ref{Int}, start_time=0.::VariableType) + update_walker_gradient!(grad, walker, start_time) + return false +end + +function walk_pathway!(pulse::RFPulseBlock, walker::PathwayWalker, pulse_effects::Vector{Symbol}, nreadout::Ref{Int}, start_time=0.::VariableType) + update_walker_pulse(walker, pulse_effects, nreadout, start_time + effective_time(pulse)) + return iszero(length(pulse_effects)) && iszero(nreadout[]) +end + + +""" + update_walker_pulse!(walker::PathwayWalker, pulse_effects::Vector, pulse_time) + +Apply the first element of `pulse_effects` to the `walker` at the given `pulse_time`. + +The following steps will be taken if the first `pulse_effect` is not `:ignore` +- if `walker.transverse` is true before the pulse, increase the `walker.bmat` by the outer product of `walker.qvec` with itself multiplied by the time since the last gradient +- update `walker.duration_states` with time since last pulse. +- update `walker.last_pulse_time` +- update `walker.is_transverse`, and `walker.is_positive` based on the first value in `pulse_effects`. +- if `walker.is_positive` changed in the previous step than the `walker.qvec` needs to be flipped. +- remove the first element from `pulse_effects`. + +If the first element is `:ignore` the only effect is that the first element is removed from `pulse_effects`. +""" +function update_walker_pulse!(walker::PathwayWalker, pulse_effects::AbstractVector{Symbol}, pulse_time::VariableType) + if length(pulse_effects) == 0 + error("Pathway definition is invalid! Another RF pulse was encountered before the number of readouts expected from `nreadout` where detected.") + end + instruction = popfirst!(pulse_effects) + if instruction == :ignore + return + end + + # update qvec/bmat + if walker.is_transverse + for gradient_tracker in values(walker.gradient_trackers) + gradient_tracker.bmat += ( + (gradient_tracker.qvec .* gradient_tracker.qvec') .* + (pulse_time - gradient_tracker.last_gradient_time) + ) + gradient_tracker.last_gradient_time = pulse_time + end + end + prev_sign = walker.is_positive + + # update durations + index = duration_state_index(walker.is_transverse, walker.is_positive) + walker.duration_states[index] = walk.duration_state[index] + (pulse_time - walker.last_pulse_time) + + walker.last_pulse_time = pulse_time + + # -transverse, +longitudinal, +transverse, -longitudinal, -transverse, +longitudinal + ordering = [(true, false), (false, true), (true, true), (false, false), (true, false)] + + if instruction == :refocus + walker.is_positive = !walker.is_positive + elseif instruction == :excite + index = findfirst(isequal(walker.is_transverse, walker.is_positive), ordering) + (walker.is_transverse, walker.is_positive) = ordering[index + 1] + elseif instruction == :neg_excite + index = findlast(isequal(walker.is_transverse, walker.is_positive), ordering) + (walker.is_transverse, walker.is_positive) = ordering[index - 1] + else + error("Invalid pulse instruction ($instruction); This error should have been caught earlier.") + end + + # flip qvec if needed + if prev_sign != walker.is_positive + for gradient_tracker in values(walker.gradient_trackers) + gradient_tracker.qvec = -gradient_tracker.qvec + end + end +end + +""" + update_walker_gradient!(gradient_block::GradientBlock, walker::PathwayWalker, gradient_start_time::VariableType; overlapping_pulses=[], overlapping_readouts=[]) + +Update the walker's `qvec` and `bmat` based on the given `gradient_block`. + +The following steps will be taken: +- Do nothing if `walker.transverse` is false +- increase the appropriate `walker.bmat` by the outer product of `walker.qvec` with itself multiplied by the time since the last gradient +- update the appropriate `walker.qvec` and `walker.bmat` based on the gradient waveform. This will require appropriate `qvec`/`bmat` functions to be defined for the gradient building block. +- update `walker.last_gradient_time` to the time at the end of the gradient. + +This requires [`bmat`](@ref) and [`qvec`](@ref) to be implemented for the [`GradientBlock`](@ref). +""" +function update_walker_gradient!(gradient::GradientBlock, walker::PathwayWalker, gradient_start_time::VariableType, internal_start_time, internal_end_time) + if walker.transverse + return + end + + if iszero(internal_start_time) || isnothing(internal_start_time) + # only worry about this for the first call + + # make sure the appropriate gradient tracker exists + key = (gradient.scale, gradient.rotate) + if !(key in keys(walker.gradient_trackers)) + walker.gradient_trackers[key] = GradientTracker() + end + tracker = walker.gradient_trackers[key] + + # update bmat till start of gradient + tracker.bmat = tracker.bmat .+ ( + (tracker.qvec .* tracker.qvec') .* + (gradient_start_time - tracker.last_gradient_time) + ) + tracker.last_gradient_time = gradient_start_time + end + + tracker.bmat = tracker.bmat .+ bmat(gradient, tracker.qvec, internal_start_time, internal_end_time) + tracker.qvec = tracker.qvec .+ qvec(gradient, internal_start_time, internal_end_time) +end + +""" + duration_state_index(transverse, positive) + +Returns the index of a specific state in the `Pathway.duration_state` vector. + +This function is used internally to access that vector. +""" +function duration_state_index(transverse, positive) + return Dict([ + (false, true) => 1, + (true, true) => 2, + (false, false) => 3, + (true, false) => 4, + ])[(Bool(transverse), Bool(positive))] +end + end \ No newline at end of file