From 14387e2115674023a704472a818990e160cd6c38 Mon Sep 17 00:00:00 2001
From: Michiel Cottaar <MichielCottaar@protonmail.com>
Date: Thu, 16 May 2024 15:15:48 +0100
Subject: [PATCH] Read pulseq files into pulseq types

---
 src/sequence_io/pulseq.jl | 719 ++++++++++++++++++++++++--------------
 1 file changed, 463 insertions(+), 256 deletions(-)

diff --git a/src/sequence_io/pulseq.jl b/src/sequence_io/pulseq.jl
index 79dd59d..95e0943 100644
--- a/src/sequence_io/pulseq.jl
+++ b/src/sequence_io/pulseq.jl
@@ -1,318 +1,525 @@
 module Pulseq
-import ...Variables: duration
-import ...Scanners: Scanner
-import ...Components: GenericPulse, GradientWaveform, ADC
-import ...Containers: BuildingBlock, Sequence, Wait
-import DataStructures: OrderedDict
-import Interpolations: linear_interpolation, Flat
-import StaticArrays: SVector
-
-struct PulseqSection
-    title :: String
-    content :: Vector{String}
+
+
+"""
+    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
 
-struct CompressedPulseqShape
-    id :: Int
-    num :: Int
-    samples :: Vector{Float64}
+"""
+    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)
+    write_section.(io, sections)
 end
 
 
 """
-    control_points(shape::CompressedPulseqShape)
+    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>
+...
+```
 
-Returns a tuple with:
-- vector of times
-- vector of amplitudes
+This includes the VERSION, DEFINITIONS, and part of the SHAPES
 """
-function control_points(pulseq::CompressedPulseqShape)
-    compressed = length(pulseq.samples) != pulseq.num
-    if !compressed
-        times = range(0, 1, length=pulseq.num)
-        return (times, pulseq.samples)
+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
-    times = [zero(Float64)]
-    amplitudes = [pulseq.samples[1]]
-    repeating = false
-    time_norm = 1 / (pulseq.num - 1)
-    prev_sample = pulseq.samples[1]
-    prev_applied = true
-    for sample in pulseq.samples[2:end]
-        if repeating
-            nrepeats = Int(sample) + 2
-            if prev_applied
-                nrepeats -= 1
-            end
-            push!(times, times[end] + nrepeats * time_norm)
-            push!(amplitudes, amplitudes[end] + nrepeats * prev_sample)
-            repeating = false
-            prev_applied = true
-        elseif sample == prev_sample
-            repeating = true
-        else
-            if !prev_applied
-                push!(times, times[end] + time_norm)
-                push!(amplitudes, amplitudes[end] + prev_sample)
-            end
-            prev_sample = sample
-            prev_applied = false
+    return result
+end
+
+function _parse_value(value::AbstractString)
+    for t in (Int, Float64)
+        try
+            return parse(t, value)
+        catch
         end
     end
-    if !prev_applied
-        push!(times, times[end] + time_norm)
-        push!(amplitudes, amplitudes[end] + prev_sample)
+    for t in (Int, Float64)
+        try
+            return parse.(t, split(value))
+        catch
+        end
     end
-    return (times, amplitudes)
+    return value
 end
 
+
+
 """
-    read_pulseq(IO; scanner=Scanner(B0=B0), B0=3., TR=<sequence duration>)
+    PulseqSection(:<title>)(lines)
 
-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/).
+Represents a section in the pulseq file format.
 """
-function read_pulseq(io::IO; kwargs...)
-    keywords = read_pulseq_sections(io)
-    return build_sequence(; kwargs..., keywords...)
+struct PulseqSection{T}
+    content :: Vector{String}
 end
 
