diff --git a/docs/src/adjust_sequences.md b/docs/src/adjust_sequences.md index 82ca76a6c85cd5295ba95294a9e0cbbe203de66d..2464ab35d0b92fc9dea8797d38f08ad031aeaa6e 100644 --- a/docs/src/adjust_sequences.md +++ b/docs/src/adjust_sequences.md @@ -11,6 +11,7 @@ Post-hoc alterations can be applied to gradients or RF pulses with a specific la Some example usages are: - Reduce the RF pulse amplitude by 20% (e.g., to model the effect of transmit bias field): `adjust(sequence, pulse=(scale=0.8, ))` - Repeat sequence 2 times with different diffusion-weighted gradient orientations (x- and y-direction) and gradient strength reduced by 30%: `adjust(sequence, diffusion=(orientation=[[1., 0., 0], [0., 1., 0.]], scale=0.7))` +- Repeat the sequence by shifting the excited slice by the given number of millimetres in the slice-select direction: `adjust(sequence, FOV=(shift=[-7.5, -2.5, 2.5, 7.5, -5., 0., 5., 10.]))`. These shifts represent an interleaved acquisition scheme, where the acquired slices/bands are 2.5 mm apart. - Rotations defined using the [`Rotations.jl`](https://github.com/JuliaGeometry/Rotations.jl) package can be applied to gradient orientations or the field of view. For example, to rotate the field of view by 45 degrees around the y-axis: ```julia using Rotations diff --git a/docs/src/sequence_optimisation.md b/docs/src/sequence_optimisation.md index 0ab0233bdcc45e99a55330c3622fd5980e74b5f1..11bd984e7cbcb1429b27abe21735dc04e705e807 100644 --- a/docs/src/sequence_optimisation.md +++ b/docs/src/sequence_optimisation.md @@ -64,7 +64,7 @@ variables.rise_time variables.slew_rate variables.slew_rate_norm variables.slice_thickness -variables.spoiler_scale +variables.spoiler variables.time_to_center variables.voxel_size variables.Δ diff --git a/src/MRIBuilder.jl b/src/MRIBuilder.jl index 6d55ef6c5abc749a18f8deea5f1d7b3917c316f0..f49f5f46786e2dc0a2233875b89fd9002bd2d08d 100644 --- a/src/MRIBuilder.jl +++ b/src/MRIBuilder.jl @@ -28,8 +28,8 @@ export variables, make_generic, @defvar, get_pulse, get_readout, get_pathway, ge import .Components: InstantPulse, ConstantPulse, SincPulse, GenericPulse, InstantGradient, SingleReadout, ADC, CompositePulse, edge_times export InstantPulse, ConstantPulse, SincPulse, GenericPulse, InstantGradient, SingleReadout, ADC, CompositePulse, edge_times -import .Containers: ContainerBlock, start_time, end_time, waveform, waveform_sequence, events, BaseBuildingBlock, BuildingBlock, Wait, BaseSequence, nrepeat, Sequence, AlternativeBlocks, match_blocks!, get_index_single_TR, iter_blocks, iter_instant_gradients, iter_instant_pulses -export ContainerBlock, start_time, end_time, waveform, waveform_sequence, events, BaseBuildingBlock, BuildingBlock, Wait, BaseSequence, nrepeat, Sequence, AlternativeBlocks, match_blocks!, get_index_single_TR, iter_blocks, iter_instant_gradients, iter_instant_pulses +import .Containers: ContainerBlock, start_time, end_time, waveform, waveform_sequence, events, BaseBuildingBlock, BuildingBlock, Wait, BaseSequence, nrepeat, Sequence, AbstractAlternativeBlocks, get_alternatives_name, get_alternatives_options, AlternativeBlocks, match_blocks!, get_index_single_TR, iter_blocks, iter_instant_gradients, iter_instant_pulses +export ContainerBlock, start_time, end_time, waveform, waveform_sequence, events, BaseBuildingBlock, BuildingBlock, Wait, BaseSequence, nrepeat, Sequence, AbstractAlternativeBlocks, get_alternatives_name, get_alternatives_options, AlternativeBlocks, match_blocks!, get_index_single_TR, iter_blocks, iter_instant_gradients, iter_instant_pulses import .Pathways: Pathway export Pathway diff --git a/src/build_sequences.jl b/src/build_sequences.jl index 64eba7a869562a17d95586202f95302ace2d11f8..c9a715a7a2daff57d504b2df2b7882e89582c54b 100644 --- a/src/build_sequences.jl +++ b/src/build_sequences.jl @@ -1,5 +1,5 @@ module BuildSequences -import JuMP: Model, optimizer_with_attributes, optimize!, AbstractJuMPScalar, value, solution_summary, termination_status, LOCALLY_SOLVED, OPTIMAL, num_variables, all_variables, set_start_value, ALMOST_LOCALLY_SOLVED, objective_value, INVALID_MODEL, @variable, @objective, @constraint +using JuMP import Ipopt import Juniper import ..Scanners: Scanner, gradient_strength, Default_Scanner @@ -95,6 +95,10 @@ function build_sequence(f::Function, scanner::Union{Nothing, Scanner}, model::Tu end end +function number_equality_constraints(model::Model) + sum([num_constraints(model, expr, comp) for (expr, comp) in JuMP.list_of_constraint_types(model) if comp <: MOI.EqualTo]) +end + function optimise_with_cost_func(jump_model::Model, cost_func, n_attempts) @objective jump_model Min cost_func min_objective = Inf @@ -107,11 +111,10 @@ function optimise_with_cost_func(jump_model::Model, cost_func, n_attempts) set_start_value(var, v) end end - optimize!(jump_model) - while termination_status(jump_model) == INVALID_MODEL + for _ in num_variables(jump_model):number_equality_constraints(jump_model) @variable(jump_model) - optimize!(jump_model) end + optimize!(jump_model) if termination_status(jump_model) in (LOCALLY_SOLVED, OPTIMAL) if objective_value(jump_model) < min_objective min_objective = objective_value(jump_model) @@ -126,7 +129,7 @@ function optimise_with_cost_func(jump_model::Model, cost_func, n_attempts) end end -function build_sequence(f::Function, scanner::Union{Nothing, Scanner}, optimiser_constructor; optimise=true, n_attempts=100, kwargs...) +function build_sequence(f::Function, scanner::Union{Nothing, Scanner}, optimiser_constructor; optimise=true, n_attempts=30, kwargs...) if optimise || GLOBAL_MODEL[] == IGNORE_MODEL model = ( Model(optimizer_with_attributes(optimiser_constructor, [string(k) => v for (k, v) in kwargs]...)), diff --git a/src/components/abstract_types.jl b/src/components/abstract_types.jl index bcbfe7dd50e461108f94682bd1715d81eb0d4d3c..88862b66cab4e5b35f6a60ebe2daccd8feac39ed 100644 --- a/src/components/abstract_types.jl +++ b/src/components/abstract_types.jl @@ -1,5 +1,5 @@ module AbstractTypes -import ...Variables: AbstractBlock, variables, adjustable, gradient_orientation, @defvar +import ...Variables: AbstractBlock, variables, adjust_groups, gradient_orientation, @defvar """ Super-type for all individual components that form an MRI sequence (i.e., RF pulse, gradient waveform, or readout event). @@ -110,8 +110,8 @@ It should be infinite if the component is linear. split_timestep(comp_tuple::Tuple{<:Number, <:EventComponent}, precision::Number) = split_timestep(comp_tuple[2], precision) -adjustable(::RFPulseComponent) = :pulse -adjustable(::GradientWaveform) = :gradient +adjust_groups(p::RFPulseComponent) = [p.group, :pulse] +adjust_groups(g::GradientWaveform) = [g.group, :gradient] gradient_orientation(gw::GradientWaveform{1}) = gw.orientation diff --git a/src/components/gradient_waveforms/no_gradient_blocks.jl b/src/components/gradient_waveforms/no_gradient_blocks.jl index 31a534ed79c512bd1cd008efe71148df261cbb7e..3b0eec892f90603cbdea2ac0dbc459c374f2a4ed 100644 --- a/src/components/gradient_waveforms/no_gradient_blocks.jl +++ b/src/components/gradient_waveforms/no_gradient_blocks.jl @@ -1,6 +1,6 @@ module NoGradientBlocks import StaticArrays: SVector, SMatrix -import ....Variables: VariableType, get_free_variable, adjustable, variables, @defvar +import ....Variables: VariableType, get_free_variable, adjust_groups, variables, @defvar import ...AbstractTypes: GradientWaveform import ..ChangingGradientBlocks: split_gradient @@ -35,6 +35,6 @@ function split_gradient(ngb::NoGradient, times::VariableType...) return [NoGradient(d) for d in durations] end -adjustable(::NoGradient) = :false +adjust_groups(::NoGradient) = Symbol[] end \ No newline at end of file diff --git a/src/components/instant_gradients.jl b/src/components/instant_gradients.jl index da550bfabdb2f9598a9d5d1e36a38ac5beda8816..ddca7f0d0a77f16ca1067ac36f59d4ebff640b32 100644 --- a/src/components/instant_gradients.jl +++ b/src/components/instant_gradients.jl @@ -1,7 +1,7 @@ module InstantGradients import StaticArrays: SVector, SMatrix import JuMP: @constraint -import ...Variables: @defvar, VariableType, variables, get_free_variable, set_simple_constraints!, make_generic, adjust_internal, adjustable, gradient_orientation, apply_simple_constraint! +import ...Variables: @defvar, VariableType, variables, get_free_variable, set_simple_constraints!, make_generic, adjust_internal, adjust_groups, gradient_orientation, apply_simple_constraint! import ..AbstractTypes: EventComponent, GradientWaveform """ @@ -15,7 +15,7 @@ If the `orientation` is set an [`InstantGradient1D`](@ref) is returned, otherwis ## Variables - [`variables.qvec`](@ref): Spatial frequency on which spins will be dephased due to this pulsed gradient in rad/um. -- [`variables.spoiler_scale`](@ref): Length-scale on which spins will be dephased by exactly 2π in mm. +- [`variables.spoiler`](@ref): Length-scale on which spins will be dephased by exactly 2π in mm. """ abstract type InstantGradient{N} <: EventComponent end @@ -62,7 +62,7 @@ end make_generic(ig::InstantGradient) = ig -adjustable(::InstantGradient) = :gradient +adjust_groups(g::InstantGradient) = [g.group, :gradient] gradient_orientation(ig::InstantGradient{1}) = ig.orientation diff --git a/src/containers/alternatives.jl b/src/containers/alternatives.jl index 8847d5f3e8f911943a812fbca9d093c97a2bae63..791e3fb1d811200cf495bdd26614be9c99f21375 100644 --- a/src/containers/alternatives.jl +++ b/src/containers/alternatives.jl @@ -4,6 +4,17 @@ import ..Abstract: ContainerBlock import ...BuildSequences: fixed import ...Variables: @defvar, make_generic, apply_simple_constraint! +""" +Parent type for all blocks that can take different MR sequence components between multiple repetitions of the sequence. + +They can be extended into their individual components using `adjust(<name>=:all)`. + +Each subtype of [`AbstractAlternativeBlock`](@ref) needs to implement two methods: +- [`get_alternatives_name`](@ref): returns the `name` used to identify this block in `adjust` +- [`get_alternatives_options`](@ref): returns a dictionary mapping the name of the different options to a [`ContainerBlock`](@ref) with the actual sequence building block. +""" +abstract type AbstractAlternativeBlocks <: ContainerBlock end + """ AlternativeBlocks(name, blocks) @@ -11,19 +22,35 @@ Represents a part of the sequence where there are multiple possible alternatives Variables can be matched across these alternatives using [`match_blocks!`](@ref). -The `name` is a symbol that is used to identify this `AlternativeBlocks` in the broader sequence. +The `name` is a symbol that is used to identify this `AlternativeBlocks` in the broader sequence (as in `adjust`). """ -struct AlternativeBlocks <: ContainerBlock +struct AlternativeBlocks <: AbstractAlternativeBlocks name :: Symbol options :: Dict{Any, <:ContainerBlock} end AlternativeBlocks(name::Symbol, options_vector::AbstractVector) = AlternativeBlocks(name, Dict(index => value for (index, value) in enumerate(options_vector))) -Base.getindex(alt::AlternativeBlocks, index) = alt.options[index] -Base.length(alt::AlternativeBlocks) = length(alt.options) +""" + get_alternatives_name(alternative_block) + +Get the name with which any [`AbstractAlternativeBlocks`](@ref) will be identified in a call to `adjust`. +""" +get_alternatives_name(alt::AlternativeBlocks) = alt.name -@defvar duration(alt::AlternativeBlocks) = maximum(variables.duration.(values(alt.options))) +""" + get_alternatives_options(alternative_block) + +Get the options available for a [`AbstractAlternativeBlocks`](@ref). +""" +get_alternatives_options(alt::AlternativeBlocks) = alt.options + +Base.getindex(alt::AbstractAlternativeBlocks, index) = get_alternatives_options(alt)[index] +Base.length(alt::AbstractAlternativeBlocks) = length(get_alternatives_options(alt)) +Base.keys(alt::AbstractAlternativeBlocks) = keys(get_alternatives_options(alt)) +Base.values(alt::AbstractAlternativeBlocks) = values(get_alternatives_options(alt)) + +@defvar duration(alt::AbstractAlternativeBlocks) = maximum(variables.duration.(values(alt.options))) """ match_blocks!(alternatives, function) @@ -32,8 +59,11 @@ Matches the outcome of given `function` on each of the building blocks in [`Alte For example, `match_blocks!(alternatives, duration)` will ensure that all the alternative building blocks have the same duration. """ -function match_blocks!(alternatives::AlternativeBlocks, func) - options = [values(alternatives.options)...] +function match_blocks!(alternatives::AbstractAlternativeBlocks, func) + options = [values(get_alternatives_options(alternatives.options))...] + if length(options) <= 1 + return + end baseline = func(options[1]) for other_block in options[2:end] apply_simple_constraint!(func(other_block), baseline) @@ -41,7 +71,7 @@ function match_blocks!(alternatives::AlternativeBlocks, func) end fixed(alt::AlternativeBlocks) = AlternativeBlocks(alt.name, Dict(key=>fixed(value) for (key, value) in alt.options)) -make_generic(alt::AlternativeBlocks) = AlternativeBlocks(alt.name, Dict(key=>make_generic(value) for (key, value) in alt.options)) +make_generic(alt::AbstractAlternativeBlocks) = AlternativeBlocks(get_alternatives_name(alt), get_alternatives_options(alt)) end \ No newline at end of file diff --git a/src/containers/containers.jl b/src/containers/containers.jl index 96198e5d98ad698ee789af1dacbcc23a85f1555e..55c92a1679bc47674301ea34abbb5227e153f97f 100644 --- a/src/containers/containers.jl +++ b/src/containers/containers.jl @@ -7,6 +7,6 @@ include("alternatives.jl") import .Abstract: ContainerBlock, start_time, end_time, iter_blocks, iter_instant_gradients, iter_instant_pulses import .BuildingBlocks: BaseBuildingBlock, BuildingBlock, Wait, waveform, waveform_sequence, events, ndim_grad import .BaseSequences: BaseSequence, Sequence, nrepeat, get_index_single_TR -import .Alternatives: AlternativeBlocks, match_blocks! +import .Alternatives: AbstractAlternativeBlocks, get_alternatives_name, get_alternatives_options, AlternativeBlocks, match_blocks! end \ No newline at end of file diff --git a/src/parts/epi_readouts.jl b/src/parts/epi_readouts.jl index ed89c70cc9701dd972bfa8119bf9d728db6c54bd..0757d2e311cacc89f9835f0951f7df0d0a960754 100644 --- a/src/parts/epi_readouts.jl +++ b/src/parts/epi_readouts.jl @@ -2,7 +2,7 @@ module EPIReadouts import ...Containers: BaseSequence, get_index_single_TR import ..Trapezoids: Trapezoid, opposite_kspace_lines, LineReadout import ...Components: ADC -import ...Variables: get_free_variable, VariableType, set_simple_constraints!, get_readout, apply_simple_constraint!, variables, @defvar +import ...Variables: get_free_variable, VariableType, set_simple_constraints!, get_readout, apply_simple_constraint!, variables, @defvar, add_cost_function! import ...Pathways: PathwayWalker, update_walker_till_time!, walk_pathway! """ @@ -58,6 +58,12 @@ function EPIReadout(; resolution::AbstractVector{<:Integer}, recenter=false, gro res.blips[shift] = Trapezoid(qvec=[0., shift * res.ky_step, 0.], group=group) end set_simple_constraints!(res, vars) + add_cost_function!(sum(variables.duration.([ + res.start_gradient, + res.positive_line, + res.negative_line, + values(res.blips)..., + ]))) return res end diff --git a/src/parts/helper_functions.jl b/src/parts/helper_functions.jl index 66a307aa81ae6aeb6595fb0e73b364a8f5706276..b89f7bfb4925de84dadf24634a2445056ccccb03 100644 --- a/src/parts/helper_functions.jl +++ b/src/parts/helper_functions.jl @@ -135,7 +135,7 @@ function refocus_pulse(; flip_angle=180, phase=0., frequency=0., shape=nothing, return SliceSelect(pulse; duration=:min, slice_thickness=slice_thickness, orientation=orientation, group=:FOV) end else - res = SpoiltSliceSelect(pulse; orientation=orientation, duration=:min, group=:FOV, slice_thickness=slice_thickness, spoiler_scale=spoiler) + res = SpoiltSliceSelect(pulse; orientation=orientation, duration=:min, group=:FOV, slice_thickness=slice_thickness, spoiler=spoiler) return res end end @@ -285,7 +285,7 @@ function interpret_image_size(fov, resolution, voxel_size, slice_thickness) end """ - gradient_spoiler(; optimise=false, orientation=[0, 0, 1], rotate=:FOV, scale=:spoiler, spoiler_scale=1., duration=:min, variables...) + gradient_spoiler(; optimise=false, orientation=[0, 0, 1], rotate=:FOV, scale=:spoiler, spoiler=1., duration=:min, variables...) Returns two DWI gradients that are guaranteed to cancel each other out. @@ -297,12 +297,12 @@ Returns two DWI gradients that are guaranteed to cancel each other out. - `scanner`: Used for testing. Do not set this parameter at this level (instead set it for the total sequence using [`build_sequence`](@ref)). ## Variables -- [`variables.spoiler_scale`](@ref): maximum spoiler scale (before applying any reductions due to `scale`). +- [`variables.spoiler`](@ref): maximum spoiler scale (before applying any reductions due to `scale`). - Any other parameters expected by [`Trapezoid`](@ref). """ -function gradient_spoiler(; optimise=false, scanner=nothing, orientation=[0, 0, 1], group=:FOV, spoiler_scale=1., duration=:min, variables...) +function gradient_spoiler(; optimise=false, scanner=nothing, orientation=[0, 0, 1], group=:FOV, spoiler=1., duration=:min, variables...) build_sequence(scanner; optimise=optimise) do - Trapezoid(; orientation=orientation, group=group, spoiler_scale=spoiler_scale, duration=duration, variables...) + Trapezoid(; orientation=orientation, group=group, spoiler=spoiler, duration=duration, variables...) end end diff --git a/src/parts/spoilt_slice_selects.jl b/src/parts/spoilt_slice_selects.jl index adef2102f4d17c0e28312183a7d11977cd810a5f..b529123c4581d99079e5666143e7b6a86c2ff99d 100644 --- a/src/parts/spoilt_slice_selects.jl +++ b/src/parts/spoilt_slice_selects.jl @@ -4,13 +4,13 @@ import LinearAlgebra: norm import StaticArrays: SVector import JuMP: @constraint, @objective, objective_function import ...BuildSequences: global_scanner -import ...Variables: VariableType, get_pulse, set_simple_constraints!, variables, @defvar, gradient_orientation +import ...Variables: VariableType, get_pulse, set_simple_constraints!, variables, @defvar, gradient_orientation, adjust, adjust_groups, adjust_internal, get_free_variable, apply_simple_constraint! import ...Components: ChangingGradient, ConstantGradient, RFPulseComponent import ...Containers: BaseBuildingBlock """ - SpoiltSliceSelect(pulse; parameters, variables...) + SpoiltSliceSelect(pulse; parameters..., variables...) Adds slice selection to the `pulse` and surrounds it with spoiler gradients. @@ -21,7 +21,7 @@ Adds slice selection to the `pulse` and surrounds it with spoiler gradients. ## Variables - `duration`: total duration of the block in ms. - `slice_thickness`: slice thickness in mm. -- `spoiler_scale`: length scale on which the spoilers achieve 2π dephasing in mm. This sets the minimum spoiling. If this spoiling level is not achieved by the slice-select gradient alone, then there will be additional gradients added. +- `spoiler`: length scale on which the spoilers achieve 2π dephasing in mm. This sets the minimum spoiling. If this spoiling level is not achieved by the slice-select gradient alone, then there will be additional gradients added. """ struct SpoiltSliceSelect <: BaseBuildingBlock orientation :: SVector{3, Float64} @@ -35,7 +35,7 @@ struct SpoiltSliceSelect <: BaseBuildingBlock slew_rate :: Number end -function SpoiltSliceSelect(pulse::RFPulseComponent; orientation=[0, 0, 1], group=nothing, slice_thickness=nothing, kwargs...) +function SpoiltSliceSelect(pulse::RFPulseComponent; orientation=[0, 0, 1], group=:FOV, slice_thickness=nothing, spoiler, kwargs...) res = nothing if slice_thickness isa Number && isinf(slice_thickness) rise_time_var = get_free_variable(nothing) @@ -49,7 +49,7 @@ function SpoiltSliceSelect(pulse::RFPulseComponent; orientation=[0, 0, 1], group flat_time_var, rise_time_var, group, - slew_rate(global_scanner()), + global_scanner().slew_rate, ) for time_var in (rise_time_var, flat_time_var) apply_simple_constraint!(time_var, :>=, 0) @@ -64,21 +64,23 @@ function SpoiltSliceSelect(pulse::RFPulseComponent; orientation=[0, 0, 1], group get_free_variable(nothing; start=0.1), get_free_variable(nothing; start=0.1), group, - slew_rate(global_scanner()), + global_scanner().slew_rate, ) for time_var in (res.rise_time1, res.flat_time1, res.diff_time, res.flat_time2, res.fall_time2) apply_simple_constraint!(time_var, :>=, 0) end apply_simple_constraint!(res.diff_time, :<=, res.rise_time1) - apply_simple_constraint!(res.diff_time, :<=, res.rise_time2) - apply_simple_constraint!(qvec(res, nothing, :pulse), qvec(res, :pulse, nothing)) + apply_simple_constraint!(res.diff_time, :<=, res.fall_time2) + apply_simple_constraint!(variables.qvec(res, nothing, :pulse), variables.qvec(res, :pulse, nothing)) + apply_simple_constraint!(variables.inverse_slice_thickness(res), 1/slice_thickness) end set_simple_constraints!(res, kwargs) + apply_simple_constraint!(variables.spoiler(res), :<=, spoiler) - max_time = gradient_strength(global_scanner()) / res.slew_rate - apply_simple_constraint!(rise_time(res)[1], :<=, max_time) - apply_simple_constraint!(fall_time(res)[2], :<=, max_time) + max_time = global_scanner().gradient / res.slew_rate + apply_simple_constraint!(variables.rise_time(res)[1], :<=, max_time) + apply_simple_constraint!(variables.fall_time(res)[2], :<=, max_time) return res end @@ -87,36 +89,37 @@ gradient_orientation(spoilt::SpoiltSliceSelect) = spoilt.orientation @defvar begin duration_trap1(spoilt::SpoiltSliceSelect) = 2 * spoilt.rise_time1 + spoilt.flat_time1 - spoilt.diff_time duration_trap2(spoilt::SpoiltSliceSelect) = 2 * spoilt.fall_time2 + spoilt.flat_time2 - spoilt.diff_time + effective_time(spoilt::SpoiltSliceSelect) = variables.effective_time(spoilt, Val(:pulse)) end Base.keys(::SpoiltSliceSelect) = Val.((:rise1, :flat1, :fall1, :flat_pulse, :pulse, :rise2, :flat2, :fall2)) -Base.getindex(spoilt::SpoiltSliceSelect, ::Val{:rise1}) = ChangingGradient(0., variables.slew_rate(spoilt), gradient_orientation(spoilt), variables.rise_time(spoilt)[1], spoilt.group) -Base.getindex(spoilt::SpoiltSliceSelect, ::Val{:flat1}) = ConstantGradient(variables.slew_rate(spoilt) * variables.rise_time(spoilt)[1], gradient_orientation(spoilt), variables.flat_time(spoilt)[1], spoilt.group) -Base.getindex(spoilt::SpoiltSliceSelect, ::Val{:fall1}) = ChangingGradient(variables.slew_rate(spoilt) * variables.rise_time(spoilt)[1], -variables.slew_rate(spoilt), gradient_orientation(spoilt), variables.fall_time(spoilt)[1], spoilt.group) -Base.getindex(spoilt::SpoiltSliceSelect, ::Val{:flat_pulse}) = ConstantGradient(variables.slew_rate(spoilt) * spoilt.diff_time, gradient_orientation(spoilt), variables.duration(spoilt.pulse), spoilt.group) -Base.getindex(spoilt::SpoiltSliceSelect, ::Val{:pulse}) = spoilt.pulse -Base.getindex(spoilt::SpoiltSliceSelect, ::Val{:rise2}) = ChangingGradient(variables.slew_rate(spoilt) * spoilt.diff_time, variables.slew_rate(spoilt), gradient_orientation(spoilt), variables.rise_time(spoilt)[2], spoilt.group) -Base.getindex(spoilt::SpoiltSliceSelect, ::Val{:flat2}) = ConstantGradient(variables.slew_rate(spoilt) * variables.fall_time(spoilt)[2], gradient_orientation(spoilt), variables.flat_time(spoilt)[2], spoilt.group) -Base.getindex(spoilt::SpoiltSliceSelect, ::Val{:fall2}) = ChangingGradient(variables.slew_rate(spoilt) * variables.fall_time(spoilt)[2], -variables.slew_rate(spoilt), gradient_orientation(spoilt), variables.fall_time(spoilt)[2], spoilt.group) +Base.getindex(spoilt::SpoiltSliceSelect, ::Val{:rise1}) = ChangingGradient(zero(SVector{3, Float64}), variables.slew_rate(spoilt), variables.rise_time(spoilt)[1], spoilt.group) +Base.getindex(spoilt::SpoiltSliceSelect, ::Val{:flat1}) = ConstantGradient(variables.slew_rate(spoilt) .* variables.rise_time(spoilt)[1], variables.flat_time(spoilt)[1], spoilt.group) +Base.getindex(spoilt::SpoiltSliceSelect, ::Val{:fall1}) = ChangingGradient(variables.slew_rate(spoilt) .* variables.rise_time(spoilt)[1], -variables.slew_rate(spoilt), variables.fall_time(spoilt)[1], spoilt.group) +Base.getindex(spoilt::SpoiltSliceSelect, ::Val{:flat_pulse}) = ConstantGradient(variables.slew_rate(spoilt) .* spoilt.diff_time, variables.duration(spoilt.pulse), spoilt.group) +Base.getindex(spoilt::SpoiltSliceSelect, ::Val{:pulse}) = (0., spoilt.pulse) +Base.getindex(spoilt::SpoiltSliceSelect, ::Val{:rise2}) = ChangingGradient(variables.slew_rate(spoilt) .* spoilt.diff_time, variables.slew_rate(spoilt), variables.rise_time(spoilt)[2], spoilt.group) +Base.getindex(spoilt::SpoiltSliceSelect, ::Val{:flat2}) = ConstantGradient(variables.slew_rate(spoilt) .* variables.fall_time(spoilt)[2], variables.flat_time(spoilt)[2], spoilt.group) +Base.getindex(spoilt::SpoiltSliceSelect, ::Val{:fall2}) = ChangingGradient(variables.slew_rate(spoilt) .* variables.fall_time(spoilt)[2], -variables.slew_rate(spoilt), variables.fall_time(spoilt)[2], spoilt.group) @defvar begin rise_time(spoilt::SpoiltSliceSelect) = (spoilt.rise_time1, spoilt.fall_time2 - spoilt.diff_time) flat_time(spoilt::SpoiltSliceSelect) = (spoilt.flat_time1, spoilt.flat_time2) fall_time(spoilt::SpoiltSliceSelect) = (spoilt.rise_time1 - spoilt.diff_time, spoilt.fall_time2) - slew_rate(spoilt::SpoiltSliceSelect) = spoilt.slew_rate + slew_rate(spoilt::SpoiltSliceSelect) = spoilt.slew_rate .* spoilt.orientation end @defvar begin - duration(spoilt::SpoiltSliceSelect) = sum(variables.rise_time(spoilt)) + sum(variables.flat_time(spoilt)) + sum(variables.flat_time(spoilt)) + variables.duration(spoilt.pulse) - inverse_slice_thickness(spoilt::SpoiltSliceSelect) = variables.slew_rate(spoilt) * spoilt.diff_time * variables.duration(spoilt.pulse) * 1e3 - gradient_strength(spoilt::SpoiltSliceSelect) = variables.slew_rate(spoilt) * max(spoilt.rise_time1, spoilt.fall_time2) + duration(spoilt::SpoiltSliceSelect) = sum(variables.rise_time(spoilt)) + sum(variables.flat_time(spoilt)) + sum(variables.fall_time(spoilt)) + variables.duration(spoilt.pulse) + inverse_slice_thickness(spoilt::SpoiltSliceSelect) = spoilt.slew_rate * spoilt.diff_time * 1e3 * variables.inverse_bandwidth(spoilt.pulse) + gradient_strength(spoilt::SpoiltSliceSelect) = variables.slew_rate(spoilt) .* max(spoilt.rise_time1, spoilt.fall_time2) end get_pulse(spoilt::SpoiltSliceSelect) = spoilt.pulse @defvar function all_gradient_strengths(spoilt::SpoiltSliceSelect) grad1 = spoilt.slew_rate * variables.rise_time(spoilt)[1] - grad2 = grad1 - spoilt.slew_rate * variables.flat_time(spoilt)[1] + grad2 = grad1 - spoilt.slew_rate * variables.fall_time(spoilt)[1] grad3 = spoilt.slew_rate * variables.fall_time(spoilt)[2] return [grad1, grad2, grad3] end @@ -134,6 +137,25 @@ variables.all_gradient_strengths Returns the time of the [`SpoiltSliceSelect`](@ref) to return to zero. """ -variables.all_gradient_strengths +variables.fall_time + +adjust_groups(ss::SpoiltSliceSelect) = [ss.group, :slice] + +function adjust_internal(ss::SpoiltSliceSelect; shift=0.) + return SpoiltSliceSelect( + ss.orientation, + ss.rise_time1, + ss.flat_time1, + ss.diff_time, + adjust( + ss.pulse; + pulse=(frequency=variables.all_gradient_strengths(ss)[2] * shift * 1e3, ) + ), + ss.flat_time2, + ss.fall_time2, + ss.group, + ss.slew_rate, + ) +end end \ No newline at end of file diff --git a/src/parts/trapezoids.jl b/src/parts/trapezoids.jl index 8a7eebb8b618e93a0ee1e1a5bfa75ccdd0bab5e3..084accce7285f0b85656a0ceb4bbbb9926262412 100644 --- a/src/parts/trapezoids.jl +++ b/src/parts/trapezoids.jl @@ -6,7 +6,7 @@ module Trapezoids import JuMP: @constraint import StaticArrays: SVector import LinearAlgebra: norm -import ...Variables: variables, @defvar, scanner_constraints!, get_free_variable, set_simple_constraints!, gradient_orientation, VariableType, get_gradient, get_pulse, get_readout, adjustable, adjust_internal, apply_simple_constraint!, add_cost_function! +import ...Variables: variables, @defvar, scanner_constraints!, get_free_variable, set_simple_constraints!, gradient_orientation, VariableType, get_gradient, get_pulse, get_readout, adjust, adjust_groups, adjust_internal, apply_simple_constraint!, add_cost_function! import ...Components: ChangingGradient, ConstantGradient, RFPulseComponent, ADC import ...Containers: BaseBuildingBlock @@ -146,7 +146,7 @@ variables.δ @defvar qvec(g::BaseTrapezoid, ::Nothing, ::Nothing) = variables.δ(g) .* variables.gradient_strength(g) .* 2π -adjustable(::BaseTrapezoid) = :gradient +adjust_groups(t::Trapezoid) = [t.group, :gradient] function adjust_internal(trap::Trapezoid1D; orientation=nothing, scale=1., rotation=nothing) if !isnothing(orientation) && !isnothing(rotation) @@ -192,12 +192,14 @@ Parameters and variables are identical as for [`Trapezoid`](@ref) with the addit struct SliceSelect{N} <: BaseTrapezoid{N} trapezoid :: Trapezoid{N} pulse :: RFPulseComponent + group :: Symbol end -function SliceSelect(pulse::RFPulseComponent; orientation=nothing, rise_time=nothing, group=nothing, slew_rate=nothing, vars...) +function SliceSelect(pulse::RFPulseComponent; orientation=nothing, rise_time=nothing, group=:FOV, slew_rate=nothing, vars...) res = SliceSelect( - Trapezoid(; orientation=orientation, rise_time=rise_time, flat_time=variables.duration(pulse), group=group, slew_rate=slew_rate), - pulse + Trapezoid(; orientation=orientation, rise_time=rise_time, flat_time=variables.duration(pulse), slew_rate=slew_rate, group=nothing), + pulse, + group ) set_simple_constraints!(res, vars) return res @@ -221,6 +223,19 @@ get_pulse(ss::SliceSelect) = ss.pulse get_gradient(ss::SliceSelect) = ss.trapezoid @defvar effective_time(ss::SliceSelect) = variables.effective_time(ss, :pulse) +adjust_groups(ss::SliceSelect) = [ss.group, :slice] +function adjust_internal(ss::SliceSelect; shift) + return SliceSelect( + ss.trapezoid, + adjust( + ss.pulse; + pulse=(frequency=variables.gradient_strength_norm(ss) * shift * 1e3,) + ), + ss.group, + ) +end + + """ LineReadout(adc; ramp_overlap=1., orientation=nothing, group=nothing, variables...) diff --git a/src/post_hoc.jl b/src/post_hoc.jl index 9a8d7f14c7f44f20d4cc5c21a8989adc436bbde3..b071ad3250226b16d9ce530a0d0c15d24d23edbc 100644 --- a/src/post_hoc.jl +++ b/src/post_hoc.jl @@ -3,10 +3,12 @@ Define post-fitting adjustments of the sequences """ module PostHoc -import ..Variables: AbstractBlock, adjust_internal, adjustable +import ..Variables: AbstractBlock, adjust_internal, adjust_groups, adjust import ..Components: GradientWaveform, RFPulseComponent, BaseComponent, NoGradient import ..Containers: ContainerBlock, Sequence, Wait +const UsedNamesType = Dict{Symbol, Set{Symbol}} + """ adjust(block; kwargs...) @@ -34,7 +36,11 @@ To affect all gradients or pulses, use `gradient=` or `pulse`, e.g. will divide the amplitude of all RV pulses by two. """ function adjust(block::AbstractBlock; merge=true, kwargs...) - used_names = Set{Symbol}() + invalid_type = Set(key for (key, value) in pairs(kwargs) if !(value isa NamedTuple)) + if length(invalid_type) > 0 + error("All `adjust` keywords except for merge should be a NamedTuple, like (scale=3, ). This is not the case for: $(invalid_type).") + end + used_names = UsedNamesType() n_adjust, kwargs_list = adjust_kwargs_list(; kwargs...) if isnothing(n_adjust) res = adjust_helper(block, used_names; kwargs_list[1]...) @@ -48,11 +54,22 @@ function adjust(block::AbstractBlock; merge=true, kwargs...) end end unused_names = filter(keys(kwargs)) do key - !(key in used_names) + !(key in keys(used_names)) end if length(unused_names) > 0 @warn "Some group/type names were not used in call to `MRIBuilder.adjust`, namely: $(unused_names)." end + for group_name in keys(kwargs) + if group_name in unused_names + continue + end + unused_keys = filter(keys(kwargs[group_name])) do key + !(key in used_names[group_name]) + end + if length(unused_keys) > 0 + @warn "Some keywords provided for group `$(group_name)` were not used, namely: $(unused_keys)." + end + end res end @@ -90,34 +107,35 @@ function adjust_kwargs_list(; kwargs...) return (n_adjust, kwargs_list) end - -function adjust_helper(block::AbstractBlock, used_names::Set{Symbol}; gradient=(), pulse=(), kwargs...) +function adjust_helper(block::AbstractBlock, used_names::UsedNamesType; kwargs...) params = [] - adjust_type = adjustable(block) - if adjust_type == :false - for prop_name in propertynames(block) - push!(params, adjust_helper(getproperty(block, prop_name), used_names; gradient=gradient, pulse=pulse, kwargs...)) - end - return typeof(block)(params...) - else - if !isnothing(block.group) && (block.group in keys(kwargs)) - push!(used_names, block.group) - return adjust_internal(block; kwargs[block.group]...) - elseif adjust_type == :gradient - push!(used_names, :gradient) - return adjust_internal(block; gradient...) - elseif adjust_type == :pulse - push!(used_names, :pulse) - return adjust_internal(block; pulse...) + + for prop_name in propertynames(block) + push!(params, adjust_helper(getproperty(block, prop_name), used_names; kwargs...)) + end + new_block = typeof(block)(params...) + + for group in adjust_groups(new_block) + if group in keys(kwargs) + if !(group in keys(used_names)) + used_names[group] = Set{Symbol}() + end + all_available_kwargs = kwargs[group] + use_kwargs = reduce(vcat, Base.kwarg_decl.(methods(adjust_internal, (typeof(new_block), )))) + @assert length(use_kwargs) > 0 "Invalid definition of `internal_kwargs` for $(typeof(new_block))" + internal_kwargs = Dict(key => value for (key, value) in pairs(all_available_kwargs) if key in use_kwargs) + union!(used_names[group], keys(internal_kwargs)) + return adjust_internal(block; internal_kwargs...) end end + return new_block end -adjust_helper(some_value, used_names::Set{Symbol}; kwargs...) = some_value -adjust_helper(array_variable::AbstractArray, used_names::Set{Symbol}; kwargs...) = map(array_variable) do v adjust_helper(v, used_names; kwargs...) end -adjust_helper(dict_variable::AbstractDict, used_names::Set{Symbol}; kwargs...) = typeof(dict_variable)(k => adjust_helper(v, used_names; kwargs...) for (k, v) in pairs(dict_variable)) -adjust_helper(tuple_variable::Tuple, used_names::Set{Symbol}; kwargs...) = map(tuple_variable) do v adjust_helper(v, used_names; kwargs...) end -adjust_helper(pair:: Pair, used_names::Set{Symbol}; kwargs...) = adjust_helper(pair[1], used_names; kwargs...) => adjust_helper(pair[2], used_names; kwargs...) +adjust_helper(some_value, used_names::UsedNamesType; kwargs...) = some_value +adjust_helper(array_variable::AbstractArray, used_names::UsedNamesType; kwargs...) = map(array_variable) do v adjust_helper(v, used_names; kwargs...) end +adjust_helper(dict_variable::AbstractDict, used_names::UsedNamesType; kwargs...) = typeof(dict_variable)(k => adjust_helper(v, used_names; kwargs...) for (k, v) in pairs(dict_variable)) +adjust_helper(tuple_variable::Tuple, used_names::UsedNamesType; kwargs...) = map(tuple_variable) do v adjust_helper(v, used_names; kwargs...) end +adjust_helper(pair:: Pair, used_names::UsedNamesType; kwargs...) = adjust_helper(pair[1], used_names; kwargs...) => adjust_helper(pair[2], used_names; kwargs...) """ diff --git a/src/variables.jl b/src/variables.jl index 3e7cac0d8f3ffb38a60af723373265676e06549b..0551d2bf3f7c961cb6165a86262a635c453bf1fc 100644 --- a/src/variables.jl +++ b/src/variables.jl @@ -37,21 +37,26 @@ end Returns the adjusted blocks and add any keywords used in the process to `names_used`. -This is a helper function used by `adjust`. +This is a helper function used by [`adjust`](@ref). +It should be defined for any block that is adjustable (as defined by [`adjust_groups`](@ref)). """ function adjust_internal end """ - adjustable(block) + adjust_groups(block) -Returns whether a sequence, building block, or component can be adjusted +Returns an array of keywords in [`adjust`](@ref) that should affect a specfic block. -Can return one of: -- `:false`: not adjustable +If any of these keywords are present in [`adjust`](@ref), then [`adjust_internal`](@ref) will be called. + +Some standard keywords are: - `:gradient`: expects gradient adjustment parameters - `:pulse`: expects RF pulse adjustment parameters """ -adjustable(::AbstractBlock) = :false +adjust_groups(::AbstractBlock) = Symbol[] + +# further defined in post_hoc.jl +function adjust end abstract type AnyVariable end @@ -245,7 +250,7 @@ function def_alternate_variable!(name::Symbol, other_var::Symbol, from_other::Fu setproperty!(variables, name, AlternateVariable(name, other_var, from_other, to_other, inverse)) end -def_alternate_variable!(:spoiler_scale, :qval, q->1e-3 * 2π/q, l->1e-3 * 2π/l, true) +def_alternate_variable!(:spoiler, :qval, q->1e-3 * 2π/q, l->1e-3 * 2π/l, true) def_alternate_variable!(:qval, :qval_square, sqrt, q -> q * q, false) def_alternate_variable!(:qval_square, :qvec, qv -> sum(q -> q * q, qv), nothing, false) @@ -257,13 +262,13 @@ The norm of the [`variables.qvec`](@ref). variables.qval """ - spoiler_scale(gradient) + spoiler(gradient) Spatial scale in mm over which the spoiler gradient will dephase by 2π. Automatically computed based on [`variables.qvec`](@ref). """ -variables.spoiler_scale +variables.spoiler for vec_variable in [:gradient_strength, :slew_rate] diff --git a/test/test_post_hoc.jl b/test/test_post_hoc.jl index 657d6b0e986ec25d87e7730ea5ba1a0a2136ee7b..072f3a27872f73df92b416d988ddeee9d5b328ed 100644 --- a/test/test_post_hoc.jl +++ b/test/test_post_hoc.jl @@ -53,5 +53,15 @@ @test all(variables.qvec(new_dwi[:gradient]) .≈ [qval_orig/√2, qval_orig/√2, 0.]) end end + @testset "Slice selection" begin + dwi = DWI(bval=1., slice_thickness=2., refocus=(spoiler=0.5, )) + @test variables.frequency(dwi) == (excitation=0., refocus=0.) + new_dwi = adjust(dwi, slice=(shift=2., )) + @test variables.frequency(new_dwi).excitation ≈ variables.bandwidth(dwi).excitation + @test variables.frequency(new_dwi).refocus ≈ variables.bandwidth(dwi).refocus + another_dwi = adjust(new_dwi, slice=(shift=-3., )) + @test variables.frequency(another_dwi).excitation ≈ -variables.bandwidth(dwi).excitation * 0.5 + @test variables.frequency(another_dwi).refocus ≈ -variables.bandwidth(dwi).refocus * 0.5 + end end end