Newer
Older
"""
Defines [`BaseBuildingBlock`](@ref), [`BuildingBlock`](@ref) and [`Wait`](@ref).
"""
module BuildingBlocks
import LinearAlgebra: norm
import JuMP: @constraint
import ..Abstract: ContainerBlock
import ...Components: BaseComponent, GradientWaveform, EventComponent, NoGradient, ChangingGradient, ConstantGradient, split_gradient, DelayedEvent, RFPulseComponent, ReadoutComponent
import ...Variables: qval, bmat_gradient, effective_time
import ...Variables: VariableType, duration, make_generic, get_pulse, get_readout, scanner_constraints!
"""
Basic BuildingBlock, which can consist of a gradient waveforms with any number of RF pulses/readouts overlaid
Main interface:
- iteration will give the gradient waveforms interspersed by RF pulses/readouts.
- Indiviual indices can be accessed using `keys(building_block)`
- [`waveform_sequence`](@ref) returns just the gradient waveform as a sequence of [`GradientWaveform`](@ref) objects.
- [`waveform`](@ref) returns just the gradient waveform as a sequence of (time, gradient_strength) tuples.
- [`events`](@ref) returns the RF pulses and readouts.
- [`qvec`](@ref) returns area under curve for (part of) the gradient waveform.
Sub-types need to implement:
- `Base.keys`: returns sequence of keys to all the components.
- `Base.getindex`: returns the actual component for each key.
- [`duration`](@ref): total duration of the building block.
"""
abstract type BaseBuildingBlock <: ContainerBlock end
# Iterator interface
Base.length(c::BaseBuildingBlock) = length(keys(c))
Base.eltype(::Type{<:BaseBuildingBlock}) = BaseComponent
Base.iterate(c::BaseBuildingBlock) = Base.iterate(c, 1)
Base.iterate(c::BaseBuildingBlock, index::Integer) = length(c) < index ? nothing : (c[keys(c)[index]], index + 1)
"""
events(building_block)
Returns just the non-gradient (i.e., RF pulses/readouts) events as a sequence of [`EventComponent`](@ref) objects (with their keys).
"""
function events(bb::BaseBuildingBlock)
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
return [(key, bb[key]) for key in keys(bb) if bb[key] isa EventComponent]
end
"""
waveform_sequence(building_block)
Returns just the gradient waveform as a sequence of [`GradientWaveform`](@ref) objects (with their keys).
"""
function waveform_sequence(bb::BaseBuildingBlock)
return [(key, bb[key]) for key in keys(bb) if bb[key] isa GradientWaveform]
end
function ndim_grad(bb::BaseBuildingBlock)
g = waveform_sequence(bb)
if izero(length(g))
return 0
end
for N in (1, 3)
if all(isa.(g, GradientWaveform{N}))
return N
end
end
error("$(typeof(bb)) contains both 1D and 3D gradient waveforms.")
end
"""
waveform(building_block)
Returns the gradient waveform of any [`BaseBuildingBlock`](@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(bb::BaseBuildingBlock)
ndim = ndim_grad(bb)
if ndim == 3
result = Tuple{VariableType, SVector{3, VariableType}}[(0., zero(SVector{3, Float64}))]
elseif ndim == 1
result = Tuple{VariableType, SVector{3, VariableType}}[(0., 0.)]
else
return []
end
for (_, block) in waveform_sequence(bb)
new_time = result[end][1] + max(duration(block), 0)
prev_grad = result[end][2]
if block isa NoGradient
@assert all(abs.(prev_grad) <= 1e-12) "$(typeof(bb)) inserts NoGradient before the gradient is zero. This is probably caused by an improper implementation of this BuildingBlock."
elseif block isa ConstantGradient
@assert all(gradient_strength(block) .≈ prev_grad) "$(typeof(bb)) inserts ConstantGradient that does not match previous gradient strength. This is probably caused by an improper implementation of this BuildingBlock."
elseif block isa ChangingGradient
@assert all(block.gradient_strength_start .≈ prev_grad) "$(typeof(bb)) inserts ChangingGradient that does not match previous gradient strength. This is probably caused by an improper implementation of this BuildingBlock."
push!(result, (new_time, prev_grad .+ slew_rate(block) .* duration(block)))
else
error("Unrecognised block type in BuildingBlock: $(typeof(bb)).")
end
end
@assert all(abs.(result[end][2]) <= 1e-12) "$(typeof(bb)) does not end up with a gradient of zero. This is probably caused by an improper implementation of this BuildingBlock."
return result
end
# Pathway support
"""
waveform_sequence(building_block, first, last)
Gets the sequence of [`GradientWaveform`](@ref) from the event with key `first` till the event with key `last`.
Setting `first` to nothing indicates to start from the beginning of the `building_block`.
Similarly, setting `last` to nothing indicates to continue till the end of the `building_block`.
"""
function waveform_sequence(bb::BaseBuildingBlock, first, last)
current_grad_key = current_start = nothing
parts = Tuple{Any, GradientWaveform}[]
for key in keys(bb)
if started && !isnothing(current_grad_key)
push!(parts, (current_grad_key, isnothing(current_start) ? bb[current_grad_key] : split_gradient(bb[current_grad_key], current_start)[2]))
current_start = effective_time(bb[key])
push!(parts, (current_grad_key, split_gradient(bb[current_grad_key], effective_time(bb[key]))[1]))
push!(parts, (current_grad_key, split_gradient(bb[current_grad_key], current_start, effective_time(bb[key]))[2]))
if !started
error("Starting index of $first not recognised.")
end
if !isnothing(last)
error("Final index of $last not recognised.")
end
return parts
end
"""

