Newer
Older
import MakieCore: generic_plot_attributes!
import ..Containers: BaseBuildingBlock, BaseSequence, waveform, events, start_time, ndim_grad, waveform_sequence
import ..Variables: duration, flip_angle, phase, make_generic, gradient_orientation
import ..Components: RFPulseComponent, ADC, InstantPulse, NoGradient, InstantGradient1D, InstantGradient3D
"""
SinglePlotLine(times, amplitudes, event_times, event_amplitudes)
A single line in a sequence diagram (e.g., RFx, Gy, ADC).
"""
struct SinglePlotLine
times :: Vector{Float64}
amplitudes :: Vector{Float64}
event_times :: Vector{Float64}
event_amplitudes :: Vector{Float64}
end
duration(pl::SinglePlotLine) = iszero(length(pl.times)) ? 0. : pl.times[end]
SinglePlotLine(times::Vector{Float64}, amplitudes::Vector{Float64}) = SinglePlotLine(times, amplitudes, Float64[], Float64[])
function SinglePlotLine(control_points::AbstractVector{<:Tuple}, duration::Number)
times = [cp[1] for cp in control_points]
amplitudes = [cp[2] for cp in control_points]
if times[1] > 0
pushfirst!(times, 0.)
pushfirst!(amplitudes, 0.)
end
if times[end] < duration
push!(times, duration)
end
return SinglePlotLine(times, amplitudes, [], [])
end
SinglePlotLine(duration::Number) = SinglePlotLine([0., duration], [0., 0.])
function SinglePlotLine(plot_lines::SinglePlotLine...)
times = Float64[]
amplitudes = Float64[]
event_times = Float64[]
event_amplitudes = Float64[]
current_time = 0.
for pl in plot_lines
append!(times, pl.times .+ current_time)
append!(amplitudes, pl.amplitudes)
append!(event_times, pl.event_times .+ current_time)
append!(event_amplitudes, pl.event_amplitudes)
current_time += duration(pl)
end
return SinglePlotLine(times, amplitudes, event_times, event_amplitudes)
"""
range_line(single_plot_line)
Returns the minimum and maximum amplitude for a [`SinglePlotLine`](@ref)
"""
range_line(spl::SinglePlotLine) = (
minimum(spl.amplitudes; init=0.),
maximum(spl.amplitudes; init=0.),
"""
range_event(single_plot_line)
Returns the minimum and maximum amplitude for the events in [`SinglePlotLine`](@ref)
"""
range_event(spl::SinglePlotLine) = (
minimum(spl.event_amplitudes; init=0.),
maximum(spl.event_amplitudes; init=0.),
)
function range_full(spl::SinglePlotLine)
(l1, u1) = range_line(spl)
(l2, u2) = range_event(spl)
return (
min(l1, l2),
max(u1, u2),
)
end
normalise(spl::SinglePlotLine, line_value, event_value) = SinglePlotLine(
spl.times, iszero(line_value) ? spl.amplitudes : (spl.amplitudes ./ line_value),
spl.event_times, iszero(event_value) ? spl.event_amplitudes : (spl.event_amplitudes ./ event_value)
)
"""
SequenceDiagram(; RFx, RFy, Gx, Gy, Gz, ADC)
All the lines forming a sequence diagram.
Each parameter should be a [`SinglePlotLine`](@ref) if provided.
Any parameters not provided will be set to a [`SinglePlotLine`](@ref) with zero amplitude.
"""
struct SequenceDiagram
RFx :: SinglePlotLine
RFy :: SinglePlotLine
G :: SinglePlotLine
Gx :: SinglePlotLine
Gy :: SinglePlotLine
Gz :: SinglePlotLine
ADC :: SinglePlotLine
end
function SequenceDiagram(actual_duration; kwargs...)
durations = duration.([values(kwargs)...])
@assert all(isapprox.(actual_duration, durations, rtol=1e-3))
res = SinglePlotLine[]
for symbol in (:RFx, :RFy, :G, :Gx, :Gy, :Gz, :ADC)
push!(res, get(kwargs, symbol, SinglePlotLine(actual_duration)))
end
return SequenceDiagram(res...)
end
function SequenceDiagram(bbb::BaseBuildingBlock)
kwargs = Dict{Symbol, SinglePlotLine}()
if !all([wvs isa NoGradient for (_, wvs) in waveform_sequence(bbb)])
orientation = ndim_grad(bbb) == 3 ? 1. : gradient_orientation(bbb)
for (index, symbol) in enumerate((:Gx, :Gy, :Gz))
kwargs[symbol] = SinglePlotLine([(time, (orientation .* amplitude)[index]) for (time, amplitude) in waveform(bbb)], duration(bbb))
end
end
for (name, raw_event) in events(bbb)
delay = start_time(bbb, name)
event = make_generic(raw_event)
if event isa RFPulseComponent
if :RFx in keys(kwargs)
error("Cannot plot a building block with more than 1 RF pulse.")
end
for (symbol, fn) in [(:RFx, cosd), (:RFy, sind)]
if event isa InstantPulse
kwargs[symbol] = SinglePlotLine([0., duration(bbb)], [0., 0.], [delay], [flip_angle(event) * fn(phase(event))])
points = Tuple{Float64, Float64}[]
t_prev = p_prev = a_prev = nothing
for (t, a, p) in zip(event.time, event.amplitude, event.phase)
if !isnothing(t_prev)
prev_phase_group = div(p_prev, 90, RoundDown)
phase_group = div(p, 90, RoundDown)
if phase_group != prev_phase_group
for edge in (phase_group < prev_phase_group ? (prev_phase_group:-1:phase_group+1) : (prev_phase_group+1:phase_group))
edge_phase = edge * 90
edge_time = (abs(edge_phase - p_prev) * t + abs(edge_phase - p) * t_prev) / abs(p - p_prev)
edge_amplitude = (abs(edge_phase - p_prev) * a + abs(edge_phase - p) * a_prev) / abs(p - p_prev)
push!(points, (edge_time + delay, edge_amplitude * fn(edge_phase)))
end
end
end
push!(points, (t + delay, a * fn(p)))
t_prev = t
p_prev = p
a_prev = a
end
kwargs[symbol] = SinglePlotLine(points, duration(bbb))
end
end
elseif event isa ADC
if :ADC in keys(kwargs)
error("Cannot plot a building block with more than 1 ADC event.")
end
if iszero(duration(event))
kwargs[:ADC] = SinglePlotLine(
[0., duration(bbb)],
[0., 0.],
[delay], [1.]
)
else
kwargs[:ADC] = SinglePlotLine(
[0., delay, delay, delay + duration(event), delay + duration(event), duration(bbb)],
[0., 0., 1., 1., 0., 0.],
elseif event isa InstantGradient1D
kwargs[:G] = SinglePlotLine([0., duration(bbb)], [0., 0.], [delay], [event.qval])
elseif event isa InstantGradient3D
for (index, symbol) in enumerate([:Gx, :Gy, :Gz])
kwargs[symbol] = SinglePlotLine([0., duration(bbb)], [0., 0.], [delay], [event.qval[index]])
end
end
function SequenceDiagram(seq::BaseSequence)
as_lines = SequenceDiagram.(seq)
return SequenceDiagram([
SinglePlotLine([getproperty(line, symbol) for line in as_lines]...)
for symbol in [:RFx, :RFy, :G, :Gx, :Gy, :Gz, :ADC]]...)
end
function normalise(sd::SequenceDiagram)
rf_line_norm = max(abs.(range_line(sd.RFx))..., abs.(range_line(sd.RFy))...)
rf_event_norm = max(abs.(range_event(sd.RFx))..., abs.(range_event(sd.RFy))...)
grad_line_norm = max(
abs.(range_line(sd.G))...,
abs.(range_line(sd.Gx))...,
abs.(range_line(sd.Gy))...,
abs.(range_line(sd.Gz))...,
)
grad_event_norm = max(
abs.(range_event(sd.G))...,
abs.(range_event(sd.Gx))...,
abs.(range_event(sd.Gy))...,
abs.(range_event(sd.Gz))...,
)
normalise(sd.RFx, rf_line_norm, rf_event_norm),
normalise(sd.RFy, rf_line_norm, rf_event_norm),
normalise(sd.G, grad_line_norm, grad_event_norm),
normalise(sd.Gx, grad_line_norm, grad_event_norm),
normalise(sd.Gy, grad_line_norm, grad_event_norm),
normalise(sd.Gz, grad_line_norm, grad_event_norm),
sd.ADC
)
end
plot_sequence(sequence; figure=(), axis=(), attributes...)
plot(sequence; attributes...)
plot!([scene,] sequence; attributes...)
Plot the sequence diagram.
Calling `plot_sequence` will result in a much cleaner sequence diagram (recommended).
However, if you want to combine this diagram with other plots you will have to use `plot` or `plot!` instead.
If called as `plot_sequence` the user can also supply `Makie.Figure` (`figure=(...)`) and `Makie.Axis` (`axis=(...)`) keywords.
If called using the `plot` or `plot!` interface, only the attributes listed below can be supplied
This function will only work if [`Makie`](https://makie.org) is installed and imported.
## Attributes
### Line properties
- `linecolor` sets the color of the lines. If you want to set the text color to the same value, you can also use `color=...`.
- `linewidth=1.5` sets the width of the lines.
- `instant_width=3.` sets the width of any instant gradients or pulses with respect to the `linewidth`.
### Text properties
- `textcolor` sets the color of the text. If you want to set the line color to the same value, you can also use `color=...`.
- `font` sets whether the rendered text is :regular, :bold, or :italic.
- `fontsize`: set the size of each character.
$(Base.Docs.doc(generic_plot_attributes!))
"""