Build the ch axis from the electrodes table, matching the live source - #25
Merged
Conversation
A live acquisition source hands downstream stages a structured ch axis -- per-channel geometry, labels, headstage, array identity. Replaying the same recording gave a plain array of strings, so every stage that rereferences per array, plots by position, or selects by headstage had to know which of the two it was fed. structured_ch_axis=True builds the live layout from the electrodes table. CHANNEL_DTYPE is field-for-field identical to ezmsg.blackrock.channel_map.CHANNEL_DTYPE -- a test parses it out of that file and compares, rather than importing it, since ezmsg-blackrock needs pycbsdk and should not become a dependency here just to read a file. array_identity is transcribed for the same reason, and pinned by a table of cases. Off by default. The columns beyond label -- rel_x, rel_y, size, bank, term, headstage -- are one acquisition stack's convention, not NWB schema. A file with none of them raises rather than returning a record of zeros: the caller asked for this explicitly, and answering with fabricated geometry is the failure this codebase keeps designing out. Positions come from rel_x/rel_y only, never NWB's x/y. They are different quantities -- rel_* is position within the electrode group, which is what a channel map describes, while x/y/z is location in the brain, nominally in metres. Falling back to x/y would fill a micrometre field with brain coordinates, and since almost every electrodes table carries x/y it would also make has_channel_metadata answer True for files with no channel map at all. The synthetic test fixture is exactly such a file, which is how the fallback got caught. Coordinates are otherwise taken verbatim: the files this reads store micrometres, matching the device's channel map and the int32 micrometres the live source emits, and rescaling on a guess is how a factor becomes silently wrong. array identity derives from connector and headstage, which repeat across devices -- two hubs each with an elec1 on hs1 both give 'hs1-elec1'. Per series that is right and matches live, since a series is one device; across devices it would merge physically separate arrays. group_name distinguishes them, so a warning fires when one identity covers more than one group. Verified against a real recording: no collision within either hub's own stream, and the warning does fire on a hand-built mixture.
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.
A live acquisition source hands downstream stages a structured
chaxis — per-channel geometry, labels, headstage, array identity. Replaying the same recording gave a plain array of strings, so every stage that rereferences per array, plots by position, or selects by headstage had to know which of the two it was being fed.On a real 384-electrode recording:
Available on
NWBSlicer,NWBIteratorSettings, andNWBClockDrivenSettings.Matching the live source
CHANNEL_DTYPEis field-for-field identical toezmsg.blackrock.channel_map.CHANNEL_DTYPE— same fields, same order, same 216-byte itemsize. That is the whole point: weights fitted offline and applied live must cluster channels identically, and a downstream stage readingch['array']orch['x']must not need to know its source.A test parses that dtype out of the blackrock source and compares, rather than importing it.
ezmsg-blackrockrequirespycbsdkand should not become a dependency here just to read a file; parsing keeps the check honest instead of letting it skip when the import fails.array_identityis transcribed for the same reason and pinned by a table of cases.CHANNEL_DTYPEfieldx,yrel_x,rel_ysizesizelabellabelbankbank(1-based int → letter)electermheadstageheadstagearrayOff by default, and loud when it can't work
These columns are one acquisition stack's convention, not NWB schema; most files carry none of them. A file with none raises, naming what was looked for, rather than returning a record of zeros — the caller asked for this explicitly, and answering with fabricated geometry is the failure this codebase keeps designing out.
rel_x/rel_yonly, never NWB'sx/yI had
x/yas a fallback initially and it was wrong twice over. They are different quantities:rel_*is position within the electrode group, which is what a channel map describes and what the live source puts in these fields, whilex/y/zis location in the brain, nominally in metres. The fallback would have filled a micrometre field with brain coordinates — and because almost every electrodes table carriesx/y, it would also have madehas_channel_metadataanswer True for files with no channel map at all.The synthetic test fixture is exactly such a file, which is how it got caught.
Coordinates are otherwise taken verbatim. The files this reads store micrometres, matching the device's channel map and the
int32micrometres the live source emits. A file that followed NWB's metres reading would land 1e6 away — nothing here rescales on a guess, for the same reasonezmsg.nwb.scalingdoesn't.Array-identity collisions are detected
arrayderives from connector and headstage, which repeat across devices: two hubs each with anelec1on headstage 1 both givehs1-elec1. Per series that is correct and matches live, since a series is one device — verified that neither hub's own stream collides internally on the real recording. Across devices it would merge physically separate arrays, sogroup_nameis used to warn when one identity covers more than one group. Verified the warning fires on a hand-built mixture of both hubs' rows.Tests
A fixture builds an electrodes table shaped like an acquisition stack's — two connectors on one headstage plus one on a second, so the identity rule has something to separate — plus a second series over a strict subset in a different order, so region handling is exercised rather than assumed.
270 tests pass; ruff clean. Verified against a real 5.7 GB recording.
🤖 Generated with Claude Code