Files
Thomas Ales aa06fa1460 Per-angle background capture: v7/v11 .sras layout
A multi-angle scan runs for hours, but every angle was referenced against
one background captured before the first row of the first angle. That
reference has drifted by the last angle, and comparing angles — the whole
point of a multi-angle scan — was comparing each one against a noise floor
measured at whichever angle came first.

Every angle now captures its own. Before each angle's rows, the operator is
prompted to switch the Genesis laser off, the engine averages a fresh CH1
record, and the operator switches it back on. The data block therefore reads
[background][scan][background][scan] …, one pair per angle.

Format v7 (scan) and v11 (SAW check) carry the background inside the data
block, one length-prefixed block ahead of each angle's rows; the single
block that sat between the preambles and the data is gone. Per-angle offsets
now come from a walk of the data block at parse time rather than arithmetic
over the geometry table, and an angle whose background is not fully on disk
is the frontier — nothing of it was written yet.

v6/v10 files still read: SrasFile hands their one background to every angle,
so readers never branch on the version. Nothing writes them, and a resume
refuses them, since a re-acquired angle writes a block the old layout has no
room for. A resumed v7 angle rewrites its background in place, and the
engine checks the new block fits the room the file has before writing it —
anything else would shift every row behind it.

Two fixes made along the way:

  * QtScanController never accepted file_version, so every scan launched
    from the app raised TypeError at construction.
  * angle_status() left its cursor parked at the frontier, so every angle
    past it reported the frontier's own data_offset — which handed a resumed
    scan the same write position for several angles. Two recorded offsets in
    tests/golden/sras_expected.json are corrected accordingly.

The v6 goldens stay as parser fixtures; the writer is now locked against
bytes the test lays out from scan_format.md itself.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 12:44:25 -05:00

59 lines
2.3 KiB
Python
Raw Permalink 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.
"""Shared constants and writers for the .sras test fixtures.
The constants mirror the values tests/gen_goldens.py used when the committed
golden files were generated against the pre-refactor code (commit d185676);
they must never change, or the comparisons against those files stop meaning
anything. Those goldens are legacy v6 files — the one background per file
layout — and are now read-only fixtures for the parser.
``write_v7`` builds the current layout (one background per angle, inside the
data block) over the same geometry, for the tests that need a file this
version of the app could actually have written.
"""
from core.scan_geometry import build_plan
from core.sras_format import VERSION, create_scan_file, write_background_block
SPF = 8
SAMPLE_RATE = 6.25e9
CHANNELS = [1, 3, 4]
PREAMBLES = [f"WFMOUTPRE:CH{ch};SYNTHETIC;PT_FMT Y;XINCR 1.6E-10" for ch in CHANNELS]
BACKGROUND = bytes(range(SPF))
# build_plan inputs for the fixture geometry: 2 angles × 3 rows × 4 frames
TINY_PLAN_ARGS = dict(x_start=1.0, y_start=1.0, x_delta=0.02, y_delta=0.02,
num_angles=2, row_spacing=0.01)
LASER_FREQ_HZ = 20000.0
VELOCITY_MM_S = 100.0
def synthetic_frame(ai, ri, ci, fi):
return bytes((ai * 7 + ri * 5 + ci * 3 + fi + s) % 256 for s in range(SPF))
def tiny_plan():
"""The fixture geometry: 2 angles × 3 rows × 4 frames."""
return build_plan(**TINY_PLAN_ARGS, laser_freq_hz=LASER_FREQ_HZ,
velocity_mm_s=VELOCITY_MM_S)
def angle_background(ai):
"""A background that differs per angle, so tests can tell them apart."""
return bytes((ai * 11 + s) % 256 for s in range(SPF))
def write_v7(path, plan=None, version=None):
"""Write a complete current-format file: [background][rows] per angle."""
plan = plan if plan is not None else tiny_plan()
f = create_scan_file(path, plan, SPF, SAMPLE_RATE, PREAMBLES,
version=VERSION if version is None else version)
try:
for ai, pa in enumerate(plan.per_angle):
write_background_block(f, angle_background(ai))
for ri in range(pa.n_rows):
for ci in range(len(CHANNELS)):
for fi in range(pa.n_frames):
f.write(synthetic_frame(ai, ri, ci, fi))
finally:
f.close()
return plan