diff --git a/src/MRIBuilder.jl b/src/MRIBuilder.jl index 4fd6904a793b98016a7631aa69d48e7e10a70745..af1c7a28378e47c5985d04af458dfca9394f0447 100644 --- a/src/MRIBuilder.jl +++ b/src/MRIBuilder.jl @@ -22,8 +22,8 @@ export build_sequence, global_model, global_scanner, fixed import .Scanners: Scanner, B0, Siemens_Connectom, Siemens_Prisma, Siemens_Terra, Default_Scanner export Scanner, B0, Siemens_Connectom, Siemens_Prisma, Siemens_Terra, Default_Scanner -import .Variables: variables, duration, effective_time, flip_angle, amplitude, phase, frequency, bandwidth, N_left, N_right, qval, δ, rise_time, flat_time, slew_rate, gradient_strength, qvec, qval_square, slice_thickness, inverse_slice_thickness, fov, inverse_fov, voxel_size, inverse_voxel_size, resolution, nsamples, oversample, dwell_time, ramp_overlap, spoiler_scale, repetition_time, TR, Δ, get_gradient, get_pulse, get_readout, TE, echo_time, diffusion_time, make_generic, slew_rate3, gradient_strength3, qval3 -export variables, duration, effective_time, flip_angle, amplitude, phase, frequency, bandwidth, N_left, N_right, qval, δ, rise_time, flat_time, slew_rate, gradient_strength, qvec, qval_square, slice_thickness, inversne_slice_thickness, fov, inverse_fov, voxel_size, inverse_voxel_size, resolution, nsamples, oversample, dwell_time, ramp_overlap, spoiler_scale, repetition_time, TR, Δ, get_gradient, get_pulse, get_readout, TE, echo_time, diffusion_time, make_generic, slew_rate3, gradient_strength3, qval3 +import .Variables: variables, effective_time, make_generic, @defvar, duration +export variables, effective_time, make_generic, @defvar, duration #import .Components: InstantPulse, ConstantPulse, SincPulse, GenericPulse, InstantGradient, SingleReadout, ADC, CompositePulse, edge_times #export InstantPulse, ConstantPulse, SincPulse, GenericPulse, InstantGradient, SingleReadout, ADC, CompositePulse, edge_times diff --git a/src/variables.jl b/src/variables.jl index 88625382dd9e2118aed65eab1c054c40f67f9e77..5d2381d647a9671a6d4764bd4f13fbde772432b8 100644 --- a/src/variables.jl +++ b/src/variables.jl @@ -65,11 +65,21 @@ It can return one of the following: - a vector of number - a NamedTuple with the values for individual sequence components """ -struct Variable <: AnyVariable +mutable struct Variable <: AnyVariable name :: Symbol f :: Function + getter :: Union{Nothing, Function} end +struct AlternateVariable <: AnyVariable + name :: Symbol + other_var :: Symbol + to_other :: Function + from_other :: Function + inverse :: Bool +end + + """ variable_defined_for(var, Val(type)) @@ -77,12 +87,50 @@ Check whether variable is defined for a specific sub-type. """ variable_defined_for(var::Variable, ::Val{T}) where {T <: AbstractBlock} = hasmethod(var.f, (T, )) -all_variables = Dict{Symbol, AnyVariable}() +struct _Variables + variables :: Dict{Symbol, AnyVariable} +end + +variables = _Variables(Dict{Symbol, AnyVariable}()) + +Base.getindex(v::_Variables, i::Symbol) = getfield(v, :variables)[i] +Base.keys(v::_Variables) = keys(getfield(v, :variables)) -macro defvar(func_def) +Base.propertynames(v::_Variables) = Tuple(keys(getfield(v, :variables))) + +Base.getproperty(v::_Variables, s::Symbol) = v[s] + + +macro defvar(func_def) + return _defvar(func_def, nothing) +end + +macro defvar(getter, func_def) + return _defvar(func_def, getter) +end + +function _defvar(func_def, getter=nothing) func_names = [] + + if getter isa Symbol + getter_dict = Dict( + :pulse => get_pulse, + :gradient => get_gradient, + :pathway => get_pathway, + :readout => get_readout, + ) + if !(getter in keys(getter_dict)) + error("label in `@defvar <label> <statement>` should be one of `pulse`/`gradient`/`pathway`/`readout`, not `$getter`") + end + getter = getter_dict[getter] + end + function adjust_function(ex) + if ex isa Expr && ex.head == :function && length(ex.args) == 1 + push!(func_names, ex.args[1]) + return :nothing + end try fn_def = MacroTools.splitdef(ex) push!(func_names, fn_def[:name]) @@ -109,18 +157,30 @@ macro defvar(func_def) expressions = Expr[] for func_name in func_names push!(expressions, quote - $(esc(func_name)) = if $(QuoteNode(func_name)) in keys(all_variables) - all_variables[$(QuoteNode(func_name))] + $(esc(func_name)) = if $(QuoteNode(func_name)) in keys(variables) + variables[$(QuoteNode(func_name))] else function $(func_name) end - all_variables[$(QuoteNode(func_name))] = Variable($(QuoteNode(func_name)), $(func_name)) + getfield(variables, :variables)[$(QuoteNode(func_name))] = Variable($(QuoteNode(func_name)), $(func_name), $getter) + end + if $(esc(func_name)) isa AlternateVariable + error("$($(esc(func_name)).name) is defined through $($(esc(func_name)).other_var). Please define that variable instead.") + end + if !isnothing($getter) && $(esc(func_name)).getter != $getter + if isnothing($(esc(func_name)).getter) + $(esc(func_name)).getter = $getter + else + name = $(esc(func_name)).name + error("$(name) is already defined as a variable for $($(esc(func_name)).getter). Cannot switch to $($getter).") + end end end ) end + args = vcat([e.args for e in expressions]...) return Expr( :block, - expressions..., + args..., new_func_def ) end @@ -132,20 +192,16 @@ Duration of the sequence or building block in ms. """ duration -struct AlternateVariable <: AnyVariable - name :: Symbol - other_var :: Symbol - to_other :: Function - from_other :: Function - inverse :: Bool +function def_alternate_variable!(name::Symbol, other_var::Symbol, to_other::Function, from_other::Function, inverse::Bool) + getfield(variables, :variables)[name] = AlternateVariable(name, other_var, to_other, from_other, inverse) end -all_variables[:qval] = AlternateVariable(:qval, :qval_square, q->q^2, sqrt, false) -all_variables[:spoiler_scale] = AlternateVariable(:spoiler_scale, :spoiler_scale, q->1e-3 * 2π/q, l->1e-3 * 2π/l, true) +def_alternate_variable!(:qval, :qval_square, q->q^2, sqrt, false) +def_alternate_variable!(:spoiler_scale, :spoiler_scale, q->1e-3 * 2π/q, l->1e-3 * 2π/l, true) for name in [:slice_thickness, :bandwidth, :fov, :voxel_size] inv_name = Symbol("inverse_" * string(name)) - all_variables[name] = AlternateVariable(name, inv_name, inv, inv, true) + def_alternate_variable!(name, inv_name, inv, inv, true) end for (name, alt_name) in [ @@ -153,7 +209,7 @@ for (name, alt_name) in [ (:TR, :repetition_time), (:Δ, :diffusion_time), ] - all_variables[name] = AlternateVariable(name, alt_name, identity, identity, false) + def_alternate_variable!(name, alt_name, identity, identity, false) end @@ -195,7 +251,7 @@ function get_free_variable(::Val{:max}; kwargs...) end """ - get_pulse(building_block)] + get_pulse(building_block) Get the pulse played out during the building block. @@ -204,7 +260,7 @@ Any `pulse` variables not explicitly defined for this building block will be pas function get_pulse end """ - get_gradient(building_block)] + get_gradient(building_block) Get the gradient played out during the building block. @@ -213,7 +269,7 @@ Any `gradient` variables not explicitly defined for this building block will be function get_gradient end """ - get_readout(building_block)] + get_readout(building_block) Get the readout played out during the building block. @@ -221,6 +277,15 @@ Any `readout` variables not explicitly defined for this building block will be p """ function get_readout end +""" + get_pathway(sequence) + +Get the default spin pathway for the sequence. + +Any `pathway` variables not explicitly defined for this building block will be passed on to the pathway. +""" +function get_pathway end + """ bmat_gradient(gradient::GradientBlock, qstart=(0, 0, 0)) @@ -243,76 +308,26 @@ function gradient_orientation end function effective_time end -""" - VariableNotAvailable(building_block, variable, alt_variable) - -Exception raised when a variable function does not support a specific `AbstractBlock`. -""" -mutable struct VariableNotAvailable <: Exception - bb :: Type{<:AbstractBlock} - variable :: Function - alt_variable :: Union{Nothing, Function} -end -VariableNotAvailable(bb::Type{<:AbstractBlock}, variable::Function) = VariableNotAvailable(bb, variable, nothing) - - - -function Base.showerror(io::IO, e::VariableNotAvailable) - if isnothing(e.alt_variable) - print(io, e.variable, " is not available for block of type ", e.bb, ".") - else - print(io, e.variable, " is not available for block of type ", e.bb, ". Please use ", e.alt_variable, " instead to set any contsraints or objective functions.") +function (var::Type{Variable})(block::AbstractBlock, args...; kwargs...) + if !applicable(var.f, block, args...) && !isnothing(var.getter) + apply_to = var.getter(block) + if apply_to isa AbstractBlock + return var(apply_to, args...; kwargs...) + elseif apply_to isa NamedTuple + return NamedTuple(k => var(v, args...; kwargs...) for (k, v) in pairs(apply_to)) + elseif apply_to isa AbstractVector{<:AbstractBlock} || apply_to isa Tuple + return var.(apply_to, args...; kwargs...) + end end + return var.f(block, args...; kwargs...) end - -for (target_name, all_vars) in all_variables_symbols - for (variable_func, _) in all_vars - if variable_func in [:qval3, :TR, :TE, :Δ] - continue - end - get_func = Symbol("get_" * string(target_name)) - use_get_func = target_name in (:pulse, :readout, :gradient) - @eval function Variables.$variable_func(bb::AbstractBlock) - try - if Variables.$variable_func in keys(alternative_variables) - alt_var, forward, backward, _ = alternative_variables[Variables.$variable_func] - try - value = alt_var(bb) - if value isa Number - return backward(value) - elseif value isa AbstractArray{<:Number} - return backward.(value) - end - catch e - if e isa VariableNotAvailable - throw(VariableNotAvailable(typeof(bb), Variables.$variable_func)) - end - rethrow() - end - throw(VariableNotAvailable(typeof(bb), Variables.$variable_func, alt_var)) - end - throw(VariableNotAvailable(typeof(bb), Variables.$variable_func)) - catch e - if $use_get_func && e isa VariableNotAvailable && hasmethod($get_func, tuple(typeof(bb))) - apply_to = try - $(get_func)(bb) - catch - throw(VariableNotAvailable(typeof(bb), Variables.$variable_func)) - end - if apply_to isa AbstractBlock - return Variables.$variable_func(apply_to) - elseif apply_to isa NamedTuple - return NamedTuple(k => Variables.$variable_func(v) for (k, v) in pairs(apply_to)) - elseif apply_to isa AbstractVector{<:AbstractBlock} || apply_to isa Tuple - return Variables.$variable_func.(apply_to) - end - error("$($(use_get_func)) returned an unexpected type for $(bb).") - end - rethrow() - end - end - end +function (var::Type{AlternateVariable})(args...; kwargs...) + other_var = variables[var.other_var] + apply_from_other(res::Number) = var.from_other(res) + apply_from_other(res::AbstractArray{<:Number}) = var.from_other.(res) + apply_from_other(res::NamedTuple) = NamedTuple(k => apply_from_other(v) for (k, v) in pairs(res)) + return apply_from_other(other_var(args...; kwargs...)) end @@ -346,25 +361,23 @@ If set to a numeric value, a constraint will be added to fix the function value If set to `:min` or `:max`, minimising or maximising this function will be added to the cost function. """ function set_simple_constraints!(block::AbstractBlock, kwargs) - for (key, value) in kwargs - if variables[key] in keys(alternative_variables) - alt_var, forward, backward, to_invert = alternative_variables[variables[key]] - invert_value(value::VariableType) = forward(value) + real_kwargs = NamedTuple(key => value for (key, value) in kwargs if !isnothing(value)) + + for (key, value) in real_kwargs + var = variables[key] + if var isa AlternateVariable + if var.other_var in real_kwargs + error("Set constraints on both $key and $(var.other_var), however they are equivalent.") + end + invert_value(value::VariableType) = var.to_other(value) invert_value(value::Symbol) = invert_value(Val(value)) - invert_value(::Val{:min}) = to_invert ? Val(:max) : Val(:min) - invert_value(::Val{:max}) = to_invert ? Val(:min) : Val(:max) + invert_value(::Val{:min}) = var.inverse ? Val(:max) : Val(:min) + invert_value(::Val{:max}) = var.inverse ? Val(:min) : Val(:max) invert_value(value::AbstractVector) = invert_value.(value) - invert_value(value) = value - try - apply_simple_constraint!(alt_var(block), invert_value(value)) - continue - catch e - if !(e isa VariableNotAvailable) - rethrow() - end - end + apply_simple_constraint!(variables[var.other_var](block), invert_value(value)) + else + apply_simple_constraint!(var(block), value) end - apply_simple_constraint!(variables[key](block), value) end nothing end @@ -381,7 +394,6 @@ Add a single constraint or objective to the `variable`. - `number`: fix variable to this value - `equation`: fix variable to the result of this equation """ -apply_simple_constraint!(variable::VariableType, ::Nothing) = nothing apply_simple_constraint!(variable::AbstractVector, value::Symbol) = apply_simple_constraint!(sum(variable), Val(value)) apply_simple_constraint!(variable::VariableType, value::Symbol) = apply_simple_constraint!(variable, Val(value)) apply_simple_constraint!(variable::VariableType, ::Val{:min}) = @objective global_model() Min objective_function(global_model()) + variable