Michiel Cottaar
committed
qval(overlapping[, first_event, last_event])
Computes the area under the curve for the gradient waveform in [`BaseBuildingBlock`](@ref).
If `first_event` is set to something else than `nothing`, only the gradient waveform after this RF pulse/Readout will be considered.
Similarly, if `last_event` is set to something else than `nothing`, only the gradient waveform up to this RF pulse/Readout will be considered.
"""
function qval(bb::BaseBuildingBlock, index1, index2)
if (!isnothing(index1)) && (index1 == index2)
sum([qval(wv) for (_, wv) in waveform_sequence(bb, index1, index2)]; init=0.)

Michiel Cottaar
committed
qval(bb::BaseBuildingBlock) = qval(bb, nothing, nothing)
function bmat_gradient(bb::BaseBuildingBlock, qstart, index1, index2)
if (!isnothing(index1)) && (index1 == index2)
return zeros(3, 3)
end
result = Matrix{VariableType}(zeros(3, 3))
qcurrent = Vector{VariableType}(qstart)
for (_, part) in waveform_sequence(bb, index1, index2)
result = result .+ bmat_gradient(part, qcurrent)
qcurrent = qcurrent .+ qvec(part, qcurrent)
end
return result
end
bmat_gradient(bb::BaseBuildingBlock, qstart) = bmat_gradient(bb, qstart, nothing, nothing)
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
"""
BuildingBlock(waveform, events; duration=nothing, orientation=nothing, group)
Generic [`BaseBuildingBlock`](@ref) that can capture any overlapping gradients, RF pulses, and/or readouts.
The gradients cannot contain any free variables.
## Arguments
- `waveform`: Sequence of 2-element tuples with (time, (Gx, Gy, Gz)). If `orientation` is set then the tuple is expected to look like (time, G). This cannot contain any free variables.
- `events`: Sequence of 2-element tuples with (index, pulse/readout). The start time of the pulse/readout at the start of the gradient waveform element with index `index` (use [`DelayedEvent`](@ref) to make this earlier or later).
- `duration`: duration of this `BuildingBlock`. If not set then it will be assumed to be the time of the last element in `waveform`.
- `orientation`: orientation of the gradients in the waveform. If not set, then the full gradient vector should be given explicitly.
- `group`: group of the gradient waveform
"""
struct BuildingBlock <: BaseBuildingBlock
parts :: Vector{<:BaseComponent}
function BuildingBlock(parts::AbstractVector{<:BaseComponent})
res = new(duration, parts)
for (_, part) in waveform_sequence(parts)
scanner_constraints!(part)
end
return res
end
end
function BuildingBlock(waveform::AbstractVector, events::AbstractVector; duration=nothing, orientation=nothing, group=nothing)
events = Any[events...]
waveform = Any[waveform...]
ndim = isnothing(orientation) ? 1 : 3
zero_grad = isnothing(orientation) ? zeros(3) : 0.
if length(waveform) == 0 || waveform[1][1] > 0.
pushfirst!(waveform, (0., zero_grad))
events = [(i+1, e) for (i, e) in events]
end
if isnothing(duration)
duration = waveform[end][1]
end
if !(duration ≈ waveform[end][1])
@assert duration > waveform[end][1]
push!(waveform, (duration, zero_grad))
end
components = BaseComponent[]
for (index_grad, ((prev_time, prev_grad), (time, grad))) in enumerate(zip(waveform[1:end-1], waveform[2:end]))
duration = time - prev_time
if norm(prev_grad) <= 1e-12 && norm(grad) <= 1e-12
push!(components, NoGradient{ndim}(duration))
elseif norm(prev_grad) ≈ norm(grad)
push!(components, ConstantGradient(prev_grad, orientation, duration, group))
else
push!(components, ChangingGradient(prev_grad, (grad .- prev_grad) ./ duration, orientation, duration, group))
end
while length(events) > 0 && index_grad == events[1][1]
(_, event) = popfirst!(events)
push!(components, event)
end
end
return components
end
make_generic(other_block::BaseBuildingBlock) = BuildingBlock(duration(other_block), [other_block...])
Base.keys(bb::BuildingBlock) = 1:length(bb.parts)
Base.getindex(bb::BuildingBlock, i::Integer) = bb.parts[i]
duration(bb::BuildingBlock) = sum(duration, waveform_sequence(bb); init=0.)
function get_pulse(bb::BuildingBlock)
pulses = [p for p in events(bb) if p isa RFPulseComponent]
if length(pulses) == 0
error("BuildingBlock does not contain any pulses.")
end
if length(pulses) == 1
return pulses[1]
end
error("BuildingBlock contains more than one pulse. Not sure which one to return.")
end
function get_readout(bb::BuildingBlock)
readouts = [r for r in events(bb) if r isa ReadoutComponent]
if length(readouts) == 0
error("BuildingBlock does not contain any readouts.")
end
if length(readouts) == 1
return readouts[1]
end
error("BuildingBlock contains more than one readout. Not sure which one to return.")
end
struct Wait <: BaseBuildingBlock
duration :: VariableType
function Wait(var)
res = new(get_free_variable(var))
if !(res.duration isa Number)
@constraint global_model() res.duration >= 0
end
return res
end
end
duration(wb::Wait) = wb.duration
Base.keys(::Wait) = (Val(:empty),)
Base.getindex(wb::Wait, ::Val{:empty}) = NoGradient{1}(wb.duration)