Skip to content

Pack format 16, 24 and 32 samples without per-byte arrays - #607

Merged
tompollard merged 1 commit into
MIT-LCP:mainfrom
TheNetherWatcher:faster-dat-packing
Oct 31, 2026
Merged

tompollard merged 1 commit into
MIT-LCP:mainfrom
TheNetherWatcher:faster-dat-packing

Conversation

@TheNetherWatcher

Copy link
Copy Markdown
Contributor

Closes #464.

wr_dat_file packed formats 16, 24 and 32 by masking every sample into a separate int64 array for each byte, interleaving those arrays, and then casting to uint8. That uses about 4 bytes of memory for every byte written, plus an up-front copy of the whole signal.

As @bemoody suggested in #464, this now converts the signal to little-endian 16 or 32 bit integers and views the result as bytes (dropping the top byte of each sample for format 24), like format 80 already does. The explicit < keeps the output little-endian on any machine.

The up-front copy is gone too. It was only there because formats 80 and 212 changed the array in place, so those two now compute their offsets out of place instead. The caller's array is still never modified.

Peak memory and time of wr_dat_file for 4 hours × 8 channels at 500 Hz (460 MB of int64 samples), best of 3:

Format Time before → after Peak memory before → after
16 331 → 25 ms 1978 → 110 MiB
24 613 → 188 ms 3021 → 385 MiB
32 840 → 56 ms 3955 → 220 MiB
80 60 → 55 ms 494 → 494 MiB
212 620 → 251 ms 961 → 961 MiB

The written files are unchanged. The new TestWrDatFile tests compare the bytes for every writable format against a sample-by-sample reference (int.to_bytes and the format 212 bit layout) for odd and even lengths, the format's minimum and maximum values, Fortran-ordered or sliced input, and multi-frequency (expanded) signals. They also check that the input array isn't modified. They pass on main as well, and fail if the byte order or the dropped byte for format 24 is changed.

pytest -m "not network" passes (121 tests), and the docs build passes.

@tompollard tompollard left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, this looks great, and the tests against a sample-by-sample reference are a nice touch!

I checked it independently:

  • The written bytes are identical to main for formats 80, 212, 16, 24, 32 and 516, with int16/int32/int64 input, C-ordered, Fortran-ordered and sliced arrays, odd and even lengths, and multi-frequency signals. The input array is never modified.
  • Removing the up-front copy is safe: the FLAC branch only shifts arrays that astype has already copied.
  • For a 1-hour, 8-channel signal at 500 Hz, peak memory went from 494 to 27 MiB for format 16, 755 to 96 MiB for format 24, and 989 to 55 MiB for format 32.

@TheNetherWatcher
TheNetherWatcher force-pushed the faster-dat-packing branch 2 times, most recently from 5e319be to 39fec24 Compare October 31, 2026 18:41
wr_dat_file split every sample into separate int64 arrays for each
byte, then interleaved and cast them, using about 4 bytes of memory per
byte written. Convert to little-endian 16 or 32 bit integers and view
the result as bytes instead (dropping the top byte for format 24), as
format 80 already does.

The input is no longer copied up front. Formats 80 and 212 now compute
their offsets out of place, so the caller's array is still not
modified.

Closes #464.
@tompollard
tompollard merged commit 3f103d2 into MIT-LCP:main Oct 31, 2026
18 checks passed
tompollard added a commit that referenced this pull request Oct 31, 2026
`check_sig_cohesion` finds the minimum and maximum of each channel to
check that the digital samples fit the format. It used Python's built-in
`min` and `max`, which go through a NumPy array one element at a time,
so this check took about 85% of the time of `wrsamp` for large records.
This uses `np.min` and `np.max` instead, in both the single-frequency
and the expanded (multi-frequency) path.

Time for a 4-hour, 8-channel record at 500 Hz (57.6 million samples),
written from digital samples:

| Format | check_sig_cohesion before → after | wrsamp before → after |
|---|---|---|
| 16 | 2.41 → 0.07 s | 2.85 → 0.45 s |
| 212 | 2.41 → 0.08 s | 3.07 → 0.78 s |

The written files are unchanged. There were no tests for this check, so
I added `TestSigCohesion`: a value just outside the range in the last
sample is rejected (in both paths), and the smallest and largest values
of each format are allowed. These pass on `main` as well, and fail if
the check skips the last sample.

Related: #607 speeds up the byte packing in the same `wrsamp` call. The
two don't touch the same lines, apart from both adding a changelog entry
and a test class at the end of the file.

`pytest -m "not network"` passes (101 tests), and the docs build passes.
@TheNetherWatcher
TheNetherWatcher deleted the faster-dat-packing branch November 1, 2026 06:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Memory Exception When Merging Large Volumes of Waveform Data Files Using wrdb.wrsamp()

2 participants