From 0ec2b5331c265af5f161dbe94e3f4cd1a8151eec Mon Sep 17 00:00:00 2001 From: Michiel Cottaar <michiel.cottaar@ndcn.ox.ac.uk> Date: Fri, 26 Jan 2024 21:28:50 +0000 Subject: [PATCH] Swtich to model where BuildingBlocks do not know their parents --- src/MRIBuilder.jl | 38 +++++++----- src/building_blocks.jl | 90 +++++---------------------- src/concrete_blocks.jl | 55 ++++------------- src/containers/containers.jl | 5 ++ src/containers/sequences.jl | 55 +++++++++++++++++ src/global_model.jl | 63 +++++++++++++++++++ src/gradients/gradients.jl | 3 +- src/gradients/instant_gradients.jl | 30 +++++---- src/gradients/integrate_gradients.jl | 27 +++----- src/gradients/pulsed_gradients.jl | 62 +++++-------------- src/inserted_building_blocks.jl | 64 +++++++++++++++++++ src/pulses/constant_pulses.jl | 37 +++++------ src/pulses/instant_pulses.jl | 35 +++++------ src/pulses/properties.jl | 39 ------------ src/pulses/pulses.jl | 4 +- src/pulses/sinc_pulses.jl | 54 ++++++++-------- src/readouts/instant_readouts.jl | 11 +--- src/variables.jl | 92 ++++++++++++++++++++++++++++ src/wait.jl | 31 +++++----- 19 files changed, 449 insertions(+), 346 deletions(-) create mode 100644 src/containers/containers.jl create mode 100644 src/containers/sequences.jl create mode 100644 src/global_model.jl create mode 100644 src/inserted_building_blocks.jl delete mode 100644 src/pulses/properties.jl create mode 100644 src/variables.jl diff --git a/src/MRIBuilder.jl b/src/MRIBuilder.jl index 281f3c0..f43eb7a 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 d9b0374..715092b 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 539b8b4..19528ac 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 0000000..591b9f0 --- /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 0000000..5dc31a0 --- /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 0000000..d845afb --- /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 907cc8e..8c6dbb6 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 a8ee301..22016ec 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 9083c05..8b28be4 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 a140346..6a91760 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 0000000..4a4b54d --- /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 c0e174d..c83209a 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 863fbfe..ffee58b 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 abde613..0000000 --- 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 fd9b986..83354e6 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 8320c91..fd6e747 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 645809b..6f3bf4c 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 0000000..bff48c7 --- /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 8888903..9bf981a 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 -- GitLab