Skip to content

Build Sequences with @addblock

Use @addblock / @addblocks to write pulse programs in block form. One block contains events that play together: RF, ADC, and extensions are positional events, while gradients are assigned to axes with x=, y=, and z=.

Direct addblock! appends one block, for example addblock!(seq, rf; z=gz). The @addblock macro uses the same destination sequence but adds syntax for common pulse-programming patterns:

  • Single block expression: (rf, z=gz).

  • Multiple blocks in one line: (rf, z=gz) + (x=gx, adc).

  • Sequence block transformations. See Transform Sequences:

    • Gradient scaling: 0.5 * (x=gx, adc).

    • Gradient rotation: rotz(θ) * (x=gx, adc).

    • RF scaling and RF/ADC phase: 2cis(ϕ) * (rf, adc).

Use @addblock to append block expressions:

julia
seq = Sequence()  # or Sequence(sys) for scanner export checks
@addblock seq += (rf, z=gz)

With @addblock, + means "append these blocks in order", so you can append multiple sequence blocks in one line. A tuple such as (rf, z=gz) becomes one copied block; a Sequence term contributes all of its blocks.

julia
@addblock seq += (rf, z=gz) + (x=gx, adc) + (Delay(TR), LabelInc(1, "ECO"))

Gradient Axes

Koma Grad events are axis-neutral. Choose the axis when adding the block with x=, y=, or z=. RF, ADC, and extensions are positional.

julia
@addblock seq += (rf, LabelSet(ky, "LIN"), z=gz)

Specify multiple gradient events in one block by giving each direction with x=, y=, and z=:

julia
slice_select = (x=gx_slice, y=gy_slice, z=gz_slice)
rewinder = (x=gx_rewind, y=gy_rewind, z=gz_rewind)

excitation = Sequence()
@addblock excitation += (rf; slice_select...) + (; rewinder...)

Block Duration

  • Delay(T): minimum block duration.

  • Duration(T): exact block duration. Errors if any event is longer than T.

julia
@addblock seq += (rf, Delay(TR), z=gz)     # at least TR
@addblock seq += (rf, Duration(TR), z=gz)  # exactly TR, or errors

Delay and Duration are construction helpers: they update seq.DUR; they are not stored as RF, gradient, ADC, or extension events.

Sequences Considering Hardware Limits

Use Sequence(sys) when export checks should use a scanner. write_seq checks raster timing and event-local hardware limits from seq.DEF, or from sys when passed. Plain Sequence() uses Pulseq file rasters and non-limiting hardware limits.

julia
sys = Scanner(Gmax=40e-3, Smax=150)
seq = Sequence(sys)
@addblock seq += (rf, z=gz)
write_seq(seq, "sequence.seq")  # checks raster and hw limits in seq.DEF

Optional checks can also be enabled per append. Pass only the checks you need; when the sequence was created with Sequence(sys), the checks can use that scanner metadata directly.

julia
@addblock check_timing=true seq += (rf, z=gz)

# Define checks once when you want to toggle them together.
checks = (; check_timing=true, check_hw_limits=true)
@addblock checks seq += (rf, z=gz)

Add Blocks in Loops

Use @addblocks around loops that append to a sequence. Inside the macro, seq += ... appends in place.

julia
@addblocks for ky in 1:Ny
    seq += (rf, z=gz)
    seq += (x=gx, y=phase_blip(ky), adc)
end

Do not use plain seq += readout in long loops. It calls Koma's +, which builds a new copied sequence from the old seq and readout. That append semantic is useful when you want a new independent sequence, but it scales poorly when repeated. Use @addblocks for in-place appends.

Sequence Building Blocks

Name a small part of the pulse program as a normal Sequence, then append it. In contrast to Pulseq helper functions that call addBlock on a destination, Koma sequence parts are block lists themselves, so they can be appended together. For example, readout below is a one-block Sequence, not a new event type.

julia
readout = Sequence()
@addblock readout += (ADC(num_readout_samples, adc_duration, ζ), x=Grad(G_readout, T_readout, ζ))

prephaser = Sequence()
@addblock prephaser += (x=Grad(Gx_prephaser, T_prephaser, ζ_prephaser), y=Grad(Gy_prephaser, T_prephaser, ζ_prephaser))

@addblock seq += prephaser + readout

Append named sequences in loops with @addblocks:

julia
@addblocks for ky in 1:Ny
    seq += rf_preparation + readout(ky)
end

Transform Sequences

Koma defines arithmetic on Sequence values. Each operation returns a copy, so the original sequence can be reused. Inside @addblock, left-multiplying a block tuple applies the same operation to that one-block sequence.

The macro lowers op * (x=gx, adc) by first making a one-block Sequence, then calling op * seq. Add new transforms by extending Base.:*(op, seq::Sequence) for the operation type.

Scale Gradients

Use real scalars to scale gradient amplitudes.

julia
@addblock seq += 0.5 * (x=gx, adc)

Rotate Gradients

real_matrix * sequence mixes gradient axes and leaves RF and ADC unchanged.

julia
@addblock seq += rotz(π / 6) * (x=gx, adc)

Phase RF and ADC

complex_scalar * sequence phase-shifts RF and ADC. Gradients are unchanged. Use cis(ϕ) for exp(im * ϕ).

julia
phase = cis(π / 2)  # exp(im * π / 2)
@addblock seq += phase * (rf, z=gz) + phase * (x=gx, adc)

Let's Put It All Together!

Rotate the readout block for each spoke. Koma can rotate the whole block tuple; Pulseq rotates the events before passing them to addBlock.

julia
@addblocks for spoke in 0:Nspokes-1
    θ = π * spoke / Nspokes
    seq += (rf, z=gz) + rotz(θ) * (x=gx, adc)
end

Append Semantics

Use this section when you need to reason about copies or performance. Tuple terms become one fresh block through addblock!. Existing Sequence terms append all of their blocks through append!, which copies those blocks into the destination.

julia
@addblock seq += (rf, z=gz) + (x=gx, adc)

@addblocks applies the same rewrite to += expressions in its scope when the left-hand side is a Sequence. Plain seq = seq + readout instead constructs a new copied Sequence, so avoid it inside long loops.