-function read_pulseq_sections(io::IO)
-    sections = PulseqSection[]
-    version = nothing
-    title = ""
+"""
+    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] == ']'
-            if title == "VERSION"
-                version = parse_pulseq_section(sections[end]).second
-            end
             # new section starts
-            title = line[2:end-1]
-            push!(sections, PulseqSection(title, String[]))
-        elseif length(sections) > 0
-            push!(sections[end].content, line)
+            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
-    Dict(filter(pair -> !isnothing(pair), parse_pulseq_section.(sections, version))...)
+    return sections
 end
 
-function parse_pulseq_ordered_dict(strings::Vector{<:AbstractString}, names, dtypes)
-    as_dict = OrderedDict{Int, NamedTuple{Tuple(names), Tuple{dtypes...}}}()
-    for line in strings
-        parts = split(line)
-        @assert length(parts) == length(names)
-        values = parse.(dtypes, split(line))
-        @assert names[1] == :id
-        as_dict[values[1]] = (; zip(names, values)...)
-    end
-    return as_dict
+# Writing pulseq files
+
+struct PulseqShape
+    samples :: Vector{Float64}
 end
 
-function parse_pulseq_properties(strings::Vector{<:AbstractString})
-    result = Dict{String, Any}()
-    for s in strings
-        (name, value) = split(s, limit=2)
-        result[name] = value
+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
+    nsamples :: Int
+    dwell_time :: 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
+
+
+
+# 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
-    return result
+    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
 
-section_headers = Dict(
-    "BLOCKS" => ([:id, :duration, :rf, :gx, :gy, :gz, :adc, :ext], fill(Int, 8)),
-    ("BLOCKS", v"1.3.1") => ([:id, :delay, :rf, :gx, :gy, :gz, :adc, :ext], fill(Int, 8)),
-    ("DELAYS", v"1.3.1") => ([:id, :delay], [Int, Int]),
-    ("RF", v"1.3.1") => ([:id, :amp, :mag_id, :phase_id, :delay, :freq, :phase], [Int, Float64, Int, Int, Int, Float64, Float64]),
-    "RF" => ([:id, :amp, :mag_id, :phase_id, :time_id, :delay, :freq, :phase], [Int, Float64, Int, Int, Int, Int, Float64, Float64]),
-    ("GRADIENTS", v"1.3.1") => ([:id, :amp, :shape_id, :delay], [Int, Float64, Int, Int]),
-    "GRADIENTS" => ([:id, :amp, :shape_id, :time_id, :delay], [Int, Float64, Int, Int, Int]),
-    "TRAP" => ([:id, :amp, :rise, :flat, :fall, :delay], [Int, Float64, Int, Int, Int, Int]),
-    "ADC" => ([:id, :num, :dwell, :delay, :freq, :phase], [Int, Int, Float64, Int, Float64, Float64]),
-)
-
-function parse_pulseq_section(section::PulseqSection, version=nothing)
-    if section.title == "VERSION"
-        props = parse_pulseq_properties(section.content)
-        result = VersionNumber(
-            parse(Int, props["major"]),
-            parse(Int, props["minor"]),
-            parse(Int, props["revision"]),
-        )
-    elseif section.title == "DEFINITIONS"
-        result = parse_pulseq_properties(section.content)
-    elseif (section.title, version) in keys(section_headers)
-        result = parse_pulseq_ordered_dict(section.content, section_headers[(section.title, version)]...)
-    elseif section.title in keys(section_headers)
-        result = parse_pulseq_ordered_dict(section.content, section_headers[section.title]...)
-    elseif section.title == "EXTENSION"
-        println("Ignoring all extensions in pulseq")
-    elseif section.title == "SHAPES"
-        current_id = -1
-        shapes = CompressedPulseqShape[]
-        for line in section.content
-            if length(line) > 8 && lowercase(line[1:8]) == "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
-                    push!(shapes, CompressedPulseqShape(current_id, parse(Int, line[length(text)+1:end]), Float64[]))
-                    current_id = -1
-                    break
+function gen_all_sections(seq:: PulseqSequence)
+    sections = [gen_section(seq, Val(symbol)) for symbol in [:version, :definitions, :shapes, :rf, :gradients, :trap, :adc, :extensions, :blocks]]
+    return [section for section in sections if length(section.content) > 0]
+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
-            end
-            if !startswith(lowercase(line), "num")
-                @assert current_id == -1
-                push!(shapes[end].samples, parse(Float64, line))
+                shapes[current_id] = CompressedPulseqShape(parse(Int, line[length(text)+1:end]), Float64[])
             end
         end
-        result = Dict(
-            [s.id => (s.num, control_points(s)...) for s in shapes]...
-        )
-    elseif section.title in ["SIGNATURE"]
-        # silently ignore these sections
-        return nothing
-    else
-        error("Unrecognised pulseq section: $(section.title)")
+        if !startswith(lowercase(line), "num")
+            push!(shapes[current_id].samples, parse(Float64, line))
+        end
     end
-    return Symbol(lowercase(section.title)) => result
+    return Dict(key => uncompress(shape) for (key, shape) in shapes)
 end
 
-function align_in_time(pairs...)
-    interps = [linear_interpolation(time, ampl; extrapolation_bc=Flat()) for (time, ampl) in pairs]
-    all_times = sort(unique(vcat([time for (time, _) in pairs]...)))
-    return (all_times, [interp.(all_times) for interp in interps]...)
+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
 
+same_shape(shape1::PulseqShape, shape2::PulseqShape) = length(shape1.samples) == length(shape2.samples) && all(shape1.samples .≈ shape2.samples)
 
-function build_sequence(; scanner=nothing, B0=3., TR=nothing, definitions, version, blocks, rf=nothing, gradients=nothing, trap=nothing, delays=nothing, shapes=nothing, adc=nothing, name=:Pulseq)
-    if isnothing(scanner)
-        scanner = Scanner(B0=B0)
-    end
-    if version == v"1.4.0"
-        # load raster times (converting seconds to milliseconds)
-        convert = key -> parse(Float64, definitions[key]) * Float64(1e3)
-        gradient_raster = convert("GradientRasterTime")
-        rf_raster = convert("RadiofrequencyRasterTime")
-        adc_raster = convert("AdcRasterTime")
-        block_duration_raster = convert("BlockDurationRaster")
-    elseif version == v"1.3.1"
-        gradient_raster = rf_raster = block_duration_raster = Float64(1e-3) # 1 microsecond as default raster
-        adc_raster = Float64(1e-6) # ADC dwell time is in ns by default
-    else
-        error("Can only load pulseq files with versions v1.3.1 and v1.4.0, not $(version)")
-    end
+"""
+    shapes(sequence::PulseqSequence)
 
