Skip to content
Snippets Groups Projects
variables.jl 19.8 KiB
Newer Older
"""
Defines the functions that can be called on parts of an MRI sequence to query or constrain any variables.

In addition this defines:
- [`variables`](@ref): module containing all variables.
- [`VariableType`](@ref): parent type for any variables (whether number or JuMP variable).
- [`get_free_variable`](@ref): helper function to create new JuMP variables.
- [`add_cost_function!`](@ref): add a specific term to the model cost functions.
Michiel Cottaar's avatar
Michiel Cottaar committed
- [`set_simple_constraints!`](@ref): call [`apply_simple_constraint!`](@ref) for each keyword argument.
- [`apply_simple_constraint!`](@ref): set a simple equality constraint.
- [`get_pulse`](@ref)/[`get_gradient`](@ref)/[`get_readout`](@ref): Used to get the pulse/gradient/readout part of a building block
- [`gradient_orientation`](@ref): returns the gradient orientation of a waveform if fixed.
import JuMP: @constraint, @variable, Model, @objective, objective_function, AbstractJuMPScalar, QuadExpr, AffExpr
import StaticArrays: SVector
import MacroTools
import ..Scanners: gradient_strength, slew_rate, Scanner
import ..BuildSequences: global_scanner, fixed, GLOBAL_MODEL
"""
Parent type of all components, building block, and sequences that form an MRI sequence.
"""
abstract type AbstractBlock end

function fixed(ab::AbstractBlock)
    params = []
    for prop_name in propertynames(ab)
        push!(params, fixed(getproperty(ab, prop_name)))
    end
    return typeof(ab)(params...)
end

Michiel Cottaar's avatar
Michiel Cottaar committed

"""
    adjust_internal(block, names_used; kwargs...)

Returns the adjusted blocks and add any keywords used in the process to `names_used`.

Michiel Cottaar's avatar
Michiel Cottaar committed
This is a helper function used by `adjust`. 
Michiel Cottaar's avatar
Michiel Cottaar committed
"""
function adjust_internal end

"""
    adjustable(block)

Returns whether a sequence, building block, or component can be adjusted

Can return one of:
- `:false`: not adjustable
- `:gradient`: expects gradient adjustment parameters
- `:pulse`: expects RF pulse adjustment parameters
"""
adjustable(::AbstractBlock) = :false

Michiel Cottaar's avatar
Michiel Cottaar committed

abstract type AnyVariable end
Michiel Cottaar's avatar
Michiel Cottaar committed

"""
A sequence property that can be constrained and/or optimised.
It acts as a function, so you can call it on a sequence or building block to get the actual values (e.g., `v(sequence)`).
It can return one of the following:
- a number
- a vector of number
- a NamedTuple with the values for individual sequence components
Michiel Cottaar's avatar
Michiel Cottaar committed
mutable struct Variable <: AnyVariable
    name :: Symbol
    f :: Function
Michiel Cottaar's avatar
Michiel Cottaar committed
    getter :: Union{Nothing, Function}
Michiel Cottaar's avatar
Michiel Cottaar committed
struct AlternateVariable <: AnyVariable
    name :: Symbol
    other_var :: Symbol
    from_other :: Function
Michiel Cottaar's avatar
Michiel Cottaar committed
    to_other :: Union{Nothing, Function}
Michiel Cottaar's avatar
Michiel Cottaar committed
    inverse :: Bool
end


    variable_defined_for(var, Val(type))
