From 98c4258cdc2173d32db1548f0191188cd62a3352 Mon Sep 17 00:00:00 2001 From: Michiel Cottaar <MichielCottaar@protonmail.com> Date: Mon, 20 May 2024 13:11:39 +0100 Subject: [PATCH] Refactor pulseq.jl into multiple files --- src/sequence_io/pulseq.jl | 670 ------------------ src/sequence_io/pulseq_io/basic_parsers.jl | 62 ++ src/sequence_io/pulseq_io/components.jl | 115 +++ src/sequence_io/pulseq_io/parse_sections.jl | 45 ++ src/sequence_io/pulseq_io/parsers/adc.jl | 34 + src/sequence_io/pulseq_io/parsers/blocks.jl | 49 ++ .../pulseq_io/parsers/definitions.jl | 13 + src/sequence_io/pulseq_io/parsers/delays.jl | 11 + .../pulseq_io/parsers/extensions.jl | 31 + .../pulseq_io/parsers/gradients.jl | 44 ++ src/sequence_io/pulseq_io/parsers/parsers.jl | 37 + src/sequence_io/pulseq_io/parsers/rf.jl | 47 ++ src/sequence_io/pulseq_io/parsers/shapes.jl | 68 ++ .../pulseq_io/parsers/trapezoids.jl | 37 + src/sequence_io/pulseq_io/parsers/version.jl | 13 + src/sequence_io/pulseq_io/pulseq_io.jl | 46 ++ src/sequence_io/pulseq_io/sections_io.jl | 49 ++ src/sequence_io/pulseq_io/types.jl | 143 ++++ src/sequence_io/sequence_io.jl | 4 +- 19 files changed, 846 insertions(+), 672 deletions(-) delete mode 100644 src/sequence_io/pulseq.jl create mode 100644 src/sequence_io/pulseq_io/basic_parsers.jl create mode 100644 src/sequence_io/pulseq_io/components.jl create mode 100644 src/sequence_io/pulseq_io/parse_sections.jl create mode 100644 src/sequence_io/pulseq_io/parsers/adc.jl create mode 100644 src/sequence_io/pulseq_io/parsers/blocks.jl create mode 100644 src/sequence_io/pulseq_io/parsers/definitions.jl create mode 100644 src/sequence_io/pulseq_io/parsers/delays.jl create mode 100644 src/sequence_io/pulseq_io/parsers/extensions.jl create mode 100644 src/sequence_io/pulseq_io/parsers/gradients.jl create mode 100644 src/sequence_io/pulseq_io/parsers/parsers.jl create mode 100644 src/sequence_io/pulseq_io/parsers/rf.jl create mode 100644 src/sequence_io/pulseq_io/parsers/shapes.jl create mode 100644 src/sequence_io/pulseq_io/parsers/trapezoids.jl create mode 100644 src/sequence_io/pulseq_io/parsers/version.jl create mode 100644 src/sequence_io/pulseq_io/pulseq_io.jl create mode 100644 src/sequence_io/pulseq_io/sections_io.jl create mode 100644 src/sequence_io/pulseq_io/types.jl diff --git a/src/sequence_io/pulseq.jl b/src/sequence_io/pulseq.jl deleted file mode 100644 index 346b67d..0000000 --- a/src/sequence_io/pulseq.jl +++ /dev/null @@ -1,670 +0,0 @@ -module Pulseq - - -""" - parse_pulseq_dict(line, names, dtypes) - -Parse a line of integers/floats with known names and dtypes. - -This is useful to parse most of the columnar data in Pulseq, such as in BLOCKS, RF, GRADIENTS, etc. -""" -function parse_pulseq_dict(line, names, dtypes) - parts = split(line) - @assert length(parts) == length(names) - values = parse.(dtypes, split(line)) - @assert names[1] == :id - return Dict(Symbol(name) => value for (name, value) in zip(names, values)) -end - -""" - parse_pulseq_properties(lines) - -Parse any `pulseq` section formatted as: -``` -<name> <value> -<name2> <value2> -... -``` - -This includes the VERSION, DEFINITIONS, and part of the SHAPES -""" -function parse_pulseq_properties(strings::Vector{<:AbstractString}) - result = Dict{String, Any}() - for s in strings - (name, value) = split(s, limit=2) - result[name] = _parse_value(value) - end - return result -end - -function _parse_value(value::AbstractString) - for t in (Int, Float64) - try - return parse(t, value) - catch - end - end - for t in (Int, Float64) - try - return parse.(t, split(value)) - catch - end - end - return value -end - - - -""" - PulseqSection(:<title>)(lines) - -Represents a section in the pulseq file format. -""" -struct PulseqSection{T} - content :: Vector{String} -end - -""" - parse_pulseq_sections(io) - -Reads a Pulseq file into a dictionary of [`PulseqSection`](@ref) objects. -""" -function parse_pulseq_sections(io::IO) - sections = Dict{String, PulseqSection}() - current_title = "" - for line in readlines(io) - line = strip(line) - if length(line) == 0 || line[1] == '#' - continue # ignore comments - end - if line[1] == '[' && line[end] == ']' - # new section starts - current_title = lowercase(line[2:end-1]) - sections[current_title] = PulseqSection{Symbol(current_title)}(String[]) - elseif length(current_title) > 0 - push!(sections[current_title].content, line) - else - error("Content found in pulseq file before first section") - end - end - return sections -end - -function write_pulseq_section(io::IO, section::PulseqSection{T}) where {T} - title = uppercase(string(T)) - write(io, "[$title]\n") - for line in section.content - if iszero(length(line)) || line[end] != '\n' - line = line * '\n' - end - write(io, line) - end - write(io, "\n") - write(io, "\n") -end - -struct PulseqShape - samples :: Vector{Float64} -end - -struct PulseqRFPulse - amplitude :: Float64 - magnitude :: PulseqShape - phase :: PulseqShape - time :: Union{Nothing, PulseqShape} - delay :: Int - frequency :: Float64 - phase_offset :: Float64 -end - -abstract type AnyPulseqGradient end -struct PulseqGradient <: AnyPulseqGradient - amplitude :: Float64 - shape :: PulseqShape - time :: Union{Nothing, PulseqShape} - delay :: Int -end - -struct PulseqTrapezoid <:AnyPulseqGradient - amplitude :: Float64 - rise :: Int - flat :: Int - fall :: Int - delay :: Int -end - -struct PulseqADC - num :: Int - dwell :: Float64 - delay :: Int - frequency :: Float64 - phase :: Float64 -end - -struct PulseqExtension - name :: String - content :: Vector{String} -end - -struct PulseqBlock - duration :: Int - rf :: Union{Nothing, PulseqRFPulse} - gx :: Union{Nothing, AnyPulseqGradient} - gy :: Union{Nothing, AnyPulseqGradient} - gz :: Union{Nothing, AnyPulseqGradient} - adc :: Union{Nothing, PulseqADC} - ext :: Vector{<:Tuple{PulseqExtension, Int}} -end - -struct PulseqSequence - version:: VersionNumber - definitions:: NamedTuple - blocks:: Vector{PulseqBlock} -end - -""" - PulseqComponents(sequence::PulseqSequence) - -Indentifies and lists all the unique components in the sequence. -""" -struct PulseqComponents - shapes:: Vector{PulseqShape} - pulses:: Vector{PulseqRFPulse} - grads:: Vector{AnyPulseqGradient} - adc:: Vector{PulseqADC} - extensions:: Vector{PulseqExtension} -end - -PulseqComponents() = PulseqComponents( - PulseqShape[], - PulseqRFPulse[], - AnyPulseqGradient[], - PulseqADC[], - PulseqExtension[], -) - -add_components!(::PulseqComponents, search_vec::Vector, ::Nothing) = 0 -function add_components!(comp::PulseqComponents, search_vec::Vector{<:T}, component::T) where {T} - for (i, c) in enumerate(search_vec) - if same_component(comp, c, component) - return i - end - end - push!(search_vec, component) - return length(search_vec) -end - -same_component(::PulseqComponents, ::Any, ::Any) = false -function same_component(comp::PulseqComponents, a::T, b::T) where {T} - for name in fieldnames(T) - v1 = getfield(a, name) - v2 = getfield(b, name) - - if v1 isa PulseqShape - v1 = add_components!(comp, v1) - end - if v2 isa PulseqShape - v2 = add_components!(comp, v2) - end - if v1 != v2 - return false - end - end - return true -end - - -add_components!(comp::PulseqComponents, ::Nothing) = 0 -function add_components!(comp::PulseqComponents, shape::PulseqShape) - for (i, s) in enumerate(comp.shapes) - if same_shape(s, shape) - return i - end - end - push!(comp.shapes, shape) - return length(comp.shapes) -end -same_shape(shape1::PulseqShape, shape2::PulseqShape) = length(shape1.samples) == length(shape2.samples) && all(shape1.samples .≈ shape2.samples) - - -# I/O all sections -function parse_all_sections(sections:: Dict{String, PulseqSection}) - sections = copy(sections) - all_parts = Dict{Symbol, Any}() - for name in ["version", "definitions", "delays", "shapes", "rf", "gradients", "trap", "adc", "extensions", "blocks"] - if name in keys(sections) - section = pop!(sections, name) - all_parts[Symbol(name)] = parse_section(section; all_parts...) - end - end - if length(sections) > 0 - @warn "Following sections in pulseq input file are not being used: $(keys(sections))" - end - return PulseqSequence(all_parts[:version], all_parts[:definitions], all_parts[:blocks]) -end - -function gen_all_sections(seq:: PulseqSequence) - sections = Dict{Symbol, PulseqSection}() - sections[:version] = gen_section(seq, Val(:version)) - sections[:definitions] = gen_section(seq, Val(:definitions)) - - comp = PulseqComponents() - sections[:blocks] = gen_section(seq, comp, Val(:blocks)) - for symbol in [:rf, :gradients, :trap, :adc, :shapes] - sections[symbol] = gen_section(comp, Val(symbol)) - end - return sections -end - -# Version I/O - -""" - parse_section(section) - -Parses any [`PulseqSection`](@ref) and return the appropriate type. - -The opposite is [`gen_section`](@ref). -""" -function parse_section(section:: PulseqSection{:version}; kwargs...) - props = parse_pulseq_properties(section.content) - return VersionNumber(props["major"], props["minor"], props["revision"]) -end - -""" - gen_section(sequence, Val(:<title>)) - -Creates a specific [`PulseqSection`](@ref){<title>} from a part of the PulseqSequence. - -This is the opposite of [`parse_section`](@ref) -""" -function gen_section(seq::PulseqSequence, ::Val{:version}) - version = seq.version - return PulseqSection{:version}([ - "major $(version.major)", - "minor $(version.minor)", - "revision $(version.patch)", - ]) -end - -# Definitions I/O - -function parse_section(section:: PulseqSection{:definitions}; kwargs...) - props = parse_pulseq_properties(section.content) - return NamedTuple(Symbol(key) => value for (key, value) in props) -end - -_to_string_value(value::AbstractString) = value -_to_string_value(value::Number) = string(value) -_to_string_value(value::Vector) = join(_to_string_value.(value), " ") - -function gen_section(seq:: PulseqSequence, ::Val{:definitions}) - definitions = seq.definitions - return PulseqSection{:definitions}(["$(key) $(_to_string_value(value))" for (key, value) in pairs(definitions)]) -end - - -# Shapes I/O -struct CompressedPulseqShape - num :: Int - samples :: Vector{Float64} -end - - -function parse_section(section:: PulseqSection{:shapes}; kwargs...) - current_id = -1 - shapes = Dict{Int, CompressedPulseqShape}() - for line in section.content - if startswith(lowercase(line), "shape_id") - current_id = parse(Int, line[9:end]) - continue - end - for text in ("num_uncompressed", "num_samples") - if startswith(lowercase(line), text) - @assert current_id != -1 - if current_id in keys(shapes) - error("Multiple shapes with the same ID detected.") - end - shapes[current_id] = CompressedPulseqShape(parse(Int, line[length(text)+1:end]), Float64[]) - end - end - if !startswith(lowercase(line), "num") - push!(shapes[current_id].samples, parse(Float64, line)) - end - end - return Dict(key => uncompress(shape) for (key, shape) in shapes) -end - -function uncompress(compressed::CompressedPulseqShape) - if compressed.num == length(compressed.samples) - # not actually compressed - return PulseqShape(compressed.samples) - end - amplitudes = [compressed.samples[1]] - repeating = false - prev_sample = compressed.samples[1] - for sample in compressed.samples[2:end] - if repeating - for _ in 1:Int(sample) - push!(amplitudes, prev_sample) - end - repeating = false - else - push!(amplitudes, sample) - if sample == prev_sample - repeating = true - end - end - end - return PulseqShape(amplitudes) -end - -function gen_section(comp:: PulseqComponents, ::Val{:shapes}) - res = PulseqSection{:shapes}(String[]) - for (index, shape) in enumerate(comp.shapes) - append!(res.content, [ - "", - "shape_id $index", - "num_samples $(length(shape.samples))" - ]) - for sample in shape.samples - push!(res.content, string(sample)) - end - end - return res -end - -function parse_section(section::PulseqSection{:delays}; version, kwargs...) - if version > v"1.3.1" - error("Did not expect a [DELAYS] section in pulseq file with version $(version)") - end - delays = Dict{Int, Int}() - for line in section.content - (id, delay) = parse.(Int, split(line)) - delays[id] = delay - end - return delays -end - -# Components I/O -_get_component(id::Int, shapes::Dict) = iszero(id) ? nothing : shapes[id] - -function parse_section(section::PulseqSection{:rf}; shapes::Dict{Int, PulseqShape}, version, kwargs...) - result = Dict{Int, PulseqRFPulse}() - for line in section.content - if version == v"1.3.1" - props = parse_pulseq_dict( - line, - [:id, :amp, :mag_id, :phase_id, :delay, :freq, :phase], - [Int, Float64, Int, Int, Int, Float64, Float64], - ) - props[:time_id] = 0 - else - props = parse_pulseq_dict( - line, - [:id, :amp, :mag_id, :phase_id, :time_id, :delay, :freq, :phase], - [Int, Float64, Int, Int, Int, Int, Float64, Float64], - ) - end - result[props[:id]] = PulseqRFPulse( - props[:amp], - _get_component(props[:mag_id], shapes), - _get_component(props[:phase_id], shapes), - _get_component(props[:time_id], shapes), - props[:delay], - props[:freq], - props[:phase], - ) - end - return result -end - -function gen_section(comp:: PulseqComponents, ::Val{:rf}) - res = PulseqSection{:rf}(String[]) - for (i, pulse) in enumerate(comp.pulses) - values = string.(Any[ - i, - pulse.amplitude, - add_components!(comp, pulse.magnitude), - add_components!(comp, pulse.phase), - add_components!(comp, pulse.time), - pulse.delay, - pulse.frequency, - pulse.phase_offset - ]) - push!(res.content, join(values, " ")) - end - return res -end - -function parse_section(section::PulseqSection{:gradients}; shapes::Dict{Int, PulseqShape}, version::VersionNumber, kwargs...) - result = Dict{Int, PulseqGradient}() - for line in section.content - if version == v"1.3.1" - props = parse_pulseq_dict( - line, - [:id, :amp, :shape_id, :delay], - [Int, Float64, Int, Int] - ) - props[:time_id] = 0 - else - props = parse_pulseq_dict( - line, - [:id, :amp, :shape_id, :time_id, :delay], - [Int, Float64, Int, Int, Int] - ) - end - result[props[:id]] = PulseqGradient( - props[:amp], - _get_component(props[:shape_id], shapes), - _get_component(props[:time_id], shapes), - props[:delay] - ) - end - return result -end - -function gen_section(comp:: PulseqComponents, ::Val{:gradients}) - res = PulseqSection{:gradients}(String[]) - for (i, grad) in enumerate(comp.grads) - if !(grad isa PulseqGradient) - continue - end - values = string.(Any[ - i, - grad.amplitude, - add_components!(comp, grad.shape), - add_components!(comp, grad.time), - grad.delay, - ]) - push!(res.content, join(values, " ")) - end - return res -end - -function parse_section(section::PulseqSection{:trap}; kwargs...) - result = Dict{Int, PulseqTrapezoid}() - for line in section.content - props = parse_pulseq_dict( - line, - [:id, :amp, :rise, :flat, :fall, :delay], - [Int, Float64, Int, Int, Int, Int], - ) - result[props[:id]] = PulseqTrapezoid( - props[:amp], - props[:rise], - props[:flat], - props[:fall], - props[:delay], - ) - end - return result -end - -function gen_section(comp:: PulseqComponents, ::Val{:trap}) - res = PulseqSection{:trap}(String[]) - for (i, grad) in enumerate(comp.grads) - if !(grad isa PulseqTrapezoid) - continue - end - values = string.(Any[ - i, - grad.amplitude, - grad.rise, - grad.flat, - grad.fall, - grad.delay, - ]) - push!(res.content, join(values, " ")) - end - return res -end - -function parse_section(section::PulseqSection{:adc}; kwargs...) - result = Dict{Int, PulseqADC}() - for line in section.content - props = parse_pulseq_dict( - line, - [:id, :num, :dwell, :delay, :freq, :phase], - [Int, Int, Float64, Int, Float64, Float64], - ) - result[props[:id]] = PulseqADC( - props[:num], - props[:dwell], - props[:delay], - props[:freq], - props[:phase], - ) - end - return result -end - -function gen_section(comp:: PulseqComponents, ::Val{:adc}) - res = PulseqSection{:adc}(String[]) - for (i, adc) in enumerate(comp.adc) - values = string.(Any[ - i, - adc.num, - adc.dwell, - adc.delay, - adc.frequency, - adc.phase, - ]) - push!(res.content, join(values, " ")) - end - return res -end - -function parse_section(section::PulseqSection{:extensions}; kwargs...) - current_extension = -1 - pre_amble = true - linked_list = Dict{Int, NTuple{3, Int}}() - extensions = Dict{Int, PulseqExtension}() - for line in section.content - if startswith(line, "extension ") - pre_amble = false - (_, name, str_id) = split(line) - current_extension = int(str_id) - extensions[current_extension] = PulseqExtension(name, String[]) - elseif pre_ample - (id, type, ref, next) = int.(split(line)) - linked_list[id] = (type=type, ref=ref, next=next) - else - push!(extensions[current_extension], line) - end - end - - function get_extension_list(key::Int) - if iszero(key) - return Tuple{PulseqExtension, Int}[] - else - base = get_extension_list(linked_list[key].next) - pushfirst!(base, (extensions[linked_list[key].type], linked_list[key].ref)) - return base - end - end - - return Dict(key => get_extension_list(key) for key in keys(linked_list)) -end - -function parse_section(section::PulseqSection{:blocks}; version, rf=Dict(), gradients=Dict(), trap=Dict(), adc=Dict(), extensions=Dict(), delays=nothing, kwargs...) - all_grad = merge(gradients, trap) - - res = Vector{PulseqBlock}() - for line in section.content - props = parse_pulseq_dict( - line, - [:id, :duration, :rf, :gx, :gy, :gz, :adc, :ext], - fill(Int, 8), - ) - if version == v"1.3.1" - props[:duration] = iszero(props[:duration]) ? 0 : delays[props[:duration]] - end - @assert length(res) + 1 == props[:id] - push!(res, PulseqBlock( - props[:duration], - _get_component(props[:rf], rf), - _get_component(props[:gx], all_grad), - _get_component(props[:gy], all_grad), - _get_component(props[:gz], all_grad), - _get_component(props[:adc], adc), - iszero(props[:ext]) ? Tuple{PulseqExtension, Int}[] : _get_component(props[:ext], extensions), - )) - end - return res -end - -function gen_section(seq::PulseqSequence, comp:: PulseqComponents, ::Val{:blocks}) - res = PulseqSection{:blocks}(String[]) - - for (i, block) in enumerate(seq.blocks) - values = Any[i, block.duration] - for (search_vec, part) in [ - (comp.pulses, block.rf) - (comp.grads, block.gx) - (comp.grads, block.gy) - (comp.grads, block.gz) - (comp.adc, block.adc) - ] - push!(values, add_components!(comp, search_vec, part)) - end - push!(values, 0) - if length(block.ext) > 0 - error("Cannot write extensions yet.") - end - push!(res.content, join(string.(values), " ")) - end - return res -end - -""" - read_pulseq(IO) - -Reads a sequence from a pulseq file (http://pulseq.github.io/). -Pulseq files can be produced using matlab (http://pulseq.github.io/) or python (https://pypulseq.readthedocs.io/en/master/). -""" -function read_pulseq(io::IO) - sections = parse_pulseq_sections(io) - return parse_all_sections(sections) -end - -""" - write_pulseq(IO, sequence) - -Writes a sequence to an output IO file. -""" -function write_pulseq(io::IO, sequence::PulseqSequence) - if sequence.version < v"1.4" - error("Can only write to pulseq version 1.4 or later.") - end - sections = gen_all_sections(sequence) - for key in [:version, :definitions, :blocks, :rf, :gradients, :trap, :adc, :shapes] - if length(sections[key].content) == 0 - continue - end - write_pulseq_section(io, sections[key]) - end -end - - - -end \ No newline at end of file diff --git a/src/sequence_io/pulseq_io/basic_parsers.jl b/src/sequence_io/pulseq_io/basic_parsers.jl new file mode 100644 index 0000000..42c3761 --- /dev/null +++ b/src/sequence_io/pulseq_io/basic_parsers.jl @@ -0,0 +1,62 @@ +module BasicParsers + +""" + parse_pulseq_dict(line, names, dtypes) + +Parse a line of integers/floats with known names and dtypes. + +This is useful to parse most of the columnar data in Pulseq, such as in BLOCKS, RF, GRADIENTS, etc. +""" +function parse_pulseq_dict(line, names, dtypes) + parts = split(line) + @assert length(parts) == length(names) + values = parse.(dtypes, split(line)) + @assert names[1] == :id + return Dict(Symbol(name) => value for (name, value) in zip(names, values)) +end + +""" + parse_pulseq_properties(lines) + +Parse any `pulseq` section formatted as: +``` +<name> <value> +<name2> <value2> +... +``` + +This includes the VERSION, DEFINITIONS, and part of the SHAPES +""" +function parse_pulseq_properties(strings::Vector{<:AbstractString}) + result = Dict{String, Any}() + for s in strings + (name, value) = split(s, limit=2) + result[name] = parse_value(value) + end + return result +end + +""" + parse_value(string) + +Tries to value the `string` as a number of sequence of numbers. +If that does not work, the `string` is returned. +""" +function parse_value(value::AbstractString) + for t in (Int, Float64) + try + return parse(t, value) + catch + end + end + for t in (Int, Float64) + try + return parse.(t, split(value)) + catch + end + end + return value +end + + +end \ No newline at end of file diff --git a/src/sequence_io/pulseq_io/components.jl b/src/sequence_io/pulseq_io/components.jl new file mode 100644 index 0000000..cd5b38b --- /dev/null +++ b/src/sequence_io/pulseq_io/components.jl @@ -0,0 +1,115 @@ +"""Define a list of all the [`PulseqShape`](@ref) and [`PulseqComponent`](@ref) that are used in a [`PulseqSequence`](@ref).""" +module Components + +import ..Types: AnyPulseqComponent, PulseqShape, PulseqRFPulse, AnyPulseqGradient, PulseqADC, PulseqExtension, PulseqSequence + +""" + PulseqComponents(shapes, pulses, grads, adcs, extensions) + +All the shapes, pulses, grads, adcs, and extensions used in a [`PulseqSequence`](@ref). + +They can be provided as a dictionary from the integer ID to the object or as a vector. +""" +struct PulseqComponents + shapes:: Dict{Int, PulseqShape} + pulses:: Dict{Int, PulseqRFPulse} + grads:: Dict{Int, AnyPulseqGradient} + adc:: Dict{Int, PulseqADC} + extensions:: Dict{Int, PulseqExtension} + PulseqComponents(shapes, pulses, grads, adc, extensions) = new( + _convert_to_dict(shapes, PulseqShape), + _convert_to_dict(pulses, PulseqRFPulse), + _convert_to_dict(grads, AnyPulseqGradient), + _convert_to_dict(adc, PulseqADC), + _convert_to_dict(extensions, PulseqExtension), + ) +end + +_convert_to_dict(d::Dict{Int, Any}, ::Type{T}) where {T} = Dict(Int, T)(d) +_convert_to_dict(d::Dict{Int, T}, ::Type{T}) where {T} = d +_convert_to_dict(vec::Vector, ::Type{T}) where {T} = Dict{Int, T}(i => v for (i, v) in enumerate(vec)) + + +""" + PulseqComponents(sequence::PulseqSequence) + +Indentifies and lists all the unique components in the sequence. +""" +function PulseqComponents(sequence::PulseqSequence) + +end + +PulseqComponents() = PulseqComponents( + PulseqShape[], + PulseqRFPulse[], + AnyPulseqGradient[], + PulseqADC[], + PulseqExtension[], +) + +""" + add_components(comp::PulseqComponents, search_vec::Vector, component) + add_components(comp::PulseqComponents, shape::PulseqShape) + +Adds a component to the [`search_vec`](@ref), which is assumed to be the appropriate vector within the `comp`. + +It will check whether the `component` is already part of the `search_vec` before adding it. +The integer ID of the position of the `component` in `search_vec` is returned. + +0 is returned if `component` is `nothing`. +""" +add_components!(::PulseqComponents, search_vec::Dict{Int, <:Any}, ::Nothing) = 0 +function add_components!(comp::PulseqComponents, search_vec::Dict{Int, <:T}, component::T) where {T <: AnyPulseqComponent} + for (i, c) in search_vec + if same_component(comp, c, component) + return i + end + end + search_vec[length(search_vec) + 1] = component + return length(search_vec) +end + +""" + same_component(comp::PulseqComponents, a, b) + +Check whether components `a` and `b` are the same when using the shapes represented in `comp`. +""" +same_component(::PulseqComponents, ::Any, ::Any) = false +function same_component(comp::PulseqComponents, a::T, b::T) where {T} + for name in fieldnames(T) + v1 = getfield(a, name) + v2 = getfield(b, name) + + if v1 isa PulseqShape + v1 = add_components!(comp, v1) + end + if v2 isa PulseqShape + v2 = add_components!(comp, v2) + end + if v1 != v2 + return false + end + end + return true +end + + +add_components!(comp::PulseqComponents, ::Nothing) = 0 +function add_components!(comp::PulseqComponents, shape::PulseqShape) + for (i, s) in comp.shapes + if same_shape(s, shape) + return i + end + end + comp.shapes[length(comp.shapes) + 1] = shape + return length(comp.shapes) +end + +""" + same_component(a, b) + +Check whether shapes `a` and `b` are the same. +""" +same_shape(shape1::PulseqShape, shape2::PulseqShape) = length(shape1.samples) == length(shape2.samples) && all(shape1.samples .≈ shape2.samples) + +end \ No newline at end of file diff --git a/src/sequence_io/pulseq_io/parse_sections.jl b/src/sequence_io/pulseq_io/parse_sections.jl new file mode 100644 index 0000000..1c3f86b --- /dev/null +++ b/src/sequence_io/pulseq_io/parse_sections.jl @@ -0,0 +1,45 @@ +""" +Translate between sets of [`PulseqSection`](@ref) objects and [`PulseqSequence`](@ref). +""" +module ParseSections + +import ..Types: PulseqSequence, PulseqSection +import ..Components: PulseqComponents +import ..Parsers: parse_section, gen_section + +""" + parse_all_sections(sections) + +Parses the sections read from a Pulseq file. + +Returns a [`PulseqSequence`](@ref) +""" +function parse_all_sections(sections:: Dict{String, PulseqSection}) + sections = copy(sections) + all_parts = Dict{Symbol, Any}() + for name in ["version", "definitions", "delays", "shapes", "rf", "gradients", "trap", "adc", "extensions", "blocks"] + if name in keys(sections) + section = pop!(sections, name) + all_parts[Symbol(name)] = parse_section(section; all_parts...) + end + end + if length(sections) > 0 + @warn "Following sections in pulseq input file are not being used: $(keys(sections))" + end + return PulseqSequence(all_parts[:version], all_parts[:definitions], all_parts[:blocks]) +end + +function gen_all_sections(seq:: PulseqSequence) + sections = Dict{Symbol, PulseqSection}() + sections[:version] = gen_section(seq, Val(:version)) + sections[:definitions] = gen_section(seq, Val(:definitions)) + + comp = PulseqComponents() + sections[:blocks] = gen_section(seq, comp, Val(:blocks)) + for symbol in [:rf, :gradients, :trap, :adc, :shapes] + sections[symbol] = gen_section(comp, Val(symbol)) + end + return sections +end + +end \ No newline at end of file diff --git a/src/sequence_io/pulseq_io/parsers/adc.jl b/src/sequence_io/pulseq_io/parsers/adc.jl new file mode 100644 index 0000000..eebeab8 --- /dev/null +++ b/src/sequence_io/pulseq_io/parsers/adc.jl @@ -0,0 +1,34 @@ +function parse_section(section::PulseqSection{:adc}; kwargs...) + result = Dict{Int, PulseqADC}() + for line in section.content + props = parse_pulseq_dict( + line, + [:id, :num, :dwell, :delay, :freq, :phase], + [Int, Int, Float64, Int, Float64, Float64], + ) + result[props[:id]] = PulseqADC( + props[:num], + props[:dwell], + props[:delay], + props[:freq], + props[:phase], + ) + end + return result +end + +function gen_section(comp:: PulseqComponents, ::Val{:adc}) + res = PulseqSection{:adc}(String[]) + for (i, adc) in comp.adc + values = string.(Any[ + i, + adc.num, + adc.dwell, + adc.delay, + adc.frequency, + adc.phase, + ]) + push!(res.content, join(values, " ")) + end + return res +end \ No newline at end of file diff --git a/src/sequence_io/pulseq_io/parsers/blocks.jl b/src/sequence_io/pulseq_io/parsers/blocks.jl new file mode 100644 index 0000000..f22f893 --- /dev/null +++ b/src/sequence_io/pulseq_io/parsers/blocks.jl @@ -0,0 +1,49 @@ +function parse_section(section::PulseqSection{:blocks}; version, rf=Dict(), gradients=Dict(), trap=Dict(), adc=Dict(), extensions=Dict(), delays=nothing, kwargs...) + all_grad = merge(gradients, trap) + + res = Vector{PulseqBlock}() + for line in section.content + props = parse_pulseq_dict( + line, + [:id, :duration, :rf, :gx, :gy, :gz, :adc, :ext], + fill(Int, 8), + ) + if version == v"1.3.1" + props[:duration] = iszero(props[:duration]) ? 0 : delays[props[:duration]] + end + @assert length(res) + 1 == props[:id] + push!(res, PulseqBlock( + props[:duration], + _get_component(props[:rf], rf), + _get_component(props[:gx], all_grad), + _get_component(props[:gy], all_grad), + _get_component(props[:gz], all_grad), + _get_component(props[:adc], adc), + iszero(props[:ext]) ? Tuple{PulseqExtension, Int}[] : _get_component(props[:ext], extensions), + )) + end + return res +end + +function gen_section(seq::PulseqSequence, comp:: PulseqComponents, ::Val{:blocks}) + res = PulseqSection{:blocks}(String[]) + + for (i, block) in enumerate(seq.blocks) + values = Any[i, block.duration] + for (search_vec, part) in [ + (comp.pulses, block.rf) + (comp.grads, block.gx) + (comp.grads, block.gy) + (comp.grads, block.gz) + (comp.adc, block.adc) + ] + push!(values, add_components!(comp, search_vec, part)) + end + push!(values, 0) + if length(block.ext) > 0 + error("Cannot write extensions yet.") + end + push!(res.content, join(string.(values), " ")) + end + return res +end \ No newline at end of file diff --git a/src/sequence_io/pulseq_io/parsers/definitions.jl b/src/sequence_io/pulseq_io/parsers/definitions.jl new file mode 100644 index 0000000..2da9962 --- /dev/null +++ b/src/sequence_io/pulseq_io/parsers/definitions.jl @@ -0,0 +1,13 @@ +function parse_section(section:: PulseqSection{:definitions}; kwargs...) + props = parse_pulseq_properties(section.content) + return NamedTuple(Symbol(key) => value for (key, value) in props) +end + +_to_string_value(value::AbstractString) = value +_to_string_value(value::Number) = string(value) +_to_string_value(value::Vector) = join(_to_string_value.(value), " ") + +function gen_section(seq:: PulseqSequence, ::Val{:definitions}) + definitions = seq.definitions + return PulseqSection{:definitions}(["$(key) $(_to_string_value(value))" for (key, value) in pairs(definitions)]) +end \ No newline at end of file diff --git a/src/sequence_io/pulseq_io/parsers/delays.jl b/src/sequence_io/pulseq_io/parsers/delays.jl new file mode 100644 index 0000000..53992db --- /dev/null +++ b/src/sequence_io/pulseq_io/parsers/delays.jl @@ -0,0 +1,11 @@ +function parse_section(section::PulseqSection{:delays}; version, kwargs...) + if version > v"1.3.1" + error("Did not expect a [DELAYS] section in pulseq file with version $(version)") + end + delays = Dict{Int, Int}() + for line in section.content + (id, delay) = parse.(Int, split(line)) + delays[id] = delay + end + return delays +end \ No newline at end of file diff --git a/src/sequence_io/pulseq_io/parsers/extensions.jl b/src/sequence_io/pulseq_io/parsers/extensions.jl new file mode 100644 index 0000000..e75298c --- /dev/null +++ b/src/sequence_io/pulseq_io/parsers/extensions.jl @@ -0,0 +1,31 @@ +function parse_section(section::PulseqSection{:extensions}; kwargs...) + current_extension = -1 + pre_amble = true + linked_list = Dict{Int, NTuple{3, Int}}() + extensions = Dict{Int, PulseqExtension}() + for line in section.content + if startswith(line, "extension ") + pre_amble = false + (_, name, str_id) = split(line) + current_extension = int(str_id) + extensions[current_extension] = PulseqExtension(name, String[]) + elseif pre_ample + (id, type, ref, next) = int.(split(line)) + linked_list[id] = (type=type, ref=ref, next=next) + else + push!(extensions[current_extension], line) + end + end + + function get_extension_list(key::Int) + if iszero(key) + return Tuple{PulseqExtension, Int}[] + else + base = get_extension_list(linked_list[key].next) + pushfirst!(base, (extensions[linked_list[key].type], linked_list[key].ref)) + return base + end + end + + return Dict(key => get_extension_list(key) for key in keys(linked_list)) +end \ No newline at end of file diff --git a/src/sequence_io/pulseq_io/parsers/gradients.jl b/src/sequence_io/pulseq_io/parsers/gradients.jl new file mode 100644 index 0000000..67a1cc9 --- /dev/null +++ b/src/sequence_io/pulseq_io/parsers/gradients.jl @@ -0,0 +1,44 @@ +function parse_section(section::PulseqSection{:gradients}; shapes::Dict{Int, PulseqShape}, version::VersionNumber, kwargs...) + result = Dict{Int, PulseqGradient}() + for line in section.content + if version == v"1.3.1" + props = parse_pulseq_dict( + line, + [:id, :amp, :shape_id, :delay], + [Int, Float64, Int, Int] + ) + props[:time_id] = 0 + else + props = parse_pulseq_dict( + line, + [:id, :amp, :shape_id, :time_id, :delay], + [Int, Float64, Int, Int, Int] + ) + end + result[props[:id]] = PulseqGradient( + props[:amp], + _get_component(props[:shape_id], shapes), + _get_component(props[:time_id], shapes), + props[:delay] + ) + end + return result +end + +function gen_section(comp:: PulseqComponents, ::Val{:gradients}) + res = PulseqSection{:gradients}(String[]) + for (i, grad) in comp.grads + if !(grad isa PulseqGradient) + continue + end + values = string.(Any[ + i, + grad.amplitude, + add_components!(comp, grad.shape), + add_components!(comp, grad.time), + grad.delay, + ]) + push!(res.content, join(values, " ")) + end + return res +end \ No newline at end of file diff --git a/src/sequence_io/pulseq_io/parsers/parsers.jl b/src/sequence_io/pulseq_io/parsers/parsers.jl new file mode 100644 index 0000000..a047f49 --- /dev/null +++ b/src/sequence_io/pulseq_io/parsers/parsers.jl @@ -0,0 +1,37 @@ +module Parsers +import ..Types: PulseqSection, PulseqSequence, PulseqRFPulse, PulseqGradient, PulseqTrapezoid, PulseqExtensionDefinition, PulseqExtension, PulseqADC, PulseqBlock, PulseqShape +import ..Components: PulseqComponents, add_components! +import ..BasicParsers: parse_pulseq_dict, parse_pulseq_properties + +""" + parse_section(section) + +Parses any [`PulseqSection`](@ref) and return the appropriate type. + +The opposite is [`gen_section`](@ref). +""" +function parse_section end + + +""" + gen_section(sequence, Val(:<title>)) + +Creates a specific [`PulseqSection`](@ref){<title>} from a part of the PulseqSequence. + +This is the opposite of [`parse_section`](@ref) +""" +function gen_section end + +_get_component(id::Int, shapes::Dict) = iszero(id) ? nothing : shapes[id] + +include("version.jl") +include("definitions.jl") +include("delays.jl") +include("shapes.jl") +include("rf.jl") +include("gradients.jl") +include("trapezoids.jl") +include("adc.jl") +include("extensions.jl") +include("blocks.jl") +end \ No newline at end of file diff --git a/src/sequence_io/pulseq_io/parsers/rf.jl b/src/sequence_io/pulseq_io/parsers/rf.jl new file mode 100644 index 0000000..c864259 --- /dev/null +++ b/src/sequence_io/pulseq_io/parsers/rf.jl @@ -0,0 +1,47 @@ +function parse_section(section::PulseqSection{:rf}; shapes::Dict{Int, PulseqShape}, version, kwargs...) + result = Dict{Int, PulseqRFPulse}() + for line in section.content + if version == v"1.3.1" + props = parse_pulseq_dict( + line, + [:id, :amp, :mag_id, :phase_id, :delay, :freq, :phase], + [Int, Float64, Int, Int, Int, Float64, Float64], + ) + props[:time_id] = 0 + else + props = parse_pulseq_dict( + line, + [:id, :amp, :mag_id, :phase_id, :time_id, :delay, :freq, :phase], + [Int, Float64, Int, Int, Int, Int, Float64, Float64], + ) + end + result[props[:id]] = PulseqRFPulse( + props[:amp], + _get_component(props[:mag_id], shapes), + _get_component(props[:phase_id], shapes), + _get_component(props[:time_id], shapes), + props[:delay], + props[:freq], + props[:phase], + ) + end + return result +end + +function gen_section(comp:: PulseqComponents, ::Val{:rf}) + res = PulseqSection{:rf}(String[]) + for (i, pulse) in comp.pulses + values = string.(Any[ + i, + pulse.amplitude, + add_components!(comp, pulse.magnitude), + add_components!(comp, pulse.phase), + add_components!(comp, pulse.time), + pulse.delay, + pulse.frequency, + pulse.phase_offset + ]) + push!(res.content, join(values, " ")) + end + return res +end \ No newline at end of file diff --git a/src/sequence_io/pulseq_io/parsers/shapes.jl b/src/sequence_io/pulseq_io/parsers/shapes.jl new file mode 100644 index 0000000..6d523df --- /dev/null +++ b/src/sequence_io/pulseq_io/parsers/shapes.jl @@ -0,0 +1,68 @@ +struct CompressedPulseqShape + num :: Int + samples :: Vector{Float64} +end + + +function parse_section(section:: PulseqSection{:shapes}; kwargs...) + current_id = -1 + shapes = Dict{Int, CompressedPulseqShape}() + for line in section.content + if startswith(lowercase(line), "shape_id") + current_id = parse(Int, line[9:end]) + continue + end + for text in ("num_uncompressed", "num_samples") + if startswith(lowercase(line), text) + @assert current_id != -1 + if current_id in keys(shapes) + error("Multiple shapes with the same ID detected.") + end + shapes[current_id] = CompressedPulseqShape(parse(Int, line[length(text)+1:end]), Float64[]) + end + end + if !startswith(lowercase(line), "num") + push!(shapes[current_id].samples, parse(Float64, line)) + end + end + return Dict(key => uncompress(shape) for (key, shape) in shapes) +end + +function uncompress(compressed::CompressedPulseqShape) + if compressed.num == length(compressed.samples) + # not actually compressed + return PulseqShape(compressed.samples) + end + amplitudes = [compressed.samples[1]] + repeating = false + prev_sample = compressed.samples[1] + for sample in compressed.samples[2:end] + if repeating + for _ in 1:Int(sample) + push!(amplitudes, prev_sample) + end + repeating = false + else + push!(amplitudes, sample) + if sample == prev_sample + repeating = true + end + end + end + return PulseqShape(amplitudes) +end + +function gen_section(comp:: PulseqComponents, ::Val{:shapes}) + res = PulseqSection{:shapes}(String[]) + for (index, shape) in comp.shapes + append!(res.content, [ + "", + "shape_id $index", + "num_samples $(length(shape.samples))" + ]) + for sample in shape.samples + push!(res.content, string(sample)) + end + end + return res +end \ No newline at end of file diff --git a/src/sequence_io/pulseq_io/parsers/trapezoids.jl b/src/sequence_io/pulseq_io/parsers/trapezoids.jl new file mode 100644 index 0000000..990d1b7 --- /dev/null +++ b/src/sequence_io/pulseq_io/parsers/trapezoids.jl @@ -0,0 +1,37 @@ +function parse_section(section::PulseqSection{:trap}; kwargs...) + result = Dict{Int, PulseqTrapezoid}() + for line in section.content + props = parse_pulseq_dict( + line, + [:id, :amp, :rise, :flat, :fall, :delay], + [Int, Float64, Int, Int, Int, Int], + ) + result[props[:id]] = PulseqTrapezoid( + props[:amp], + props[:rise], + props[:flat], + props[:fall], + props[:delay], + ) + end + return result +end + +function gen_section(comp:: PulseqComponents, ::Val{:trap}) + res = PulseqSection{:trap}(String[]) + for (i, grad) in comp.grads + if !(grad isa PulseqTrapezoid) + continue + end + values = string.(Any[ + i, + grad.amplitude, + grad.rise, + grad.flat, + grad.fall, + grad.delay, + ]) + push!(res.content, join(values, " ")) + end + return res +end diff --git a/src/sequence_io/pulseq_io/parsers/version.jl b/src/sequence_io/pulseq_io/parsers/version.jl new file mode 100644 index 0000000..d70ec1a --- /dev/null +++ b/src/sequence_io/pulseq_io/parsers/version.jl @@ -0,0 +1,13 @@ +function parse_section(section:: PulseqSection{:version}; kwargs...) + props = parse_pulseq_properties(section.content) + return VersionNumber(props["major"], props["minor"], props["revision"]) +end + +function gen_section(seq::PulseqSequence, ::Val{:version}) + version = seq.version + return PulseqSection{:version}([ + "major $(version.major)", + "minor $(version.minor)", + "revision $(version.patch)", + ]) +end \ No newline at end of file diff --git a/src/sequence_io/pulseq_io/pulseq_io.jl b/src/sequence_io/pulseq_io/pulseq_io.jl new file mode 100644 index 0000000..d60141e --- /dev/null +++ b/src/sequence_io/pulseq_io/pulseq_io.jl @@ -0,0 +1,46 @@ +""" +Stand-alone module that reads/writes Pulseq files. + +The pulseq files are read into or written from a set of types that closely match the Pulseq file format. +The translation of these types into MRIBuilder types is defined in "../pulseq.jl" (i.e., `MRIBuilder.SequenceIO.Pulseq`) +""" +module PulseqIO +include("types.jl") +include("basic_parsers.jl") +include("sections_io.jl") +include("components.jl") +include("parsers/parsers.jl") +include("parse_sections.jl") + + + +""" + read_pulseq(IO) + +Reads a sequence from a pulseq file (http://pulseq.github.io/). +Pulseq files can be produced using matlab (http://pulseq.github.io/) or python (https://pypulseq.readthedocs.io/en/master/). +""" +function read_pulseq(io::IO) + sections = SectionsIO.parse_pulseq_sections(io) + return ParseSections.parse_all_sections(sections) +end + +""" + write_pulseq(IO, sequence) + +Writes a sequence to an output IO file. +""" +function write_pulseq(io::IO, sequence::Types.PulseqSequence) + if sequence.version < v"1.4" + error("Can only write to pulseq version 1.4 or later.") + end + sections = ParseSections.gen_all_sections(sequence) + for key in [:version, :definitions, :blocks, :rf, :gradients, :trap, :adc, :shapes] + if length(sections[key].content) == 0 + continue + end + SectionsIO.write_pulseq_section(io, sections[key]) + end +end + +end \ No newline at end of file diff --git a/src/sequence_io/pulseq_io/sections_io.jl b/src/sequence_io/pulseq_io/sections_io.jl new file mode 100644 index 0000000..96616c1 --- /dev/null +++ b/src/sequence_io/pulseq_io/sections_io.jl @@ -0,0 +1,49 @@ +"""Define IO for [`PulseqSection`](@ref).""" +module SectionsIO +import ..Types: PulseqSection + +""" + parse_pulseq_sections(io) + +Reads a Pulseq file into a dictionary of [`PulseqSection`](@ref) objects. +""" +function parse_pulseq_sections(io::IO) + sections = Dict{String, PulseqSection}() + current_title = "" + for line in readlines(io) + line = strip(line) + if length(line) == 0 || line[1] == '#' + continue # ignore comments + end + if line[1] == '[' && line[end] == ']' + # new section starts + current_title = lowercase(line[2:end-1]) + sections[current_title] = PulseqSection{Symbol(current_title)}(String[]) + elseif length(current_title) > 0 + push!(sections[current_title].content, line) + else + error("Content found in pulseq file before first section") + end + end + return sections +end + +""" + write_pulseq_section(io, section::PulseqSection) + +Writes a Pulseq `section` to the `IO`. +""" +function write_pulseq_section(io::IO, section::PulseqSection{T}) where {T} + title = uppercase(string(T)) + write(io, "[$title]\n") + for line in section.content + if iszero(length(line)) || line[end] != '\n' + line = line * '\n' + end + write(io, line) + end + write(io, "\n") + write(io, "\n") +end + +end \ No newline at end of file diff --git a/src/sequence_io/pulseq_io/types.jl b/src/sequence_io/pulseq_io/types.jl new file mode 100644 index 0000000..3aa4089 --- /dev/null +++ b/src/sequence_io/pulseq_io/types.jl @@ -0,0 +1,143 @@ +""" +Define the main types forming a [`PulseqSequence`](@ref). + +Extensions and sections types are defined in their own modules. +""" +module Types + +""" + PulseqSection(:<title>)(lines) + +Represents a section in the pulseq file format. +""" +struct PulseqSection{T} + content :: Vector{String} +end + +""" + PulseqShape(samples) + +Define the shape of a [`PulseqRFPulse`](@ref) or [`PulseqGradient`](@ref). +""" +struct PulseqShape + samples :: Vector{Float64} +end + +""" +Super-type for any RF pulses/gradients/ADC/extensions that can play out during a [`PulseqBlock`](@ref). +""" +abstract type AnyPulseqComponent end + + +""" + PulseqRFPulse(amplitude::Number, magnitude::PulseqShape, phase::PulseqShape, time::PulseqShape, delay::Int, frequency::Number, phase_offset::Number) + +An RF pulse defined in Pulseq (see [specification](https://raw.githubusercontent.com/pulseq/pulseq/master/doc/specification.pdf)). +""" +struct PulseqRFPulse <: AnyPulseqComponent + amplitude :: Float64 + magnitude :: PulseqShape + phase :: PulseqShape + time :: Union{Nothing, PulseqShape} + delay :: Int + frequency :: Float64 + phase_offset :: Float64 +end + +""" +Super-type of Pulseq gradients: +- [`PulseqGradient`](@ref) +- [`PulseqTrapezoid`](@ref) +""" +abstract type AnyPulseqGradient <: AnyPulseqComponent end + +""" + PulseqGradient(amplitude::Number, shape::PulseqShape, time::PulseqShape, delay::int) + +A generic gradient waveform defined in Pulseq (see [specification](https://raw.githubusercontent.com/pulseq/pulseq/master/doc/specification.pdf)). +""" +struct PulseqGradient <: AnyPulseqGradient + amplitude :: Float64 + shape :: PulseqShape + time :: Union{Nothing, PulseqShape} + delay :: Int +end + +""" + PulseqTrapezoid(amplitude::Number, rise::Int, flat::Int, fall::Int, delay::Int) + +A trapezoidal gradient pulse defined in Pulseq (see [specification](https://raw.githubusercontent.com/pulseq/pulseq/master/doc/specification.pdf)). +""" +struct PulseqTrapezoid <:AnyPulseqGradient + amplitude :: Float64 + rise :: Int + flat :: Int + fall :: Int + delay :: Int +end + +""" + PulseqADC(num::Int, dwell::Float64, delay::Int, frequency::Number, phase::Number) + +A trapezoidal gradient pulse defined in Pulseq (see [specification](https://raw.githubusercontent.com/pulseq/pulseq/master/doc/specification.pdf)). +""" +struct PulseqADC <: AnyPulseqComponent + num :: Int + dwell :: Float64 + delay :: Int + frequency :: Float64 + phase :: Float64 +end + +""" + PulseqExtensionDefinition(name, content) + +Abstract definition of an unknown Pulseq extension. +""" +struct PulseqExtensionDefinition + name :: String + content :: Vector{String} +end + +""" + PulseqExtension(definition::PulseqExtensionDefinition, id::Int) + +Reference to a specific implementation of the [`PulseqExtensionDefinition`](@ref). +""" +struct PulseqExtension <: AnyPulseqComponent + definition::PulseqExtensionDefinition + id :: Int +end + +""" + PulseqBlock(duration::Int, rf::PulseqRFPulse, gx::AnyPulseqGradient, gy::AnyPulseqGradient, gz::AnyPulseqGradient, adc::PulseqADC, ext) + +Defines a Building Block with the Pulseq sequence (see [specification](https://raw.githubusercontent.com/pulseq/pulseq/master/doc/specification.pdf)). + +The RF pulse, gradients, and ADC can be set to `nothing`. + +The `ext` is a sequence of extension blocks that will be played out. +Set this to a sequence of zero length to not have any extensions. +""" +struct PulseqBlock + duration :: Int + rf :: Union{Nothing, PulseqRFPulse} + gx :: Union{Nothing, AnyPulseqGradient} + gy :: Union{Nothing, AnyPulseqGradient} + gz :: Union{Nothing, AnyPulseqGradient} + adc :: Union{Nothing, PulseqADC} + ext :: Vector{<:Tuple{PulseqExtension, Int}} +end + +""" + PulseqSequence(version::VersionNumber, definitions::NamedTuple, blocks::Vector{PulseqBlock}) + +A full sequence defined according to the Pulseq [specification](https://raw.githubusercontent.com/pulseq/pulseq/master/doc/specification.pdf). +""" +struct PulseqSequence + version:: VersionNumber + definitions:: NamedTuple + blocks:: Vector{PulseqBlock} +end + +end \ No newline at end of file diff --git a/src/sequence_io/sequence_io.jl b/src/sequence_io/sequence_io.jl index 7bd11d6..a451ff6 100644 --- a/src/sequence_io/sequence_io.jl +++ b/src/sequence_io/sequence_io.jl @@ -1,8 +1,8 @@ module SequenceIO -include("pulseq.jl") +include("pulseq_io/pulseq_io.jl") -import .Pulseq: read_pulseq +import .PulseqIO: read_pulseq import Serialization: serialize, deserialize -- GitLab