Refactor channel position to x/y/size/headstage to match CereLink #184#32
Merged
Conversation
…am changes in CereLink (its PR #184).
CerebusOSS/CereLink#184 — the channel-position rework this branch targets — is released in pycbsdk 9.10.0. Bump the floor so the (bank, term)-keyed parse_cmp, x/y/size/headstage position layout, and ChanInfoField.BANK/TERM chaninfo fields are guaranteed present. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When the incoming ch axis already carries structured geometry (e.g. a CereLink source that read x/y/size/bank/elec/headstage from device chaninfo), copy it through verbatim instead of discarding it — so a map already present upstream needs no .cmp file. A new src_mask records the positioned channels; the auto-grid skips them, and a CMP overlay still overrides them (cmp wins over source). A channel counts as positioned when it has a non-origin coordinate, or it is the first channel at the (0, 0) origin: a lone origin electrode is a legitimate corner, but the device parks every unmapped channel at the origin, so origin pile-ups beyond the first fall through to the auto-grid. Unstructured / label-only incoming axes copy nothing and keep the original pure auto-grid behavior. The auto-grid pitch/offset now derive from all placed geometry (CMP or source), not just CMP. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tion
The old API split three mutually-exclusive intents across two fields
(channels=None meaning "all", plus a restrict_to_enabled bool that silently
overrode an explicit channel list). Collapse them into a single field that
carries the intent, so a provided list is always respected:
channels: list[int] | ChannelSelection = ChannelSelection.ALL
- list[int] -> enable exactly these (disable the rest)
- ChannelSelection.ALL -> enable all matching channel_type (default)
- ChannelSelection.ENABLED -> leave the device's enabled set as-is;
only consume what's already on
ENABLED is stream-specific via a polymorphic _enabled_channels hook: the
signal source retunes channels already in a continuous sample group
(disable_others=False), while the spike source leaves spike extraction
untouched (skips set_spike_extraction) and subscribes to whatever has the
SPKOPTS extract bit set. A shared _resolve_channels resolves the selection
to a concrete id list for both sources. SliceConfig.__post_init__ rejects a
non-list/non-enum channels value (catches the bare-string "all" footgun).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.
This branch updates
ezmsg-blackrockto track the channel-metadata rework in CerebusOSS/CereLink#184. Upstream repurposedcbPKT_CHANINFO.position[]to carry true electrode geometry(x, y, size, headstage_id)in micrometers, changedpycbsdk.cmp.parse_cmpto key entries by device(bank, term)with flat fields and verbatim labels, and moved bank/electrode out ofpositioninto the chaninfoBANK/TERMfields. OurCereLink*Producerdevice path and our offlineChannelMapProcessorboth consumed the old layout, so both needed updating. While here, the per-channelchaxis dtype was reworked:x/y/sizeare now int32 (matching the library's native storage), aheadstagefield was added, and the unuseddevicefield was removed.Motivation
In the old layout,
position[]held(col, row, bank_letter_index, electrode)andparse_cmpreturned a dict keyed by ordinalchan_idwith anentry.position4-tuple andhs{N}--prefixed labels. CereLink #184 frees thepositionslots to hold real geometry and matches CMP rows to live channels by the device's own(bank, term)instead of a fragile ordinal assumption. Without these changes our channelx/ywould be misread (the slots now mean something different),bank/elecwould be garbage, andChannelMapProcessorwould crash outright becauseentry.positionno longer exists and the parser keys are now(bank, term)tuples rather than integers.What changed
Device path —
src/ezmsg/blackrock/cerelink.py_cache_channel_metadatanow readsbank/termfrom chaninfo viaget_channels_field(ChannelType.FRONTEND, ChanInfoField.BANK/TERM)(these fields predate #184 and exist in both pycbsdk versions) and keeps the fullpositiontuple(x, y, size, headstage_id). The cache value is nowch_id -> (x, y, size, headstage, bank_num, term)._build_ch_infopopulates the newsizeandheadstagefields, takesx/yfromposition[0:2](now micrometers), and derivesbank/elecfrom the cached chaninfobank_num/termrather than the oldposition[2:4].CMP overlay path —
src/ezmsg/blackrock/channel_map.py(bank, term)keys and computes the channel index directly as(bank - 1) * 32 + (term - 1);start_chanis already folded intobankupstream via its// 32bank offset, so no separate ordinal handling is needed. It writesentry.x/entry.y/entry.size(cast to int),entry.headstage, the verbatimentry.label, and derivesbank/elecfrom the key._fill_auto_gridnow scales its grid step to the CMP's detected electrode pitch (new_cmp_pitchhelper, ≈400 µm for a default Utah array, falling back to 1 when there is no CMP) so auto-laid channels share the CMP's micrometer scale instead of clumping near the origin. Auto-grid channels getsize = stepandheadstage = 0.devicefield forward, which no longer exists, and every dtype field is now set by the label/overlay/auto-grid passes.CHANNEL_DTYPEx,ychanged from float32 to int32, matching how the library storesposition. A new int32sizefield (electrode size in µm, 0 = unspecified) and int32headstagefield (1-based headstage id, 0 = none/auto) were added. The unuseddevicefield was removed — stream provenance will instead be folded in naturally during concat from eachAxisArray's.attrs, rather than being a per-channel column nothing populated.Behavior changes worth calling out
hs_idno longer prefixes labels withhs{N}-; it populates theheadstagefield instead. Two channels on different headstages can now carry identical label strings, distinguished byheadstage/bank/elec. Any downstream code keying channels purely by label string should be aware.x/y/sizeare now real geometry in µm (the manufacturer index grids are scaled by the 400 µm Utah-array pitch upstream), not column/row indices.start_chanis quantized to whole banks on the CMP path. Because #184 appliesstart_chanas a// 32bank offset rather than a per-row ordinal, values that are not of the form1, 33, 65, 129, …snap to a bank boundary. This differs from the old arbitrary-offset behavior.CHANNEL_DTYPEfield set changed (addedsize,headstage; removeddevice;x/ynow int). Consumers that read these fields by name are unaffected by the additions/removal; any code that assumed floatx/yor the presence ofdevicewill need a look.Dependency note
These changes require a
pycbsdkbuild that includes CerebusOSS/CereLink#184, which is not yet released to PyPI. During development the working tree pointspyproject.toml's[tool.uv.sources]at a local editable checkout of the patchedpycbsdk; that local-path source is machine-specific and is not intended to merge as-is. Before merging, thepycbsdkfloor in[project.dependencies]should be bumped to the released version that contains #184 and the local[tool.uv.sources]entry dropped. Note also that exercising the live device path against hardware additionally requires a rebuiltlibcbsdknative library from the #184 C++ source; the pure-Pythonparse_cmp(CMP overlay path) needs only the Python package.TODO