diff --git a/src/MRIBuilder.jl b/src/MRIBuilder.jl index 281f3c0f4e0152effc7ac14ad35b5c4165e6bba2..f43eb7a93148fb54e6c12701486b0e59c37a9c68 100644 --- a/src/MRIBuilder.jl +++ b/src/MRIBuilder.jl @@ -3,40 +3,48 @@ Builds and optimises NMR/MRI sequences. """ module MRIBuilder +include("global_model.jl") include("scanners.jl") +include("variables.jl") include("building_blocks.jl") -include("sequence_builders.jl") include("concrete_blocks.jl") include("wait.jl") +include("containers/containers.jl") include("gradients/gradients.jl") include("pulses/pulses.jl") include("readouts/readouts.jl") -import .BuildingBlocks: BuildingBlock, scanner_constraints! -export BuildingBlock, scanner_constraints! +import .GlobalModel: set_model +export set_model -import .SequenceBuilders: SequenceBuilder, start_time, end_time, duration, TR -export SequenceBuilder, start_time, end_time, duration, TR +import .Scanners: Scanner, B0, Siemens_Connectom, Siemens_Prisma, Siemens_Terra +export Scanner, B0, Siemens_Connectom, Siemens_Prisma, Siemens_Terra -import .ConcreteBlocks: ConcreteBlock, Sequence -export ConcreteBlock, Sequence +import .Variables: variables, duration, start_time, end_time, flip_angle, amplitude, phase, frequency, bandwidth, N_left, N_right, qval, area_under_curve, δ, rise_time, flat_time, slew_rate, gradient_strength +export variables, duration, start_time, end_time, flip_angle, amplitude, phase, frequency, bandwidth, N_left, N_right, qval, area_under_curve, δ, rise_time, flat_time, slew_rate, gradient_strength + +import .BuildingBlocks: BuildingBlock +export BuildingBlocks + +import .ConcreteBlocks: ConcreteBlock, AbstractConcreteBlock +export ConcreteBlocks, AbstractConcreteBlock import .Wait: WaitBlock export WaitBlock -import .Gradients: PulsedGradient, InstantGradientBlock, qval, rise_time, flat_time, slew_rate, gradient_strength, bval -export PulsedGradient, InstantGradientBlock, qval, rise_time, flat_time, slew_rate, gradient_strength, bval +import .Containers: Sequence +export Sequence -import .Pulses: InstantRFPulseBlock, ConstantPulse, SincPulse, flip_angle, phase, frequency, bandwidth, N_left, N_right -export InstantRFPulseBlock, ConstantPulse, SincPulse, flip_angle, phase, frequency, bandwidth, N_left, N_right +import .Gradients: PulsedGradient, InstantGradientBlock +export PulsedGradient, InstantGradientBlock + +import .Pulses: InstantRFPulseBlock, ConstantPulse, SincPulse +export InstantRFPulseBlock, ConstantPulse, SincPulse import .Readouts: InstantReadout export InstantReadout -import .Scanners: Scanner, Siemens_Connectom, Siemens_Prisma, Siemens_Terra -export Scanner, Siemens_Connectom, Siemens_Prisma, Siemens_Terra - -using JuMP +import JuMP: @constraint, @objective, objective_function, optimize!, has_values, value export @constraint, @objective, objective_function, optimize!, has_values, value end diff --git a/src/building_blocks.jl b/src/building_blocks.jl index d9b0374dbe6b122ceca451a32849079a72d57589..715092b4a7aaa25bd097087d410daa5438cd9e06 100644 --- a/src/building_blocks.jl +++ b/src/building_blocks.jl @@ -1,44 +1,36 @@ module BuildingBlocks import JuMP: has_values, GenericVariableRef, value, Model, @constraint, @objective, owner_model, objective_function import Printf: @sprintf -import ..Scanners: Scanner, gradient_strength, slew_rate +import ..Scanners: Scanner +import ..Variables: variables, start_time, duration, end_time, gradient_strength, slew_rate """ Parent type for all individual components out of which a sequence can be built. Required methods: - [`duration`](@ref)(block, parameters): returns block duration in ms. -- [`to_concrete_block`](@ref)(sequence, block): converts the block into a `ConcreteBlock`, which will be part of given `Sequence`. +- `to_concrete_block`(sequence, block): converts the block into a `ConcreteBlock`, which will be part of given `Sequence`. - [`variables`](@ref): A list of all functions that are used to compute variables of the building block. Any of these can be used in constraints or objective functions. """ abstract type BuildingBlock end """ - duration(building_block) + start_time(container, args...) -The duration of the building block in ms. +Returns the starting time of the specific [`BuildingBlock`](@ref) within the container. +The [`BuildingBlock`](@ref) is defined by one or more indices as defined below. """ -function duration end +start_time(bb) = 0. - -""" - gradient_strength(gradient) - -Maximum gradient strength in kHz/um. - -If a [`Scanner`](@ref) is provided, this will be constrained to be lower than the maximum scanner gradient strength. """ -function gradient_strength end + end_time(container, args...) +Returns the end time of the specific [`BuildingBlock`](@ref) within the container. +The [`BuildingBlock`](@ref) is defined by one or more indices as defined below. """ - slew_rate(gradient) +end_time(bb) = duration(bb) -Maximum rate of increase (and decrease) of the gradient strength in kHz/um/ms. - -If a [`Scanner`](@ref) is provided, this will be constrained to be lower than the maximum scanner slew rate. -""" -function slew_rate end """ scanner_constraints!([model, ]building_block, scanner) @@ -65,24 +57,14 @@ Returns a list of function that can be called to constrain the `building_block`. """ variables(bb::BuildingBlock) = variables(typeof(bb)) -struct _BuildingBlockPrinter - bb :: BuildingBlock - number :: Integer -end function Base.show(io::IO, block::BuildingBlock) print(io, string(typeof(block)), "(") - if has_values(block) - if iszero(value(duration(block))) - print(io, "time=", @sprintf("%.3f", value(start_time(block))), ", ") - else - print( - io, "time=", - @sprintf("%.3f", value(start_time(block))), " - ", - @sprintf("%.3f", value(end_time(block))), ", " - ) - end - end + internal_print(io, block) + print(io, ")") +end + +function internal_print(io::IO, block::BuildingBlock) for name in propertynames(block) value = getproperty(block, name) if value isa GenericVariableRef || name == :parent || string(name)[1] == '_' @@ -101,14 +83,12 @@ function Base.show(io::IO, block::BuildingBlock) print(io, "$(nameof(fn))=$(printed_value), ") end end - print(io, ")") end # The `start_time` and `end_time` functions are properly defined in "sequence_builders.jl" function start_time end function end_time end - """ set_simple_constraints!(model, block, kwargs) @@ -158,42 +138,4 @@ function match_blocks!(block1::BuildingBlock, block2::BuildingBlock) match_blocks!(block1, block2, property_list) end - -""" -Stores the parameters passed on a [`BuildingBlock`](@ref) constructor (of type `T`). - -The parameters are temporarily stored in this format, until they can be added to a `SequenceBuilder`. - -For example, the following -```julia -pg = PulsedGradient(qval=2) -``` -will return a `BuildingBlockPlaceholder` object rather than a `PulsedGradient` object. -Only when this object is added to a `SequenceBuilder`, is the `PulsedGradient` actually initialised using the JuMP model of that `SequenceBuilder`: -```julia -sb = SequenceBuilder([pg]) -``` -You can access the initialised `PulsedGradient` through the `BuildingBlockPlaceholder` (`pg.concrete[]`) or directly through the `SequenceBuilder` (`sb[1]`) - -Each Placeholder can be added to only a single `SequenceBuilder`, but it can be added multiple times to the same `SequenceBuilder`. -If added multiple times to the same `SequenceBuilder`, all variables will be matched between them. -""" -struct BuildingBlockPlaceholder{T<:BuildingBlock} - args - kwargs - concrete :: Ref{T} - BuildingBlockPlaceholder{T}(args...; kwargs...) where {T<:BuildingBlock} = new{T}(args, kwargs, Ref{T}()) -end - -function Base.show(io::IO, placeholder::BuildingBlockPlaceholder{T}) where {T} - if isassigned(placeholder.concrete) - print(io, "Assigned BuildingBlockPlaceholder for $(placeholder.concrete[])") - else - args = join(placeholder.args, ", ") - kwargs = join(["$key=$value" for (key, value) in pairs(placeholder.kwargs)], ", ") - print(io, "Unassigned BuildingBlockPlaceholder{$T}($args; $kwargs)") - end -end - - end \ No newline at end of file diff --git a/src/concrete_blocks.jl b/src/concrete_blocks.jl index 539b8b4f1709b02d300265e622776ed3e2c3cd59..19528ac25b1de660a39e3aab87129607e814a08c 100644 --- a/src/concrete_blocks.jl +++ b/src/concrete_blocks.jl @@ -1,9 +1,8 @@ module ConcreteBlocks import JuMP: has_values, optimize!, value -import ..BuildingBlocks: BuildingBlock, BuildingBlockPlaceholder, properties, duration -import ..SequenceBuilders: SequenceBuilder, to_block, AbstractSequence, TR, get_blocks +import ..Variables: variables, duration +import ..BuildingBlocks: BuildingBlock -abstract type AbstractConcreteBlock <: BuildingBlock end struct ConcreteRFPulse time :: Vector{Number} @@ -64,13 +63,14 @@ ConcreteGradient(values::Tuple{<:Vector, <:Vector}) = ConcreteGradient(values... ConcreteGradient(values::Tuple{<:Vector, <:Vector, <:Vector, <:Vector}) = ConcreteGradient(values...) +abstract type AbstractConcreteBlock <: BuildingBlock end + """ ConcreteBlock(duration; pulse=nothing, gradient=nothing, rotate_bvec=false, readout_times=nothing) A [`BuildingBlock`](@ref) that is fully defined (i.e., there are no variables to be optimised). """ struct ConcreteBlock <: AbstractConcreteBlock - builder :: AbstractSequence duration :: Float64 pulse :: Union{ConcreteRFPulse, Nothing} gradient :: Union{ConcreteGradient, Nothing} @@ -78,31 +78,29 @@ struct ConcreteBlock <: AbstractConcreteBlock rotate_gradient :: Bool end -ConcreteBlock(args...; kwargs...) = BuildingBlockPlaceholder{ConcreteBlock}(args...; kwargs...) - -function ConcreteBlock(builder::AbstractSequence, duration::Number; pulse=nothing, gradient=nothing, readout_times=Number[], rotate_gradient=false) - ConcreteBlock(builder, duration, ConcreteRFPulse(pulse), ConcreteGradient(gradient), Float64.(readout_times), rotate_gradient) +function ConcreteBlock(duration::Number; pulse=nothing, gradient=nothing, readout_times=Number[], rotate_gradient=false) + ConcreteBlock(duration, ConcreteRFPulse(pulse), ConcreteGradient(gradient), Float64.(readout_times), rotate_gradient) end -has_values(c::AbstractConcreteBlock) = has_values(c.builder) +has_values(c::AbstractConcreteBlock) = true duration(c::AbstractConcreteBlock) = 0. duration(c::ConcreteBlock) = c.duration """ - ConcreteBlock(sequence, other_building_block) + ConcreteBlock(other_building_block) Creates a [`ConcreteBlock`](@ref) from another [`BuildingBlock`](@ref). This will raise an error if the other [`BuildingBlock`](@ref) has not been optimised yet. If it has been optimised, then [`to_concrete_block`](@ref) will be called. """ -function ConcreteBlock(sequence::AbstractSequence, block::BuildingBlock) +function ConcreteBlock(block::BuildingBlock) if !has_values(block) error("Making `BuildingBlock` objects concrete is only possible after optimisation.") end - return to_concrete_block(sequence, block) + return to_concrete_block(block) end @@ -113,40 +111,11 @@ Internal function used to create [`ConcreteBlock`](@ref) from any [`BuildingBloc This needs to be defined for every [`BuildingBlock`](@ref) """ -function to_concrete_block(sequence::AbstractSequence, cb::ConcreteBlock) - return ConcreteBlock(sequence, cb.duration, cb.pulse, cb.gradient, cb.readout_times) +function to_concrete_block(cb::ConcreteBlock) + return ConcreteBlock(cb.duration, cb.pulse, cb.gradient, cb.readout_times, cb.rotate_gradient, value.(cb.start_time)) end variables(::Type{<:AbstractConcreteBlock}) = [] -""" - Sequence(builder::SequenceBuilder) - -A fully defined sequence with no free variables. - -When created from a [`SequenceBuilder`](@ref), all non-fixed variables are optimised given any constraints -and the resulting sequence is returned. -""" -struct Sequence <: AbstractSequence - blocks :: Vector{<:AbstractConcreteBlock} - TR :: Number -end - -TR(seq::Sequence) = seq.TR -get_blocks(seq::Sequence) = seq.blocks -has_values(seq::Sequence) = true - -function Sequence(builder::SequenceBuilder) - if !has_values(builder) - optimize!(builder.model) - end - seq = Sequence(AbstractConcreteBlock[], value(TR(builder))) - for block in builder.blocks - @show block - push!(seq.blocks, ConcreteBlock(seq, block)) - end - return seq -end - end \ No newline at end of file diff --git a/src/containers/containers.jl b/src/containers/containers.jl new file mode 100644 index 0000000000000000000000000000000000000000..591b9f09a426b9d327daf5bd879a95c9efb929e9 --- /dev/null +++ b/src/containers/containers.jl @@ -0,0 +1,5 @@ +module Containers +include("sequences.jl") + +import .Sequences: Sequence +end \ No newline at end of file diff --git a/src/containers/sequences.jl b/src/containers/sequences.jl new file mode 100644 index 0000000000000000000000000000000000000000..5dc31a06ebbb64f7d8740f2dde94acb682025744 --- /dev/null +++ b/src/containers/sequences.jl @@ -0,0 +1,55 @@ +module Sequences +import JuMP: Model +import ...GlobalModel: @global_model_constructor +import ...Variables: variables, start_time, duration, VariableType +import ...BuildingBlocks: BuildingBlock +import ...InsertedBuildingBlocks: InsertedBuildingBlock + +""" + Sequence(building_blocks...) + Sequence([building_blocks]) + +Represents a series of [`BuildingBlock`](@ref) objects run in turn. +""" +struct Sequence <: BuildingBlock + model :: Model + blocks :: Vector{<:BuildingBlock} +end + +@global_model_constructor Sequence + +Sequence(model::Model, blocks...) = Sequence(model, blocks) + +Base.length(seq::Sequence) = length(seq) +Base.getindex(seq::Sequence, index) = seq[index] + +""" + start_time(sequence::Sequence, index::Integer, args...) + +Returns the starting time of the [`BuildingBlock`](@ref) with index `index`. +Additional `args` can be used to select a sub-block of that [`BuildingBlock`](@ref). +The starting time is returned with respect to the start of this sequence. +""" +start_time(seq::Sequence) = 0. +start_time(seq::Sequence, index::Integer) = iszero(index) ? start_time(seq) : (start_time(seq, index-1) + duration(seq[index])) +start_time(seq::Sequence, index::Integer, args...) = start_time(seq, index) + start_time(seq[index], args...) + +""" + end_time(sequence::Sequence, index::Integer, args...) + +Returns the end time of the [`BuildingBlock`](@ref) with index `index`. +Additional `args` can be used to select a sub-block of that [`BuildingBlock`](@ref). +The end time is returned with respect to the start of this sequence. +""" +end_time(seq::Sequence, index::Integer) = start_time(seq, index) + duration(seq[index]) +end_time(seq::Sequence, index::Integer, args...) = start_time(seq, index) + end_time(seq[index], args...) + +duration(seq::Sequence) = end_time(seq, length(seq)) + + + +end + + + + diff --git a/src/global_model.jl b/src/global_model.jl new file mode 100644 index 0000000000000000000000000000000000000000..d845afb46f6334eafeb55bca3a55b1b79884b71b --- /dev/null +++ b/src/global_model.jl @@ -0,0 +1,63 @@ +module GlobalModel +import JuMP: Model + +const GLOBAL_MODEL = Ref(Model()) +const IGNORE_MODEL = GLOBAL_MODEL[] + +""" +Sets a global JuMP `Model`. + +Use as +```julia +model = set_model([model]) do + ... +end +``` +Any sequences created within the code block will be assigned the same JuMP `Model`. +If no model is provided a new one is created (using a Juniper optimizer based on the Ipopt non-linear optimizer). + +The function will return the fully formed JuMP `Model`. +""" +function set_model(f::Function, model::Model) + prev_model = global_model[] + global_model[] = model + try + f() + finally + global_model[] = prev_model + end + return model +end + +function set_model(f::Function) + ipopt_opt = optimizer_with_attributes(Ipopt.Optimizer, "print_level" => 0) + juniper_opt = optimizer_with_attributes(Juniper.Optimizer, "nl_solver" => ipopt_opt) + model = Model(juniper_opt) + set_model(f, model) +end + + +function get_global_model() + if GLOBAL_MODEL[] == IGNORE_MODEL + error("No global model has been set. Please explicitly set one in the constructor or set a global model using `set_model`.") + end + return GLOBAL_MODEL[] +end + + +""" + @global_model_constructor BuildingBlockType + +Add a onstructor the [`BuildingBlock`](@ref) subtype that fetches the global JuMP model (set by [`set_model`](@ref)) and assigns it to the first argument. +``` +BuildingBlockType(args...; kwargs...) = BuildingBlockType(global_model::JuMP.Model, args...; kwargs...) +``` +""" +macro global_model_constructor(bb) + quote + $(esc(bb))(args...; kwargs...) = $(esc(bb))(get_global_model(), args...; kwargs...) + end +end + + +end \ No newline at end of file diff --git a/src/gradients/gradients.jl b/src/gradients/gradients.jl index 907cc8e3c832a2c4930a93506fd86da082a96d89..8c6dbb6f64a21323e34cc8dda586a9a276e403d2 100644 --- a/src/gradients/gradients.jl +++ b/src/gradients/gradients.jl @@ -3,7 +3,6 @@ include("integrate_gradients.jl") include("pulsed_gradients.jl") include("instant_gradients.jl") -import .IntegrateGradients: qval, bval -import .PulsedGradients: PulsedGradient, rise_time, flat_time, slew_rate, gradient_strength +import .PulsedGradients: PulsedGradient import .InstantGradients: InstantGradientBlock end \ No newline at end of file diff --git a/src/gradients/instant_gradients.jl b/src/gradients/instant_gradients.jl index a8ee301d74714cde249c9eb49a295d9a78e05fc1..22016ec190c3dd226f5f423532f31ca3aabf6a22 100644 --- a/src/gradients/instant_gradients.jl +++ b/src/gradients/instant_gradients.jl @@ -1,9 +1,9 @@ module InstantGradients -import JuMP: @constraint, @variable, VariableRef -import ...BuildingBlocks: BuildingBlock, properties, BuildingBlockPlaceholder, set_simple_constraints!, duration -import ...SequenceBuilders: SequenceBuilder, owner_model, start_time -import ...ConcreteBlocks: to_concrete_block, AbstractConcreteBlock, Sequence, AbstractSequence -import ..IntegrateGradients: qval, bval +import JuMP: @constraint, @variable, Model, owner_model +import ...Variables: qval, bval, start_time, duration, variables, get_free_variable, VariableType +import ...BuildingBlocks: BuildingBlock +import ...ConcreteBlocks: to_concrete_block, AbstractConcreteBlock +import ...GlobalModel: @global_model_constructor """ InstantGradientBlock(; orientation=:bvec, qval=nothing) @@ -19,21 +19,20 @@ This is a [`BuildingBlock`](@ref) for the [`SequenceBuilder`](@ref). - [`qval`](@ref): Spatial scale on which spins will be dephased due to this pulsed gradient in rad/um. """ struct InstantGradientBlock <: BuildingBlock - builder::SequenceBuilder + model::Model orientation :: Any - qval :: VariableRef + qval :: VariableType end -InstantGradientBlock(; kwargs...) = BuildingBlockPlaceholder{InstantGradientBlock}(; kwargs...) +@global_model_constructor InstantGradientBlock -function InstantGradientBlock(builder::SequenceBuilder; orientation=:bvec, kwargs...) - model = owner_model(builder) +function InstantGradientBlock(model::Model; orientation=:bvec, qval=nothing) res = InstantGradientBlock( - builder, + model, orientation, - @variable(model) + get_free_variable(model, qval), ) - set_simple_constraints!(model, res, kwargs) + @constraint model model.qval >= 0 return res end @@ -51,13 +50,12 @@ Instantaneous MR gradient with no free variables. See [`InstantGradientBlock`](@ref) for a version where [`qval`](@ref) is variable. """ struct ConcreteInstantGradient <: AbstractConcreteBlock - builder :: AbstractSequence orientation :: Any qval :: Number end -function to_concrete_block(builder::Sequence, block::InstantGradientBlock) - return ConcreteInstantGradient(builder, block.orientation, value(qval(block))) +function to_concrete_block(block::InstantGradientBlock) + return ConcreteInstantGradient(block.orientation, value(qval(block))) end diff --git a/src/gradients/integrate_gradients.jl b/src/gradients/integrate_gradients.jl index 9083c050c61c4d3932342a5eb7f2e230d2e1741c..8b28be490b51009ee4bad341b77ea394a8b5024f 100644 --- a/src/gradients/integrate_gradients.jl +++ b/src/gradients/integrate_gradients.jl @@ -1,11 +1,11 @@ module IntegrateGradients +import ...Variables: qval, bval import ...BuildingBlocks: BuildingBlock -import ...SequenceBuilders: SequenceBuilder, TR, duration, builder, index +import ...Containers: Sequence """ - qval(blocks) - qval(builder::SequenceBuilder, indices) + qval(container, indices) Computes the total q-value summed over multiple gradient [`BuildingBlock`](@ref) objects. @@ -19,12 +19,10 @@ In addition, in this interface one can provide negative indices to indicate that The integral can occur over multiple repetition times by including [`BuildingBlock`](@ref) objects out of order (or by using :TR). """ -qval(builder::SequenceBuilder, indices::AbstractVector) = full_integral(builder, indices)[1] -qval(indices::AbstractVector) = full_integral(indices)[1] +qval(builder::Sequence, indices::AbstractVector) = full_integral(builder, indices)[1] """ - bval(blocks) - bval(builder::SequenceBuilder, indices) + bval(container, indices) Computes the total b-value combined over multiple gradient [`BuildingBlock`](@ref) objects. @@ -38,20 +36,9 @@ In addition, in this interface one can provide negative indices to indicate that The integral can occur over multiple repetition times by including [`BuildingBlock`](@ref) objects out of order (or by using :TR). """ -bval(builder::SequenceBuilder, indices::AbstractVector) = full_integral(builder, indices)[2] -bval(indices::AbstractVector) = full_integral(indices)[2] +bval(builder::Sequence, indices::AbstractVector) = full_integral(builder, indices)[2] -function full_integral(blocks::AbstractVector) - actual_blocks = filter(b -> b isa BuildingBlock) - if length(actual_blocks) == 0 - return (0., 0.) - end - sb = builder(blocks[1]) - - return full_integral(sb, map(b -> b isa BuildingBlock ? index(sb, b) : b)) -end - -function full_integral(builder::SequenceBuilder, indices::AbstractVector) +function full_integral(builder::Sequence, indices::AbstractVector) qval_current = 0. current_index = 0 bval_current = 0. diff --git a/src/gradients/pulsed_gradients.jl b/src/gradients/pulsed_gradients.jl index a140346656a3f6f032736b1c8f13bbcc934a5e75..6a91760c355235c899d44deda07fd50f25385ac5 100644 --- a/src/gradients/pulsed_gradients.jl +++ b/src/gradients/pulsed_gradients.jl @@ -5,10 +5,10 @@ module PulsedGradients import JuMP: @constraint, @variable, Model, VariableRef, owner_model, value import StaticArrays: SVector -import ...BuildingBlocks: BuildingBlock, duration, properties, set_simple_constraints!, BuildingBlockPlaceholder, gradient_strength, slew_rate -import ...SequenceBuilders: SequenceBuilder, start_time -import ...ConcreteBlocks: ConcreteBlock, to_concrete_block, AbstractSequence -import ..IntegrateGradients: qval, bval +import ...Variables: qval, bval, rise_time, flat_time, slew_rate, gradient_strength, variables, duration, δ, get_free_variable, VariableType +import ...BuildingBlocks: BuildingBlock, duration, set_simple_constraints! +import ...ConcreteBlocks: ConcreteBlock, to_concrete_block +import ...GlobalModel: @global_model_constructor """ @@ -36,66 +36,36 @@ If not set, they will be determined during the sequence optimisation. The [`bvalue`](@ref) can be constrained for multiple gradient pulses. """ mutable struct PulsedGradient <: BuildingBlock - builder::SequenceBuilder + model :: Model orientation :: Any - slew_rate :: VariableRef - rise_time :: VariableRef - flat_time :: VariableRef + slew_rate :: VariableType + rise_time :: VariableType + flat_time :: VariableType end -function PulsedGradient(; kwargs...) - return BuildingBlockPlaceholder{PulsedGradient}(; kwargs...) -end +@global_model_constructor PulsedGradient -function PulsedGradient(builder::SequenceBuilder; orientation=:bvec, kwargs...) +function PulsedGradient(model::Model; orientation=:bvec, slew_rate=nothing, rise_time=nothing, flat_time=nothing, kwargs...) model = owner_model(builder) res = PulsedGradient( builder, orientation, - @variable(model), - @variable(model), - @variable(model) + [get_free_variable(model, value) for value in (slew_rate, rise_time, flat_time)]... ) set_simple_constraints!(model, res, kwargs) - @constraint model flat_time(res) >= 0 - @constraint model rise_time(res) >= 0 - @constraint model slew_rate(res) >= 0 + @constraint model res.flat_time >= 0 + @constraint model res.rise_time >= 0 + @constraint model res.slew_rate >= 0 return res end -""" - rise_time(pulsed_gradient) - -The time from 0 till the maximum gradient strength in ms. -""" rise_time(pg::PulsedGradient) = pg.rise_time - -""" - flat_time(pulsed_gradient) - -The time spent at the maximum gradient strength in ms. -""" flat_time(pg::PulsedGradient) = pg.flat_time - gradient_strength(g::PulsedGradient) = rise_time(g) * slew_rate(g) - slew_rate(g::PulsedGradient) = g.slew_rate - -""" - δ(pulsed_gradient) - -Pulse gradient duration (`rise_time + `flat_time`). This is the effective duration of the gradient. The real duration is longer (and given by [`duration`](@ref)). -""" δ(g::PulsedGradient) = rise_time(g) + flat_time(g) - duration(g::PulsedGradient) = 2 * rise_time(g) + flat_time(g) - -""" - qval(gradient) - -q-value at the end of the gradient (rad/um). -""" qval(g::PulsedGradient) = (g.orientation == :neg_bvec ? -1 : 1) * gradient_strength(g) * δ(g) @@ -116,7 +86,7 @@ end variables(::Type{<:PulsedGradient}) = [qval, δ, gradient_strength, duration, rise_time, flat_time, slew_rate] -function to_concrete_block(s::AbstractSequence, block::PulsedGradient) +function to_concrete_block(block::PulsedGradient) if block.orientation == :bvec rotate = true qvec = [value(qval(block)), 0., 0.] @@ -131,7 +101,7 @@ function to_concrete_block(s::AbstractSequence, block::PulsedGradient) end t_rise = value(rise_time(block)) t_d = value(δ(block)) - return ConcreteBlock(s, t_d + t_rise, gradient=[ + return ConcreteBlock(t_d + t_rise, gradient=[ (0., zeros(3)), (t_rise, qvec), (t_d, qvec), diff --git a/src/inserted_building_blocks.jl b/src/inserted_building_blocks.jl new file mode 100644 index 0000000000000000000000000000000000000000..4a4b54db309d47141f2303e23e45bd25c6dc1e71 --- /dev/null +++ b/src/inserted_building_blocks.jl @@ -0,0 +1,64 @@ +module InsertedBuildingBlocks +import Printf: @sprintf +import JuMP: owner_model, has_values, Model +import ..GlobalModel: @global_model_constructor +import ..Variables: Variables, duration, start_time, variables, end_time, get_free_variable +import ..BuildingBlocks: BuildingBlock, internal_print + +""" + InsertedBuildingBlock(building_block; start_time=nothing) + InsertedBuildingBlock(building_block, index) + +Represents a specific insertion of the [`BuildingBlock`](@ref) object within a larger sequence. + +If `index` is set an existing [`InsertedBuildingBlock`](@ref) is returned. Otherwise, a new one is created with the given `start_time` as constraint. +""" +struct InsertedBuildingBlock{T<:BuildingBlock} + bb :: T + index :: Int + function InsertedBuildingBlock(bb, index) + if index < 1 || index > length(bb) + error("$index is out of range for $bb") + end + return new{typeof(bb)}(bb, index) + end +end + +owner_model(inserted::InsertedBuildingBlock) = owner_model(inserted.bb) +has_values(inserted::InsertedBuildingBlock) = has_values(owner_model(inserted)) + +@global_model_constructor InsertedBuildingBlock + +function InsertedBuildingBlock(model::Model, bb::BuildingBlock; start_time=nothing) + index = length(bb.start_time) + 1 + push!(bb.start_time, get_free_variable(model, start_time)) + InsertedBuildingBlock(bb, index) +end + + +function Base.show(io::IO, inserted::InsertedBuildingBlock) + print(io, string(typeof(block)), "(") + if has_values(inserted) + if iszero(value(duration(block))) + print(io, "time=", @sprintf("%.3f", value(start_time(block))), ", ") + else + print( + io, "time=", + @sprintf("%.3f", value(start_time(block))), "-", + @sprintf("%.3f", value(end_time(block))), ", " + ) + end + end + internal_print(io, inserted.bb) + print(io, ")") +end + +for func in variables() + if func in (start_time, end_time) + continue + end + @eval Variables.$(nameof(func))(inserted::InsertedBuildingBlock, args...; kwargs...) = Variables.$(nameof(func))(inserted.bb, args...; kwargs...) +end + + +end \ No newline at end of file diff --git a/src/pulses/constant_pulses.jl b/src/pulses/constant_pulses.jl index c0e174d5e0bec71e10ffd58f26ee19a2f8585bf2..c83209a1dc800a8e517eb95621492e4b81bfed68 100644 --- a/src/pulses/constant_pulses.jl +++ b/src/pulses/constant_pulses.jl @@ -1,9 +1,9 @@ module ConstantPulses -import JuMP: VariableRef, @constraint, @variable, value -import ...BuildingBlocks: BuildingBlock, properties, BuildingBlockPlaceholder, set_simple_constraints!, duration -import ...SequenceBuilders: SequenceBuilder, owner_model, start_time, end_time, AbstractSequence +import JuMP: VariableRef, @constraint, @variable, value, Model +import ...BuildingBlocks: BuildingBlock, set_simple_constraints! import ...ConcreteBlocks: ConcreteBlock, to_concrete_block -import ..Properties: flip_angle, phase, amplitude, frequency, bandwidth +import ...Variables: variables, get_free_variable, flip_angle, phase, amplitude, frequency, bandwidth, start_time, end_time, VariableType, duration +import ...GlobalModel: @global_model_constructor """ ConstantPulse(; variables...) @@ -18,24 +18,21 @@ Represents an radio-frequency pulse with a constant amplitude and frequency (i.e - [`frequency`](@ref): frequency of the RF pulse relative to the Larmor frequency (in kHz). """ struct ConstantPulse <: BuildingBlock - builder :: SequenceBuilder - amplitude :: VariableRef - duration :: VariableRef - phase :: VariableRef - frequency :: VariableRef + model :: Model + amplitude :: VariableType + duration :: VariableType + phase :: VariableType + frequency :: VariableType end -ConstantPulse(; kwargs...) = BuildingBlockPlaceholder{ConstantPulse}(; kwargs...) -function ConstantPulse(builder::SequenceBuilder; kwargs...) - model = owner_model(builder) +@global_model_constructor ConstantPulse + +function ConstantPulse(model::Model; amplitude=nothing, duration=nothing, phase=nothing, frequency=nothing, kwargs...) res = ConstantPulse( - builder, - @variable(model), - @variable(model), - @variable(model), - @variable(model) + model, + [get_free_variable(model, value) for value in (amplitude, duration, phase, frequency)]... ) - @constraint model amplitude(res) >= 0 + @constraint model res.amplitude >= 0 set_simple_constraints!(model, res, kwargs) return res end @@ -49,10 +46,10 @@ bandwidth(pulse::ConstantPulse) = 3.79098854 / duration(pulse) variables(::Type{<:ConstantPulse}) = [amplitude, duration, phase, frequency, flip_angle, bandwidth] -function to_concrete_block(s::AbstractSequence, block::ConstantPulse) +function to_concrete_block(block::ConstantPulse) d = value(duration(block)) final_phase = value(phase(block)) + d * value(frequency(block)) * 360 - return ConcreteBlock(s, value(duration(block)), pulse=[ + return ConcreteBlock(value(duration(block)), pulse=[ ([0., d]), value.([amplitude(block), amplitude(block)]), value.([phase(block), final_phase]) diff --git a/src/pulses/instant_pulses.jl b/src/pulses/instant_pulses.jl index 863fbfe04109cdd412c42da371d0aa218262a749..ffee58be9710c8c189ef20f37ca7030a21c0dff6 100644 --- a/src/pulses/instant_pulses.jl +++ b/src/pulses/instant_pulses.jl @@ -1,25 +1,25 @@ module InstantPulses -import JuMP: @constraint, @variable, VariableRef, value -import ...BuildingBlocks: BuildingBlock, properties, BuildingBlockPlaceholder, set_simple_constraints!, duration -import ...SequenceBuilders: SequenceBuilder, owner_model, start_time -import ...ConcreteBlocks: to_concrete_block, AbstractConcreteBlock, Sequence, AbstractSequence -import ..Properties: flip_angle, phase +import JuMP: @constraint, @variable, VariableRef, value, Model +import ...BuildingBlocks: BuildingBlock +import ...ConcreteBlocks: to_concrete_block, AbstractConcreteBlock +import ...Variables: flip_angle, phase, start_time, variables, duration, get_free_variable, VariableType +import ...GlobalModel: @global_model_constructor struct InstantRFPulseBlock <: BuildingBlock - builder :: SequenceBuilder - flip_angle :: VariableRef - phase :: VariableRef + model :: Model + flip_angle :: VariableType + phase :: VariableType end -InstantRFPulseBlock(; kwargs...) = BuildingBlockPlaceholder{InstantRFPulseBlock}(; kwargs...) -function InstantRFPulseBlock(builder::SequenceBuilder; kwargs...) - model = owner_model(builder) +@global_model_constructor InstantRFPulseBlock + +function InstantRFPulseBlock(model::Model; flip_angle=nothing, phase=nothing) res = InstantRFPulseBlock( - builder, - @variable(model), - @variable(model) + model, + get_free_variable(model, flip_angle), + get_free_variable(model, phase) ) - @constraint model flip_angle(res) >= 0 + @constraint model res.flip_angle >= 0 set_simple_constraints!(model, res, kwargs) return res end @@ -38,13 +38,12 @@ Instantaneous RF pulse with no free variables. See [`InstantRFPulseBlock`](@ref) for a version where [`flip_angle`](@ref) and [`phase`](@ref) are variables. """ struct ConcreteInstantRFPulse <: AbstractConcreteBlock - builder :: AbstractSequence flip_angle :: Number phase :: Number end -function to_concrete_block(builder::Sequence, block::InstantRFPulseBlock) - return ConcreteInstantRFPulse(builder, value(flip_angle(block)), value(phase(block))) +function to_concrete_block(block::InstantRFPulseBlock) + return ConcreteInstantRFPulse(value(flip_angle(block)), value(phase(block))) end end \ No newline at end of file diff --git a/src/pulses/properties.jl b/src/pulses/properties.jl deleted file mode 100644 index abde613511dfb62df9450f4f80f0a34d0d88b2a4..0000000000000000000000000000000000000000 --- a/src/pulses/properties.jl +++ /dev/null @@ -1,39 +0,0 @@ -module Properties - -""" - flip_angle(pulse_block) - -The flip angle of the RF pulse in a [`BuildingBlock`](@ref) in degrees. -""" -function flip_angle end - -""" - phase(pulse_block) - -The angle of the phase at the start of the RF pulse in a [`BuildingBlock`](@ref) in degrees. -""" -function phase end - -""" - amplitude(pulse_block) - -The maximum amplitude during the RF pulse in a [`BuildingBlock`](@ref) in kHz. -""" -function amplitude end - -""" - frequency(pulse_block) - -The maximum frequency during the RF pulse in a [`BuildingBlock`](@ref) relative to the Larmor frequency in kHz. -""" -function frequency end - - -""" - bandwidth(pulse_block) - -FWHM of the frequency content of the RF pulse in kHz. -""" -function bandwidth end - -end \ No newline at end of file diff --git a/src/pulses/pulses.jl b/src/pulses/pulses.jl index fd9b9862c34f32c9ff6509db662fc904db7770b9..83354e61977277fe49ab1f2504bb21a092f5ebce 100644 --- a/src/pulses/pulses.jl +++ b/src/pulses/pulses.jl @@ -1,12 +1,10 @@ module Pulses -include("properties.jl") include("instant_pulses.jl") include("constant_pulses.jl") include("sinc_pulses.jl") -import .Properties: flip_angle, phase, amplitude, frequency, bandwidth import .InstantPulses: InstantRFPulseBlock import .ConstantPulses: ConstantPulse -import .SincPulses: SincPulse, N_left, N_right +import .SincPulses: SincPulse end \ No newline at end of file diff --git a/src/pulses/sinc_pulses.jl b/src/pulses/sinc_pulses.jl index 8320c91855be8a4e593122512d264148007bf53a..fd6e7472678e76a42c3514dd3230967d3c941a03 100644 --- a/src/pulses/sinc_pulses.jl +++ b/src/pulses/sinc_pulses.jl @@ -1,12 +1,12 @@ module SincPulses -import JuMP: VariableRef, @constraint, @variable, value +import JuMP: VariableRef, @constraint, @variable, value, Model import QuadGK: quadgk import Polynomials: fit, Polynomial -import ...BuildingBlocks: BuildingBlock, properties, BuildingBlockPlaceholder, set_simple_constraints!, duration -import ...SequenceBuilders: SequenceBuilder, owner_model, start_time, end_time, AbstractSequence +import ...BuildingBlocks: BuildingBlock, set_simple_constraints! import ...ConcreteBlocks: ConcreteBlock, to_concrete_block -import ..Properties: flip_angle, phase, amplitude, frequency, bandwidth +import ...Variables: flip_angle, phase, amplitude, frequency, bandwidth, VariableType, variables, get_free_variable, duration +import ...GlobalModel: @global_model_constructor """ SincPulse(; symmetric=true, max_Nlobes=nothing, apodise=true, variables...) @@ -29,27 +29,32 @@ Represents an radio-frequency pulse with a constant amplitude and frequency. - [`bandwidth`](@ref): width of the rectangular function in frequency space (in kHz). If the `duration` is short (compared with 1/`bandwidth`), this bandwidth will only be approximate. """ struct SincPulse <: BuildingBlock - builder :: SequenceBuilder + model :: Model symmetric :: Bool apodise :: Bool nlobe_integral :: Polynomial - N_left :: VariableRef - N_right :: VariableRef - amplitude :: VariableRef - phase :: VariableRef - frequency :: VariableRef - lobe_duration :: VariableRef + N_left :: VariableType + N_right :: VariableType + amplitude :: VariableType + phase :: VariableType + frequency :: VariableType + lobe_duration :: VariableType end -SincPulse(; kwargs...) = BuildingBlockPlaceholder{SincPulse}(; kwargs...) -function SincPulse(builder::SequenceBuilder; symmetric=true, max_Nlobes=nothing, apodise=true, kwargs...) - model = owner_model(builder) +@global_model_constructor SincPulse + +function SincPulse(model::Model; + symmetric=true, max_Nlobes=nothing, apodise=true, N_lobes=nothing, N_left=nothing, N_right=nothing, + amplitude=nothing, phase=nothing, frequency=nothing, lobe_duration=nothing, kwargs... +) if symmetric - N_lobes = @variable(model, integer=true) + N_lobes = get_free_variable(model, N_lobes) + @assert isnothing(N_left) && isnothing(N_right) "N_left and N_right cannot be set if symmetric=true (default)" N_left_var = N_right_var = N_lobes else - N_left_var = @variable(model, integer=true) - N_right_var = @variable(model, integer=true) + @assert isnothing(N_lobes) "N_lobes cannot be set if symmetric=true (default)" + N_left_var = get_free_variable(model, N_left) + N_right_var = get_free_variable(model, N_right) end res = SincPulse( builder, @@ -58,15 +63,12 @@ function SincPulse(builder::SequenceBuilder; symmetric=true, max_Nlobes=nothing, nlobe_integral_params(max_Nlobes, apodise), N_left_var, N_right_var, - @variable(model), - @variable(model), - @variable(model), - @variable(model) + [get_free_variable(model, value) for value in (amplitude, phase, frequeny, lobe_duration)]... ) - @constraint model amplitude(res) >= 0 - @constraint model N_left(res) >= 1 + @constraint model res.amplitude >= 0 + @constraint model res.N_left >= 1 if !symmetric - @constraint model N_right(res) >= 1 + @constraint model res.N_right >= 1 end set_simple_constraints!(model, res, kwargs) return res @@ -100,13 +102,13 @@ lobe_duration(pulse::SincPulse) = pulse.lobe_duration bandwidth(pulse::SincPulse) = 1 / lobe_duration(pulse) variables(::Type{<:SincPulse}) = [amplitude, N_left, N_right, duration, phase, frequency, flip_angle, lobe_duration, bandwidth] -function to_concrete_block(s::AbstractSequence, block::SincPulse) +function to_concrete_block(block::SincPulse) normed_times = -value(N_left(block)):0.1:value(N_right(block)) + 1e-5 times = ((normed_times .+ value(N_left(block))) .* value(lobe_duration(block))) amplitudes = value(amplitude(block)) .* (normalised_function.(normed_times; apodise=block.apodise)) phases = (value(frequency(block)) .* value(lobe_duration(block))) .* normed_times * 360 return ConcreteBlock( - s, value(duration(block)); + value(duration(block)); pulse=(times, amplitudes, phases) ) end diff --git a/src/readouts/instant_readouts.jl b/src/readouts/instant_readouts.jl index 645809ba6d9c746eaa25c280d22604b479eb7816..6f3bf4cd5da847b7935aea7a6e0301d4f7a0300b 100644 --- a/src/readouts/instant_readouts.jl +++ b/src/readouts/instant_readouts.jl @@ -1,7 +1,7 @@ module InstantReadouts -import ...BuildingBlocks: BuildingBlock, BuildingBlockPlaceholder, duration, properties -import ...SequenceBuilders: SequenceBuilder, start_time, to_block, AbstractSequence +import ...BuildingBlocks: BuildingBlock import ...ConcreteBlocks: AbstractConcreteBlock, to_concrete_block +import ...Variables: variables """ InstantReadout() @@ -11,13 +11,8 @@ Represents an instantaneous `Readout` of the signal. It has no parameters or properties to set. """ struct InstantReadout <: AbstractConcreteBlock - builder::AbstractSequence end -InstantReadout() = BuildingBlockPlaceholder{InstantReadout}() - variables(::Type{<:InstantReadout}) = [] -to_block(builder::SequenceBuilder, cls::Type{<:InstantReadout}) = cls(builder) -to_concrete_block(builder::AbstractSequence, ::InstantReadout) = InstantReadout(builder) - +to_concrete_block(::InstantReadout) = InstantReadout() end \ No newline at end of file diff --git a/src/variables.jl b/src/variables.jl new file mode 100644 index 0000000000000000000000000000000000000000..bff48c7d4c369b798bacd83ac00505c30ceb1921 --- /dev/null +++ b/src/variables.jl @@ -0,0 +1,92 @@ +module Variables +import JuMP: @variable, Model, @objective, objective_function, owner_model, has_values, value, AbstractJuMPScalar +import ..Scanners: gradient_strength, slew_rate + +all_variables_symbols = [ + # general + :duration => (:block, "duration of the building block in ms."), + + # RF pulse + :flip_angle => (:pulse, "The flip angle of the RF pulse in degrees"), + :amplitude => (:pulse, "The maximum amplitude of an RF pulse in kHz"), + :phase => (:pulse, "The angle of the phase of an RF pulse in KHz"), + :frequency => (:pulse, "The off-resonance frequency of an RF pulse (relative to the Larmor frequency of water) in KHz"), + :bandwidth => (:pulse, "Bandwidth of the RF pulse in kHz"), + :N_left => (:pulse, "The number of zero crossings of the RF pulse before the main peak"), + :N_right => (:pulse, "The number of zero crossings of the RF pulse after the main peak"), + + # gradients + :qval => (:gradient, "The spatial range on which the displacements can be detected due to this gradient in 1/um (equivalent to [`area_under_curve`](@ref))"), + :area_under_curve => (:gradient, "Area under the curve of the gradient in 1/um (equivalent to [`qval`](@ref))."), + :δ => (:gradient, "Effective duration of a gradient pulse ([`rise_time`](@ref) + [`flat_time`](@ref)) in ms."), + :rise_time => (:gradient, "Time for gradient pulse to reach its maximum value in ms."), + :flat_time => (:gradient, "Time of gradient pulse at maximum value in ms."), + :slew_rate => (:gradient, "maximum increase of a gradient (kHz/um/ms)"), + :gradient_strength => (:gradient, "maximum strength of a gradient (kHz/um)"), +] + +symbol_to_func = Dict{Symbol, Function}() + + + +for (func_symbol, (block_symbol, description)) in all_variables_symbols + as_string = " $func_symbol($block_symbol)\n\n$description\n\nThis represents a variable within the sequence. Variables can be set during the construction of a [`BuildingBlock`](@ref) or used to create constraints after the fact." + new_func = @eval begin + $as_string + function $func_symbol end + end + symbol_to_func[func_symbol] = new_func +end + + +""" + variables(building_block) + variables() + +Returns all functions representing properties of a [`BuildingBlock`](@ref) object. +""" +variables() = [values(symbol_to_func)...] + + +# Some universal truths +area_under_curve(bb) = qval(bb) + + +function start_time end +function end_time end + + +const VariableType = Union{Number, AbstractJuMPScalar} + + +""" + get_free_variable(model, value; integer=false) + +Get a representation of a given `variable` given a user-defined constraint. +""" +get_free_variable(::Model, value::Number; integer=false) = integer ? Int(value) : Float64(value) +function get_free_variable(model::Model, value::VariableType; integer=false) + if owner_model(value) != model + if has_values(value) + return value(value) + end + error("Cannot set any constraints between sequences stored in different JuMP models.") + end + return value +end +get_free_variable(model::Model, ::Nothing; integer=false) = @variable(model, integer=integer) +get_free_variable(model::Model, value::Symbol; integer=false) = integer ? error("Cannot maximise or minimise an integer variable") : get_free_variable(model, Val(value)) +function get_free_variable(model::Model, ::Val{:min}) + var = get_free_variable(model, nothing) + @objective model Min objective_function(model) + var + return var +end +function get_free_variable(model::Model, ::Val{:max}) + var = get_free_variable(model, nothing) + @objective model Min objective_function(model) - var + return var +end + +function bval end + +end \ No newline at end of file diff --git a/src/wait.jl b/src/wait.jl index 8888903aebb84792b76016bb0b2ed0db12a0c3fb..9bf981af88d237c6d59160669a2b7204a8ff3e4e 100644 --- a/src/wait.jl +++ b/src/wait.jl @@ -1,8 +1,9 @@ module Wait import JuMP: Model, @constraint, @variable, VariableRef, owner_model, value -import ..BuildingBlocks: BuildingBlock, duration, properties, apply_simple_constraint!, BuildingBlockPlaceholder -import ..SequenceBuilders: SequenceBuilder, to_block, AbstractSequence +import ..Variables: VariableType, variables, duration, get_free_variable +import ..BuildingBlocks: BuildingBlock import ..ConcreteBlocks: to_concrete_block, ConcreteBlock +import ..GlobalModel: @global_model_constructor import ...Scanners: Scanner """ @@ -17,31 +18,29 @@ Duration can be set to one of: - `nothing` to make it fully determined by external constraints and objectives """ struct WaitBlock <: BuildingBlock - builder :: SequenceBuilder - duration :: VariableRef + model :: Model + duration :: VariableType end -function WaitBlock(builder::SequenceBuilder, duration_constraint=nothing) - model = owner_model(builder) - res = WaitBlock(builder, @variable(model)) +@global_model_constructor WaitBlock + +function WaitBlock(model::Model, duration=nothing) + res = WaitBlock( + model, + get_free_variable(model, duration), + VariableType[] + ) @constraint model duration(res) >= 0 - if !isnothing(duration_constraint) - apply_simple_constraint!(model, duration(res), duration_constraint) - end return res end -WaitBlock(duration_constraint=nothing) = BuildingBlockPlaceholder{WaitBlock}(duration_constraint) - -to_block(builder::SequenceBuilder, time::Union{Number, Symbol, Nothing, Val{:min}, Val{:max}}) = WaitBlock(builder, time) +to_block(time::Union{VariableType, Symbol, Nothing, Val{:min}, Val{:max}}) = WaitBlock(time) variables(::Type{WaitBlock}) = [duration] duration(wb::WaitBlock) = wb.duration -scanner_constraints!(::Model, ::WaitBlock, ::Scanner) = nothing - -to_concrete_block(builder::AbstractSequence, wb::WaitBlock) = ConcreteBlock(builder, value(duration(wb))) +to_concrete_block(wb::WaitBlock) = ConcreteBlock(value(duration(wb))) end \ No newline at end of file