pre merge cleanup commit

This commit is contained in:
Thomas Ales
2026-07-28 09:20:58 -05:00
parent 1014533626
commit 2041fc8439
52 changed files with 58333 additions and 545 deletions
Regular → Executable
+86 -88
View File
@@ -1,19 +1,27 @@
# SRAS Scan Binary Format — Version 4
# SRAS Scan Binary Format — Version 6
Each `.sras` file contains **one complete scan**: all GR rotation angles and all
Y rows. Files are named `{prefix}.sras`.
Starting in v6, each angle only scans the **bounding box of the nominal ROI
rotated by that specific angle** — not the worst case across all angles — so
`x_start`, `x_delta` (and therefore `n_frames`, the points/row count) and
`n_rows` all vary per angle. A 0°/180° scan of a wide, short ROI needs far
fewer rows than a 45° scan of the same ROI, and the file format reflects that
instead of forcing every angle to the largest bounding box.
---
## 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]
[Global Header — 49 bytes]
[Angle Table — n_angles × 4 bytes (float32 per angle, degrees)]
[Per-Angle Geometry Table— n_angles × 14 bytes (x_start f32, x_delta f32, n_frames u32, n_rows u16)]
[Row Table (ragged) — sum(n_rows) × 4 bytes (float32 per row, angle-major)]
[Preamble Blocks — n_channels × (uint16 length + UTF-8 WFMOutpre string)]
[Background Block — uint32 n_bg_samples + n_bg_samples × int8 bytes]
[Waveform Data (ragged) — per angle: n_rows[a] × n_channels × n_frames[a] × samples_per_frame × bps bytes]
```
All multi-byte integers and floats use **big-endian** byte order
@@ -21,33 +29,40 @@ All multi-byte integers and floats use **big-endian** byte order
---
## Global Header (42 bytes)
## Global Header (49 bytes)
| Offset | Size | Type | Field | Description |
|--------|------|-----------|--------------------|--------------------------------------------------|
| 0 | 4 | `4s` | `magic` | Always `SRAS` (0x53 0x52 0x41 0x53) |
| 4 | 1 | `uint8` | `version` | Format version — `4` |
| 4 | 1 | `uint8` | `version` | Format version — `6` |
| 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`) |
| 7 | 4 | `float32` | `x_start_nominal` | Nominal (pre-rotation) X scan start, mm |
| 11 | 4 | `float32` | `y_start_nominal` | Nominal (pre-rotation) Y scan start, mm |
| 15 | 4 | `float32` | `x_delta_nominal` | Nominal (pre-rotation) X scan width, mm |
| 19 | 4 | `float32` | `y_delta_nominal` | Nominal (pre-rotation) Y scan height, mm |
| 23 | 4 | `float32` | `row_spacing_mm` | Y spacing between rows, mm |
| 27 | 4 | `float32` | `velocity_mm_s` | Stage scan velocity in mm/s |
| 31 | 4 | `float32` | `laser_freq_hz` | Laser repetition rate in Hz |
| 35 | 4 | `uint32` | `samples_per_frame`| Time samples per waveform |
| 39 | 8 | `float64` | `sample_rate_hz` | Oscilloscope sample rate in Hz (e.g. 6.25e9) |
| 47 | 1 | `uint8` | `bytes_per_sample` | Bytes per ADC sample: `1` = int8, `2` = int16 |
| 48 | 1 | `uint8` | `n_channels` | Number of channels recorded (currently `3`) |
**Total header size:** 43 bytes — verified:
`struct.calcsize(">4sBHHffffIIdBB") == 43`.
**Total header size:** 49 bytes — verified:
`struct.calcsize(">4sBHfffffffIdBB") == 49`.
The `*_nominal` fields describe the ROI as originally entered on the New Scan
page (XS/YS/XD/YD), **before** per-angle bounding-box expansion. They are for
reference/reconstruction only — the actual per-angle scan geometry used for
acquisition is in the Per-Angle Geometry Table below.
---
## Angle Table
Immediately after the header: **n_angles** big-endian float32 values, one per
GR angle (degrees, 0–180).
GR angle (degrees, signed; magnitude 0–180, sign gives physical rotation
direction — negative for the current CW-rotating GR stage).
```
angle[0], angle[1], …, angle[n_angles - 1]
@@ -55,15 +70,36 @@ angle[0], angle[1], …, angle[n_angles - 1]
---
## Row Table
## Per-Angle Geometry Table
Immediately after the angle table: **n_rows** big-endian float32 values, one
per Y row (mm).
Immediately after the angle table: **n_angles** fixed-size records, one per
angle (same order as the angle table), each 14 bytes:
| Size | Type | Field | Description |
|------|-----------|------------|-------------------------------------------------------|
| 4 | `float32` | `x_start` | X scan start for this angle's bounding box, mm |
| 4 | `float32` | `x_delta` | X scan width for this angle's bounding box, mm |
| 4 | `uint32` | `n_frames` | A-scans per row for this angle (FastFrame count) |
| 2 | `uint16` | `n_rows` | Number of Y rows scanned for this angle |
Format string per record: `">ffIH"`.
---
## Row Table (ragged)
Immediately after the per-angle geometry table: for each angle in order,
that angle's `n_rows` big-endian float32 Y positions (mm), concatenated with
no padding between angles.
```
y_mm[0], y_mm[1], …, y_mm[n_rows - 1]
# angle 0's rows, then angle 1's rows, …
y_mm[0][0], …, y_mm[0][n_rows[0]-1], y_mm[1][0], …, y_mm[n_angles-1][n_rows[-1]-1]
```
Row-table boundaries for angle *a* are derived from the per-angle geometry
table: `sum(n_rows[0:a])` gives the starting index into the flattened array.
---
## Preamble Blocks
@@ -97,17 +133,20 @@ int8[] bg_data — raw ADC samples (same encoding as waveform data)
---
## Waveform Data
## Waveform Data (ragged)
Immediately after the background block. Data is stored in **angle-major, row-minor**
order. Within each row, channels are interleaved in ascending channel-index
Immediately after the background block. Data is stored in **angle-major,
row-minor** order, but unlike earlier versions each angle contributes a
different number of rows (`n_rows[a]`) and a different number of frames per
row (`n_frames[a]`), both taken from that angle's Per-Angle Geometry Table
entry. 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 angle a in 0 … n_angles-1:
for row in 0 … n_rows[a]-1:
for channel in [CH1, CH3, CH4]: # 3 channels, fixed order
for frame in 0 … n_frames-1:
for frame in 0 … n_frames[a]-1:
samples[0 … samples_per_frame-1] # bps bytes each
```
@@ -117,81 +156,37 @@ int16**.
Total data size:
```
n_angles × n_rows × 3 × n_frames × samples_per_frame × bytes_per_sample
sum over angles a of: n_rows[a] × 3 × n_frames[a] × 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.
> the data block will be shorter than the expected size. Readers should
> reconstruct the expected per-angle byte offsets from the Per-Angle Geometry
> Table and check `file_size` against the running total before reshaping —
> a fixed `(n_angles, n_rows, ...)` reshape (as in pre-v6 readers) will not
> work since row/frame counts are no longer uniform across angles.
---
## 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:
hit the sample. For a row belonging to angle *a*, the physical X position of
that pulse is:
```
x_k = x_start_mm + k * (velocity_mm_s / laser_freq_hz)
x_k = x_start[a] + 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,
}
```
using that angle's `x_start` from the Per-Angle Geometry Table (not
`x_start_nominal`).
---
## 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) |
@@ -211,3 +206,6 @@ def read_sras(path):
| 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. |
| 5 | (skipped) |
| 6 | Each angle now scans only the bounding box of the nominal ROI rotated by that angle instead of the AABB-expanded worst case across all angles. Header no longer carries a single global `x_start`/`x_delta`/`n_rows` — replaced with `*_nominal` reference fields plus a new Per-Angle Geometry Table (`x_start`, `x_delta`, `n_frames`, `n_rows` per angle) and a ragged Row Table / Waveform Data block sized per angle. **Not compatible with v4 readers** (e.g. `sras_viewer.py`, which has not yet been updated for v6). |