SYNCDATA Payloads
Siemens .dat files can contain MDH packets tagged with the MDH_SYNCDATA bit (bit 5 of evalInfoMask). Their contents are not part of the k-space data stream — they carry auxiliary information such as:
- Physio / PMU (pulse-oximeter, ECG, respiration) traces,
- Coil-sensitivity adjustment (
AdjCoilSensSeq) frames, - Sequence-specific binary blobs written by custom pulse sequences (precomputed gradient shapes, k-space trajectories, ...).
Because the layout is entirely sequence-specific, MRITwixTools does not parse SYNCDATA payloads. Instead, if a scan contains any SYNCDATA packets, their raw payload bytes (everything after the MDH header) are collected into a Vector{Vector{UInt8}} and stored on the TwixObj under the key "syncdata".
Access
twx = read_twix("meas.dat")
# Present only if the scan actually contains SYNCDATA packets
if haskey(getfield(twx, :_data), "syncdata")
sync = twx.syncdata # ::Vector{Vector{UInt8}}
@show length(sync) # number of packets
@show sum(length, sync) # total bytes
@show length.(sync[1:min(5,end)])
endThe show method of TwixObj lists SYNCDATA with a summary line, e.g.:
TwixObj
📋 hdr
📊 image (327680 acq, size [128, 32, 128, 128])
🧬 syncdata (1798 packet(s), 3122000 bytes)For multi-raid files, read_twix returns a Vector{TwixObj} and each element may independently carry its own syncdata (or not).
Discovering the ASCII Tags in a Packet
If you don't know the tag string that identifies a packet, the exported helper syncdata_strings walks a packet and returns every run of consecutive printable-ASCII bytes (0x20..0x7E, whitespace/control bytes excluded), together with their byte offsets:
using MRITwixTools
twx = read_twix("meas.dat")
for s in syncdata_strings(twx.syncdata[1]; min_length = 16)
println(@sprintf(" @%6d len=%-4d %s", s.offset, s.length, s.str))
endA typical output looks like:
@ 8 len=8 PMUData_v1
@ 24 len=4 SEQD
...For a quick tour of a scan, use summarize_syncdata:
summarize_syncdata(twx.syncdata; min_length = 16)The min_length keyword controls how many consecutive printable bytes qualify as an "ASCII run". Two kinds of noise typically show up if it is set too low:
- Some Siemens SEQData framing packets carry short trailing fragments of the ASCII XProtocol header (e.g.
"<Comment>","<Dependency>",<Context> "NORMAL"). Because the XProtocol string spans packet boundaries, only ~10–20 characters of it survive as a run in any single packet — amin_lengthof 20–32 hides all of them. - The bit patterns of small-magnitude
Float32numbers cluster around0x3E..0x3F(bytes>and?), which fall in the printable-ASCII range. Sequences of small floats therefore occasionally produce short "ASCII runs" that are pure noise (typically ≤ ~12 chars).
A versioned tag like "SpiralGradShape_v1" is usually 15–30 characters long, so choosing min_length at ~16 gives a good signal-to-noise ratio. If your tag is shorter, drop min_length and inspect the output — real tags tend to appear in exactly one packet at a fixed, small offset, while noise fluctuates in offset from one packet to the next.
SYNCDATA streams are usually dominated by two kinds of noisy packets:
- Silent framing packets (PMU telemetry, timing frames, …) with no printable-ASCII content at all.
- XProtocol-fragment packets whose trailing bytes carry a slice of the ASCII XProtocol header — producing ASCII runs at offsets near the end of the packet.
summarize_syncdata hides both categories by default and reports them in a single aggregate line at the end, keeping the output focused on packets that plausibly carry a sequence tag:
- Packets with no qualifying ASCII run are omitted (case 1).
- Packets whose runs all start past
max_start_frac × length(pkt)(default0.5) are omitted as "late-only" (case 2). Sequence tags that identify a payload are almost always near the start of their packet, so the heuristic reliably keeps them visible.
To disable one of these filters:
show_empty = true— list every packet, including silent ones.max_start_frac = 1.0— do not treat late-only packets as noise.
Once you know the tag, use payload_offset to jump straight to the first byte that is not printable ASCII after that tag — i.e. the plausible start of the binary payload:
p = payload_offset(pkt, "PMUData_v1") # 1-based index into `pkt`, or `nothing`The helper skips all trailing printable bytes past the tag, so if the tag is immediately followed by another ASCII field name it will keep sliding. Sequence-specific parsers usually still need to skip a few extra NUL / framing bytes past this offset until the parsed fields look plausible — see the pattern below.
Auto-splitting a packet by "where do the printable characters end?" works whenever the binary payload is dominated by non-printable bytes (small integers, floats near zero, etc.). It is not reliable if the payload itself contains a long printable-ASCII string — that string would be reported as its own ASCII run. Manual inspection with summarize_syncdata remains the safest first step for a new format.
Parsing a Sequence-Specific Payload
Most custom payloads are introduced by an ASCII tag (a version-stamped sequence-specific string). To locate and parse one, iterate over packets and use findfirst to locate the tag inside the raw bytes:
tag = Vector{UInt8}(codeunits("MyCustomTag"))
for pkt in twx.syncdata
r = findfirst(tag, pkt)
r === nothing && continue
# Bytes immediately after the tag — parse whatever layout the sequence uses.
# For example, if the payload is Float64, Int32, Int32, Float32[N], ...
p = last(r) + 1 # 1-based index
a = reinterpret(Float64, pkt[p : p + 7 ])[1]
b = reinterpret(Int32, pkt[p + 8: p + 11])[1]
c = reinterpret(Int32, pkt[p +12: p + 15])[1]
# ...
break
endSiemens wraps custom payloads in an sSYNCDATA / SEQData frame, so a few bytes usually sit between the tag and the start of the actual binary layout. A simple robust pattern is to slide forward skip = 0..256 bytes after the tag until the first candidate header parses to plausible values (finite floats, positive lengths that fit in the packet). See examples/read_spiral_gradshape.jl in the repository for a complete worked example that handles this.
Complete Example: SpiralGradShape
A pulse sequence writes a spiral gradient shape into SYNCDATA with the following layout after the ASCII tag SpiralGradShape:
| Type | Field |
|---|---|
Float64 | dMax_Ampl |
Int32 | lGradLength |
Int32 | lROLength |
Float32[lGradLength] | X |
Float32[lGradLength] | Z |
using MRITwixTools
const TAG = Vector{UInt8}(codeunits("SpiralGradShape"))
function read_spiral_gradshape(filename; max_skip = 256)
twx = read_twix(filename; verbose = false)
scans = twx isa AbstractVector ? twx : [twx]
for t in scans
haskey(getfield(t, :_data), "syncdata") || continue
for pkt in t.syncdata
r = findfirst(TAG, pkt)
r === nothing && continue
after_tag = last(r) + 1
for skip in 0:max_skip
p = after_tag + skip
p + 15 > length(pkt) && break
dMax = reinterpret(Float64, pkt[p : p + 7 ])[1]
nGrad = reinterpret(Int32, pkt[p + 8 : p + 11])[1]
nRO = reinterpret(Int32, pkt[p + 12: p + 15])[1]
(isfinite(dMax) && 0 < abs(dMax) < 1e6 &&
1 <= nGrad <= 10_000_000 && 1 <= nRO <= 10_000_000) || continue
need = 2 * Int64(nGrad) * sizeof(Float32)
p + 15 + need > length(pkt) && continue
xs = p + 16
zs = xs + nGrad * sizeof(Float32)
X = collect(reinterpret(Float32, pkt[xs : xs + nGrad*4 - 1]))
Z = collect(reinterpret(Float32, pkt[zs : zs + nGrad*4 - 1]))
return (; dMaxAmpl = dMax,
lGradLength = Int(nGrad),
lROLength = Int(nRO),
X = X, Z = Z)
end
end
end
error("No plausible SpiralGradShape payload found in $filename")
end
s = read_spiral_gradshape("meas.dat")
@show s.dMaxAmpl s.lGradLength s.lROLength
@show maximum(abs, s.X) maximum(abs, s.Z)API
The full docstrings for the SYNCDATA helpers live in the API Reference:
Implementation Notes
- SYNCDATA payload bytes are extracted directly during the MDH scan pass in
loop_mdh_read. The reader saves the file position, readsulDMALength - byteMDHbytes, and seeks back so the surrounding skip-based MDH loop advancement is not disturbed. - The
"syncdata"entry is stored on theTwixObjonly when the scan actually produced at least one SYNCDATA packet; it is not present otherwise. Usehaskey(getfield(twx, :_data), "syncdata")to test. - Post-MDH processing (
tryAndFixLastMdh!,compute_dims!) checks forRawDataexplicitly and skips thesyncdataentry. - The
TwixObjtype-narrowing helper_twixobj_valreturnsUnion{TwixHdr, RawData, Vector{Vector{UInt8}}}, which preserves REPL tab-completion even whensyncdatais present.