Check whether variable is defined for a specific sub-type.
"""
variable_defined_for(var::Variable, ::Val{T}) where {T <: AbstractBlock} = hasmethod(var.f, (T, ))

Main module containing all the MRIBuilder sequence variables.
All variables are available as members of this module, e.g.
`variables.echo_time` returns the echo time variable.
Michiel Cottaar's avatar
Michiel Cottaar committed
New variables can be defined using `@defvar`.

Set constraints on variables by passing them on as keywords during the sequence generation,
e.g., `seq=SpinEcho(echo_time=70)`.

After sequence generation you can get the variable values by calling
`variables.echo_time(seq)`.
For the sequence defined above this would return 70. (or a number very close to that).
"""
baremodule variables
end
"""
    @defvar([getter, ], function(s))

Defines new [`variables`](@ref).

Each variable is defined as regular Julia functions embedded within a `@defvar` macro.
For example, to define a `variables.echo_time` variable for a `SpinEcho` sequence, one can use:
```julia
@defvar echo_time(ge::SpinEcho) = 2 * (variables.effective_time(ge, :refocus) - variables.effective_time(ge, :excitation))
```

Multiple variables can be defined in a single `@defvar` by including them in a code block:
```julia
@defvar begin
    function var1(seq::SomeSequenceType)
        ...
    end
    function var2(seq::SomeSequenceType)
        ...
    end
end
```

Before the variable function definitions one can include a `getter`.
This `getter` defines the type of the sequence component for which the variables will be defined.
If the variable is not defined for the sequence, the variable will be extracted for those type of sequence components instead.
The following sequence component types are provided:
- `pulse`: use [`get_pulse`](@ref)
- `gradient`: use [`get_gradient`](@ref)
- `readout`: use [`get_readout`](@ref)
- `pathway`: use [`get_pathway`](@ref)
e.g. the following defines a `flip_angle` variable, which is marked as a property of an RF pulse.
```julia
@defvar pulse flip_angle(...) = ...
```
If after this definition, `flip_angle` is not explicitly defined for any sequence, it will be extracted for the RF pulses in that sequence instead.
"""
Michiel Cottaar's avatar
Michiel Cottaar committed
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)
Michiel Cottaar's avatar
Michiel Cottaar committed

    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 == :block
            return Expr(:block, adjust_function.(ex.args)...)
        end
Michiel Cottaar's avatar
Michiel Cottaar committed
        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])
Michiel Cottaar's avatar
Michiel Cottaar committed
            new_def = Dict{Symbol, Any}()
            new_def[:name] = Expr(:., Expr(:., :variables, QuoteNode(fn_def[:name])), QuoteNode(:f))
Michiel Cottaar's avatar
Michiel Cottaar committed
            new_def[:args] = esc.(fn_def[:args])
            new_def[:kwargs] = esc.(fn_def[:kwargs])
            new_def[:body] = esc(fn_def[:body])