-    full_blocks = BuildingBlock[]
-
-    for block in values(blocks)
-        block_duration = 0.
-        events = []
-        if !iszero(block.rf)
-            proc = rf[block.rf]
-            (num, times_shape, amplitudes_shape) = shapes[proc.mag_id]
-            block_duration = max(num * rf_raster + proc.delay * 1e-3, block_duration)
-            ampl_times = times_shape .* (num * rf_raster)
-            ampl_size = amplitudes_shape .* (proc.amp * 1e-3)
-            if iszero(proc.phase_id)
-                phase_times = ampl_times
-                phase_size = ampl_times .* 0
-            else
-                (num, times_shape, phase_shape) = shapes[proc.phase_id]
-                block_duration = max(num * rf_raster + proc.delay * 1e-3, block_duration)
-                phase_times = times_shape .* (num * rf_raster)
-                phase_size = rad2deg.(phase_shape) .+ phase_times .* (proc.freq * 1e-3 * 360) .+ rad2deg(proc.phase)
-            end
-            if version != v"1.3.1" && !iszero(proc.time_id)
-                (num, _, time_shape) = shapes[proc.time_id]
-                times = time_shape .* rf_raster
-                ampl = ampl_size
-                phase = phase_size
-                @assert length(times) == length(ampl) == length(phase)
-            else
-                (times, ampl, phase) = align_in_time((ampl_times, ampl_size), (phase_times, phase_size))
+Return all the unique shapes used in the MR sequence.
+"""
+function shapes(sequence:: PulseqSequence)
+    result = PulseqShape[]
+    for block in sequence.blocks
+        for new_shape in _all_shapes(block)
+            if !any(s -> same_shape(s, new_shape), result)
+                push!(result, new_shape)
             end
-            push!(events, (proc.delay * 1e-3, GenericPulse(times, ampl, phase)))
         end
-        if !iszero(block.adc)
-            proc = adc[block.adc]
-            push!(events, (proc.delay * 1e-3, ADC(proc.num, proc.dwell * 1e-6, proc.dwell * proc.num * 1e-6 / 2, 1.)))
-            block_duration = max(proc.delay * 1e-3 + proc.dwell * proc.num * 1e-6, block_duration)
+    end
+    return result
+end
+
+_all_shapes(block::PulseqBlock) = vcat(_all_shapes.([block.gx, block.gy, block.gz, block.rf])...)
+_all_shapes(block::PulseqRFPulse) = vcat(_all_shapes.([block.magnitude, block.phase, block.time])...)
+_all_shapes(block::PulseqGradient) = vcat(_all_shapes.([block.shape, block.time])...)
+_all_shapes(shape::PulseqShape) = PulseqShape[shape]
+_all_shapes(::PulseqTrapezoid) = PulseqShape[]
+_all_shapes(::Nothing) = PulseqShape[]
+
+
+function gen_section(seq:: PulseqSequence, ::Val{:shapes})
+    res = PulseqSection{:shapes}(String[])
+    for (index, shape) in enumerate(shapes(seq))
+        append!(res.content, [
+            "",
+            "shape_id $index",
+            "num_samples $(length(shape.samples))"
+        ])
+        for sample in shape.samples
+            push!(res.content, string(sample))
         end
-        grad_shapes = []
-        for symbol_grad in [:gx, :gy, :gz]
-            grad_id = getfield(block, symbol_grad)
-            if iszero(grad_id)
-                push!(grad_shapes, (Float64[], Float64[]))
-                continue
-            end
-            if !isnothing(gradients) && grad_id in keys(gradients)
-                proc = gradients[grad_id]
-                start_time = proc.delay * 1e-3
-                (num, grad_time, grad_shape) = shapes[proc.shape_id]
-
-                if version != v"1.3.1" && !iszero(proc.time_id)
-                    (num, _, time_shape) = shapes[proc.time_id]
-                    times = time_shape .* gradient_raster .+ start_time
-                    @assert length(times) == length(grad_shape)
-                else
-                    times = grad_time .* (num * gradient_raster) .+ start_time
-                end
-                push!(grad_shapes, (times, grad_shape .* (proc.amp * 1e-9)))
-                block_duration = max(start_time + num * gradient_raster, block_duration)
-            elseif !isnothing(trap) && grad_id in keys(trap)
-                proc = trap[grad_id]
-                start_time = proc.delay * 1e-3
-                times = (cumsum([0, proc.rise, proc.flat, proc.fall]) .* 1e-3) .+ start_time
-                push!(grad_shapes, (times, [0, proc.amp * 1e-9, proc.amp * 1e-9, 0]))
-                block_duration = max((start_time + proc.rise + proc.flat + proc.fall) * 1e-3, block_duration)
-            else
-                error("Gradient ID $grad_id not found in either of [GRADIENTS] or [TRAP] sections")
-            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 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"
-            if !iszero(block.delay)
-                actual_duration = max(block_duration, delays[block.delay].delay * 1e-3)
-            else
-                actual_duration = block_duration
-            end
+            props = parse_pulseq_dict(
+                line,
+                [:id, :amp, :shape_id, :delay],
+                [Int, Float64, Int, Int]
+            )
+            props[:time_id] = 0
         else
-            actual_duration = block.duration * block_duration_raster
-            @assert actual_duration >= block_duration
+            props = parse_pulseq_dict(
+                line,
+                [:id, :amp, :shape_id, :time_id, :delay],
+                [Int, Float64, Int, Int, Int]
+            )
         end
-        function extend_grad!(pair)
-            (times, amplitudes) = pair
-            if iszero(length(times)) || !iszero(times[1])
-                pushfirst!(times, 0.)
-                pushfirst!(amplitudes, 0.)
-            end
-            if times[end] != actual_duration
-                push!(times, actual_duration)
-                push!(amplitudes, 0.)
-            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 _get_shape_id(shapes, shape)
+    for (i, s) in enumerate(shapes)
+        if same_shape(s, shape)
+            return i
+        end
+    end
+    error("Shape not found.")
+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 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 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
-        extend_grad!.(grad_shapes)
-        arrs = align_in_time(grad_shapes...)
-        waveform = [(t, SVector{3, Float64}(gx, gy, gz)) for (t, gx, gy, gz) in zip(arrs...)]
-        push!(full_blocks, BuildingBlock(waveform, events))
     end
-    full_block_duration = sum(duration.(full_blocks))
-    if isnothing(TR)
-        total_duration = parse(Float64, get(definitions, "TotalDuration", "0")) * 1000
-        TR = iszero(total_duration) ? full_block_duration : total_duration
+
+    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
-    if TR > full_block_duration
-        push!(full_blocks, Wait(TR - full_block_duration))
-    elseif TR < full_block_duration
-        @warn "Given TR ($TR) is shorter than the total duration of all the building blocks ($full_block_duration). Ignoring TR..."
+
+    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
-    Sequence(full_blocks; scanner=scanner, name=name)
+    return res
 end
 
 end
\ No newline at end of file
-- 
GitLab