original program
This commit is contained in:
@@ -0,0 +1,761 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>SRAS File Format Specification</title>
|
||||
<style>
|
||||
/* From extension vscode.github */
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.vscode-dark img[src$=\#gh-light-mode-only],
|
||||
.vscode-light img[src$=\#gh-dark-mode-only],
|
||||
.vscode-high-contrast:not(.vscode-high-contrast-light) img[src$=\#gh-light-mode-only],
|
||||
.vscode-high-contrast-light img[src$=\#gh-dark-mode-only] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/Microsoft/vscode/extensions/markdown-language-features/media/markdown.css">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/Microsoft/vscode/extensions/markdown-language-features/media/highlight.css">
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe WPC', 'Segoe UI', system-ui, 'Ubuntu', 'Droid Sans', sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
.task-list-item {
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.task-list-item-checkbox {
|
||||
margin-left: -20px;
|
||||
vertical-align: middle;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
:root {
|
||||
--color-note: #0969da;
|
||||
--color-tip: #1a7f37;
|
||||
--color-warning: #9a6700;
|
||||
--color-severe: #bc4c00;
|
||||
--color-caution: #d1242f;
|
||||
--color-important: #8250df;
|
||||
}
|
||||
|
||||
</style>
|
||||
<style>
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--color-note: #2f81f7;
|
||||
--color-tip: #3fb950;
|
||||
--color-warning: #d29922;
|
||||
--color-severe: #db6d28;
|
||||
--color-caution: #f85149;
|
||||
--color-important: #a371f7;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
<style>
|
||||
.markdown-alert {
|
||||
padding: 0.5rem 1rem;
|
||||
margin-bottom: 16px;
|
||||
color: inherit;
|
||||
border-left: .25em solid #888;
|
||||
}
|
||||
|
||||
.markdown-alert>:first-child {
|
||||
margin-top: 0
|
||||
}
|
||||
|
||||
.markdown-alert>:last-child {
|
||||
margin-bottom: 0
|
||||
}
|
||||
|
||||
.markdown-alert .markdown-alert-title {
|
||||
display: flex;
|
||||
font-weight: 500;
|
||||
align-items: center;
|
||||
line-height: 1
|
||||
}
|
||||
|
||||
.markdown-alert .markdown-alert-title .octicon {
|
||||
margin-right: 0.5rem;
|
||||
display: inline-block;
|
||||
overflow: visible !important;
|
||||
vertical-align: text-bottom;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.markdown-alert.markdown-alert-note {
|
||||
border-left-color: var(--color-note);
|
||||
}
|
||||
|
||||
.markdown-alert.markdown-alert-note .markdown-alert-title {
|
||||
color: var(--color-note);
|
||||
}
|
||||
|
||||
.markdown-alert.markdown-alert-important {
|
||||
border-left-color: var(--color-important);
|
||||
}
|
||||
|
||||
.markdown-alert.markdown-alert-important .markdown-alert-title {
|
||||
color: var(--color-important);
|
||||
}
|
||||
|
||||
.markdown-alert.markdown-alert-warning {
|
||||
border-left-color: var(--color-warning);
|
||||
}
|
||||
|
||||
.markdown-alert.markdown-alert-warning .markdown-alert-title {
|
||||
color: var(--color-warning);
|
||||
}
|
||||
|
||||
.markdown-alert.markdown-alert-tip {
|
||||
border-left-color: var(--color-tip);
|
||||
}
|
||||
|
||||
.markdown-alert.markdown-alert-tip .markdown-alert-title {
|
||||
color: var(--color-tip);
|
||||
}
|
||||
|
||||
.markdown-alert.markdown-alert-caution {
|
||||
border-left-color: var(--color-caution);
|
||||
}
|
||||
|
||||
.markdown-alert.markdown-alert-caution .markdown-alert-title {
|
||||
color: var(--color-caution);
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
</head>
|
||||
<body class="vscode-body vscode-light">
|
||||
<h1 id="sras-file-format-specification">SRAS File Format Specification</h1>
|
||||
<p><strong>Format family:</strong> <code>.sras</code><br>
|
||||
<strong>Byte order:</strong> Big-endian (network byte order) throughout, unless noted.<br>
|
||||
<strong>Version history:</strong> v2 (baseline), v3 (scope calibration), v4 (background waveform), v5 (precomputed images + guaranteed frame count).</p>
|
||||
<hr>
|
||||
<h2 id="table-of-contents">Table of Contents</h2>
|
||||
<ol>
|
||||
<li><a href="#overview">Overview</a></li>
|
||||
<li><a href="#type-notation">Type notation</a></li>
|
||||
<li><a href="#version-history">Version history</a></li>
|
||||
<li><a href="#file-structure">File structure</a>
|
||||
<ul>
|
||||
<li><a href="#1-fixed-header-43-bytes-all-versions">Fixed header (all versions)</a></li>
|
||||
<li><a href="#2-angle-table-all-versions">Angle table (all versions)</a></li>
|
||||
<li><a href="#3-row-position-table-all-versions">Row position table (all versions)</a></li>
|
||||
<li><a href="#4-channel-preambles-v3">Channel preambles (v3+)</a></li>
|
||||
<li><a href="#5-background-waveform-v4">Background waveform (v4+)</a></li>
|
||||
<li><a href="#6-waveform-data-all-versions">Waveform data (all versions)</a></li>
|
||||
<li><a href="#7-prec-section-v5">PREC section (v5)</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#derived-quantities">Derived quantities</a></li>
|
||||
<li><a href="#adc-calibration">ADC calibration</a></li>
|
||||
<li><a href="#waveform-data-layout-detail">Waveform data layout detail</a></li>
|
||||
<li><a href="#size-reference">Size reference</a></li>
|
||||
<li><a href="#compatibility-notes">Compatibility notes</a></li>
|
||||
</ol>
|
||||
<hr>
|
||||
<h2 id="overview">Overview</h2>
|
||||
<p>An SRAS file stores the raw RF waveforms captured during a Surface-acoustic-wave Resonance And Spectroscopy (SRAS) scan, along with the scan geometry and scope calibration metadata needed to interpret them.</p>
|
||||
<p>A scan consists of one or more <strong>angles</strong> (rotation positions of the sample), each containing a 2-D raster of <strong>rows</strong> × <strong>frames</strong>. At every pixel, <code>n_channels</code> waveforms of <code>samples_per_frame</code> ADC counts are stored. Channel order is fixed:</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Index</th>
|
||||
<th>Hardware channel</th>
|
||||
<th>Signal</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>0</td>
|
||||
<td>CH1</td>
|
||||
<td>RF acoustic packet (AC-coupled)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1</td>
|
||||
<td>CH3</td>
|
||||
<td>Bias A — DC mean used for masking</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2</td>
|
||||
<td>CH4</td>
|
||||
<td>Bias B — DC mean used for masking</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr>
|
||||
<h2 id="type-notation">Type notation</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Symbol</th>
|
||||
<th>C type</th>
|
||||
<th>Size</th>
|
||||
<th>Notes</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>u8</code></td>
|
||||
<td><code>uint8_t</code></td>
|
||||
<td>1 byte</td>
|
||||
<td>unsigned</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>u16</code></td>
|
||||
<td><code>uint16_t</code></td>
|
||||
<td>2 bytes</td>
|
||||
<td>big-endian</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>u32</code></td>
|
||||
<td><code>uint32_t</code></td>
|
||||
<td>4 bytes</td>
|
||||
<td>big-endian</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>i8</code></td>
|
||||
<td><code>int8_t</code></td>
|
||||
<td>1 byte</td>
|
||||
<td>signed, used for ADC samples when <code>bytes_per_sample == 1</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>i16</code></td>
|
||||
<td><code>int16_t</code></td>
|
||||
<td>2 bytes</td>
|
||||
<td>big-endian signed, used when <code>bytes_per_sample == 2</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>f32</code></td>
|
||||
<td><code>float</code></td>
|
||||
<td>4 bytes</td>
|
||||
<td>big-endian IEEE 754 single</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>f64</code></td>
|
||||
<td><code>double</code></td>
|
||||
<td>8 bytes</td>
|
||||
<td>big-endian IEEE 754 double</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>char[N]</code></td>
|
||||
<td>—</td>
|
||||
<td>N bytes</td>
|
||||
<td>raw bytes, no null terminator unless noted</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>utf8[N]</code></td>
|
||||
<td>—</td>
|
||||
<td>N bytes</td>
|
||||
<td>UTF-8 string, length-prefixed (see preamble section)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr>
|
||||
<h2 id="version-history">Version history</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Version</th>
|
||||
<th>Added</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>2</td>
|
||||
<td>Baseline: fixed header, angle table, row table, raw waveform data. No scope calibration (fallback constants used by readers).</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>3</td>
|
||||
<td>Per-channel Tektronix WFMOutpre preamble strings carrying YMULT / YOFF / YZERO calibration.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>4</td>
|
||||
<td>Background waveform section: one CH1 reference shot subtracted from each CH1 frame before FFT.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>5</td>
|
||||
<td><strong>(this document)</strong> Version byte incremented to 5. <code>n_frames_hdr</code> is now the <em>actual</em> acquired frame count (authoritative). PREC section appended after waveform data with precomputed FFT-peak and DC images for instant re-display.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<blockquote>
|
||||
<p><strong>v2 note:</strong> Version 1 is not defined; version 2 is the lowest observed in the field.</p>
|
||||
</blockquote>
|
||||
<hr>
|
||||
<h2 id="file-structure">File structure</h2>
|
||||
<pre><code>┌─────────────────────────────────────────────┐
|
||||
│ 1. Fixed header (43 bytes) │ all versions
|
||||
├─────────────────────────────────────────────┤
|
||||
│ 2. Angle table (n_angles × 4 bytes)│ all versions
|
||||
├─────────────────────────────────────────────┤
|
||||
│ 3. Row position table (n_rows × 4 bytes)│ all versions
|
||||
├─────────────────────────────────────────────┤
|
||||
│ 4. Channel preambles (variable) │ v3+
|
||||
├─────────────────────────────────────────────┤
|
||||
│ 5. Background waveform (variable) │ v4+
|
||||
├─────────────────────────────────────────────┤
|
||||
│ 6. Waveform data (variable) │ all versions
|
||||
├─────────────────────────────────────────────┤
|
||||
│ 7. PREC section (variable) │ v5 only
|
||||
└─────────────────────────────────────────────┘
|
||||
</code></pre>
|
||||
<hr>
|
||||
<h3 id="1-fixed-header-43-bytes-all-versions">1. Fixed header (43 bytes, all versions)</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Offset</th>
|
||||
<th>Size</th>
|
||||
<th>Type</th>
|
||||
<th>Field</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>0</td>
|
||||
<td>4</td>
|
||||
<td><code>char[4]</code></td>
|
||||
<td><code>magic</code></td>
|
||||
<td><code>SRAS</code> (ASCII, no null terminator). Reject file if this does not match.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>4</td>
|
||||
<td>1</td>
|
||||
<td><code>u8</code></td>
|
||||
<td><code>version</code></td>
|
||||
<td>Format version. This document describes version <strong>5</strong>.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>5</td>
|
||||
<td>2</td>
|
||||
<td><code>u16</code></td>
|
||||
<td><code>n_angles</code></td>
|
||||
<td>Number of scan angles (rotation positions). ≥ 1.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>7</td>
|
||||
<td>2</td>
|
||||
<td><code>u16</code></td>
|
||||
<td><code>n_rows</code></td>
|
||||
<td>Number of scan rows (Y positions). ≥ 1.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>9</td>
|
||||
<td>4</td>
|
||||
<td><code>f32</code></td>
|
||||
<td><code>x_start_mm</code></td>
|
||||
<td>X position of the first frame in the first row, in millimetres.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>13</td>
|
||||
<td>4</td>
|
||||
<td><code>f32</code></td>
|
||||
<td><code>x_delta_mm</code></td>
|
||||
<td>Pre-computed pixel pitch in mm (<code>velocity_mm_s / laser_freq_hz</code>). Provided for convenience; readers should prefer the derived value.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>17</td>
|
||||
<td>4</td>
|
||||
<td><code>f32</code></td>
|
||||
<td><code>velocity_mm_s</code></td>
|
||||
<td>Scanner stage velocity, mm s⁻¹. Used together with <code>laser_freq_hz</code> to compute pixel pitch.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>21</td>
|
||||
<td>4</td>
|
||||
<td><code>f32</code></td>
|
||||
<td><code>laser_freq_hz</code></td>
|
||||
<td>Laser repetition rate, Hz.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>25</td>
|
||||
<td>4</td>
|
||||
<td><code>u32</code></td>
|
||||
<td><code>n_frames_hdr</code></td>
|
||||
<td><strong>v2–v4:</strong> the <em>configured</em> frame count written before acquisition; may exceed actual frames acquired (use file-size arithmetic to obtain the true count). <strong>v5:</strong> the <em>actual</em> acquired frame count — authoritative; readers must not re-derive it from file size.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>29</td>
|
||||
<td>4</td>
|
||||
<td><code>u32</code></td>
|
||||
<td><code>samples_per_frame</code></td>
|
||||
<td>ADC samples per waveform (<code>spf</code>).</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>33</td>
|
||||
<td>8</td>
|
||||
<td><code>f64</code></td>
|
||||
<td><code>sample_rate_hz</code></td>
|
||||
<td>Oscilloscope sample rate, Hz (e.g. 5 × 10⁹ for 5 GS/s).</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>41</td>
|
||||
<td>1</td>
|
||||
<td><code>u8</code></td>
|
||||
<td><code>bytes_per_sample</code></td>
|
||||
<td>ADC word size: <code>1</code> → <code>i8</code>, <code>2</code> → <code>i16</code> (big-endian).</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>42</td>
|
||||
<td>1</td>
|
||||
<td><code>u8</code></td>
|
||||
<td><code>n_channels</code></td>
|
||||
<td>Number of channels per frame. Currently always <code>3</code>.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr>
|
||||
<h3 id="2-angle-table-all-versions">2. Angle table (all versions)</h3>
|
||||
<p>Immediately follows the fixed header.</p>
|
||||
<pre><code>n_angles × f32 — scan angle in degrees
|
||||
</code></pre>
|
||||
<p>Each entry is a big-endian <code>f32</code> giving the sample rotation angle in degrees at which that angle index was acquired.</p>
|
||||
<hr>
|
||||
<h3 id="3-row-position-table-all-versions">3. Row position table (all versions)</h3>
|
||||
<p>Immediately follows the angle table.</p>
|
||||
<pre><code>n_rows × f32 — Y position of each row, in millimetres
|
||||
</code></pre>
|
||||
<hr>
|
||||
<h3 id="4-channel-preambles-v3">4. Channel preambles (v3+)</h3>
|
||||
<p>One entry per channel, in channel-index order (CH1 first).</p>
|
||||
<pre><code>for each channel:
|
||||
u16 preamble_length — byte count of the UTF-8 string that follows
|
||||
utf8[N] preamble — Tektronix WFMOutpre string
|
||||
</code></pre>
|
||||
<p>The preamble is the oscilloscope's <code>WFMOutpre</code> response string. Readers extract the following keys (case-insensitive, space-separated value):</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Key</th>
|
||||
<th>Stored unit</th>
|
||||
<th>Conversion to mV</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>YMULT</code></td>
|
||||
<td>V count⁻¹</td>
|
||||
<td>multiply by 1000</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>YOFF</code></td>
|
||||
<td>ADC counts</td>
|
||||
<td>used directly</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>YZERO</code></td>
|
||||
<td>V</td>
|
||||
<td>multiply by 1000</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p><strong>v2 fallback:</strong> when preambles are absent, readers use:</p>
|
||||
<ul>
|
||||
<li><code>YMULT</code> = 1.5625 mV count⁻¹ (50 mV/div, 8 div, 8-bit ADC)</li>
|
||||
<li><code>YOFF</code> = −87.04 ADC counts (scope position = −2.72 div)</li>
|
||||
<li><code>YZERO</code> = 0 mV</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h3 id="5-background-waveform-v4">5. Background waveform (v4+)</h3>
|
||||
<pre><code>u32 n_bg_samples — number of i8 ADC samples that follow
|
||||
i8[n_bg] background — one representative CH1 background shot
|
||||
</code></pre>
|
||||
<p>The background waveform has the same <code>samples_per_frame</code> length as a normal CH1 waveform. It is subtracted from each CH1 waveform before FFT processing when background subtraction is enabled. When <code>n_bg_samples == 0</code> the section is present but empty.</p>
|
||||
<hr>
|
||||
<h3 id="6-waveform-data-all-versions">6. Waveform data (all versions)</h3>
|
||||
<p>Begins immediately after the fixed header (v2), preambles (v3), or background waveform (v4+). The waveform data is a flat, contiguous array with the following logical shape, stored in row-major (C) order:</p>
|
||||
<pre><code>waveform_data[n_angles][n_rows][n_channels][n_frames][samples_per_frame]
|
||||
</code></pre>
|
||||
<p>Each element is a signed ADC count of size <code>bytes_per_sample</code>:</p>
|
||||
<ul>
|
||||
<li><code>bytes_per_sample == 1</code> → <code>i8</code></li>
|
||||
<li><code>bytes_per_sample == 2</code> → <code>i16</code> big-endian</li>
|
||||
</ul>
|
||||
<p><strong>Total byte count:</strong></p>
|
||||
<pre><code>waveform_bytes = n_angles × n_rows × n_channels × n_frames × samples_per_frame × bytes_per_sample
|
||||
</code></pre>
|
||||
<h4 id="index-semantics">Index semantics</h4>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Dimension</th>
|
||||
<th>Range</th>
|
||||
<th>Meaning</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>[a]</code></td>
|
||||
<td>0 … n_angles−1</td>
|
||||
<td>Scan angle (rotation position)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>[r]</code></td>
|
||||
<td>0 … n_rows−1</td>
|
||||
<td>Row (Y position); row 0 is the first acquired</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>[c]</code></td>
|
||||
<td>0 … n_channels−1</td>
|
||||
<td>Channel (0=CH1 RF, 1=CH3 Bias A, 2=CH4 Bias B)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>[f]</code></td>
|
||||
<td>0 … n_frames−1</td>
|
||||
<td>Frame (X position) within the row</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>[s]</code></td>
|
||||
<td>0 … spf−1</td>
|
||||
<td>Sample index within the waveform</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4 id="frame-count-determination">Frame-count determination</h4>
|
||||
<ul>
|
||||
<li><strong>v5:</strong> use <code>n_frames_hdr</code> directly; do not use file-size arithmetic.</li>
|
||||
<li><strong>v2–v4:</strong> <code>n_frames = floor((file_bytes_after_header_sections) / (bytes_per_sample × n_angles × n_rows × n_channels × samples_per_frame))</code>. Any remainder bytes are a partial trailing row and are discarded.</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h3 id="7-prec-section-v5">7. PREC section (v5)</h3>
|
||||
<p>The PREC section is appended immediately after the waveform data and is present if and only if <code>version == 5</code> and the file size exceeds <code>waveform_end_offset</code>.</p>
|
||||
<pre><code>waveform_end_offset = data_offset + waveform_bytes
|
||||
</code></pre>
|
||||
<p>where <code>data_offset</code> is the file offset of the first waveform byte (the byte immediately after the background waveform, or after the angle/row tables for v2 files).</p>
|
||||
<h4 id="prec-header-8-bytes">PREC header (8 bytes)</h4>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Offset (relative)</th>
|
||||
<th>Size</th>
|
||||
<th>Type</th>
|
||||
<th>Field</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>0</td>
|
||||
<td>4</td>
|
||||
<td><code>char[4]</code></td>
|
||||
<td><code>prec_magic</code></td>
|
||||
<td><code>PREC</code> (ASCII). Absent or wrong magic → ignore section.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>4</td>
|
||||
<td>1</td>
|
||||
<td><code>u8</code></td>
|
||||
<td><code>prec_version</code></td>
|
||||
<td>PREC format version. Currently <code>1</code>.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>5</td>
|
||||
<td>1</td>
|
||||
<td><code>u8</code></td>
|
||||
<td><code>flags</code></td>
|
||||
<td>Bitmask (see below).</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>6</td>
|
||||
<td>2</td>
|
||||
<td><code>u16</code></td>
|
||||
<td><code>n_stored</code></td>
|
||||
<td>Number of angle entries that follow. 0 ≤ <code>n_stored</code> ≤ <code>n_angles</code>.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h5 id="flags-byte">Flags byte</h5>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Bit</th>
|
||||
<th>Mask</th>
|
||||
<th>Meaning when set</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>0</td>
|
||||
<td><code>0x01</code></td>
|
||||
<td><code>bg_sub_applied</code> — background waveform was subtracted from CH1 before the FFT when these images were computed.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1–7</td>
|
||||
<td>—</td>
|
||||
<td>Reserved, must be zero on write; readers must ignore.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4 id="prec-angle-entries">PREC angle entries</h4>
|
||||
<p>Repeated <code>n_stored</code> times, in arbitrary angle-index order:</p>
|
||||
<pre><code>for each stored angle:
|
||||
u16 angle_idx — index into the angle table (0-based)
|
||||
f32[n_rows×n_frames] peak_freq_mhz — CH1 FFT peak frequency, MHz, row-major
|
||||
f32[n_rows×n_frames] dc4_mv — CH4 waveform mean, mV, row-major
|
||||
f32[n_rows×n_frames] dc3_mv — CH3 waveform mean, mV, row-major
|
||||
</code></pre>
|
||||
<p>All image arrays are <code>f32</code> big-endian, stored in row-major order: element <code>[r][f]</code> is at offset <code>(r × n_frames + f) × 4</code> bytes within the array.</p>
|
||||
<p><strong><code>peak_freq_mhz</code></strong> is computed without any DC-threshold masking (i.e. the FFT is run on every pixel unconditionally). Readers apply the <code>dc4_mv</code> threshold at display time:</p>
|
||||
<pre><code>pixel is valid ⟺ dc4_mv[r][f] ≥ threshold_mv
|
||||
display_value = peak_freq_mhz[r][f] if valid, else 0
|
||||
</code></pre>
|
||||
<p><strong><code>dc4_mv</code> / <code>dc3_mv</code></strong> are the mean of all ADC samples in the respective channel waveform, converted to millivolts using the channel calibration:</p>
|
||||
<pre><code>dc_mv = (adc_mean − YOFF) × YMULT + YZERO
|
||||
</code></pre>
|
||||
<h4 id="when-readers-must-bypass-the-prec-fast-path">When readers must bypass the PREC fast path</h4>
|
||||
<p>Readers must fall back to real-time FFT computation (ignoring stored <code>peak_freq_mhz</code>) when any of the following are true:</p>
|
||||
<ul>
|
||||
<li>Time-domain gating is active (zeroing samples outside a time window changes peak frequency).</li>
|
||||
<li>Zero-padding (<code>n_fft ≠ samples_per_frame</code>) is requested (changes bin spacing).</li>
|
||||
<li>The reader's background-subtraction setting does not match <code>flags.bg_sub_applied</code>.</li>
|
||||
</ul>
|
||||
<hr>
|
||||
<h2 id="derived-quantities">Derived quantities</h2>
|
||||
<pre><code>pixel_pitch_mm = velocity_mm_s / laser_freq_hz
|
||||
|
||||
x_axis_mm[f] = x_start_mm + f × pixel_pitch_mm (f = 0 … n_frames−1)
|
||||
|
||||
time_axis_ns[s] = s / sample_rate_hz × 1e9 (s = 0 … spf−1)
|
||||
|
||||
freq_axis_mhz[k] = k × sample_rate_hz / (n_fft × 1e6) (k = 0 … n_fft/2)
|
||||
where n_fft = samples_per_frame unless zero-padding is active
|
||||
|
||||
velocity_ms[r][f] = peak_freq_mhz[r][f] × grating_um (grating_um user-supplied)
|
||||
</code></pre>
|
||||
<hr>
|
||||
<h2 id="adc-calibration">ADC calibration</h2>
|
||||
<p>Convert raw ADC counts to millivolts:</p>
|
||||
<pre><code>voltage_mv = (adc_count − YOFF) × YMULT_mv + YZERO_mv
|
||||
</code></pre>
|
||||
<p>Invert (mV → ADC count):</p>
|
||||
<pre><code>adc_count = (voltage_mv − YZERO_mv) / YMULT_mv + YOFF
|
||||
</code></pre>
|
||||
<p>where <code>YMULT_mv</code> is YMULT in mV count⁻¹ (= scope YMULT in V count⁻¹ × 1000).</p>
|
||||
<hr>
|
||||
<h2 id="waveform-data-layout-detail">Waveform data layout detail</h2>
|
||||
<p>For a scan with <code>n_angles=2</code>, <code>n_rows=3</code>, <code>n_channels=3</code>, <code>n_frames=4</code>, <code>spf=5</code> the layout is:</p>
|
||||
<pre><code>angle 0
|
||||
row 0
|
||||
CH1: [s0 s1 s2 s3 s4] [s0 s1 s2 s3 s4] [s0 s1 s2 s3 s4] [s0 s1 s2 s3 s4]
|
||||
frame 0 frame 1 frame 2 frame 3
|
||||
CH3: …(same layout)…
|
||||
CH4: …(same layout)…
|
||||
row 1
|
||||
…
|
||||
row 2
|
||||
…
|
||||
angle 1
|
||||
…
|
||||
</code></pre>
|
||||
<p>The flat byte offset of sample <code>s</code> of frame <code>f</code>, channel <code>c</code>, row <code>r</code>, angle <code>a</code> is:</p>
|
||||
<pre><code>offset = data_offset
|
||||
+ (a × n_rows × n_channels × n_frames × spf
|
||||
+ r × n_channels × n_frames × spf
|
||||
+ c × n_frames × spf
|
||||
+ f × spf
|
||||
+ s)
|
||||
× bytes_per_sample
|
||||
</code></pre>
|
||||
<hr>
|
||||
<h2 id="size-reference">Size reference</h2>
|
||||
<p>Approximate sizes for representative scans (<code>bytes_per_sample = 1</code>, <code>n_channels = 3</code>).</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>n_angles</th>
|
||||
<th>n_rows</th>
|
||||
<th>n_frames</th>
|
||||
<th>spf</th>
|
||||
<th>Waveform data</th>
|
||||
<th>PREC section</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>1</td>
|
||||
<td>500</td>
|
||||
<td>500</td>
|
||||
<td>400</td>
|
||||
<td>300 MB</td>
|
||||
<td>12 MB</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>4</td>
|
||||
<td>500</td>
|
||||
<td>500</td>
|
||||
<td>400</td>
|
||||
<td>1.2 GB</td>
|
||||
<td>48 MB</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>1</td>
|
||||
<td>2000</td>
|
||||
<td>2000</td>
|
||||
<td>400</td>
|
||||
<td>4.8 GB</td>
|
||||
<td>48 MB</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>4</td>
|
||||
<td>2000</td>
|
||||
<td>2000</td>
|
||||
<td>400</td>
|
||||
<td>19.2 GB</td>
|
||||
<td>192 MB</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>8</td>
|
||||
<td>2000</td>
|
||||
<td>2000</td>
|
||||
<td>400</td>
|
||||
<td>38.4 GB</td>
|
||||
<td>384 MB</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>16</td>
|
||||
<td>2000</td>
|
||||
<td>2000</td>
|
||||
<td>400</td>
|
||||
<td>76.8 GB</td>
|
||||
<td>768 MB</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p><strong>PREC section size formula:</strong></p>
|
||||
<pre><code>prec_bytes = 8 + n_stored × (2 + 3 × n_rows × n_frames × 4)
|
||||
</code></pre>
|
||||
<hr>
|
||||
<h2 id="compatibility-notes">Compatibility notes</h2>
|
||||
<h3 id="reading-v5-files-with-a-v4-reader">Reading v5 files with a v4 reader</h3>
|
||||
<p>A v4 reader that only accepts versions <code>{2, 3, 4}</code> will reject a v5 file with an "unsupported version" error. This is intentional: a v4 reader would derive <code>n_frames</code> from the file size, incorrectly including the PREC bytes in the sample count, producing a silently wrong reshape.</p>
|
||||
<h3 id="producing-v5-files">Producing v5 files</h3>
|
||||
<p>v5 files are produced by the SRAS viewer's <strong>"Pre-process and Save as v5"</strong> action. The procedure is:</p>
|
||||
<ol>
|
||||
<li>Copy the source file (any version) verbatim.</li>
|
||||
<li>Set <code>version = 5</code> at byte offset 4.</li>
|
||||
<li>Set <code>n_frames_hdr</code> at byte offset 25 to the actual acquired frame count.</li>
|
||||
<li>Truncate the copy to <code>data_offset + waveform_bytes</code> (removes any pre-existing stale PREC tail).</li>
|
||||
<li>Compute <code>peak_freq_mhz</code>, <code>dc4_mv</code>, and <code>dc3_mv</code> for every angle using chunked FFT.</li>
|
||||
<li>Append the PREC section.</li>
|
||||
</ol>
|
||||
<h3 id="partially-written-prec-sections">Partially-written PREC sections</h3>
|
||||
<p>If <code>n_stored < n_angles</code> (e.g. pre-processing was interrupted), the file is still valid. Readers use stored images for the angles present in the PREC section and fall back to real-time FFT for the remainder. Readers must check <code>angle_idx</code> bounds on each entry and stop parsing on an out-of-range value.</p>
|
||||
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
+379
@@ -0,0 +1,379 @@
|
||||
# SRAS File Format Specification
|
||||
|
||||
**Format family:** `.sras`
|
||||
**Byte order:** Big-endian (network byte order) throughout, unless noted.
|
||||
**Version history:** v2 (baseline), v3 (scope calibration), v4 (background waveform), v5 (precomputed images + guaranteed frame count).
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. [Overview](#overview)
|
||||
2. [Type notation](#type-notation)
|
||||
3. [Version history](#version-history)
|
||||
4. [File structure](#file-structure)
|
||||
- [Fixed header (all versions)](#1-fixed-header-43-bytes-all-versions)
|
||||
- [Angle table (all versions)](#2-angle-table-all-versions)
|
||||
- [Row position table (all versions)](#3-row-position-table-all-versions)
|
||||
- [Channel preambles (v3+)](#4-channel-preambles-v3)
|
||||
- [Background waveform (v4+)](#5-background-waveform-v4)
|
||||
- [Waveform data (all versions)](#6-waveform-data-all-versions)
|
||||
- [PREC section (v5)](#7-prec-section-v5)
|
||||
5. [Derived quantities](#derived-quantities)
|
||||
6. [ADC calibration](#adc-calibration)
|
||||
7. [Waveform data layout detail](#waveform-data-layout-detail)
|
||||
8. [Size reference](#size-reference)
|
||||
9. [Compatibility notes](#compatibility-notes)
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
An SRAS file stores the raw RF waveforms captured during a Surface-acoustic-wave Resonance And Spectroscopy (SRAS) scan, along with the scan geometry and scope calibration metadata needed to interpret them.
|
||||
|
||||
A scan consists of one or more **angles** (rotation positions of the sample), each containing a 2-D raster of **rows** × **frames**. At every pixel, `n_channels` waveforms of `samples_per_frame` ADC counts are stored. Channel order is fixed:
|
||||
|
||||
| Index | Hardware channel | Signal |
|
||||
|-------|-----------------|--------|
|
||||
| 0 | CH1 | RF acoustic packet (AC-coupled) |
|
||||
| 1 | CH3 | Bias A — DC mean used for masking |
|
||||
| 2 | CH4 | Bias B — DC mean used for masking |
|
||||
|
||||
---
|
||||
|
||||
## Type notation
|
||||
|
||||
| Symbol | C type | Size | Notes |
|
||||
|--------|--------|------|-------|
|
||||
| `u8` | `uint8_t` | 1 byte | unsigned |
|
||||
| `u16` | `uint16_t` | 2 bytes | big-endian |
|
||||
| `u32` | `uint32_t` | 4 bytes | big-endian |
|
||||
| `i8` | `int8_t` | 1 byte | signed, used for ADC samples when `bytes_per_sample == 1` |
|
||||
| `i16` | `int16_t` | 2 bytes | big-endian signed, used when `bytes_per_sample == 2` |
|
||||
| `f32` | `float` | 4 bytes | big-endian IEEE 754 single |
|
||||
| `f64` | `double` | 8 bytes | big-endian IEEE 754 double |
|
||||
| `char[N]` | — | N bytes | raw bytes, no null terminator unless noted |
|
||||
| `utf8[N]` | — | N bytes | UTF-8 string, length-prefixed (see preamble section) |
|
||||
|
||||
---
|
||||
|
||||
## Version history
|
||||
|
||||
| Version | Added |
|
||||
|---------|-------|
|
||||
| 2 | Baseline: fixed header, angle table, row table, raw waveform data. No scope calibration (fallback constants used by readers). |
|
||||
| 3 | Per-channel Tektronix WFMOutpre preamble strings carrying YMULT / YOFF / YZERO calibration. |
|
||||
| 4 | Background waveform section: one CH1 reference shot subtracted from each CH1 frame before FFT. |
|
||||
| 5 | **(this document)** Version byte incremented to 5. `n_frames_hdr` is now the *actual* acquired frame count (authoritative). PREC section appended after waveform data with precomputed FFT-peak and DC images for instant re-display. |
|
||||
|
||||
> **v2 note:** Version 1 is not defined; version 2 is the lowest observed in the field.
|
||||
|
||||
---
|
||||
|
||||
## File structure
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ 1. Fixed header (43 bytes) │ all versions
|
||||
├─────────────────────────────────────────────┤
|
||||
│ 2. Angle table (n_angles × 4 bytes)│ all versions
|
||||
├─────────────────────────────────────────────┤
|
||||
│ 3. Row position table (n_rows × 4 bytes)│ all versions
|
||||
├─────────────────────────────────────────────┤
|
||||
│ 4. Channel preambles (variable) │ v3+
|
||||
├─────────────────────────────────────────────┤
|
||||
│ 5. Background waveform (variable) │ v4+
|
||||
├─────────────────────────────────────────────┤
|
||||
│ 6. Waveform data (variable) │ all versions
|
||||
├─────────────────────────────────────────────┤
|
||||
│ 7. PREC section (variable) │ v5 only
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 1. Fixed header (43 bytes, all versions)
|
||||
|
||||
| Offset | Size | Type | Field | Description |
|
||||
|--------|------|------|-------|-------------|
|
||||
| 0 | 4 | `char[4]` | `magic` | `SRAS` (ASCII, no null terminator). Reject file if this does not match. |
|
||||
| 4 | 1 | `u8` | `version` | Format version. This document describes version **5**. |
|
||||
| 5 | 2 | `u16` | `n_angles` | Number of scan angles (rotation positions). ≥ 1. |
|
||||
| 7 | 2 | `u16` | `n_rows` | Number of scan rows (Y positions). ≥ 1. |
|
||||
| 9 | 4 | `f32` | `x_start_mm` | X position of the first frame in the first row, in millimetres. |
|
||||
| 13 | 4 | `f32` | `x_delta_mm` | Pre-computed pixel pitch in mm (`velocity_mm_s / laser_freq_hz`). Provided for convenience; readers should prefer the derived value. |
|
||||
| 17 | 4 | `f32` | `velocity_mm_s` | Scanner stage velocity, mm s⁻¹. Used together with `laser_freq_hz` to compute pixel pitch. |
|
||||
| 21 | 4 | `f32` | `laser_freq_hz` | Laser repetition rate, Hz. |
|
||||
| 25 | 4 | `u32` | `n_frames_hdr` | **v2–v4:** the *configured* frame count written before acquisition; may exceed actual frames acquired (use file-size arithmetic to obtain the true count). **v5:** the *actual* acquired frame count — authoritative; readers must not re-derive it from file size. |
|
||||
| 29 | 4 | `u32` | `samples_per_frame` | ADC samples per waveform (`spf`). |
|
||||
| 33 | 8 | `f64` | `sample_rate_hz` | Oscilloscope sample rate, Hz (e.g. 5 × 10⁹ for 5 GS/s). |
|
||||
| 41 | 1 | `u8` | `bytes_per_sample` | ADC word size: `1` → `i8`, `2` → `i16` (big-endian). |
|
||||
| 42 | 1 | `u8` | `n_channels` | Number of channels per frame. Currently always `3`. |
|
||||
|
||||
---
|
||||
|
||||
### 2. Angle table (all versions)
|
||||
|
||||
Immediately follows the fixed header.
|
||||
|
||||
```
|
||||
n_angles × f32 — scan angle in degrees
|
||||
```
|
||||
|
||||
Each entry is a big-endian `f32` giving the sample rotation angle in degrees at which that angle index was acquired.
|
||||
|
||||
---
|
||||
|
||||
### 3. Row position table (all versions)
|
||||
|
||||
Immediately follows the angle table.
|
||||
|
||||
```
|
||||
n_rows × f32 — Y position of each row, in millimetres
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. Channel preambles (v3+)
|
||||
|
||||
One entry per channel, in channel-index order (CH1 first).
|
||||
|
||||
```
|
||||
for each channel:
|
||||
u16 preamble_length — byte count of the UTF-8 string that follows
|
||||
utf8[N] preamble — Tektronix WFMOutpre string
|
||||
```
|
||||
|
||||
The preamble is the oscilloscope's `WFMOutpre` response string. Readers extract the following keys (case-insensitive, space-separated value):
|
||||
|
||||
| Key | Stored unit | Conversion to mV |
|
||||
|-----|-------------|-----------------|
|
||||
| `YMULT` | V count⁻¹ | multiply by 1000 |
|
||||
| `YOFF` | ADC counts | used directly |
|
||||
| `YZERO` | V | multiply by 1000 |
|
||||
|
||||
**v2 fallback:** when preambles are absent, readers use:
|
||||
- `YMULT` = 1.5625 mV count⁻¹ (50 mV/div, 8 div, 8-bit ADC)
|
||||
- `YOFF` = −87.04 ADC counts (scope position = −2.72 div)
|
||||
- `YZERO` = 0 mV
|
||||
|
||||
---
|
||||
|
||||
### 5. Background waveform (v4+)
|
||||
|
||||
```
|
||||
u32 n_bg_samples — number of i8 ADC samples that follow
|
||||
i8[n_bg] background — one representative CH1 background shot
|
||||
```
|
||||
|
||||
The background waveform has the same `samples_per_frame` length as a normal CH1 waveform. It is subtracted from each CH1 waveform before FFT processing when background subtraction is enabled. When `n_bg_samples == 0` the section is present but empty.
|
||||
|
||||
---
|
||||
|
||||
### 6. Waveform data (all versions)
|
||||
|
||||
Begins immediately after the fixed header (v2), preambles (v3), or background waveform (v4+). The waveform data is a flat, contiguous array with the following logical shape, stored in row-major (C) order:
|
||||
|
||||
```
|
||||
waveform_data[n_angles][n_rows][n_channels][n_frames][samples_per_frame]
|
||||
```
|
||||
|
||||
Each element is a signed ADC count of size `bytes_per_sample`:
|
||||
- `bytes_per_sample == 1` → `i8`
|
||||
- `bytes_per_sample == 2` → `i16` big-endian
|
||||
|
||||
**Total byte count:**
|
||||
|
||||
```
|
||||
waveform_bytes = n_angles × n_rows × n_channels × n_frames × samples_per_frame × bytes_per_sample
|
||||
```
|
||||
|
||||
#### Index semantics
|
||||
|
||||
| Dimension | Range | Meaning |
|
||||
|-----------|-------|---------|
|
||||
| `[a]` | 0 … n_angles−1 | Scan angle (rotation position) |
|
||||
| `[r]` | 0 … n_rows−1 | Row (Y position); row 0 is the first acquired |
|
||||
| `[c]` | 0 … n_channels−1 | Channel (0=CH1 RF, 1=CH3 Bias A, 2=CH4 Bias B) |
|
||||
| `[f]` | 0 … n_frames−1 | Frame (X position) within the row |
|
||||
| `[s]` | 0 … spf−1 | Sample index within the waveform |
|
||||
|
||||
#### Frame-count determination
|
||||
|
||||
- **v5:** use `n_frames_hdr` directly; do not use file-size arithmetic.
|
||||
- **v2–v4:** `n_frames = floor((file_bytes_after_header_sections) / (bytes_per_sample × n_angles × n_rows × n_channels × samples_per_frame))`. Any remainder bytes are a partial trailing row and are discarded.
|
||||
|
||||
---
|
||||
|
||||
### 7. PREC section (v5)
|
||||
|
||||
The PREC section is appended immediately after the waveform data and is present if and only if `version == 5` and the file size exceeds `waveform_end_offset`.
|
||||
|
||||
```
|
||||
waveform_end_offset = data_offset + waveform_bytes
|
||||
```
|
||||
|
||||
where `data_offset` is the file offset of the first waveform byte (the byte immediately after the background waveform, or after the angle/row tables for v2 files).
|
||||
|
||||
#### PREC header (8 bytes)
|
||||
|
||||
| Offset (relative) | Size | Type | Field | Description |
|
||||
|-------------------|------|------|-------|-------------|
|
||||
| 0 | 4 | `char[4]` | `prec_magic` | `PREC` (ASCII). Absent or wrong magic → ignore section. |
|
||||
| 4 | 1 | `u8` | `prec_version` | PREC format version. Currently `1`. |
|
||||
| 5 | 1 | `u8` | `flags` | Bitmask (see below). |
|
||||
| 6 | 2 | `u16` | `n_stored` | Number of angle entries that follow. 0 ≤ `n_stored` ≤ `n_angles`. |
|
||||
|
||||
##### Flags byte
|
||||
|
||||
| Bit | Mask | Meaning when set |
|
||||
|-----|------|-----------------|
|
||||
| 0 | `0x01` | `bg_sub_applied` — background waveform was subtracted from CH1 before the FFT when these images were computed. |
|
||||
| 1–7 | — | Reserved, must be zero on write; readers must ignore. |
|
||||
|
||||
#### PREC angle entries
|
||||
|
||||
Repeated `n_stored` times, in arbitrary angle-index order:
|
||||
|
||||
```
|
||||
for each stored angle:
|
||||
u16 angle_idx — index into the angle table (0-based)
|
||||
f32[n_rows×n_frames] peak_freq_mhz — CH1 FFT peak frequency, MHz, row-major
|
||||
f32[n_rows×n_frames] dc4_mv — CH4 waveform mean, mV, row-major
|
||||
f32[n_rows×n_frames] dc3_mv — CH3 waveform mean, mV, row-major
|
||||
```
|
||||
|
||||
All image arrays are `f32` big-endian, stored in row-major order: element `[r][f]` is at offset `(r × n_frames + f) × 4` bytes within the array.
|
||||
|
||||
**`peak_freq_mhz`** is computed without any DC-threshold masking (i.e. the FFT is run on every pixel unconditionally). Readers apply the `dc4_mv` threshold at display time:
|
||||
|
||||
```
|
||||
pixel is valid ⟺ dc4_mv[r][f] ≥ threshold_mv
|
||||
display_value = peak_freq_mhz[r][f] if valid, else 0
|
||||
```
|
||||
|
||||
**`dc4_mv` / `dc3_mv`** are the mean of all ADC samples in the respective channel waveform, converted to millivolts using the channel calibration:
|
||||
|
||||
```
|
||||
dc_mv = (adc_mean − YOFF) × YMULT + YZERO
|
||||
```
|
||||
|
||||
#### When readers must bypass the PREC fast path
|
||||
|
||||
Readers must fall back to real-time FFT computation (ignoring stored `peak_freq_mhz`) when any of the following are true:
|
||||
|
||||
- Time-domain gating is active (zeroing samples outside a time window changes peak frequency).
|
||||
- Zero-padding (`n_fft ≠ samples_per_frame`) is requested (changes bin spacing).
|
||||
- The reader's background-subtraction setting does not match `flags.bg_sub_applied`.
|
||||
|
||||
---
|
||||
|
||||
## Derived quantities
|
||||
|
||||
```
|
||||
pixel_pitch_mm = velocity_mm_s / laser_freq_hz
|
||||
|
||||
x_axis_mm[f] = x_start_mm + f × pixel_pitch_mm (f = 0 … n_frames−1)
|
||||
|
||||
time_axis_ns[s] = s / sample_rate_hz × 1e9 (s = 0 … spf−1)
|
||||
|
||||
freq_axis_mhz[k] = k × sample_rate_hz / (n_fft × 1e6) (k = 0 … n_fft/2)
|
||||
where n_fft = samples_per_frame unless zero-padding is active
|
||||
|
||||
velocity_ms[r][f] = peak_freq_mhz[r][f] × grating_um (grating_um user-supplied)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ADC calibration
|
||||
|
||||
Convert raw ADC counts to millivolts:
|
||||
|
||||
```
|
||||
voltage_mv = (adc_count − YOFF) × YMULT_mv + YZERO_mv
|
||||
```
|
||||
|
||||
Invert (mV → ADC count):
|
||||
|
||||
```
|
||||
adc_count = (voltage_mv − YZERO_mv) / YMULT_mv + YOFF
|
||||
```
|
||||
|
||||
where `YMULT_mv` is YMULT in mV count⁻¹ (= scope YMULT in V count⁻¹ × 1000).
|
||||
|
||||
---
|
||||
|
||||
## Waveform data layout detail
|
||||
|
||||
For a scan with `n_angles=2`, `n_rows=3`, `n_channels=3`, `n_frames=4`, `spf=5` the layout is:
|
||||
|
||||
```
|
||||
angle 0
|
||||
row 0
|
||||
CH1: [s0 s1 s2 s3 s4] [s0 s1 s2 s3 s4] [s0 s1 s2 s3 s4] [s0 s1 s2 s3 s4]
|
||||
frame 0 frame 1 frame 2 frame 3
|
||||
CH3: …(same layout)…
|
||||
CH4: …(same layout)…
|
||||
row 1
|
||||
…
|
||||
row 2
|
||||
…
|
||||
angle 1
|
||||
…
|
||||
```
|
||||
|
||||
The flat byte offset of sample `s` of frame `f`, channel `c`, row `r`, angle `a` is:
|
||||
|
||||
```
|
||||
offset = data_offset
|
||||
+ (a × n_rows × n_channels × n_frames × spf
|
||||
+ r × n_channels × n_frames × spf
|
||||
+ c × n_frames × spf
|
||||
+ f × spf
|
||||
+ s)
|
||||
× bytes_per_sample
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Size reference
|
||||
|
||||
Approximate sizes for representative scans (`bytes_per_sample = 1`, `n_channels = 3`).
|
||||
|
||||
| n_angles | n_rows | n_frames | spf | Waveform data | PREC section |
|
||||
|----------|--------|----------|-----|---------------|-------------|
|
||||
| 1 | 500 | 500 | 400 | 300 MB | 12 MB |
|
||||
| 4 | 500 | 500 | 400 | 1.2 GB | 48 MB |
|
||||
| 1 | 2000 | 2000 | 400 | 4.8 GB | 48 MB |
|
||||
| 4 | 2000 | 2000 | 400 | 19.2 GB | 192 MB |
|
||||
| 8 | 2000 | 2000 | 400 | 38.4 GB | 384 MB |
|
||||
| 16 | 2000 | 2000 | 400 | 76.8 GB | 768 MB |
|
||||
|
||||
**PREC section size formula:**
|
||||
|
||||
```
|
||||
prec_bytes = 8 + n_stored × (2 + 3 × n_rows × n_frames × 4)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Compatibility notes
|
||||
|
||||
### Reading v5 files with a v4 reader
|
||||
|
||||
A v4 reader that only accepts versions `{2, 3, 4}` will reject a v5 file with an "unsupported version" error. This is intentional: a v4 reader would derive `n_frames` from the file size, incorrectly including the PREC bytes in the sample count, producing a silently wrong reshape.
|
||||
|
||||
### Producing v5 files
|
||||
|
||||
v5 files are produced by the SRAS viewer's **"Pre-process and Save as v5"** action. The procedure is:
|
||||
|
||||
1. Copy the source file (any version) verbatim.
|
||||
2. Set `version = 5` at byte offset 4.
|
||||
3. Set `n_frames_hdr` at byte offset 25 to the actual acquired frame count.
|
||||
4. Truncate the copy to `data_offset + waveform_bytes` (removes any pre-existing stale PREC tail).
|
||||
5. Compute `peak_freq_mhz`, `dc4_mv`, and `dc3_mv` for every angle using chunked FFT.
|
||||
6. Append the PREC section.
|
||||
|
||||
### Partially-written PREC sections
|
||||
|
||||
If `n_stored < n_angles` (e.g. pre-processing was interrupted), the file is still valid. Readers use stored images for the angles present in the PREC section and fall back to real-time FFT for the remainder. Readers must check `angle_idx` bounds on each entry and stop parsing on an out-of-range value.
|
||||
+210
@@ -0,0 +1,210 @@
|
||||
# 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 — 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
|
||||
(`>` in Python's `struct` module).
|
||||
|
||||
---
|
||||
|
||||
## 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 — `6` |
|
||||
| 5 | 2 | `uint16` | `n_angles` | Number of GR rotation angles |
|
||||
| 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:** 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).
|
||||
|
||||
```
|
||||
angle[0], angle[1], …, angle[n_angles - 1]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Per-Angle Geometry Table
|
||||
|
||||
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.
|
||||
|
||||
```
|
||||
# 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
|
||||
|
||||
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 (ragged)
|
||||
|
||||
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 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[a]-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:
|
||||
```
|
||||
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
|
||||
> 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. For a row belonging to angle *a*, the physical X position of
|
||||
that pulse is:
|
||||
|
||||
```
|
||||
x_k = x_start[a] + k * (velocity_mm_s / laser_freq_hz)
|
||||
```
|
||||
|
||||
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) |
|
||||
| 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. |
|
||||
| 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). |
|
||||
|
||||
+251
@@ -0,0 +1,251 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
sras_average.py — Waveform-averaging utility for .sras files.
|
||||
|
||||
Reduces memory footprint by coherently averaging every N consecutive frames
|
||||
along the acquisition axis, writing a new .sras file with n_frames / N frames.
|
||||
|
||||
Usage:
|
||||
python sras_average.py input.sras output.sras --n 10
|
||||
python sras_average.py input.sras output.sras --n 10 --discard-remainder
|
||||
|
||||
Options:
|
||||
--n INT Number of frames to average into one (required).
|
||||
--discard-remainder Drop trailing frames that don't fill a complete group.
|
||||
Default: include a partial average for the last group.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import struct
|
||||
import argparse
|
||||
import numpy as np
|
||||
from pathlib import Path
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Header format — must match sras_viewer.py exactly
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
HDR_FMT = ">4sBHHffffIIdBB"
|
||||
HDR_SIZE = struct.calcsize(HDR_FMT) # 43 bytes
|
||||
|
||||
|
||||
def parse_args():
|
||||
p = argparse.ArgumentParser(
|
||||
description="Average every N waveforms in a .sras file and write a new file."
|
||||
)
|
||||
p.add_argument("input", help="Input .sras file")
|
||||
p.add_argument("output", help="Output .sras file")
|
||||
p.add_argument("--n", type=int, required=True, metavar="N",
|
||||
help="Number of consecutive frames to average into one")
|
||||
p.add_argument("--discard-remainder", action="store_true",
|
||||
help="Drop trailing frames that don't fill a complete group of N")
|
||||
return p.parse_args()
|
||||
|
||||
|
||||
def read_sras(path: Path):
|
||||
"""Read all sections of a .sras file and return them as a dict."""
|
||||
with open(path, "rb") as f:
|
||||
header_bytes = f.read(HDR_SIZE)
|
||||
fields = struct.unpack(HDR_FMT, header_bytes)
|
||||
|
||||
(magic, ver, n_angles, n_rows, x_start, x_delta, vel, freq,
|
||||
n_frames_hdr, spf, sr, bps, n_ch) = fields
|
||||
|
||||
if magic != b"SRAS":
|
||||
raise ValueError(f"Not a .sras file (bad magic: {magic!r})")
|
||||
if ver not in (2, 3, 4):
|
||||
raise ValueError(f"Unsupported .sras version: {ver}")
|
||||
|
||||
with open(path, "rb") as f:
|
||||
f.seek(HDR_SIZE)
|
||||
|
||||
angles = f.read(n_angles * 4) # big-endian float32 array, raw bytes
|
||||
y_pos = f.read(n_rows * 4) # big-endian float32 array, raw bytes
|
||||
|
||||
preambles = [] # list of raw bytes (length-prefixed strings)
|
||||
if ver >= 3:
|
||||
for _ in range(n_ch):
|
||||
(length,) = struct.unpack(">H", f.read(2))
|
||||
preambles.append(f.read(length))
|
||||
|
||||
background = b"" # raw bytes for the v4 background block
|
||||
if ver >= 4:
|
||||
(n_bg,) = struct.unpack(">I", f.read(4))
|
||||
background = f.read(n_bg)
|
||||
|
||||
raw = f.read() # all waveform data
|
||||
|
||||
# -----------------------------------------------------------------------
|
||||
# Determine actual frame count from file size (the header value can be
|
||||
# wrong — the viewer does the same correction)
|
||||
# -----------------------------------------------------------------------
|
||||
total_samples = len(raw) // bps
|
||||
samples_per_pixel = n_ch * spf # samples in one (angle, row, frame) cell
|
||||
samples_per_full = n_angles * n_rows * samples_per_pixel
|
||||
|
||||
actual_n_frames = total_samples // (n_angles * n_rows * samples_per_pixel)
|
||||
good_bytes = actual_n_frames * samples_per_full * bps
|
||||
|
||||
# Decode waveform data
|
||||
dtype = np.int8 if bps == 1 else ">i2"
|
||||
data = np.frombuffer(raw[:good_bytes], dtype=dtype)
|
||||
data = data.reshape(n_angles, n_rows, n_ch, actual_n_frames, spf)
|
||||
# Work in int16 (safe intermediate for both int8 and int16 inputs)
|
||||
data = data.astype(np.int16)
|
||||
|
||||
return {
|
||||
"ver": ver, "n_angles": n_angles, "n_rows": n_rows,
|
||||
"x_start": x_start, "x_delta": x_delta, "vel": vel, "freq": freq,
|
||||
"n_frames_hdr": n_frames_hdr, "spf": spf, "sr": sr,
|
||||
"bps": bps, "n_ch": n_ch,
|
||||
"angles_raw": angles, "y_pos_raw": y_pos,
|
||||
"preambles": preambles, "background": background,
|
||||
"data": data, # shape: (n_angles, n_rows, n_ch, n_frames, spf), int16
|
||||
}
|
||||
|
||||
|
||||
def average_frames(data: np.ndarray, n: int, discard_remainder: bool) -> np.ndarray:
|
||||
"""Average every N frames along axis 3.
|
||||
|
||||
data shape: (n_angles, n_rows, n_ch, n_frames, spf)
|
||||
Returns array of shape (n_angles, n_rows, n_ch, n_out, spf).
|
||||
"""
|
||||
n_frames = data.shape[3]
|
||||
|
||||
n_full = n_frames // n
|
||||
remainder = n_frames % n
|
||||
|
||||
# Average full groups using reshape-trick (no Python loop)
|
||||
if n_full > 0:
|
||||
full = data[:, :, :, :n_full * n, :] # trim to full groups
|
||||
full = full.reshape(data.shape[0], data.shape[1], data.shape[2],
|
||||
n_full, n, data.shape[4]) # (..., n_out, n, spf)
|
||||
averaged = full.mean(axis=4).astype(np.int16) # (..., n_out, spf)
|
||||
else:
|
||||
averaged = np.empty((*data.shape[:3], 0, data.shape[4]), dtype=np.int16)
|
||||
|
||||
if remainder > 0 and not discard_remainder:
|
||||
tail = data[:, :, :, n_full * n:, :] # shape (..., remainder, spf)
|
||||
tail_avg = tail.mean(axis=3, keepdims=True).astype(np.int16)
|
||||
averaged = np.concatenate([averaged, tail_avg], axis=3)
|
||||
|
||||
return averaged
|
||||
|
||||
|
||||
def write_sras(path: Path, src: dict, data_out: np.ndarray):
|
||||
"""Write a new .sras file with the averaged waveform data."""
|
||||
ver = src["ver"]
|
||||
bps = src["bps"]
|
||||
n_out = data_out.shape[3]
|
||||
|
||||
# Pack header — update only n_frames_hdr; everything else stays the same
|
||||
header = struct.pack(
|
||||
HDR_FMT,
|
||||
b"SRAS",
|
||||
ver,
|
||||
src["n_angles"],
|
||||
src["n_rows"],
|
||||
src["x_start"],
|
||||
src["x_delta"],
|
||||
src["vel"],
|
||||
src["freq"],
|
||||
n_out, # updated frame count
|
||||
src["spf"],
|
||||
src["sr"],
|
||||
bps,
|
||||
src["n_ch"],
|
||||
)
|
||||
|
||||
# Encode waveform data back to original dtype
|
||||
if bps == 1:
|
||||
raw_out = np.clip(data_out, -128, 127).astype(np.int8).tobytes()
|
||||
else:
|
||||
# big-endian int16
|
||||
raw_out = data_out.astype(">i2").tobytes()
|
||||
|
||||
with open(path, "wb") as f:
|
||||
f.write(header)
|
||||
f.write(src["angles_raw"])
|
||||
f.write(src["y_pos_raw"])
|
||||
|
||||
if ver >= 3:
|
||||
for preamble_bytes in src["preambles"]:
|
||||
f.write(struct.pack(">H", len(preamble_bytes)))
|
||||
f.write(preamble_bytes)
|
||||
|
||||
if ver >= 4:
|
||||
bg = src["background"]
|
||||
f.write(struct.pack(">I", len(bg)))
|
||||
f.write(bg)
|
||||
|
||||
f.write(raw_out)
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
|
||||
if args.n < 1:
|
||||
print("Error: --n must be at least 1.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
in_path = Path(args.input)
|
||||
out_path = Path(args.output)
|
||||
|
||||
if not in_path.exists():
|
||||
print(f"Error: input file not found: {in_path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if out_path.resolve() == in_path.resolve():
|
||||
print("Error: output path must differ from input path.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print(f"Reading {in_path} ...", flush=True)
|
||||
src = read_sras(in_path)
|
||||
|
||||
n_frames_in = src["data"].shape[3]
|
||||
print(f" Version : v{src['ver']}")
|
||||
print(f" Angles : {src['n_angles']}")
|
||||
print(f" Rows : {src['n_rows']}")
|
||||
print(f" Frames (actual): {n_frames_in}")
|
||||
print(f" Channels : {src['n_ch']}")
|
||||
print(f" Samples/frame : {src['spf']}")
|
||||
print(f" Bytes/sample : {src['bps']}")
|
||||
|
||||
if args.n == 1:
|
||||
print("--n 1: no averaging needed; copying file as-is.")
|
||||
import shutil
|
||||
shutil.copy2(in_path, out_path)
|
||||
print(f"Wrote {out_path}")
|
||||
return
|
||||
|
||||
if args.n > n_frames_in:
|
||||
print(f"Warning: --n ({args.n}) exceeds available frames ({n_frames_in}). "
|
||||
"The entire dataset will be averaged into a single frame.")
|
||||
|
||||
print(f"\nAveraging every {args.n} frames ...", flush=True)
|
||||
data_out = average_frames(src["data"], args.n, args.discard_remainder)
|
||||
n_frames_out = data_out.shape[3]
|
||||
|
||||
n_full = n_frames_in // args.n
|
||||
remainder = n_frames_in % args.n
|
||||
if remainder and not args.discard_remainder:
|
||||
status = f"({n_full} full groups + 1 partial group of {remainder})"
|
||||
elif remainder and args.discard_remainder:
|
||||
status = f"({n_full} full groups, {remainder} trailing frames discarded)"
|
||||
else:
|
||||
status = f"({n_full} full groups)"
|
||||
|
||||
print(f" {n_frames_in} frames -> {n_frames_out} frames {status}")
|
||||
|
||||
print(f"\nWriting {out_path} ...", flush=True)
|
||||
write_sras(out_path, src, data_out)
|
||||
|
||||
in_mb = in_path.stat().st_size / 1024**2
|
||||
out_mb = out_path.stat().st_size / 1024**2
|
||||
print(f" Input size : {in_mb:.1f} MB")
|
||||
print(f" Output size: {out_mb:.1f} MB ({out_mb/in_mb*100:.1f}% of input)")
|
||||
print("Done.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+2953
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,3 @@
|
||||
PyQt6==6.10.2
|
||||
numpy==2.4.1
|
||||
matplotlib==3.10.8
|
||||
Reference in New Issue
Block a user