Michiel Cottaar's avatar
Michiel Cottaar committed
            new_def[:whereparams] = esc.(fn_def[:whereparams])
            return MacroTools.combinedef(new_def)
        catch e
            if e isa AssertionError
                return ex
            end
            rethrow()
        end
    end
    new_func_def = adjust_function(func_def)
    function fix_function_name(ex)
        if ex in func_names
            return esc(ex)
        else
            return ex
    new_func_def = MacroTools.postwalk(fix_function_name, new_func_def)

    expressions = Expr[]
    for func_name in func_names
        push!(expressions, quote
            if !($(QuoteNode(func_name)) in names(variables; all=true))
                function $(func_name) end
                variables.$(func_name) = Variable($(QuoteNode(func_name)), $(func_name), $getter)
Michiel Cottaar's avatar
Michiel Cottaar committed
            end
            if variables.$(func_name) isa AlternateVariable
                error("$($(esc(func_name)).name) is defined through $(variables.$(func_name).other_var). Please define that variable instead.")
Michiel Cottaar's avatar
Michiel Cottaar committed
            end
            if !isnothing($getter) && variables.$(func_name).getter != $getter
                if isnothing(variables.$(func_name).getter)
                    variables.$(func_name).getter = $getter
Michiel Cottaar's avatar
Michiel Cottaar committed
                else
                    name = variables.$(func_name).name
                    error("$(name) is already defined as a variable for $(variables.$(func_name).getter). Cannot switch to $($getter).")
Michiel Cottaar's avatar
Michiel Cottaar committed
                end
Michiel Cottaar's avatar
Michiel Cottaar committed
    args = vcat([e.args for e in expressions]...)
    return Expr(
        :block,
Michiel Cottaar's avatar
Michiel Cottaar committed
        args...,
@defvar function duration end
Michiel Cottaar's avatar
Michiel Cottaar committed
"""
    duration(block)
Duration of the sequence or building block in ms.
Michiel Cottaar's avatar
Michiel Cottaar committed
""" 
variables.duration
Michiel Cottaar's avatar
Michiel Cottaar committed
function def_alternate_variable!(name::Symbol, other_var::Symbol, from_other::Function, to_other::Union{Nothing, Function}, inverse::Bool)
    setproperty!(variables, name, AlternateVariable(name, other_var, from_other, to_other, inverse))
Michiel Cottaar's avatar
Michiel Cottaar committed
def_alternate_variable!(:spoiler_scale, :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)

"""
    qval(gradient)

The norm of the [`variables.qvec`](@ref).
"""
variables.qval

"""
    spoiler_scale(gradient)

Spatial scale in mm over which the spoiler gradient will dephase by 2π.

Automatically computed based on [`variables.qvec`](@ref).
"""
variables.spoiler_scale


Michiel Cottaar's avatar
Michiel Cottaar committed
for vec_variable in [:gradient_strength, :slew_rate]
    vec_square = Symbol(string(vec_variable) * "_square")
    vec_norm = Symbol(string(vec_variable) * "_norm")
    def_alternate_variable!(vec_norm, vec_square, sqrt, v -> v * v, false)
    def_alternate_variable!(vec_square, vec_variable, v -> v[1] * v[1] + v[2] * v[2] + v[3] * v[3], nothing, false)
Michiel Cottaar's avatar
Michiel Cottaar committed
end
"""
    gradient_strength_norm(gradient)

The norm of the [`variables.gradient_strength`](@ref).
"""
variables.gradient_strength_norm

"""
    slew_rate_norm(gradient)

The norm of the [`variables.slew_rate`](@ref).
"""
variables.slew_rate_norm

for name in [:slice_thickness, :bandwidth, :fov, :voxel_size]
    inv_name = Symbol("inverse_" * string(name))
Michiel Cottaar's avatar
Michiel Cottaar committed
    def_alternate_variable!(name, inv_name, inv, inv, true)
end

for (name, alt_name) in [
    (:TE, :echo_time),
    (:TR, :repetition_time),
    (:Δ, :diffusion_time),
]
Michiel Cottaar's avatar
Michiel Cottaar committed
    def_alternate_variable!(name, alt_name, identity, identity, false)
"""
    TE(sequence)

Alternative name to compute the [`variables.echo_time`](@ref) of a sequence in ms.
"""
variables.TE

"""
    TR(sequence)

Alternative name to compute the [`variables.repetition_time`](@ref) of a sequence in ms.
"""
variables.TR

"""
    Δ(sequence)

Alternative name to compute the [`variables.diffusion_time`](@ref) of a sequence in ms.
"""
variables.Δ

"""
Parent type for any variable in the MRI sequence.
Each variable can be one of:
- a new JuMP variable
- an expression linking this variable to other JuMP variable
- a number
Create these using [`get_free_variable`](@ref).
"""
const VariableType = Union{Number, AbstractJuMPScalar}


"""
    get_free_variable(value; integer=false, start=0.01)

Get a representation of a given `variable` given a user-defined constraint.

The result is guaranteed to be a [`VariableType`](@ref).
get_free_variable(value::Number; integer=false, kwargs...) = integer ? Int(value) : Float64(value)
get_free_variable(value::VariableType; kwargs...) = value
get_free_variable(::Nothing; integer=false, start=0.01) = @variable(GLOBAL_MODEL[][1], start=start, integer=integer)
get_free_variable(value::Symbol; integer=false, kwargs...) = integer ? error("Cannot maximise or minimise an integer variable") : get_free_variable(Val(value); kwargs...)
function get_free_variable(::Val{:min}; kwargs...)
    var = get_free_variable(nothing; kwargs...)
    add_cost_function!(var, 1)
function get_free_variable(::Val{:max}; kwargs...)
    var = get_free_variable(nothing; kwargs...)
    add_cost_function!(-var, 1)
Michiel Cottaar's avatar
Michiel Cottaar committed
    get_pulse(sequence)
Michiel Cottaar's avatar
Michiel Cottaar committed
Get the main pulses played out during the sequence.
Michiel Cottaar's avatar
Michiel Cottaar committed
This has to be defined for individual sequences to work.

Any `pulse` variables not explicitly defined for this sequence will be passed on to the pulse.
Michiel Cottaar's avatar
Michiel Cottaar committed
    get_gradient(sequence)

Get the main gradients played out during the sequence.
Michiel Cottaar's avatar
Michiel Cottaar committed
This has to be defined for individual sequences to work.
Michiel Cottaar's avatar
Michiel Cottaar committed
Any `gradient` variables not explicitly defined for this sequence will be passed on to the gradient.
"""
function get_gradient end

"""
Michiel Cottaar's avatar
Michiel Cottaar committed
    get_readout(sequence)
Michiel Cottaar's avatar
Michiel Cottaar committed
Get the main readout events played out during the sequence.
Michiel Cottaar's avatar
Michiel Cottaar committed
This has to be defined for individual sequences to work.

Any `readout` variables not explicitly defined for this sequence will be passed on to the readout.
Michiel Cottaar's avatar
Michiel Cottaar committed
function get_readout end
Michiel Cottaar's avatar
Michiel Cottaar committed
"""
    get_pathway(sequence)

Michiel Cottaar's avatar
Michiel Cottaar committed
Get the default spin pathway(s) for the sequence.

This has to be defined for individual sequences to work.
Michiel Cottaar's avatar
Michiel Cottaar committed
Any `pathway` variables not explicitly defined for this sequence will be passed on to the pathway.
Michiel Cottaar's avatar
Michiel Cottaar committed
"""
function get_pathway end

"""
    gradient_orientation(building_block)

Returns the gradient orientation.
"""
function gradient_orientation end
Michiel Cottaar's avatar
Michiel Cottaar committed
function (var::Variable)(block::AbstractBlock, args...; kwargs...)
Michiel Cottaar's avatar
Michiel Cottaar committed
    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...)
        else
            error("$(var.getter) returned an unexpected type: $(typeof(apply_to)).")
Michiel Cottaar's avatar
Michiel Cottaar committed
        end
Michiel Cottaar's avatar
Michiel Cottaar committed
    return var.f(block, args...; kwargs...)
Michiel Cottaar's avatar
Michiel Cottaar committed
# Special case for BuildingBlock events
function (var::Variable)(event::Tuple{<:VariableType, <:AbstractBlock}, args...; kwargs...)
Michiel Cottaar's avatar
Michiel Cottaar committed
    if applicable(var.f, event, args...; kwargs...)
        return var.f(event, args...; kwargs...)
    end
    # falling back to just processing the `AbstractBlock`
    return var(event[2], args...; kwargs...)
end

Michiel Cottaar's avatar
Michiel Cottaar committed
function (var::AlternateVariable)(args...; kwargs...)
    other_var = getproperty(variables, var.other_var)
Michiel Cottaar's avatar
Michiel Cottaar committed
    apply_from_other(res::VariableType) = var.from_other(res)
    function apply_from_other(res::AbstractVector{<:VariableType}) 
        try
            return var.from_other(res)
        catch e
            if e isa MethodError
                return var.from_other.(res)
            end
        end
    end
Michiel Cottaar's avatar
Michiel Cottaar committed
    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...))
"""
    add_cost_function!(function, level=2)

Adds an additional term to the cost function.

This term will be minimised together with any other terms in the cost function.
Terms added at a lower level will be optimised before any terms with a higher level.

By default, the term is added to the `level=2`, which is appropriate for any cost functions added by the developer,
which will generally only be optimised after any user-defined cost functions (which are added at `level=1` by [`add_simple_constraint!`](@ref) or [`set_simple_constraints!`](@ref).

Any sequence will also have a `level=3` cost function, which minimises the total sequence duration.
function add_cost_function!(func::AbstractJuMPScalar, level=2)
    push!(GLOBAL_MODEL[][2], (Float64(level), func))
end

add_cost_function!(func::Number, level=2) = nothing


"""
    set_simple_constraints!(block, kwargs)

Michiel Cottaar's avatar
Michiel Cottaar committed
Add any constraints or objective functions to the variables of a [`AbstractBlock`](@ref).

Each keyword argument has to match one of the functions in [`variables`](@ref)(block).
If set to a numeric value, a constraint will be added to fix the function value to that numeric value.
If set to `:min` or `:max`, minimising or maximising this function will be added to the cost function.
"""
Michiel Cottaar's avatar
Michiel Cottaar committed
function set_simple_constraints!(block::AbstractBlock, kwargs)
Michiel Cottaar's avatar
Michiel Cottaar committed
    real_kwargs = Dict(key => value for (key, value) in kwargs if !isnothing(value))
Michiel Cottaar's avatar
Michiel Cottaar committed

    for (key, value) in real_kwargs
        var = getproperty(variables, key)
Michiel Cottaar's avatar
Michiel Cottaar committed
        if var isa AlternateVariable
Michiel Cottaar's avatar
Michiel Cottaar committed
            if var.other_var in keys(real_kwargs)
Michiel Cottaar's avatar
Michiel Cottaar committed
                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))
Michiel Cottaar's avatar
Michiel Cottaar committed
            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)
            apply_simple_constraint!(getproperty(variables, var.other_var)(block), invert_value(value))
