diff --git a/src/MRIBuilder.jl b/src/MRIBuilder.jl index 0368a586f678df6284a5a782c9155421534ef24b..a6e81ba1d15def24cced5d5bf9a5dd88b5fe2acc 100644 --- a/src/MRIBuilder.jl +++ b/src/MRIBuilder.jl @@ -1,5 +1,31 @@ +""" +Builds and optimises NMR/MRI sequences. +""" module MRIBuilder -# Write your package code here. +include("building_blocks.jl") +include("sequence_builders.jl") +include("wait.jl") +include("gradients/gradients.jl") +include("pulses/pulses.jl") +include("readouts/readouts.jl") + +import .BuildingBlocks: BuildingBlock, scanner_constraints! +export BuildingBlock, scanner_constraints! + +import .Wait: WaitBlock +export WaitBlock + +import .SequenceBuilders: SequenceBuilder, start_time, end_time, duration, TR +export SequenceBuilder, start_time, end_time, duration, TR + +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 .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 .Readouts: InstantReadout +export InstantReadout end diff --git a/src/building_blocks.jl b/src/building_blocks.jl new file mode 100644 index 0000000000000000000000000000000000000000..3a072eda680e421d3aab8b4590f0427d5c7da4c4 --- /dev/null +++ b/src/building_blocks.jl @@ -0,0 +1,214 @@ +module BuildingBlocks +import JuMP: has_values, GenericVariableRef, value, Model, @constraint, @objective, owner_model, objective_function +import Printf: @sprintf +import ...Sequences: RFPulse, InstantRFPulse, MRGradients, InstantGradient +import ...Scanners: Scanner + +""" +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_mcmr_components`](@ref)(block, parameters): converts the block into components recognised by the MCMR simulator +- [`properties`](@ref): A list of all functions that are used to compute properties of the building block. Any of these can be used in constraints or objective functions. +""" +abstract type BuildingBlock end + + +""" + duration(building_block) + +The duration of the building block in ms. +""" +function duration end + +""" + to_mcmr_components(building_block, parameters) + +Converts the building block into components recognised by the MCMR simulator. These components are: +- [`RFPulse`](@ref) +- [`InstantRFPulse`](@ref) +- [`MRGradients`](@ref) +- [`InstantGradient`](@ref) +""" +function to_mcmr_components end + + +""" + 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 + +""" + slew_rate(gradient) + +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) + +Adds any constraints from a specific scanner to a [`BuildingBlock`]{@ref}. +If a [`Scanner`](@ref) is provided to the `SequenceBuilder`, this will be called under the hood on every [`BuildingBlock`]@(ref). +""" +function scanner_constraints!(building_block::BuildingBlock, scanner::Scanner) + scanner_constraints!(owner_model(building_block), building_block, scanner) +end + +function scanner_constraints!(model::Model, building_block::BuildingBlock, scanner::Scanner) + for (func, property_name) in [ + (gradient_strength, :gradient) + (slew_rate, :slew_rate) + ] + if (func in properties(building_block)) && isfinite(getproperty(scanner, property_name)) + @constraint model func(building_block) <= getproperty(scanner, property_name) + end + end +end + +""" + properties(building_block) + +Returns a list of function that can be called to constrain the `building_block`. +""" +properties(bb::BuildingBlock) = properties(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 + for name in propertynames(block) + value = getproperty(block, name) + if value isa GenericVariableRef || name == :builder || string(name)[1] == '_' + continue + end + print(io, name, "=", repr(value), ", ") + end + + if has_values(block) + for fn in properties(block) + if fn == duration + continue + end + numeric_value = value(fn(block)) + printed_value = @sprintf("%.3g", numeric_value) + 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) + +Add any constraints or objective functions to the properties of a [`BuildingBlock`](@ref) in the JuMP `model`. + +Each keyword argument has to match one of the functions in [`properties`](@ref)(block). +If set to a numeric value, a constraint will be added to fix the function value to that numeric value. +If set to `:min` or `:max`, minimising or maximising this function will be added to the cost function. +""" +function set_simple_constraints!(model::Model, block::BuildingBlock, kwargs) + to_funcs = Dict(nameof(fn) => fn for fn in properties(block)) + for (key, value) in kwargs + apply_simple_constraint!(model, to_funcs[key](block), value) + end + nothing +end + +""" + apply_simple_constraint!(model, variable, value) + +Add a single constraint or objective to the JuMP `model`. +This is an internal function used by [`set_simple_constraints`](@ref). +""" +apply_simple_constraint!(model::Model, variable, ::Nothing) = nothing +apply_simple_constraint!(model::Model, variable, value::Symbol) = apply_simple_constraint!(model, variable, Val(value)) +apply_simple_constraint!(model::Model, variable, ::Val{:min}) = @objective model Min objective_function(model) + variable +apply_simple_constraint!(model::Model, variable, ::Val{:max}) = @objective model Min objective_function(model) - variable +apply_simple_constraint!(model::Model, variable, value::Number) = @constraint model variable == value + + +""" + match_blocks!(block1, block2[, property_list]) + +Matches the listed properties between two [`BuildingBlock`](@ref) objects. +By default all shared properties (i.e., those with the same name) are matched. +""" +function match_blocks!(block1::BuildingBlock, block2::BuildingBlock, property_list) + model = owner_model(block1) + @assert model == owner_model(block2) + for fn in property_list + @constraint model fn(block1) == fn(block2) + end +end + +function match_blocks!(block1::BuildingBlock, block2::BuildingBlock) + property_list = intersect(properties(block1), properties(block2)) + 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/gradients/gradients.jl b/src/gradients/gradients.jl new file mode 100644 index 0000000000000000000000000000000000000000..907cc8e3c832a2c4930a93506fd86da082a96d89 --- /dev/null +++ b/src/gradients/gradients.jl @@ -0,0 +1,9 @@ +module Gradients +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 .InstantGradients: InstantGradientBlock +end \ No newline at end of file diff --git a/src/gradients/instant_gradients.jl b/src/gradients/instant_gradients.jl new file mode 100644 index 0000000000000000000000000000000000000000..9ecaff0ad50e989301e632f45609b99fc27a07dc --- /dev/null +++ b/src/gradients/instant_gradients.jl @@ -0,0 +1,64 @@ +module InstantGradients +import JuMP: @constraint, @variable, VariableRef +import ....Sequences: InstantGradient +import ...BuildingBlocks: BuildingBlock, properties, BuildingBlockPlaceholder, set_simple_constraints!, duration, to_mcmr_components +import ...SequenceBuilders: SequenceBuilder, owner_model, start_time +import ..IntegrateGradients: qval, bval + +""" + InstantGradientBlock(; orientation=:bvec, qval=nothing) + +Defines an instantaneous gradient. + +This is a [`BuildingBlock`](@ref) for the [`SequenceBuilder`](@ref). + +## Parameters +- `orientation` sets the gradient orienation. Can be set to a vector for a fixed orientation. Alternatively, can be set to :bvec (default) to rotate with the user-provided `bvecs` or to :neg_bvec to always be the reverse of the `bvecs`. + +## Variables +- [`qval`](@ref): Spatial scale on which spins will be dephased due to this pulsed gradient in rad/um. +""" +struct InstantGradientBlock <: BuildingBlock + builder::SequenceBuilder + orientation :: Any + qval :: VariableRef +end + +InstantGradientBlock(; kwargs...) = BuildingBlockPlaceholder{InstantGradientBlock}(; kwargs...) + +function InstantGradientBlock(builder::SequenceBuilder; orientation=:bvec, kwargs...) + model = owner_model(builder) + res = InstantGradientBlock( + builder, + orientation, + @variable(model) + ) + set_simple_constraints!(model, res, kwargs) + return res +end + + +qval(instant::InstantGradientBlock) = instant.qval +bval(instant::InstantGradientBlock) = 0. +duration(instant::InstantGradientBlock) = 0. +properties(::Type{<:InstantGradientBlock}) = [qval] + + +function to_mcmr_components(block::InstantGradientBlock) + if block.orientation == :bvec + rotate = true + qvec = [value(qval(block)), 0., 0.] + elseif block.orientation == :neg_bvec + rotate = true + qvec = [-value(qval(block)), 0., 0.] + elseif block.orientation isa AbstractVector && size(block.orientation) == (3, ) + rotate = false + qvec = block.orientation .* (value(qval(block)) / norm(block.orientation)) + else + error("Gradient orientation should be :bvec, :neg_bvec or a length-3 vector, not $(block.orienation)") + end + return InstantGradient(qvec, zeros(3), value(start_time(block)), rotate) +end + + +end \ No newline at end of file diff --git a/src/gradients/integrate_gradients.jl b/src/gradients/integrate_gradients.jl new file mode 100644 index 0000000000000000000000000000000000000000..9083c050c61c4d3932342a5eb7f2e230d2e1741c --- /dev/null +++ b/src/gradients/integrate_gradients.jl @@ -0,0 +1,83 @@ +module IntegrateGradients +import ...BuildingBlocks: BuildingBlock +import ...SequenceBuilders: SequenceBuilder, TR, duration, builder, index + + +""" + qval(blocks) + qval(builder::SequenceBuilder, indices) + +Computes the total q-value summed over multiple gradient [`BuildingBlock`](@ref) objects. + +The `blocks` should contain the actual [`BuildingBlock`](@ref) objects, possibly interspersed with one of: +- `:TR` wait for one TR (has no effect, but included for consistency with [`bval`](@ref)). +- `:flip` flips the current `qval` value (e.g., if a refocus pulse has happened). + +When a `builder` is provided, the `indices` are expected to be the integer indices of the individual blocks rather than the actual [`BuildingBlock`](@ref) objects. +`:TR` and `:flip` can also still be provided. +In addition, in this interface one can provide negative indices to indicate that a specific gradient should have a negative contribution to the q-value (this is an alternative for using :flip). + +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] + +""" + bval(blocks) + bval(builder::SequenceBuilder, indices) + +Computes the total b-value combined over multiple gradient [`BuildingBlock`](@ref) objects. + +The `blocks` should contain the actual [`BuildingBlock`](@ref) objects, possibly interspersed with one of: +- `:TR` wait for one TR (has no effect, but included for consistency with [`bval`](@ref)). +- `:flip` flips the current `qval` value (e.g., if a refocus pulse has happened). + +When a `builder` is provided, the `indices` are expected to be the integer indices of the individual blocks rather than the actual [`BuildingBlock`](@ref) objects. +`:TR` and `:flip` can also still be provided. +In addition, in this interface one can provide negative indices to indicate that a specific gradient should have a negative contribution to the q-value (this is an alternative for using :flip). + +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] + +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) + qval_current = 0. + current_index = 0 + bval_current = 0. + for index in indices + if index == :flip + qval_current = -qval_current + elseif index == :TR + bval_current = bval_current + qval_current^2 * TR(builder) + elseif index isa Integer + next_gradient = index < 0 ? -index : index + wait_time = duration(builder, current_index + 1, next_gradient - 1) + + bval_current = ( + bval_current + + qval_current^2 * wait_time + + bval(builder[next_gradient], index < 0 ? -qval_current : qval_current) + ) + qval_current = ( + qval_current + + sign(index) * qval(builder[next_gradient]) + ) + else + error("qval/bval indices should refer to gradient blocks or :flip/:TR, not $index") + end + end + return (qval_current, bval_current) +end + +end \ No newline at end of file diff --git a/src/gradients/pulsed_gradients.jl b/src/gradients/pulsed_gradients.jl new file mode 100644 index 0000000000000000000000000000000000000000..0a65891456413f0f70a8c908afdcf9d05b3d567c --- /dev/null +++ b/src/gradients/pulsed_gradients.jl @@ -0,0 +1,144 @@ +""" +Defines a set of different options for MRI gradients. +""" +module PulsedGradients + +import JuMP: @constraint, @variable, Model, VariableRef, owner_model, value +import StaticArrays: SVector +import ....Sequences: MRGradients +import ...BuildingBlocks: BuildingBlock, duration, properties, set_simple_constraints!, BuildingBlockPlaceholder, gradient_strength, slew_rate, to_mcmr_components +import ...SequenceBuilders: SequenceBuilder, start_time +import ..IntegrateGradients: qval, bval + + +""" + PulsedGradient(; orientation=:bvec, variables...) + +Defines a trapezoidal pulsed gradient + +This is a [`BuildingBlock`](@ref) for a [`SequenceBuilder`](@ref). + +## Parameters +- `orientation` sets the gradient orienation. Can be set to a vector for a fixed orientation. Alternatively, can be set to :bvec (default) to rotate with the user-provided `bvecs` or to :neg_bvec to always be the reverse of the `bvecs`. + +## Variables +Variables can be set during construction or afterwards as an attribute. +If not set, they will be determined during the sequence optimisation. +### Timing variables +- [`rise_time`](@ref): Time of the gradient to reach from 0 to maximum in ms. If explicitly set to 0, the scanner slew rate will be ignored. +- [`flat_time`](@ref): Time that the gradient stays at maximum strength in ms. +- [`δ`](@ref): effective pulse duration (`rise_time` + `flat_time`) in ms. +- [`duration`](@ref): total pulse duration (2 * `rise_time` + `flat_time`) in ms. +### Gradient variables +- [`gradient_strength`](@ref): Maximum gradient strength achieved during the pulse in kHz/um +- [`qval`](@ref): Spatial scale on which spins will be dephased due to this pulsed gradient in rad/um (given by `δ` * `gradient_strength`). + +The [`bvalue`](@ref) can be constrained for multiple gradient pulses. +""" +mutable struct PulsedGradient <: BuildingBlock + builder::SequenceBuilder + orientation :: Any + slew_rate :: VariableRef + rise_time :: VariableRef + flat_time :: VariableRef +end + +function PulsedGradient(; kwargs...) + return BuildingBlockPlaceholder{PulsedGradient}(; kwargs...) +end + +function PulsedGradient(builder::SequenceBuilder; orientation=:bvec, kwargs...) + model = owner_model(builder) + res = PulsedGradient( + builder, + orientation, + @variable(model), + @variable(model), + @variable(model) + ) + + set_simple_constraints!(model, res, kwargs) + @constraint model flat_time(res) >= 0 + @constraint model rise_time(res) >= 0 + @constraint model slew_rate(res) >= 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) + + +function bval(g::PulsedGradient, qstart=0.) + tr = rise_time(g) + td = δ(g) + grad = gradient_strength(g) + return ( + # b-value due to just the gradient + grad * (1//60 * tr^3 - 1//12 * tr^2 * td + 1//2 * tr * td^2 + 1//3 * td^3) + + # b-value due to cross-term + qstart * grad * (td * (td + tr)) + + # b-value due to just `qstart` + (td + tr) * qstart^2 + ) +end + +properties(::Type{<:PulsedGradient}) = [qval, δ, gradient_strength, duration, rise_time, flat_time, slew_rate] + + +function to_mcmr_components(block::PulsedGradient) + if block.orientation == :bvec + rotate = true + qvec = [value(qval(block)), 0., 0.] + elseif block.orientation == :neg_bvec + rotate = true + qvec = [-value(qval(block)), 0., 0.] + elseif block.orientation isa AbstractVector && size(block.orientation) == (3, ) + rotate = false + qvec = block.orientation .* (value(qval(block)) / norm(block.orientation)) + else + error("Gradient orientation should be :bvec, :neg_bvec or a length-3 vector, not $(block.orienation)") + end + t_start = value(start_time(block)) + t_rise = value(rise_time(block)) + t_d = value(δ(block)) + return MRGradients([ + (t_start, zeros(3)), + (t_start + t_rise, qvec), + (t_start + t_d, qvec), + (t_start + t_d + t_rise, zeros(3)), + ]) +end + + +end \ No newline at end of file diff --git a/src/pulses/constant_pulses.jl b/src/pulses/constant_pulses.jl new file mode 100644 index 0000000000000000000000000000000000000000..d237bb1c02914d503aa5958da17e5e4a9020c584 --- /dev/null +++ b/src/pulses/constant_pulses.jl @@ -0,0 +1,62 @@ +module ConstantPulses +import JuMP: VariableRef, @constraint, @variable, value +import ....Sequences: RFPulse +import ...BuildingBlocks: BuildingBlock, properties, BuildingBlockPlaceholder, set_simple_constraints!, duration, to_mcmr_components +import ...SequenceBuilders: SequenceBuilder, owner_model, start_time, end_time +import ..Properties: flip_angle, phase, amplitude, frequency, bandwidth + +""" + ConstantPulse(; variables...) + +Represents an radio-frequency pulse with a constant amplitude and frequency (i.e., a rectangular function). + +## Properties +- [`flip_angle`](@ref): rotation expected for on-resonance spins in degrees. +- [`duration`](@ref): duration of the RF pulse in ms. +- [`amplitude`](@ref): amplitude of the RF pulse in kHz. +- [`phase`](@ref): phase at the start of the RF pulse in degrees. +- [`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 +end + +ConstantPulse(; kwargs...) = BuildingBlockPlaceholder{ConstantPulse}(; kwargs...) +function ConstantPulse(builder::SequenceBuilder; kwargs...) + model = owner_model(builder) + res = ConstantPulse( + builder, + @variable(model), + @variable(model), + @variable(model), + @variable(model) + ) + @constraint model amplitude(res) >= 0 + set_simple_constraints!(model, res, kwargs) + return res +end + +amplitude(pulse::ConstantPulse) = pulse.amplitude +duration(pulse::ConstantPulse) = pulse.duration +phase(pulse::ConstantPulse) = pulse.phase +frequency(pulse::ConstantPulse) = pulse.frequency +flip_angle(pulse::ConstantPulse) = amplitude(pulse) * duration(pulse) * 360 +bandwidth(pulse::ConstantPulse) = 3.79098854 / duration(pulse) + +properties(::Type{<:ConstantPulse}) = [amplitude, duration, phase, frequency, flip_angle, bandwidth] + +function to_mcmr_components(block::ConstantPulse) + final_phase = phase(block) + duration(block) * frequency(block) * 360 + return RFPulse( + value.([start_time(block), end_time(block)]), + value.([amplitude(block), amplitude(block)]), + value.([phase(block), final_phase]) + ) +end + + +end \ No newline at end of file diff --git a/src/pulses/instant_pulses.jl b/src/pulses/instant_pulses.jl new file mode 100644 index 0000000000000000000000000000000000000000..55515ad1ecad9d31ea82ef880ad0d15a75e51c59 --- /dev/null +++ b/src/pulses/instant_pulses.jl @@ -0,0 +1,41 @@ +module InstantPulses +import JuMP: @constraint, @variable, VariableRef, value +import ....Sequences: InstantRFPulse +import ...BuildingBlocks: BuildingBlock, properties, BuildingBlockPlaceholder, set_simple_constraints!, duration, to_mcmr_components +import ...SequenceBuilders: SequenceBuilder, owner_model, start_time +import ..Properties: flip_angle, phase + +struct InstantRFPulseBlock <: BuildingBlock + builder :: SequenceBuilder + flip_angle :: VariableRef + phase :: VariableRef +end + +InstantRFPulseBlock(; kwargs...) = BuildingBlockPlaceholder{InstantRFPulseBlock}(; kwargs...) +function InstantRFPulseBlock(builder::SequenceBuilder; kwargs...) + model = owner_model(builder) + res = InstantRFPulseBlock( + builder, + @variable(model), + @variable(model) + ) + @constraint model flip_angle(res) >= 0 + set_simple_constraints!(model, res, kwargs) + return res +end + +flip_angle(instant::InstantRFPulseBlock) = instant.flip_angle +phase(instant::InstantRFPulseBlock) = instant.phase +duration(instant::InstantRFPulseBlock) = 0. +properties(::Type{<:InstantRFPulseBlock}) = [flip_angle, phase] + + +function to_mcmr_components(block::InstantRFPulseBlock) + return InstantRFPulse( + time=value(start_time(block)), + flip_angle=value(flip_angle(block)), + phase=value(phase(block)), + ) +end + +end \ No newline at end of file diff --git a/src/pulses/properties.jl b/src/pulses/properties.jl new file mode 100644 index 0000000000000000000000000000000000000000..abde613511dfb62df9450f4f80f0a34d0d88b2a4 --- /dev/null +++ b/src/pulses/properties.jl @@ -0,0 +1,39 @@ +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 new file mode 100644 index 0000000000000000000000000000000000000000..fd9b9862c34f32c9ff6509db662fc904db7770b9 --- /dev/null +++ b/src/pulses/pulses.jl @@ -0,0 +1,12 @@ +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 + +end \ No newline at end of file diff --git a/src/pulses/sinc_pulses.jl b/src/pulses/sinc_pulses.jl new file mode 100644 index 0000000000000000000000000000000000000000..53ddbaa2603708ba73f08da3ecafbab10cecd41a --- /dev/null +++ b/src/pulses/sinc_pulses.jl @@ -0,0 +1,111 @@ +module SincPulses + +import JuMP: VariableRef, @constraint, @variable, value +import QuadGK: quadgk +import Polynomials: fit, Polynomial +import ....Sequences: RFPulse +import ...BuildingBlocks: BuildingBlock, properties, BuildingBlockPlaceholder, set_simple_constraints!, duration, to_mcmr_components +import ...SequenceBuilders: SequenceBuilder, owner_model, start_time, end_time +import ..Properties: flip_angle, phase, amplitude, frequency, bandwidth + +""" + SincPulse(; symmetric=true, max_Nlobes=nothing, apodise=true, variables...) + +Represents an radio-frequency pulse with a constant amplitude and frequency. + +## Parameters +- `symmetric`: by default the sinc pulse will be symmetric (i.e., `N_left` == `N_right`). Set `symmetric=false` to independently control `N_left` and `N_right`. +- `max_Nlobes`: by default the computed [`flip_angle`](@ref) is only approximated as `amplitude` * `lobe_duration`. By setting `max_Nlobes` the flip_angle will be given by the actual integral of the sinc function, which will be more accurate for small number of lobes. However, the number of lobes will not be able to exceed this `max_Nlobes`. +- `apodise`: if true (default) applies a Hanning apodising window to the sinc pulse. + +## Variables +- [`N_left`](@ref): number of zero-crossings of the sinc function before the main peak (minimum of 1). +- [`N_right`](@ref): number of zero-crossings of the sinc function after the main peak (minimum of 1). +- [`flip_angle`](@ref): rotation expected for on-resonance spins in degrees (only approximate if `max_Nlobes` is not set). +- [`duration`](@ref): duration of the RF pulse in ms. +- [`amplitude`](@ref): amplitude of the RF pulse in kHz. +- [`phase`](@ref): phase at the start of the RF pulse in degrees. +- [`frequency`](@ref): frequency of the RF pulse relative to the Larmor frequency (in kHz). +- [`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 + symmetric :: Bool + apodise :: Bool + nlobe_integral :: Polynomial + N_left :: VariableRef + N_right :: VariableRef + amplitude :: VariableRef + phase :: VariableRef + frequency :: VariableRef + lobe_duration :: VariableRef +end + +SincPulse(; kwargs...) = BuildingBlockPlaceholder{SincPulse}(; kwargs...) +function SincPulse(builder::SequenceBuilder; symmetric=true, max_Nlobes=nothing, apodise=true, kwargs...) + model = owner_model(builder) + if symmetric + N_lobes = @variable(model, integer=true) + N_left_var = N_right_var = N_lobes + else + N_left_var = @variable(model, integer=true) + N_right_var = @variable(model, integer=true) + end + res = SincPulse( + builder, + symmetric, + apodise, + nlobe_integral_params(max_Nlobes, apodise), + N_left_var, + N_right_var, + @variable(model), + @variable(model), + @variable(model), + @variable(model) + ) + @constraint model amplitude(res) >= 0 + @constraint model N_left(res) >= 1 + if !symmetric + @constraint model N_right(res) >= 1 + end + set_simple_constraints!(model, res, kwargs) + return res +end + +function normalised_function(x; apodise=false) + if apodise + return (0.54 + 0.46 * cos(π * x)) * sin(π * x) / (π * x) + else + return sin(π * x) / (π * x) + end +end + +function nlobe_integral_params(Nlobe_max, apodise=false) + if isnothing(Nlobe_max) + return fit([1], [0.5], 0) + end + f = x -> normalised_function(x; apodise=apodise) + integral_values = [quadgk(f, 0, i)[1] for i in 1:Nlobe_max] + return fit(1:Nlobe_max, integral_values, Nlobe_max - 1) +end + +amplitude(pulse::SincPulse) = pulse.amplitude +N_left(pulse::SincPulse) = pulse.N_left +N_right(pulse::SincPulse) = pulse.N_right +duration(pulse::SincPulse) = (N_left(pulse) + N_right(pulse)) * lobe_duration(pulse) +phase(pulse::SincPulse) = pulse.phase +frequency(pulse::SincPulse) = pulse.frequency +flip_angle(pulse::SincPulse) = (pulse.nlobe_integral(N_left(pulse)) + pulse.nlobe_integral(N_right(pulse))) * amplitude(pulse) * lobe_duration(pulse) * 360 +lobe_duration(pulse::SincPulse) = pulse.lobe_duration +bandwidth(pulse::SincPulse) = 1 / lobe_duration(pulse) +properties(::Type{<:SincPulse}) = [amplitude, N_left, N_right, duration, phase, frequency, flip_angle, lobe_duration, bandwidth] + +function to_mcmr_components(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))) .+ value(start_time(block)) + amplitudes = value(amplitude(block)) .* (normalised_function.(normed_times; apodise=block.apodise)) + phases = (value(frequency(block)) .* value(lobe_duration(block))) .* normed_times * 360 + return RFPulse(times, amplitudes, phases) +end + +end \ No newline at end of file diff --git a/src/readouts/instant_readouts.jl b/src/readouts/instant_readouts.jl new file mode 100644 index 0000000000000000000000000000000000000000..f4e3db799c1b68fece1df6ec9621acee21e47fd0 --- /dev/null +++ b/src/readouts/instant_readouts.jl @@ -0,0 +1,23 @@ +module InstantReadouts +import ...BuildingBlocks: BuildingBlock, BuildingBlockPlaceholder, duration, properties, to_mcmr_components +import ...SequenceBuilders: SequenceBuilder, start_time, to_block + +""" + InstantReadout() + +Represents an instantaneous `Readout` of the signal. + +It has no parameters or properties to set. +""" +struct InstantReadout <: BuildingBlock + builder::SequenceBuilder +end + +InstantReadout() = BuildingBlockPlaceholder{InstantReadout}() + +duration(::InstantReadout) = 0. +properties(::Type{<:InstantReadout}) = [] +to_block(builder::SequenceBuilder, cls::Type{<:InstantReadout}) = cls(builder) + +to_mcmr_components(readout::InstantReadout) = Readout(value(start_time(readout))) +end \ No newline at end of file diff --git a/src/readouts/readouts.jl b/src/readouts/readouts.jl new file mode 100644 index 0000000000000000000000000000000000000000..51ee349883d8432228fb45db03a557a2bb8e3f77 --- /dev/null +++ b/src/readouts/readouts.jl @@ -0,0 +1,5 @@ +module Readouts +include("instant_readouts.jl") + +import .InstantReadouts: InstantReadout +end \ No newline at end of file diff --git a/src/sequence_builders.jl b/src/sequence_builders.jl new file mode 100644 index 0000000000000000000000000000000000000000..5f8642900d5a750b923d1bbebbe5dcc5997ba010 --- /dev/null +++ b/src/sequence_builders.jl @@ -0,0 +1,187 @@ +module SequenceBuilders +import JuMP: Model, owner_model, index, VariableRef, @constraint, @variable, has_values, optimize!, value, optimizer_with_attributes +import Juniper +import Ipopt +import ..BuildingBlocks: BuildingBlock, BuildingBlockPlaceholder, match_blocks!, duration, apply_simple_constraint!, scanner_constraints!, to_mcmr_components, start_time, end_time +import ...Sequences: Sequence +import ...Scanners: Scanner + +""" + SequenceBuilder(blocks...) + +Defines a sequence as a series of [`BuildingBlock`](@ref) objects. + +After defining the blocks, the user can add one or more constraints and an objective function to the properties of the [`BuildingBlock`](@ref) objects. +A sequence matching these constraints will be produced by calling [`solve`](@ref)(builder) or [`Sequence`](@ref)(builder). +""" +struct SequenceBuilder + model :: Model + scanner :: Scanner + blocks :: Vector{<:BuildingBlock} + TR :: VariableRef + function SequenceBuilder(model::Model, blocks...; scanner=3., TR=nothing) + if scanner isa Number + scanner = Scanner(B0=scanner) + end + builder = new(model, scanner, BuildingBlock[], @variable(model)) + for block in blocks + push!(builder.blocks, to_block(builder, block)) + end + @constraint model builder.TR >= duration(builder) + apply_simple_constraint!(model, builder.TR, TR) + for block in builder.blocks + scanner_constraints!(block, scanner) + end + return builder + end +end + +function to_block(model::SequenceBuilder, placeholder::BuildingBlockPlaceholder{T}) where {T} + block = T(model, placeholder.args...; placeholder.kwargs...) + if isassigned(placeholder.concrete) + match_blocks!(placeholder.concrete[], block) + else + placeholder.concrete[] = block + end + return block +end + +Base.getindex(model::SequenceBuilder, i::Integer) = model.blocks[i] + +function SequenceBuilder(blocks...; kwargs...) + 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) + SequenceBuilder(model, blocks...; kwargs...) +end + + +function Base.show(io::IO, builder::SequenceBuilder) + if has_values(builder) + print(io, "Solved SequenceBuilder:\n") + for block in builder.blocks + print(io, "- ", block, "\n") + end + else + print(io, "SequenceBuilder($(builder.blocks)) being solved by ") + show(io, builder.model) + end +end + + +Base.length(sb::SequenceBuilder) = length(sb.blocks) +builder(bb::BuildingBlock) = bb.builder +owner_model(bb::BuildingBlock) = owner_model(builder(bb)) +owner_model(sb::SequenceBuilder) = sb.model +has_values(object::Union{BuildingBlock, SequenceBuilder}) = has_values(owner_model(object)) +optimize!(sb::SequenceBuilder) = optimize!(owner_model(sb)) + +""" + Sequence(builder::SequenceBuilder) + +Creates a sequence that can be run in MCMR from the [`SequenceBuilder`](@ref). +If the `builder` has not been optimised yet, `optimize!(builder)` will be called first. +""" +function Sequence(sb::SequenceBuilder) + if !has_values(sb) + optimize!(sb) + end + components = [] + for block in sb.blocks + new_components = to_mcmr_components(block) + if new_components isa AbstractVector + append!(components, new_components) + else + push!(components, new_components) + end + end + return Sequence(scanner=sb.scanner, components=components, TR=value(TR(sb))) +end + +""" + TR(sequence::SequenceBuilder) + +Return the repetition time of the sequence (in ms). +""" +TR(sb::SequenceBuilder) = sb.TR + +""" + index(bb::BuildingBlock) + +Returns the index of the [`BuildingBlock`](@ref) in the parent [`SequenceBuilder`](@ref). +""" +index(sb::SequenceBuilder, bb::BuildingBlock) = findfirst(isequal(bb), sb.blocks) +index(bb::BuildingBlock) = index(builder(bb), bb) + + +""" + duration(bb1::BuildingBlock, bb2::BuildingBlock) + duration(sb::SequenceBuilder, bb1::Integer, bb2::Integer) + +The duration of the sequence from the start of [`BuildingBlock`](@ref) `bb1` till the end of `bb2`. +Returns an error if they are not part of the same sequence. +If `bb1` plays after `bb2` then this will calculate the time between `bb2` and the `bb1` in the next repetition time +(i.e., `TR(sb) + end_time(bb2) - start_time(bb1)`) +""" +function duration(bb1::BuildingBlock, bb2::BuildingBlock) + if builder(bb1) != builder(bb2) + error("Cannot compute duration between blocks that are part of different sequences!") + end + sb = builder(bb1) + return duration(sb, index(sb, bb1), index(sb, bb2)) +end + +function duration(sb::SequenceBuilder, index1::Integer, index2::Integer) + if index2 == index1 + return duration(sb[index1]) + elseif index2 == index1 - 1 + return TR(sb) + elseif index2 > index1 + return sum(duration(sb, i) for i in index1:index2) + else + return TR(sb) - duration(sb, index2+1, index1-1) + end +end + +duration(sb::SequenceBuilder) = duration(sb, 1, length(sb)) +duration(sb::SequenceBuilder, index::Integer) = duration(sb[index]) + + +""" + start_time(building_block::BuildingBlock) + start_time(seq::SequenceBuilder, building_block::Integer) + +Return the start time of the given [`BuildingBlock`](@ref) within the [`SequenceBuilder`](@ref). +You can pass on the actual [`BuildingBlock`](@ref) object or the [`SequenceBuilder`](@ref) together with the index of the `building_block` +""" +start_time(bb::BuildingBlock) = start_time(builder(bb), bb) + +function start_time(sb::SequenceBuilder, bb::BuildingBlock) + if builder(bb) != sb + error("No start time of $bb within $sb, because this BuildingBlock is not part of that sequence.") + end + return start_time(sb, index(bb)) +end + +start_time(sb::SequenceBuilder, index::Integer) = index == 1 ? 0. : duration(sb, 1, index - 1) + +""" + end_time(building_block::BuildingBlock) + end_time(seq::SequenceBuilder, building_block::Integer) + +Return the end time of the given [`BuildingBlock`](@ref) within the [`SequenceBuilder`](@ref). +You can pass on the actual [`BuildingBlock`](@ref) object or the [`SequenceBuilder`](@ref) together with the index of the `building_block` +""" +end_time(bb::BuildingBlock) = end_time(builder(bb), bb) + +function end_time(sb::SequenceBuilder, bb::BuildingBlock) + if builder(bb) != sb + error("No end time of $bb within $sb, because this BuildingBlock is not part of that sequence.") + end + return end_time(sb, index(bb)) +end + +end_time(sb::SequenceBuilder, index::Integer) = duration(sb, 1, index) + + +end diff --git a/src/wait.jl b/src/wait.jl new file mode 100644 index 0000000000000000000000000000000000000000..35f6212e4e6502eb3dc11a741e93eff70615761f --- /dev/null +++ b/src/wait.jl @@ -0,0 +1,46 @@ +module Wait +import JuMP: Model, @constraint, @variable, VariableRef, owner_model +import ..BuildingBlocks: BuildingBlock, duration, properties, apply_simple_constraint!, BuildingBlockPlaceholder, to_mcmr_components +import ..SequenceBuilders: SequenceBuilder, to_block +import ...Scanners: Scanner + +""" + WaitBlock(duration) + +An empty [`BuildingBlock`](@ref) of given `duration` (in ms). + +Duration can be set to one of: +- numeric value to fix it +- `:min` to minimise its value given any external constraints +- `:max` to maximise its value given any external constraints +- `nothing` to make it fully determined by external constraints and objectives +""" +struct WaitBlock <: BuildingBlock + builder :: SequenceBuilder + duration :: VariableRef +end + +function WaitBlock(builder::SequenceBuilder, duration_constraint=nothing) + model = owner_model(builder) + res = WaitBlock(builder, @variable(model)) + @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) + + +properties(::Type{WaitBlock}) = [duration] + +duration(wb::WaitBlock) = wb.duration + +scanner_constraints!(::Model, ::WaitBlock, ::Scanner) = nothing + +to_mcmr_components(::WaitBlock) = [] + +end \ No newline at end of file