Newer
Older
"""
Defines [`BaseSequence`](@ref) and [`Sequence`](@ref)
"""
import StaticArrays: SVector
import JuMP: @constraint
import ...Variables: get_free_variable, VariableType, variables, set_simple_constraints!, make_generic, get_gradient, get_pulse, get_gradient, @defvar, add_cost_function!, base_variables
import ...BuildSequences: global_scanner
import ...Components: EventComponent, NoGradient, edge_times
import ..Abstract: ContainerBlock, start_time
import ..BuildingBlocks: Wait, BuildingBlock, BaseBuildingBlock
"""
Super-type of any sequence of non-overlapping building blocks that should be played after each other.
It contains `N` [`ContainerBlock`](@ref) objects (e.g., building blocks or other sequences).
Main interface:
- Acts as an iterable containing the blocks and sequences.
- Indiviual blocks/sequences can be obtained using indexing.
- If there is a finite number of repeats, the iteration will continue over all repeats.
- `get_index_single_TR`: return the index assuming it is between 1 and N
"""
abstract type BaseSequence{N} <: ContainerBlock end
function Base.getindex(bs::BaseSequence{N}, index::Integer) where {N}
if nrepeat(bs) > 0 && (index < 1 || index > length(bs))
throw(BoundsError(bs, index))
end
base_index = ((index - 1) % N) + 1
return get_index_single_TR(bs, base_index)
end
function (sequence::BaseSequence{N})(time::AbstractFloat) where {N}
var_time = mod(time, variables.duration(sequence))
var_time -= variables.duration(block)
return (var_time + variables.duration(block), block)
return (variables.duration(sequence[N]) + var_time, sequence[N])
error("Total duration of building blocks does not match duration of the sequence.")
Base.iterate(bs::BaseSequence) = Base.iterate(bs, 1)
Base.iterate(bs::BaseSequence{N}, index::Integer) where {N} = index > length(bs) ? nothing : (bs[index], index + 1)
Base.length(bs::BaseSequence{N}) where {N} = iszero(nrepeat(bs)) ? N : (nrepeat(bs) * N)
Base.eltype(::Type{<:BaseSequence}) = ContainerBlock
Base.keys(seq::BaseSequence{N}) where {N} = 1:N
function start_time(bs::BaseSequence{N}, index::Integer) where {N}
nTR = div(index-1, N, RoundDown)
if 0 < nrepeat(bs) <= nTR
throw(BoundsError(bs, index))
end
base_index = ((index - 1) % N) + 1
base_time = sum(i -> variables.duration(bs[i]), 1:base_index-1; init=0.)
if iszero(nTR)
return base_time
else
return nTR * variables.duration(bs) + base_time
function start_time(bs::BaseSequence{N}, s::Symbol) where {N}
index = findfirst(p -> p[1] == s, bs.blocks)
if isnothing(index)
error("Sequence does not contain a block with name $s")
end
return start_time(bs, index)
end
function gradient_orientation(seq::BaseSequence)
return gradient_orientation(get_gradient(seq))
end
gradient_orientation(nt::NamedTuple) = map(gradient_orientation, nt)
"""
get_index_single_TR(sequence, index)
Used internally by any [`BaseSequence`](@ref) to get a specific block.
The `index` should be between 1 and N.
It should be implemented for any sub-classes of [`BaseSequence`](@ref).
"""
get_index_single_TR(seq::BaseSequence, index::Integer) = get_index_single_TR(seq, Val(index))
"""
nrepeat(sequence)
How often sequence should be repeated.
"""
nrepeat(bs::BaseSequence) = 1
duration(bs::BaseSequence{0}) = 0.
duration(bs::BaseSequence) = sum(variables.duration.(bs); init=0.)
function edge_times(seq::BaseSequence; tol=1e-6)
res = Float64[]
for (index, block) in enumerate(seq)
append!(res, edge_times(block) .+ start_time(seq, index))
end
if !isapprox(edge, unique_res[end], atol=tol)
for fn in (:gradient_strength, :amplitude, :phase, :frequency)
@eval function variables.$fn.f(sequence::BaseSequence, time::AbstractFloat)
(block_time, block) = sequence(time)
return variables.$fn.f(block, block_time)
for fn in (:get_gradient, :get_pulse)
@eval function $fn(sequence::BaseSequence, time::AbstractFloat)
(block_time, block) = sequence(time)
return $fn(block, block_time)
end
end
Sequence(blocks; name=:Sequence, variables...)
Sequence(blocks...; name=:Sequence, variables...)
Defines an MRI sequence from a vector of building blocks.
## Arguments
- `blocks`: The actual building blocks that will be played in sequence. All the building blocks must be of type [`ContainerBlock`](@ref), which means that they cannot only contain actual [`BaseBuildingBlock`](@ref) objects, but also other [`BaseSequence`](@ref) objects.
Objects of a different type are converted into a [`ContainerBlock`](@ref) internally:
- numbers/`nothing`/`:min`/`:max` : replaced with a [`Wait`](@ref) block with the appropriate constraint/objective added to its [`variables.duration`](@ref).
- RF pulse or readout: will be embedded within a [`BuildingBlock`](@ref) of the appropriate length
Specific named sequences might define additional variables.
blocks :: SVector{N, Pair{<:Union{Symbol, Nothing}, <:ContainerBlock}}
function Sequence(blocks::AbstractVector; name=:Sequence, scanner=nothing, vars...)
res = Sequence{name, length(blocks)}(SVector{length(blocks)}(blocks), isnothing(scanner) ? global_scanner() : scanner)
set_simple_constraints!(res, vars)
add_cost_function!(variables.duration(res), 3)
return res
end
Base.show(io::IO, ::Type{<:Sequence{S, N}}) where {S, N} = print(io, S, "{$N}")
function Base.propertynames(seq::T) where {T <: Sequence}
f = Base.fieldnames(T)
var_names = [k for k in keys(base_variables(T)) if !(k in f)]
intermediate = (f..., var_names...)
part_names = [k for (k, _) in values(seq.blocks) if !isnothing(k) && !(k in intermediate)]
return (intermediate..., part_names...)
end
function Base.getproperty(seq::T, v::Symbol) where T <: Sequence
if v in Base.fieldnames(T)
return getfield(seq, v)
end
vars = base_variables(T)
if v in keys(vars)
return vars[v](seq)
end
if v in [k for (k, _) in values(seq.blocks)]
return seq[v]
end
error("Type $(T) has no field, block, or variable $(v)")
end
Sequence(blocks...; kwargs...) = Sequence([blocks...]; kwargs...)
get_index_single_TR(s::Sequence, i::Integer) = s.blocks[i][2]
Base.getindex(seq::Sequence, sym::Symbol) = seq[findfirst(p -> p[1] == sym, seq.blocks)]
nrepeat(::Sequence) = 0
@defvar repetition_time(seq::Sequence) = variables.duration(seq)
to_block_pair(pair::Pair) = pair[1] => to_block(pair[2])
to_block_pair(other) = nothing => to_block(other)
"""
to_block(block_like)
Converst object into something that can be included in the sequence:
- :min/:max/number/variable/nothing => [`Wait`](@ref).
- `building_block` or `sequence` => no change.
- RF pulse/readout => will be embedded within a [`BuildingBlock`](@ref).
"""
to_block(cb::ContainerBlock) = cb
to_block(s::Symbol) = to_block(Val(s))
to_block(s::Union{VariableType, Nothing, Val{:min}, Val{:max}}) = Wait(s)
to_block(ec::EventComponent) = BuildingBlock([NoGradient(variables.duration(ec)), (0., ec)])
function make_generic(seq::BaseSequence)
blocks = BaseBuildingBlock[]
function add_sequence!(to_add, sub_seq)
for block in sub_seq
if block isa BaseSequence
add_sequence!(to_add, block)
else
push!(to_add, make_generic(block))
end
end
end
add_sequence!(blocks, seq)
return Sequence(blocks)
end