Skip to content

Speed up load_xdf and fix reading truncated sample chunks - #180

Merged
cbrnr merged 1 commit into
xdf-modules:mainfrom
cbrnr:speedup
Oct 24, 2026
Merged

cbrnr merged 1 commit into
xdf-modules:mainfrom
cbrnr:speedup

Conversation

@cbrnr

@cbrnr cbrnr commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

This picks up the proposals from #179 (after a critical review) plus a few things the issue missed. It differs in three ways:

  • When a chunk's length doesn't match its samples, it falls back to the old per-sample reader instead of failing.
  • It deletes the unused .pxd instead of editing it.
  • It also adds lazy debug logging and a fix for truncated chunks.

Measured speedups were 2.5–5x (claimed: 2.7–7x), almost all from chunk parsing. Time series are identical; time stamps differ by up to about 1e-11 s when clock sync and dejittering are on.

Changes

  • Read numeric samples chunks at once instead of sample by sample. Chunks where all or no samples have a time stamp are parsed in a single NumPy call; mixed chunks are walked in memory. If the chunk length does not match the samples (or the file ends early), we fall back to the previous per-sample reader, so files that loaded before still load.
  • Only format per-chunk debug messages when they are logged (this previously also called f.tell() for every chunk).
  • Closed-form line fit in _jitter_removal instead of lstsq (~6.7x faster, no large temporary arrays).
  • Simplified ADMM iteration in _robust_fit (mathematically equivalent, ~2.5x faster, but only ~10 ms per stream).
  • Fix: a chunk cut off at the end of the file is now dropped instead of returning a last sample filled with leftover bytes from the previous sample (readinto silently returned a short read).
  • Remove pyxdf.pxd, which was never part of the build and no longer matched the code.

Performance

Synthetic recordings (64 channels, float32, 500 Hz, 30 min, 32-sample chunks; last row 8 channels, 1000 Hz, 10 min, 2-sample chunks), load_xdf with default settings:

Time stamps in file main this PR Speedup
every sample 0.69 s 0.14 s 5.0x
none 0.63 s 0.19 s 3.4x
first sample per chunk 0.63 s 0.24 s 2.6x
alternating 0.65 s 0.26 s 2.5x
every sample, 2-sample chunks 1.05 s 0.79 s 1.3x

Time series are byte-identical to main. Time stamps are identical without clock synchronization and dejittering; with them, they differ by at most ~1e-11 s because the fits are computed in a different order.

Tests

New test/test_read_chunk.py covers all numeric formats with all time stamp patterns, wrong chunk lengths (first and last chunk), truncated files, empty chunks, flag/length mismatches, and checks _robust_fit against the original implementation.

Read numeric samples chunks at once instead of sample by sample, falling
back to the per-sample reader if the chunk length does not match the
samples. Only format per-chunk debug messages when they are logged,
replace lstsq in the jitter removal with a closed-form line fit, and
simplify the ADMM iteration in _robust_fit.

Also drop a chunk that is cut off at the end of the file instead of
returning a last sample with leftover values from the previous sample,
and remove the unused pyxdf.pxd.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cbrnr

cbrnr commented Oct 23, 2026

Copy link
Copy Markdown
Contributor Author

Ping @sappelhoff - Gemini was not wrong! Could you review and test these changes, because you said another agent did not review it favorably?

@sappelhoff

Copy link
Copy Markdown
Contributor

Thanks for picking this up, I am glad there was something salvageable.

Will dig out the review and will have another look at it (personally and with another Gemini agent)

@cbrnr

cbrnr commented Oct 23, 2026

Copy link
Copy Markdown
Contributor Author

Thanks! It would be helpful if you tried loading some of your real-world XDF files to see if nothing broke.

@sappelhoff

Copy link
Copy Markdown
Contributor

This was the critical agent feedback and how you addressed it:

  1. Bit-for-bit claim on time_stamps when clock sync / dejittering are enabled:
  1. Cython src/pyxdf/pyxdf.pxd incompatibility with np.cumsum(stamps, out=stamps):
  1. Potential 64-bit integer overflow in n * (n * n - 1) / 12 in _jitter_removal:
  1. Updating s.last_timestamp across all fast paths:

Generally I think this PR is great and it does its job.

Tested on a lot (nearly 100) of real life recordings I have here. Some numbers:

image

@cbrnr
cbrnr merged commit cab977a into xdf-modules:main Oct 24, 2026
6 checks passed
@cbrnr
cbrnr deleted the speedup branch October 24, 2026 10:25
@cbrnr

cbrnr commented Oct 24, 2026

Copy link
Copy Markdown
Contributor Author

Very nice, thanks for testing!

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.

2 participants