Skip to content
Snippets Groups Projects
abstract.jl 8.46 KiB
module Abstract

import StaticArrays: SVector
import ...BuildingBlocks: BuildingBlock, ContainerBlock, RFPulseBlock, get_children_indices, VariableNotAvailable
import ...Wait: WaitBlock
import ...Readouts: InstantReadout
import ...Gradients: GradientBlock, split_gradient, ConstantGradientBlock, ChangingGradientBlock
import ...Variables: duration, start_time, qvec, bmat_gradient, gradient_strength, slew_rate, VariableType, effective_time, gradient_strength, slew_rate
import ...Variables: flip_angle, amplitude, phase, frequency, bandwidth, inverse_bandwidth, N_left, N_right, slice_thickness, all_variables_symbols

"""
Parent type for any gradient waveform that might overlap with RF pulses and/or readouts.

New waveforms should sub-type from this one.
At the very least they should implement:
- [`waveform_sequence`](@ref): return the subsequent [`GradientBlock`](@ref) and [`WaitBlock`](@ref) objects describing the gradient waveform. To get the wavform in a more useful format use [`waveform`](@ref).
- [`interruptions`](@ref): return a vector of all [`RFPulseBlock`](@ref) and [`InstantReadout`](@ref) with information on which part of the waveform they interrupt.
"""
abstract type AbstractOverlapping <: ContainerBlock end

"""
    SingleInterrupted(grad_like_block, interruptions)

Represents a single part of the waveform within [`GenericOverlapping`](@ref).
"""
struct SingleInterrupted{T<:Union{WaitBlock, GradientBlock}}
    block :: T
    interruptions :: Vector{NamedTuple{(:index, :time, :block), Tuple{Int64, Float64, <:Union{RFPulseBlock, InstantReadout}}}}
end

"""
    waveform_sequence(overlapping)

Returns the gradient waveform as a sequence of [`WaitBlock`](@ref), [`ChangingGradientBlock`](@ref), and [`ConstantGradientBlock`](@ref) objects.

This should be implemented for any sub-type of [`AbstractOverlapping`](@ref).
"""
function waveform_sequence end


"""
    waveform(overlapping)

Returns the gradient waveform of a [`AbstractOverlapping`](@ref) as a sequence of control points.
Each control point is stored as a tuple with the time in ms and the gradient as a length-3 vector.
The gradient is linearly interpolated between these points (see [`waveform_sequence`](@ref)).
"""
function waveform(ao::AbstractOverlapping)
    result = Tuple{VariableType, SVector{3, VariableType}}[(0, zero(SVector{3, Float64}))]
    for block in waveform_sequence(ao)
        new_time = result[end][1] + max(duration(block), 0)
        prev_grad = result[end][2]
        if block isa WaitBlock
            @assert all(abs.(gradient_strength(block)) <= 1e-12) "$(typeof(ao)) inserts WaitBlock before the gradient is zero. This is probably caused by an improper implementation of this gradient waveform."
            push!(result, (new_time, prev_grad))
        elseif block isa ConstantGradientBlock
            @assert all(gradient_strength(block) .≈ prev_grad) "$(typeof(ao)) inserts ConstantGradientBlock that does not match previous gradient strength. This is probably caused by an improper implementation of this gradient waveform."
            push!(result, (new_time, prev_grad))
        elseif block isa ChangingGradientBlock
            @assert all(block.gradient_strength_start .≈ prev_grad) "$(typeof(ao)) inserts ChangingGradientBlock that does not match previous gradient strength. This is probably caused by an improper implementation of this gradient waveform."
            push!(result, (new_time, prev_grad .+ slew_rate(block) .* duration(block)))
        else
            error("Unrecognised block type in gradient waveform: $(typeof(ao)).")
        end
    end
    return result
end


"""
    interruptions(overlapping)

Returns the interruptions as a sequence named tuples with 3 elements:
- `index`: which object in the [`waveform`](@ref) should be interrupted.
- `time`: time since the start of the [`waveform`](@ref) object that the interruptions takes place ([`effective_time`](@ref) for RF pulses).
- `object`: [`RFPulseBlock`](@ref) or [`InstantReadout`](@ref).

This should be implemented for any sub-type of [`AbstractOverlapping`](@ref).
"""
function interruptions end

duration(ao::AbstractOverlapping) = sum(duration.(waveform_sequence(ao)))
duration(single::SingleInterrupted) = duration(single.block)

