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:
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.
@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.
@addblock seq += (rf, LabelSet(ky, "LIN"), z=gz)Specify multiple gradient events in one block by giving each direction with x=, y=, and z=:
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 thanT.
@addblock seq += (rf, Delay(TR), z=gz) # at least TR
@addblock seq += (rf, Duration(TR), z=gz) # exactly TR, or errorsDelay 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.
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.DEFOptional 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.
@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.
@addblocks for ky in 1:Ny
seq += (rf, z=gz)
seq += (x=gx, y=phase_blip(ky), adc)
endDo 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.
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 + readoutAppend named sequences in loops with @addblocks:
@addblocks for ky in 1:Ny
seq += rf_preparation + readout(ky)
endTransform 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.
@addblock seq += 0.5 * (x=gx, adc)Rotate Gradients
real_matrix * sequence mixes gradient axes and leaves RF and ADC unchanged.
@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 * ϕ).
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.
@addblocks for spoke in 0:Nspokes-1
θ = π * spoke / Nspokes
seq += (rf, z=gz) + rotz(θ) * (x=gx, adc)
endAppend 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.
@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.