Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions docs/createc_dat_reader.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,22 @@ contains decoded DAC values after ProbeFlow's safety cleanup:
partial scan;
5. the first stored column is removed by default.

## Trailing Appendix

Every healthy real Createc image fixture carries a small appendix after the
image planes: four spare scan-line buffers (`4 * Num.X` floats), plus a
32-float zero block in files with the newer header variant. The buffers are
zero-filled apart from an occasional stray sample at the start — the
turnaround point of a line that was never scanned. The appendix size does not
scale with the channel count (10- and 40-channel fixtures carry the same
`4 * Num.X + 32` floats as 4-channel ones).

Because this appendix is normal format layout rather than data loss, the
reader records its size in `ignored_tail_float_count` but does not emit a
warning for tails within the `4 * Num.X + 32` budget. Tails larger than that
budget indicate payload the reader does not understand and still produce a
decode warning, which load paths surface to users.

The historical `raw_channels_dac` property remains as a compatibility alias, but
it points to the same cleaned decoded arrays. Use `original_header`,
`original_Nx`, and `original_Ny` when the acquisition dimensions or raw header
Expand Down
17 changes: 15 additions & 2 deletions probeflow/io/readers/createc_dat.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,17 @@
z_scale_m_per_dac,
)

# Createc writes a small appendix after the image planes: four spare
# scan-line buffers (4 * Nx floats, zero-filled apart from an occasional
# stray turnaround sample), plus a 32-float zero block in newer headers.
# Every healthy real fixture carries it regardless of channel count, so a
# tail within this budget is normal format layout, not data loss; it is
# still recorded as ``ignored_tail_float_count`` on the report. Only tails
# larger than the budget indicate payload this reader does not understand
# and stay loud.
_TAIL_SCANLINE_BUFFER_COUNT = 4
_TAIL_TRAILER_FLOAT_COUNT = 32


@dataclass(frozen=True)
class CreatecChannelInfo:
Expand Down Expand Up @@ -133,10 +144,12 @@ def read_createc_dat_report(
num_chan = _detect_channel_count(payload_float_count, Ny, Nx, header)
needed = num_chan * Ny * Nx
ignored_tail = payload_float_count - needed
if ignored_tail:
expected_tail = _TAIL_SCANLINE_BUFFER_COUNT * Nx + _TAIL_TRAILER_FLOAT_COUNT
if ignored_tail > expected_tail:
warnings.append(
f"ignored {ignored_tail} trailing float32 value(s) after "
f"{num_chan} channel(s)"
f"{num_chan} channel(s) — more than the expected Createc "
f"appendix of {expected_tail} value(s) for this image width"
)

arr = np.frombuffer(payload, dtype="<f4", count=needed).reshape((num_chan, Ny, Nx))
Expand Down
35 changes: 35 additions & 0 deletions tests/test_createc_dat_decode.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

TESTDATA = Path(__file__).resolve().parents[1] / "test_data"
QPLUS_10CH_DAT = TESTDATA / "createc_scan_qplus_10ch_afm.dat"
CREATEC_SCAN_FIXTURES = sorted(TESTDATA.glob("createc_scan_*.dat"))


def test_report_records_trim_first_column_and_tail(first_sample_dat):
Expand Down Expand Up @@ -279,6 +280,40 @@ def test_header_two_channels_with_extra_telemetry_tail_stays_two(tmp_path):
assert report.ignored_tail_float_count >= 12


@pytest.mark.parametrize("path", CREATEC_SCAN_FIXTURES, ids=lambda p: p.name)
def test_normal_appendix_tail_is_recorded_but_not_warned(path):
"""Every healthy Createc image carries a zero appendix after the planes
(four spare scan-line buffers plus an optional 32-float block). The
viewer surfaces ``scan.warnings`` in the status bar, so this normal
format layout must not produce a user-facing warning; the size stays
available on the report for diagnostics.
"""
report = read_createc_dat_report(path, include_raw=False)

assert 0 < report.ignored_tail_float_count <= 4 * report.original_Nx + 32
assert not any("trailing float32" in w for w in report.warnings)

scan = load_scan(path)
assert not any("trailing float32" in w for w in scan.warnings)


def test_oversized_tail_still_warns(tmp_path):
"""A tail beyond the known appendix budget means payload the reader does
not understand and must stay loud."""
dat = tmp_path / "oversized_tail.dat"
header = b"[Paramco32]\nNum.X=2\nNum.Y=2\nChannels=2\n"
# 2 channels of (2,2) pixels = 8 floats, then 41 tail floats: one more
# than the 4*Nx + 32 = 40 appendix budget.
payload = np.arange(1, 50, dtype="<f4").tobytes()
dat.write_bytes(header + b"DATA" + zlib.compress(payload))

report = read_createc_dat_report(dat, include_raw=False)

assert report.detected_channel_count == 2
assert report.ignored_tail_float_count == 41
assert any("trailing float32" in w for w in report.warnings)


def test_anonymized_qplus_fixture_decodes_all_10_channels():
report = read_createc_dat_report(QPLUS_10CH_DAT)
scan = load_scan(QPLUS_10CH_DAT)
Expand Down
Loading