Michiel Cottaar's avatar
Michiel Cottaar committed
        else
            apply_simple_constraint!(var(block), value)
        end
    end
    nothing
end

"""
    apply_simple_constraint!(variable, value)

Add a single constraint or objective to the `variable`.

`value` can be one of:
- `nothing`: do nothing
- `:min`: minimise the variable
- `:max`: maximise the variable
- `number`: fix variable to this value
- `equation`: fix variable to the result of this equation


    apply_simple_constraint!(variable, :>=/:<=, value)

Set an inequality constraint to the `variable`.

`value` can be a number of an equation.
apply_simple_constraint!(variable::AbstractVector, value::Symbol) = apply_simple_constraint!(sum(variable), Val(value))
apply_simple_constraint!(variable, value::Nothing) = nothing
apply_simple_constraint!(variable::NamedTuple, value::Nothing) = nothing
apply_simple_constraint!(variable::VariableType, value::Symbol) = apply_simple_constraint!(variable, Val(value))
apply_simple_constraint!(variable::VariableType, ::Val{:min}) = add_cost_function!(variable, 1)
apply_simple_constraint!(variable::VariableType, ::Val{:max}) = add_cost_function!(-variable, 1)
apply_simple_constraint!(variable::VariableType, value::VariableType) = @constraint GLOBAL_MODEL[][1] variable == value
apply_simple_constraint!(variable::AbstractVector, value::AbstractVector) = [apply_simple_constraint!(v1, v2) for (v1, v2) in zip(variable, value)]
apply_simple_constraint!(variable::AbstractVector, value::VariableType) = [apply_simple_constraint!(v1, value) for v1 in variable]
apply_simple_constraint!(variable::Number, value::Number) = @assert variable  value "Variable set to multiple incompatible values."
function apply_simple_constraint!(variable::NamedTuple, value)
    for sub_var in variable
        apply_simple_constraint!(sub_var, value)
    end
end
function apply_simple_constraint!(variable::NamedTuple, value::NamedTuple)
    for key in keys(value)
        apply_simple_constraint!(variable[key], value[key])
    end
end
apply_simple_constraint!(variable::VariableType, sign::Symbol, value) = apply_simple_constraint!(variable, Val(sign), value)
apply_simple_constraint!(variable::VariableType, ::Val{:(==)}, value) = apply_simple_constraint!(variable, value)
apply_simple_constraint!(variable::VariableType, ::Val{:(<=)}, value::VariableType) = apply_simple_constraint!(value, Val(:>=), variable)
function apply_simple_constraint!(variable::VariableType, ::Val{:(>=)}, value::VariableType)
    @constraint GLOBAL_MODEL[][1] variable >= value
    if value isa Number && iszero(value)
        @constraint GLOBAL_MODEL[][1] variable * 1e6 >= value
        @constraint GLOBAL_MODEL[][1] variable * 1e12 >= value

"""
    make_generic(sequence/building_block/component)

Returns a generic version of the `BaseSequence`, `BaseBuildingBlock`, or `BaseComponent`

- Sequences are all flattened and returned as a single `Sequence` containing only `BuildingBlock` objects.
- Any `BaseBuildingBlock` is converted into a `BuildingBlock`.
- Pulses are replaced with `GenericPulse` (except for instant pulses).
- Instant readouts are replaced with `ADC`.
"""
function make_generic end


"""
    scanner_constraints!(block)

Constraints [`variables.gradient_strength`](@ref) and [`variables.slew_rate`](@ref) to be less than the [`global_scanner`](@ref) maximum.
"""
function scanner_constraints!(bb::AbstractBlock)
    for (var, max_value) in [
        (variables.slew_rate, global_scanner().slew_rate),
        (variables.gradient_strength, global_scanner().gradient),
    ]
        value = nothing
            value = var(bb)
            continue
        for v in value
            if v isa Number || ((v isa Union{QuadExpr, AffExpr}) && length(v.terms) == 0)
                continue
            end
            apply_simple_constraint!(v, :<=, max_value)
            apply_simple_constraint!(v, :>=, -max_value)