Skip to content

Refactor channel position to x/y/size/headstage to match CereLink #184#32

Merged
cboulay merged 4 commits into
mainfrom
cboulay/cmp_position
Jun 5, 2026
Merged

Refactor channel position to x/y/size/headstage to match CereLink #184#32
cboulay merged 4 commits into
mainfrom
cboulay/cmp_position

Conversation

@cboulay

@cboulay cboulay commented Jun 4, 2026

Copy link
Copy Markdown
Member

This branch updates ezmsg-blackrock to track the channel-metadata rework in CerebusOSS/CereLink#184. Upstream repurposed cbPKT_CHANINFO.position[] to carry true electrode geometry (x, y, size, headstage_id) in micrometers, changed pycbsdk.cmp.parse_cmp to key entries by device (bank, term) with flat fields and verbatim labels, and moved bank/electrode out of position into the chaninfo BANK/TERM fields. Our CereLink*Producer device path and our offline ChannelMapProcessor both consumed the old layout, so both needed updating. While here, the per-channel ch axis dtype was reworked: x/y/size are now int32 (matching the library's native storage), a headstage field was added, and the unused device field was removed.

Motivation

In the old layout, position[] held (col, row, bank_letter_index, electrode) and parse_cmp returned a dict keyed by ordinal chan_id with an entry.position 4-tuple and hs{N}--prefixed labels. CereLink #184 frees the position slots 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 channel x/y would be misread (the slots now mean something different), bank/elec would be garbage, and ChannelMapProcessor would crash outright because entry.position no 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_metadata now reads bank/term from chaninfo via get_channels_field(ChannelType.FRONTEND, ChanInfoField.BANK/TERM) (these fields predate #184 and exist in both pycbsdk versions) and keeps the full position tuple (x, y, size, headstage_id). The cache value is now ch_id -> (x, y, size, headstage, bank_num, term).
  • _build_ch_info populates the new size and headstage fields, takes x/y from position[0:2] (now micrometers), and derives bank/elec from the cached chaninfo bank_num/term rather than the old position[2:4].

CMP overlay path — src/ezmsg/blackrock/channel_map.py

  • The overlay loop iterates the new (bank, term) keys and computes the channel index directly as (bank - 1) * 32 + (term - 1); start_chan is already folded into bank upstream via its // 32 bank offset, so no separate ordinal handling is needed. It writes entry.x/entry.y/entry.size (cast to int), entry.headstage, the verbatim entry.label, and derives bank/elec from the key.
  • _fill_auto_grid now scales its grid step to the CMP's detected electrode pitch (new _cmp_pitch helper, ≈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 get size = step and headstage = 0.
  • The base-layer carry-through block was removed: it existed only to copy the device field forward, which no longer exists, and every dtype field is now set by the label/overlay/auto-grid passes.

CHANNEL_DTYPE

  • x, y changed from float32 to int32, matching how the library stores position. A new int32 size field (electrode size in µm, 0 = unspecified) and int32 headstage field (1-based headstage id, 0 = none/auto) were added. The unused device field was removed — stream provenance will instead be folded in naturally during concat from each AxisArray's .attrs, rather than being a per-channel column nothing populated.

Behavior changes worth calling out

  • Labels are now verbatim. hs_id no longer prefixes labels with hs{N}-; it populates the headstage field instead. Two channels on different headstages can now carry identical label strings, distinguished by headstage/bank/elec. Any downstream code keying channels purely by label string should be aware.
  • Coordinates are micrometers, as integers. x/y/size are now real geometry in µm (the manufacturer index grids are scaled by the 400 µm Utah-array pitch upstream), not column/row indices.
  • start_chan is quantized to whole banks on the CMP path. Because #184 applies start_chan as a // 32 bank offset rather than a per-row ordinal, values that are not of the form 1, 33, 65, 129, … snap to a bank boundary. This differs from the old arbitrary-offset behavior.
  • CHANNEL_DTYPE field set changed (added size, headstage; removed device; x/y now int). Consumers that read these fields by name are unaffected by the additions/removal; any code that assumed float x/y or the presence of device will need a look.

Dependency note

These changes require a pycbsdk build that includes CerebusOSS/CereLink#184, which is not yet released to PyPI. During development the working tree points pyproject.toml's [tool.uv.sources] at a local editable checkout of the patched pycbsdk; that local-path source is machine-specific and is not intended to merge as-is. Before merging, the pycbsdk floor 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 rebuilt libcbsdk native library from the #184 C++ source; the pure-Python parse_cmp (CMP overlay path) needs only the Python package.

TODO

  • bump pycbsdk dependency

cboulay and others added 4 commits June 3, 2026 22:51
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>
@cboulay cboulay merged commit 1911fec into main Jun 5, 2026
16 checks passed
@cboulay cboulay deleted the cboulay/cmp_position branch June 5, 2026 05:37
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.

1 participant