Match CMP rows to channels by (bank, term); repurpose position[] for x/y/size/headstage#184
Merged
Conversation
Rework how Blackrock .cmp channel-map files are applied to a device's
in-memory channel config.
Matching: rows are now matched to live channels by the device's own
(bank, term) read from chaninfo, instead of by an ordinal channel ID
(the old sort-and-assign start_chan+N scheme). start_chan shifts the
CMP's bank letters by start_chan/32 banks to target a headstage's banks
(129 -> +4 -> bank A maps to device bank E), which makes (bank, term) a
globally-unique join key. This is robust to incomplete CMPs.
position[]: the four cbPKT_CHANINFO.position slots now carry real
geometry {x, y, size, headstage_id} instead of {col, row, bank, elec}.
Columns: the //-comment header line immediately before the data (when
present) is the ground truth for column order, matched by name
(col/c, row/r, bank/b, elec/e, size/s, label/l); no header falls back
to the legacy "col row bank electrode [label]" order, so existing files
parse unchanged.
Units: with no size column and a unit-spaced index grid (every non-zero
col/row delta is exactly 1), values are electrode indices -> size 1 and
x/y/size scaled by the 400 um Utah-array pitch. With a size column, or
non-uniform spacing, values are taken at face value.
Labels: stored verbatim; the hs{N}- prefix is dropped since the stored
headstage id disambiguates reused labels.
pycbsdk/cmp.py is kept in sync with the C++ parser; session.py
docstrings corrected. C++ and Python unit tests updated; the
hardware-gated nplay integration tests join device readback to parser
output via ChanInfoField.BANK/TERM.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ng, because some arrays will have gaps in their grid and therefore have >1 spacing in some dimensions.
Merged
1 task
cboulay
added a commit
to ezmsg-org/ezmsg-blackrock
that referenced
this pull request
Jun 5, 2026
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>
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 reworks how Blackrock
.cmpchannel-map files are applied to a device's in-memory channel config. Previously a CMP row was matched to a channel by an ordinal channel ID (the Nth row, after sorting by bank/electrode, was assignedstart_chan + N), and the fourcbPKT_CHANINFO.position[]slots held{col, row, bank_letter_idx, electrode}. We now match each row to a live channel by the device's own(bank, term)— read straight from chaninfo — and repurposeposition[]to carry true geometry:{x, y, size, headstage_id}. Matching by(bank, term)is more robust to incomplete CMPs (a file missing channels still lands on exactly the right ones) and frees thepositionslots that were previously doing double duty as match keys. Column order is now read from the file's header line rather than assumed, and default electrode-index grids are converted to micrometers.Motivation
bankandtermalready exist oncbPKT_CHANINFOand are exactly what the oldposition[2]/position[3](bank-letter-index, electrode) duplicated, so keying on them removes the duplication and the fragile ordinal assumption. With bank/term out ofposition, the four slots are freed to carry real geometry (x,y,size,headstage_id).What changed
Bank/term matching (C++ SDK)
parseCmpFilenow returns entries keyed bycmpKey(bank, term)instead of by ordinal channel ID.CmpEntrycarries{x, y, size, headstage, bank, term, label}. The previous sort-and-assign step is gone because row order no longer matters.start_channow selects which physical banks a CMP targets: every CMP bank letter is shifted bystart_chan / 32banks (e.g.start_chan = 129→+4banks → CMP bank A maps to device bank E). This makes(bank, term)globally unique across headstages on one device (headstage 1 → banks A–D, headstage 2 → banks E–H), so it is a valid join key.SdkSession::applyCmpToAllChannelsand the CHANREP receive-thread overlay now iterate live channels, look up the CMP entry by the channel's own(bank, term), and writeposition[0..3].loadChannelMapdiscovers which channels matched (for the device-side label push) by joining live chaninfo on(bank, term), andclearChannelMapdiscovers the previously-mapped channels the same way before dropping the overlay.position[] now holds {x, y, size, headstage_id}
position[0]/position[1]are the electrode column/row, as before.position[2]is a new electrodesize(same units as x/y; 0 = unspecified).position[3]is theheadstage_id(thehs_idargument). These replace the old{col, row, bank, electrode}layout.getChannelPositionsis unchanged (it already returns all four slots); the values it surfaces now carry the new meaning.Header-driven columns and a
sizecolumn//-comment header line immediately before the data when present (manufacturer files carry one, e.g.//col row bank elec label; manually authored files may add asizecolumn). Column names are matched case-insensitively (col/c, row/r, bank/b, elec/e, size/s, label/l), so the order is not assumed. When no header is present the legacy positional ordercol row bank electrode [label]is used, so every existing.cmpfile parses unchanged.Electrode positions in micrometers
sizecolumn is supplied and the col/row values form a unit-spaced index grid (every non-zero delta among the distinct col and row values is exactly 1 — the manufacturer default), the values are interpreted as electrode indices:sizebecomes 1 and x/y/size are scaled by the 400 µm Blackrock Utah-array electrode pitch. When asizecolumn is present, or the spacing is non-uniform (e.g. the 128-channel manufacturer sample, whose row deltas are {1, 4}), col/row/size are taken at face value. The delta test — rather than a hard value-range cap — is the scaling trigger, because the default files have col indices up to 15 yet are still unit-spaced grids that should scale.Labels are no longer prefixed with
hs{N}-position[3], it no longer needs to be encoded into the label to keep reused labels unique across headstages. Labels are taken verbatim from the CMP file.hs_idnow only feeds theheadstagefield; it does not alter labels. Trade-off: two channels on different headstages can now carry identical label strings incbPKT_CHANINFO.label, distinguished only byheadstage/bank/term. Any consumer keying channels purely by label string should be aware of this.pycbsdk parser brought in sync
pycbsdk/src/pycbsdk/cmp.py(a standalone dump/CLI tool — live device config re-parses the file in C++ viaload_channel_map) was updated to mirror the C++ parser: entries keyed by(bank, term), thestart_chan // 32bank offset, the optionalsizecolumn, verbatim labels, andhs_idstored asheadstage. Two stale docstrings inpycbsdk/src/pycbsdk/session.py(load_channel_map,get_channels_positions) were corrected to describe the new behavior.Testing
tests/unit/test_cmp_parser.cpp) were rewritten for the new keys/struct and cover the bank offset, the optional size column, verbatim labels, and thaths_idsetsheadstagerather than the label. All pass against a cleancbsdkbuild.pycbsdk/tests/test_cmp.py) were rewritten similarly — 12 pass.pycbsdk/tests/test_configuration.py::TestCMP) were reworked to join device readback to parser output by(bank, term)viaChanInfoField.BANK/TERM, compareposition == (x, y, size, headstage), and expect verbatim labels. They collect cleanly; they require an nplay device to run.Notes / assumptions
CHANSETLABEL).