pulses(ao::AbstractOverlapping) = [pulse for (_, _, pulse) in interruptions(ao) if pulse isa RFPulseBlock]
readouts(ao::AbstractOverlapping) = [readout for (_, _, readout) in interruptions(ao) if readout isa InstantReadout]

# For any overlapping building blocks with a single RF pulse, get information about that RF pulse.
for (func_symbol, _) in [Dict(all_variables_symbols)[:pulse]..., :effective_time => ""]
    if func_symbol == :slice_thickness
        continue
    end
    @eval function $func_symbol(ao::AbstractOverlapping, args...; kwargs...)
        single_pulse = pulses(ao)
        if iszero(length(single_pulse))
            throw(VariableNotAvailable(typeof(ao), $func_symbol))
        elseif length(single_pulse) > 1
            error("Building block has multipl pulses, so cannot compute ", $func_symbol, ".")
        end
        $func_symbol(single_pulse[1], args...; kwargs...)
    end
end

# Acting as a valid container
get_children_indices(ao::AbstractOverlapping) = 1:length(waveform_sequence(ao))
function Base.getindex(ao::AbstractOverlapping, index::Integer)
    grad_like_block = waveform_sequence(ao)[index]
    inter = interruptions(ao)
    first_interrupt = findfirst(int -> int[2] == index, inter)
    if isnothing(first_interrupt)
        return grad_like_block
    else
        last_interrupt = findlast(int -> int[2] == index, inter)
        return SingleInterrupted(grad_like_block, inter[first_interrupt:last_interrupt])
    end
end

start_time(ao::AbstractOverlapping, index::Integer) = sum(duration.(waveform_sequence(ao)[1:index]))

function get_part(si::SingleInterrupted, first::Union{Nothing, Number}, last::Union{Nothing, Number})
    if isnothing(first)
        if isnothing(last)
            part = si.block
        else
            (part, _) = split_gradient(si, si.interruptions[last][1])
        end
    else
        tfirst = si.interruptions[first][1]
        if isnothing(last)
            (_, part) = split_gradient(si, tfirst)
        else
            (_, part, _) = split_gradient(si, tfirst, so.interruptions[last][1])
        end
    end
    return part
end

function get_parts(ao::AbstractOverlapping, first::Union{Nothing, Integer}, last::Union{Nothing, Integer})
    inter = interruptions(ao)
    form = waveform_sequence(ao)
    whole_start_index = isnothing(first) ? 0 : inter[first].index
    whole_final_index = isnothing(last) ? length(form) + 1 : inter[last].index
    if whole_start_index == whole_final_index
        return [split_gradient(form[whole_start_index], inter[first].time, inter[last].time)]
    end

    parts = form[whole_start_index+1:whole_final_index-1]
    if !isnothing(first)
        pushfirst!(parts, split_gradient(form[whole_start_index], inter[first].time)[2])
    end
    if !isnothing(last)
        push!(parts, split_gradient(form[whole_final_index], inter[last].time)[1])
    end
    return parts
end


"""
    qvec(overlapping[, first_interruption, last_interruption])

Computes the area under the curve for the gradient waveform in [`AbstractOverlapping`](@ref).

If `first_interruption` is set to something else than `nothing`, only the gradient waveform after this RF pulse/Readout will be considered.
Similarly, if `last_interruption` is set to something else than `nothing`, only the gradient waveform up to this RF pulse/Readout will be considered.
"""
function qvec(ao::AbstractOverlapping, index1::Union{Nothing, Integer}, index2::Union{Nothing, Integer})
    @assert isnothing(index1) || isnothing(index2) || index2 >= index1
    if (index1 isa Number) && (index1 == index2)
        return zeros(3)
    end
    sum(qvec.(get_parts(ao, index1, index2)); init=zero(SVector{3, Float64}))
end
qvec(ao::AbstractOverlapping) = qvec(ao, nothing, nothing)

function bmat_gradient(ao::AbstractOverlapping, qstart, index1::Union{Nothing, Integer}, index2::Union{Nothing, Integer})
    @assert isnothing(index1) || isnothing(index2) || index2 >= index1
    if (index1 isa Number) && (index1 == index2)
        return zeros(3, 3)
    end
    result = Matrix{VariableType}(zeros(3, 3))
    qcurrent = Vector{VariableType}(qstart)

    for part in get_parts(ao, index1, index2)
        result = result .+ bmat_gradient(part, qcurrent)
        qcurrent = qcurrent .+ qvec(part, qcurrent)
    end
    return result
end
bmat_gradient(ao::AbstractOverlapping, qstart) = bmat_gradient(ao, qstart, nothing, nothing)

end