Declare chunk_dim and prime the channel fingerprint - #5
Merged
Merged
Conversation
The file predates the repo's current ruff config and had never been run through it, so the pre-commit hook reflows it wholesale on first touch. Separated here so the change that follows is readable.
Two things every consumer of a replayed file needs, and only the reader can supply. `chunk_dim` names the dimension messages accumulate along. A consumer caches state against the stream's configuration -- channel count, labels, sample rate -- and must exclude the one dimension whose length is just however much of the file this chunk covered. It cannot reliably infer which that is: it is `time` here but `win` downstream of a windowing stage, so a guess either thrashes on chunk-size jitter or stops noticing real changes. Both iterators declare it, and it holds whether the stream is regular or carries per-sample timestamps. `CoordinateAxis.fingerprint` is a content digest, computed on first access and cached on the instance. Each template builds its `ch` axis once and every message reuses that object, so priming costs one checksum per stream. Left cold it is computed by the first stateful consumer in this process -- and, because unpickling builds a new axis object per message, by the first consumer in every other process, on every message. Unverified by tests: this repo has no XDF fixture, only the placeholder in tests/test_iter.py. The change mirrors ezmsg-neo and ezmsg-nwb, where it is covered. Requires ezmsg 3.10.0b2 for both fields.
`tests/test_iter.py` was a `test_dummy` placeholder with a TODO asking for a small XDF source, so nothing in this package was covered -- including the chunk_dim and fingerprint change in the previous commit. The fixture is generated rather than checked in as a binary, so it stays readable and adjustable: a test needing an irregular stream, a string stream or a different channel layout changes an argument instead of asking someone to produce a new recording. `create_test_xdf.py` writes the XDF chunked format directly, with each field pinned against what pyxdf's reader actually consumes -- `_read_varlen_int`, `_read_chunk3` and the tag dispatch in `load_xdf` -- since that is the only reader these files ever meet. Three choices in the generator are deliberate and would otherwise look arbitrary: * Every sample carries an explicit timestamp rather than relying on delta decompression. The compressed form encodes "same as last plus 1/srate", which would make the fixture silently agree with any reader that got the nominal rate wrong. * Streams start at t=10 s, not 0, so a reader honouring `rezero` is distinguishable from one ignoring it. * ClockOffset chunks are written even though there is no clock skew to model. Without them pyxdf logs "Segments and clock-segments differ" on every load, and a fixture that warns every time trains readers to ignore warnings. The default file has two streams -- a 100 Hz 4-channel float32 ramp and an irregular string marker stream -- split across several sample chunks so the reader's chunk stitching is exercised rather than arriving as one block. 20 tests cover sample values and ordering, chunk_dur, rezero, the nominal rate reaching the axis gain, per-sample timestamps on the irregular stream, force_single_sample, and both fields the previous commit added. A class at the end checks the fixture itself, since a wrong fixture would make every other assertion agree with the wrong thing. Verified by mutation rather than by the tests merely passing: dropping chunk_dim fails 3, dropping the fingerprint priming fails 3, and rebuilding the channel axis per message instead of reusing it fails 2.
The push and pull_request triggers were commented out, leaving workflow_dispatch as the only way to run the suite. That was reasonable when the suite was a single `test_dummy` placeholder, and is not now that it covers the iterators, the producers and both units. Every other ezmsg source package triggers on push to main and on pull requests; blackrock, neo and nwb also include dev, which is where these PRs are based, so that is matched here. Without it a PR to dev reports only the publish workflow's build job, which does not run a single test.
setup-uv is configured with `cache-dependency-glob: "uv.lock"`, but no
uv.lock is committed here -- nor in any sibling ezmsg package. The glob matches
nothing and the action fails the job before a single dependency is installed:
##[error]No file in /home/runner/work/ezmsg-xdf/ezmsg-xdf matched to
[uv.lock], make sure you have checked out the target repository
Latent since the workflow was written, and invisible until the previous commit
turned the triggers on. ezmsg-lsl already keys on pyproject.toml; this matches
it.
ruff has flagged W291 on line 65 since before any of this work. It went unnoticed because the test workflow, which runs the lint step, was never triggered; with the triggers on it fails every matrix entry. Fixing it means touching the file, and this one predates the repo's current ruff config just as iter.py did, so the pre-commit hook reflows it wholesale on first touch. Both are in one commit here because the whitespace fix cannot be staged without the reformat.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two things every consumer of a replayed file needs, and only the reader can supply. Both are set once per stream; neither costs anything per message.
Two commits. The first is
ruff-formatonly:iter.pypredates the repo's current ruff config and had never been run through it, so the pre-commit hook reflows it wholesale on first touch. Separated so the second commit reads as the 18 lines it actually is.chunk_dimA stateful consumer caches state — filter coefficients, per-channel history, resolved channel indices — against the stream's configuration: channel count, labels, sample rate. It has to exclude the one dimension whose length is just however much of the file this chunk covered.
It cannot reliably infer which that is. It's
timehere, butwindownstream of a windowing stage, so a consumer that assumestimeeither thrashes on chunk-size jitter or, worse, stops noticing a real change:Only the producer, which named the dims, knows. Both iterators declare it, and it holds whether the stream is regular or carries per-sample timestamps. Both emit paths already go through
replace(template, ..., axes={**template.axes, "time": ...}), sofast_replacecarries the field for free.CoordinateAxis.fingerprintA content digest, computed on first access and cached on the instance — it's what lets a consumer notice that channels were relabelled at a fixed channel count, which is otherwise silent and numerically destructive:
Each template builds its
chaxis once and every message reuses that object, so priming costs one checksum per stream. Left cold it's computed by the first stateful consumer in this process — and, because unpickling builds a new axis object per message, by the first consumer in every other process, on every message.Testing
20 passed (was 1 placeholder). A third commit adds the XDF fixture this repo never had, because
tests/test_iter.pywas stilltest_dummywith its TODO and nothing here was covered.tests/create_test_xdf.pywrites the XDF chunked format directly, with each field pinned against what pyxdf's reader actually consumes. Generated rather than checked in as a binary, so a test needing a different stream layout changes an argument instead of asking someone for a new recording. Three choices are deliberate:rezerois distinguishable from one ignoring it;Segments and clock-segments differon every load.Beyond the two fields above, the tests cover sample values and ordering,
chunk_dur,rezero, the nominal rate reaching the axis gain, per-sample timestamps on the irregular stream, andforce_single_sample. A final class checks the fixture itself, since a wrong fixture would make every other assertion agree with the wrong thing.Verified by mutation rather than by passing: dropping
chunk_dimfails 3 tests, dropping the fingerprint priming fails 3, and rebuilding the channel axis per message instead of reusing it fails 2.Notes
src/ezmsg/xdf/source.pystill failsruffon trailing whitespace. Pre-existing and unrelated, so left alone.ezmsg>=3.10.0b2for both fields. This is a pre-release pin until 3.10.0 final ships.🤖 Generated with Claude Code
Update: the tests now actually run in CI
Adding tests exposed that this repo never ran any. Three commits follow from that, all pre-existing defects that were invisible while the workflow was dormant:
The triggers were commented out.
pushandpull_requestwere both disabled, leavingworkflow_dispatchas the only route — reasonable when the suite was onetest_dummy, not now. Every sibling package triggers on push to main and on pull requests; blackrock, neo and nwb also includedev, which is where these PRs are based, so that's matched here.setup-uvwas keyed on a lock file that doesn't exist.cache-dependency-glob: "uv.lock", but nouv.lockis committed here — nor in any sibling package. The glob matched nothing and the action failed the job before installing a single dependency:ezmsg-lsl already keys on
pyproject.toml; this matches it.source.py:65had trailing whitespace that fails the lint step. I'd noted this as pre-existing and left it alone; with CI on it blocks every matrix entry, so it's fixed. Touching the file meant the pre-commit hook reflowed it, same asiter.py— both are in that one commit since the fix can't be staged without the reformat.13 checks passing across Python 3.10–3.13 on Linux, macOS and Windows.