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

214 lines
7.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
```python
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. |