Skip to content

Commit 9b452f2

Browse files
committed
add test and move mux logic to read_spikeglx
1 parent 2cec18e commit 9b452f2

3 files changed

Lines changed: 94 additions & 25 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,4 @@ cov.xml
2727

2828

2929
neuropixel_library_generated/*
30+
uv.lock

src/probeinterface/neuropixels_tools.py

Lines changed: 42 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -471,31 +471,16 @@ def build_neuropixels_probe(probe_part_number: str) -> Probe:
471471
lf_sample_frequency_hz=float(probe_spec_dict["lf_sample_frequency_hz"]),
472472
)
473473

474-
# ===== 8. Add MUX table annotations =====
475-
mux_table_string = probe_features["z_mux_tables"][probe_spec_dict["mux_table_format_type"]]
476-
if mux_table_string is not None:
477-
# Parse MUX table string: (num_adcs,num_channels_per_adc)(int int ...)(int int ...)...
478-
adc_info = mux_table_string.split(")(")[0]
479-
split_mux = mux_table_string.split(")(")[1:]
480-
num_adcs, num_channels_per_adc = map(int, adc_info[1:].split(","))
481-
adc_groups_list = [
482-
np.array(each_mux.replace("(", "").replace(")", "").split(" ")).astype("int") for each_mux in split_mux
483-
]
484-
mux_table = np.transpose(np.array(adc_groups_list))
485-
486-
# Map contacts to ADC groups and sample order
487-
num_contacts = positions.shape[0]
488-
adc_groups = np.zeros(num_contacts, dtype="int64")
489-
adc_sample_order = np.zeros(num_contacts, dtype="int64")
490-
for adc_index, adc_groups_per_adc in enumerate(mux_table):
491-
adc_groups_per_adc = adc_groups_per_adc[adc_groups_per_adc < num_contacts]
492-
adc_groups[adc_groups_per_adc] = adc_index
493-
adc_sample_order[adc_groups_per_adc] = np.arange(len(adc_groups_per_adc))
494-
495-
probe.annotate(num_adcs=num_adcs)
496-
probe.annotate(num_channels_per_adc=num_channels_per_adc)
497-
probe.annotate_contacts(adc_group=adc_groups)
498-
probe.annotate_contacts(adc_sample_order=adc_sample_order)
474+
# ===== 8. Store ADC sampling table =====
475+
# The ADC sampling table describes how readout channels map to ADCs, not electrodes.
476+
# Per-contact annotations (adc_group, adc_sample_order) can only be correctly
477+
# assigned when reading a recording with a known channel map (via read_spikeglx),
478+
# because the table indices are readout channel indices, not electrode indices.
479+
# We store the full table string so it's available after slicing.
480+
mux_table_format_type = probe_spec_dict["mux_table_format_type"]
481+
adc_sampling_table = probe_features["z_mux_tables"].get(mux_table_format_type)
482+
if adc_sampling_table is not None:
483+
probe.annotate(adc_sampling_table=adc_sampling_table)
499484

500485
return probe
501486

@@ -861,6 +846,38 @@ def read_spikeglx(file: str | Path) -> Probe:
861846
annotations[pi_field] = values
862847
probe.annotate_contacts(**annotations)
863848

849+
# ===== 6b. Add ADC sampling annotations =====
850+
# The ADC sampling table describes which ADC samples each readout channel and in what order.
851+
# At this point, contacts are ordered by readout channel (0-383), so we can directly
852+
# apply the mapping. This must be done here (not in build_neuropixels_probe)
853+
# because the table indices are readout channel indices, not electrode indices.
854+
adc_sampling_table = probe.annotations.get("adc_sampling_table")
855+
if adc_sampling_table is not None:
856+
# Parse table string: (num_adcs,num_channels_per_adc)(ch ch ...)(ch ch ...)...
857+
adc_info = adc_sampling_table.split(")(")[0]
858+
split_mux = adc_sampling_table.split(")(")[1:]
859+
num_adcs, num_channels_per_adc = map(int, adc_info[1:].split(","))
860+
adc_groups_list = [
861+
np.array(each_mux.replace("(", "").replace(")", "").split(" ")).astype("int")
862+
for each_mux in split_mux
863+
]
864+
mux_table = np.transpose(np.array(adc_groups_list))
865+
866+
# Map readout channels to ADC groups and sample order
867+
num_readout_channels = probe.get_contact_count()
868+
adc_groups = np.zeros(num_readout_channels, dtype="int64")
869+
adc_sample_order = np.zeros(num_readout_channels, dtype="int64")
870+
for adc_index, channels_per_adc in enumerate(mux_table):
871+
# Filter out placeholder values (e.g., 128 in mux_np1200 for unused slots)
872+
valid_channels = channels_per_adc[channels_per_adc < num_readout_channels]
873+
adc_groups[valid_channels] = adc_index
874+
adc_sample_order[valid_channels] = np.arange(len(valid_channels))
875+
876+
probe.annotate(num_adcs=num_adcs)
877+
probe.annotate(num_channels_per_adc=num_channels_per_adc)
878+
probe.annotate_contacts(adc_group=adc_groups)
879+
probe.annotate_contacts(adc_sample_order=adc_sample_order)
880+
864881
# ===== 7. Slice to saved channels (if subset was saved) =====
865882
# This is DIFFERENT from IMRO selection: IMRO selects which electrodes to acquire,
866883
# but SpikeGLX can optionally save only a subset of acquired channels to reduce file size.

tests/test_io/test_spikeglx.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,57 @@ def test_CatGT_NP1():
320320
assert "1.0" in probe.description
321321

322322

323+
def test_mux_annotations_match_readout_channels():
324+
"""
325+
Test that MUX annotations (adc_group, adc_sample_order) correctly map
326+
readout channels to their ADC assignments.
327+
328+
The MUX table defines which ADC samples each readout channel and in what order.
329+
For NP1.0 (mux_np1000), the pattern is:
330+
- Readout channel 0 -> ADC 0, sample order 0
331+
- Readout channel 1 -> ADC 1, sample order 0
332+
- Readout channel 2 -> ADC 0, sample order 1
333+
- Readout channel 3 -> ADC 1, sample order 1
334+
- etc.
335+
336+
The contacts in the probe returned by read_spikeglx are ordered by readout channel,
337+
so adc_group[i] should be the ADC for readout channel i.
338+
339+
This test uses allan-longcol which has alternating banks (0, 1, 0, 1, ...),
340+
meaning electrode indices differ from readout channel indices. This exposes
341+
bugs where MUX table indices (readout channels) are incorrectly treated as
342+
electrode indices.
343+
"""
344+
# allan-longcol uses alternating banks: channel 0->electrode 0, channel 1->electrode 385, etc.
345+
probe = read_spikeglx(data_path / "allan-longcol_g0_t0.imec0.ap.meta")
346+
347+
assert probe.get_contact_count() == 384
348+
349+
# Verify the bank pattern - this file alternates between bank 0 and bank 1
350+
banks = probe.contact_annotations.get("banks")
351+
assert list(banks[:10]) == [0, 1, 0, 1, 0, 1, 0, 1, 0, 1], "Expected alternating banks"
352+
353+
adc_groups = probe.contact_annotations.get("adc_group")
354+
adc_sample_order = probe.contact_annotations.get("adc_sample_order")
355+
356+
assert adc_groups is not None, "adc_group annotation should be present"
357+
assert adc_sample_order is not None, "adc_sample_order annotation should be present"
358+
assert len(adc_groups) == 384
359+
assert len(adc_sample_order) == 384
360+
361+
# According to mux_np1000 (32 ADCs, 12 channels per ADC):
362+
# Readout channel 0 -> ADC 0, readout channel 1 -> ADC 1, etc.
363+
# The MUX assignment follows readout channel order, NOT electrode order.
364+
# Even though electrode indices are [0, 385, 2, 387, ...], the ADC groups
365+
# should still be [0, 1, 0, 1, ...] because that's the readout channel pattern.
366+
expected_adc_for_first_10 = [0, 1, 0, 1, 0, 1, 0, 1, 0, 1]
367+
assert list(adc_groups[:10]) == expected_adc_for_first_10, (
368+
f"ADC groups for first 10 channels should be {expected_adc_for_first_10}, "
369+
f"got {list(adc_groups[:10])}. "
370+
f"MUX annotations must follow readout channel order, not electrode order."
371+
)
372+
373+
323374
def test_snsGeomMap():
324375
# check when snsGeomMap is present if contact positions are the same from imroTbl
325376

0 commit comments

Comments
 (0)