Files
scanengine-3/scan_format.md
T
Thomas Ales [M S E] a0e0151b5d pre uc480 integration
2026-05-22 09:38:39 -05:00

7.6 KiB
Raw Blame History

SRAS Scan Binary Format — Version 4

Each .sras file contains one complete scan: all GR rotation angles and all Y rows. Files are named {prefix}.sras.


File Layout

[Global Header      — 43 bytes]
[Angle Table        — n_angles × 4 bytes  (float32 per angle)]
[Row Table          — n_rows   × 4 bytes  (float32 per row)]
[Preamble Blocks    — n_channels × (uint16 length + UTF-8 WFMOutpre string)]
[Background Block   — uint32 n_bg_samples + n_bg_samples × int8 bytes]
[Waveform Data      — n_angles × n_rows × n_channels × n_frames × samples_per_frame × bps bytes]

All multi-byte integers and floats use big-endian byte order (> in Python's struct module).


Global Header (42 bytes)

Offset Size Type Field Description
0 4 4s magic Always SRAS (0x53 0x52 0x41 0x53)
4 1 uint8 version Format version — 4
5 2 uint16 n_angles Number of GR rotation angles
7 2 uint16 n_rows Number of Y rows per angle
9 4 float32 x_start_mm X scan start position in mm
13 4 float32 x_delta_mm X scan width in mm
17 4 float32 velocity_mm_s Stage scan velocity in mm/s
21 4 float32 laser_freq_hz Laser repetition rate in Hz
25 4 uint32 n_frames A-scans per row (= FastFrame count per channel)
29 4 uint32 samples_per_frame Time samples per waveform
33 8 float64 sample_rate_hz Oscilloscope sample rate in Hz (e.g. 6.25e9)
41 1 uint8 bytes_per_sample Bytes per ADC sample: 1 = int8, 2 = int16
42 1 uint8 n_channels Number of channels recorded (currently 3)

Total header size: 43 bytes — verified: struct.calcsize(">4sBHHffffIIdBB") == 43.


Angle Table

Immediately after the header: n_angles big-endian float32 values, one per GR angle (degrees, 0–180).

angle[0], angle[1], …, angle[n_angles - 1]

Row Table

Immediately after the angle table: n_rows big-endian float32 values, one per Y row (mm).

y_mm[0], y_mm[1], …, y_mm[n_rows - 1]

Preamble Blocks

Immediately after the row table: n_channels length-prefixed UTF-8 strings, one per channel in SCAN_CHANNELS order (CH1, CH3, CH4). Each block is:

uint16  length   — byte length of the following UTF-8 string
bytes   preamble — WFMOutpre response string from the oscilloscope

The preamble captures per-channel scaling constants (YMULT, YOFF, YZERO) needed to convert raw ADC values to volts.


Background Block

Immediately after the preamble blocks: a single CH1 waveform captured with the Helios (generation) laser enabled and the Genesis (detection) laser disabled. This provides a noise/background reference for subtraction during post-processing.

uint32  n_bg_samples   — number of samples in the background waveform
int8[]  bg_data        — raw ADC samples (same encoding as waveform data)

n_bg_samples equals samples_per_frame under normal acquisition settings.


Waveform Data

Immediately after the background block. Data is stored in angle-major, row-minor order. Within each row, channels are interleaved in ascending channel-index order, with each channel's FastFrame data written in frame order.

for angle in 0 … n_angles-1:
  for row in 0 … n_rows-1:
    for channel in [CH1, CH3, CH4]:          # 3 channels, fixed order
      for frame in 0 … n_frames-1:
        samples[0 … samples_per_frame-1]     # bps bytes each

Each sample is a raw signed ADC value. With bytes_per_sample = 1 this is int8 (−128 … +127). With bytes_per_sample = 2 this is big-endian int16.

Total data size:

n_angles × n_rows × 3 × n_frames × samples_per_frame × bytes_per_sample

Incomplete files: If a scan is aborted the file is closed immediately and the data block will be shorter than the expected size. Readers should check file_size >= header + angle_table + row_table + data before reshaping.


Spatial Mapping

The k-th waveform (frame) in a row corresponds to the k-th laser pulse that hit the sample. The physical X position of that pulse is:

x_k = x_start_mm + k * (velocity_mm_s / laser_freq_hz)

Python Read Example

import struct, numpy as np
from pathlib import Path

HDR_FMT  = ">4sBHHffffIIdBB"
HDR_SIZE = struct.calcsize(HDR_FMT)   # 43 bytes

def read_sras(path):
    with open(path, "rb") as f:
        hdr = struct.unpack(HDR_FMT, f.read(HDR_SIZE))
        magic, ver, n_angles, n_rows, xs, xd, vel, freq, nf, spf, sr, bps, n_ch = hdr
        assert magic == b"SRAS" and ver == 4, "Not a v4 SRAS file"

        angles      = np.frombuffer(f.read(n_angles * 4), dtype=">f4")
        y_positions = np.frombuffer(f.read(n_rows * 4),   dtype=">f4")

        # Preamble blocks (one per channel)
        preambles = []
        for _ in range(n_ch):
            (plen,) = struct.unpack(">H", f.read(2))
            preambles.append(f.read(plen).decode("utf-8"))

        # Background waveform block (v4+)
        (n_bg,) = struct.unpack(">I", f.read(4))
        background = np.frombuffer(f.read(n_bg), dtype=np.int8)

        dtype = np.int8 if bps == 1 else ">i2"
        data = np.frombuffer(f.read(), dtype=dtype).reshape(
            n_angles, n_rows, n_ch, nf, spf
        )

    return {
        "angles_deg":     angles,
        "y_positions_mm": y_positions,
        "x_start_mm":     xs,
        "x_delta_mm":     xd,
        "velocity_mm_s":  vel,
        "laser_freq_hz":  freq,
        "sample_rate_hz": sr,
        "n_channels":     n_ch,      # 3: CH1, CH3, CH4 (see Acquisition Settings)
        "preambles":      preambles, # WFMOutpre strings, same order as n_channels
        "background":     background,# shape: (n_bg_samples,) — CH1 noise reference
        # shape: (n_angles, n_rows, n_channels, n_frames, samples_per_frame)
        "data":           data,
    }

Acquisition Settings (fixed by sc3_aui_app.py)

Parameter Value
Oscilloscope trigger CH2, rising edge, 1.24 V
Trigger offset 0 % (trigger at left edge)
Sample rate 6.25 GS/s (160 ps/sample)
Channels recorded CH1, CH3, CH4
Stage X velocity 100 mm/s
Stage X acceleration 1500 mm/s²
Stage X trigger out Logic-high at max velocity
Acquisition mode FastFrame, Normal trigger

Version History

Version Change
1 One file per row; header included angle_idx, row_idx, angle_deg, y_mm.
2 One file per scan; global header with n_angles/n_rows; separate angle and row tables; three channels (CH1, CH3, CH4) per row.
3 Added preamble blocks (WFMOutpre strings) after the row table, one length-prefixed UTF-8 block per channel.
4 Added background waveform block (CH1, Helios ON / Genesis OFF) after the preamble blocks; stored as uint32 sample count followed by raw int8 ADC bytes.