@@ -125,6 +125,10 @@ def make_mux_table_array(mux_information) -> np.array:
125125
126126 Returns
127127 -------
128+ num_adcs: int
129+ Number of ADCs used in the probe's readout system.
130+ num_channels_per_adc: int
131+ Number of readout channels assigned to each ADC.
128132 adc_groups_array : np.array
129133 Array of which channels are in each adc group, shaped (number of `adc`s, number of channels in each `adc`).
130134 """
@@ -334,20 +338,10 @@ def _make_npx_probe_from_description(probe_description, model_name, elec_ids, sh
334338 # annotate with MUX table
335339 if mux_info is not None :
336340 # annotate each contact with its mux channel
337- num_adcs , num_channels_per_adc , mux_table = make_mux_table_array (mux_info )
338- num_contacts = positions .shape [0 ]
339- # ADC group: which adc is used for each contact
340- adc_groups = np .zeros (num_contacts , dtype = "int64" )
341- # ADC sample order: order of sampling of the contact in the adc group
342- adc_sample_order = np .zeros (num_contacts , dtype = "int64" )
343- for adc_idx , adc_groups_per_adc in enumerate (mux_table ):
344- adc_groups_per_adc = adc_groups_per_adc [adc_groups_per_adc < num_contacts ]
345- adc_groups [adc_groups_per_adc ] = adc_idx
346- adc_sample_order [adc_groups_per_adc ] = np .arange (len (adc_groups_per_adc ))
341+ num_adcs , num_channels_per_adc , adc_groups_array = make_mux_table_array (mux_info )
347342 probe .annotate (num_adcs = num_adcs )
348343 probe .annotate (num_channels_per_adc = num_channels_per_adc )
349- probe .annotate_contacts (adc_group = adc_groups )
350- probe .annotate_contacts (adc_sample_order = adc_sample_order )
344+ _annotate_contacts_from_mux_table (probe , adc_groups_array )
351345
352346 return probe
353347
@@ -488,6 +482,74 @@ def build_neuropixels_probe(probe_part_number: str) -> Probe:
488482 return probe
489483
490484
485+ def _annotate_contacts_from_mux_table (probe : Probe , adc_groups_array : np .array ):
486+ """
487+ Annotate a Probe object with ADC group and sample order information based on the MUX table.
488+
489+ This function is used when building a complete Neuropixels probe from the ProbeTable specifications
490+ (via build_neuropixels_probe) to assign per-contact annotations for adc_group and adc_sample_order,
491+ which describe how each contact maps to the ADCs during recording.
492+ The function annotates the probe in place.
493+
494+ Parameters
495+ ----------
496+ probe : Probe
497+ The Probe object to annotate. Must have device_channel_indices set.
498+ adc_groups_array : np.array
499+ The ADC groups array from the probe features, which describes how readout channels map to ADCs.
500+ """
501+ # Map readout channels to ADC groups and sample order
502+ num_readout_channels = probe .get_contact_count ()
503+ adc_groups = np .zeros (num_readout_channels , dtype = "int64" )
504+ adc_sample_order = np .zeros (num_readout_channels , dtype = "int64" )
505+ for adc_index , channels_per_adc in enumerate (adc_groups_array ):
506+ # Filter out placeholder values (e.g., 128 in mux_np1200 for unused slots)
507+ valid_channels = channels_per_adc [channels_per_adc < num_readout_channels ]
508+ adc_groups [valid_channels ] = adc_index
509+ adc_sample_order [valid_channels ] = np .arange (len (valid_channels ))
510+
511+ probe .annotate_contacts (adc_group = adc_groups )
512+ probe .annotate_contacts (adc_sample_order = adc_sample_order )
513+
514+
515+ def annotate_probe_with_adc_sampling_info (probe : Probe , adc_sampling_table : str | None ):
516+ """
517+ Annotate a Probe object with ADC group and sample order information based on the ADC sampling table.
518+
519+ This function is used when reading a recording with a known channel map (via read_spikeglx, read_openephys)
520+ to assign per-contact annotations for adc_group and adc_sample_order, which describe how
521+ each contact maps to the ADCs during recording, and global annotations for num_adcs and num_channels_per_adc.
522+
523+ Parameters
524+ ----------
525+ probe : Probe
526+ The Probe object to annotate. Must have device_channel_indices set.
527+ adc_sampling_table : str
528+ The ADC sampling table string from the probe features, which describes how readout channels map to ADCs.
529+
530+ Returns
531+ -------
532+ None
533+ The function modifies the Probe object in place.
534+ """
535+ # Parse table string: (num_adcs,num_channels_per_adc)(ch ch ...)(ch ch ...)...
536+ if adc_sampling_table is None :
537+ return
538+ adc_info = adc_sampling_table .split (")(" )[0 ]
539+ split_mux = adc_sampling_table .split (")(" )[1 :]
540+ num_adcs , num_channels_per_adc = map (int , adc_info [1 :].split ("," ))
541+ probe .annotate (num_adcs = num_adcs )
542+ probe .annotate (num_channels_per_adc = num_channels_per_adc )
543+
544+ adc_groups_list = [
545+ np .array (each_mux .replace ("(" , "" ).replace (")" , "" ).split (" " )).astype ("int" ) for each_mux in split_mux
546+ ]
547+ adc_groups_array = np .transpose (np .array (adc_groups_list ))
548+
549+ # Map readout channels to ADC groups and sample order
550+ _annotate_contacts_from_mux_table (probe , adc_groups_array )
551+
552+
491553def _read_imro_string (imro_str : str , imDatPrb_pn : Optional [str ] = None ) -> Probe :
492554 """
493555 Parse the IMRO table when presented as a string and create a Probe object.
@@ -883,30 +945,7 @@ def read_spikeglx(file: str | Path) -> Probe:
883945 # apply the mapping. This must be done here (not in build_neuropixels_probe)
884946 # because the table indices are readout channel indices, not electrode indices.
885947 adc_sampling_table = probe .annotations .get ("adc_sampling_table" )
886- if adc_sampling_table is not None :
887- # Parse table string: (num_adcs,num_channels_per_adc)(ch ch ...)(ch ch ...)...
888- adc_info = adc_sampling_table .split (")(" )[0 ]
889- split_mux = adc_sampling_table .split (")(" )[1 :]
890- num_adcs , num_channels_per_adc = map (int , adc_info [1 :].split ("," ))
891- adc_groups_list = [
892- np .array (each_mux .replace ("(" , "" ).replace (")" , "" ).split (" " )).astype ("int" ) for each_mux in split_mux
893- ]
894- mux_table = np .transpose (np .array (adc_groups_list ))
895-
896- # Map readout channels to ADC groups and sample order
897- num_readout_channels = probe .get_contact_count ()
898- adc_groups = np .zeros (num_readout_channels , dtype = "int64" )
899- adc_sample_order = np .zeros (num_readout_channels , dtype = "int64" )
900- for adc_index , channels_per_adc in enumerate (mux_table ):
901- # Filter out placeholder values (e.g., 128 in mux_np1200 for unused slots)
902- valid_channels = channels_per_adc [channels_per_adc < num_readout_channels ]
903- adc_groups [valid_channels ] = adc_index
904- adc_sample_order [valid_channels ] = np .arange (len (valid_channels ))
905-
906- probe .annotate (num_adcs = num_adcs )
907- probe .annotate (num_channels_per_adc = num_channels_per_adc )
908- probe .annotate_contacts (adc_group = adc_groups )
909- probe .annotate_contacts (adc_sample_order = adc_sample_order )
948+ annotate_probe_with_adc_sampling_info (probe , adc_sampling_table )
910949
911950 # ===== 7. Slice to saved channels (if subset was saved) =====
912951 # This is DIFFERENT from IMRO selection: IMRO selects which electrodes to acquire,
@@ -1059,7 +1098,7 @@ def _parse_openephys_settings(
10591098 - probe_part_number, serial_number, name, slot, port, dock
10601099 - selected_electrode_indices: list of int (from SELECTED_ELECTRODES), or None
10611100 - contact_ids: list of str (reverse-engineered from CHANNELS), or None
1062- - channel_names : np.array of str, or None
1101+ - settings_channel_keys : np.array of str, or None
10631102 - elec_ids, shank_ids, pt_metadata, mux_info: for legacy fallback
10641103 """
10651104 ET = import_safely ("xml.etree.ElementTree" )
@@ -1207,8 +1246,7 @@ def _parse_openephys_settings(
12071246 "mux_info" : mux_info ,
12081247 "selected_electrode_indices" : None ,
12091248 "contact_ids" : None ,
1210- "channel_names" : None ,
1211- "plugin_channel_keys" : None ,
1249+ "settings_channel_keys" : None ,
12121250 "elec_ids" : None ,
12131251 "shank_ids" : None ,
12141252 }
@@ -1287,8 +1325,7 @@ def _parse_openephys_settings(
12871325 )
12881326 elec_ids .append (elec_id )
12891327
1290- info ["channel_names" ] = channel_names
1291- info ["plugin_channel_keys" ] = channel_names
1328+ info ["settings_channel_keys" ] = channel_names
12921329 info ["shank_ids" ] = shank_ids
12931330 info ["elec_ids" ] = elec_ids
12941331
@@ -1419,13 +1456,12 @@ def _select_openephys_probe_info(
14191456 return probes_info [0 ]
14201457
14211458
1422- def _slice_catalogue_probe (full_probe : Probe , probe_info : dict ) -> Probe :
1459+ def _slice_openephys_catalogue_probe (full_probe : Probe , probe_info : dict ) -> Probe :
14231460 """
14241461 Slice a full catalogue probe using the electrode selection from probe_info.
14251462
14261463 For SELECTED_ELECTRODES (newer plugin), uses the indices directly.
14271464 For CHANNELS (older plugin), matches reverse-engineered contact_ids to the catalogue.
1428- Falls back to legacy `_make_npx_probe_from_description` if matching fails.
14291465
14301466 Parameters
14311467 ----------
@@ -1467,9 +1503,6 @@ def _annotate_openephys_probe(probe: Probe, probe_info: dict) -> None:
14671503 probe_info : dict
14681504 Probe info dict from `_parse_openephys_settings`.
14691505 """
1470- if probe_info ["channel_names" ] is not None :
1471- probe .annotate_contacts (channel_name = probe_info ["channel_names" ])
1472-
14731506 probe .serial_number = probe_info ["serial_number" ]
14741507 probe .name = probe_info ["name" ]
14751508 probe .annotate (part_number = probe_info ["probe_part_number" ])
@@ -1480,8 +1513,11 @@ def _annotate_openephys_probe(probe: Probe, probe_info: dict) -> None:
14801513 probe .annotate (port = probe_info ["port" ])
14811514 if probe_info ["dock" ] is not None :
14821515 probe .annotate (dock = probe_info ["dock" ])
1483- if probe_info .get ("plugin_channel_keys" ) is not None :
1484- probe .annotate_contacts (plugin_channel_key = probe_info ["plugin_channel_keys" ])
1516+ if probe_info .get ("settings_channel_keys" ) is not None :
1517+ probe .annotate_contacts (settings_channel_key = probe_info ["settings_channel_keys" ])
1518+
1519+ adc_sampling_table = probe .annotations .get ("adc_sampling_table" )
1520+ annotate_probe_with_adc_sampling_info (probe , adc_sampling_table )
14851521
14861522
14871523def _compute_device_channel_indices_from_oebin (
@@ -1501,7 +1537,7 @@ def _compute_device_channel_indices_from_oebin(
15011537 Parameters
15021538 ----------
15031539 probe : Probe
1504- Probe with contact_ids set (e.g. from ``_slice_catalogue_probe ``).
1540+ Probe with contact_ids set (e.g. from ``_slice_openephys_catalogue_probe ``).
15051541 oebin_file : str or Path
15061542 Path to the structure.oebin JSON file.
15071543 stream_name : str
@@ -1541,16 +1577,11 @@ def _compute_device_channel_indices_from_oebin(
15411577 # This was added in neuropixels-pxi plugin v0.5.0 (January 2023).
15421578 oebin_electrode_indices = []
15431579 for ch in oebin_channels :
1544- electrode_index = None
15451580 for m in ch .get ("channel_metadata" , []):
15461581 if m .get ("name" ) == "electrode_index" :
15471582 electrode_index = m ["value" ][0 ]
1583+ oebin_electrode_indices .append (electrode_index )
15481584 break
1549- oebin_electrode_indices .append (electrode_index )
1550-
1551- # Filter out non-electrode channels (e.g. the sync channel) that lack electrode_index
1552- electrode_channel_indices = [i for i , ei in enumerate (oebin_electrode_indices ) if ei is not None ]
1553- oebin_electrode_indices = [oebin_electrode_indices [i ] for i in electrode_channel_indices ]
15541585
15551586 if len (oebin_electrode_indices ) != num_contacts :
15561587 raise ValueError (
@@ -1680,7 +1711,7 @@ def read_openephys(
16801711 return None
16811712
16821713 full_probe = build_neuropixels_probe (probe_part_number = probe_info ["probe_part_number" ])
1683- probe = _slice_catalogue_probe (full_probe , probe_info )
1714+ probe = _slice_openephys_catalogue_probe (full_probe , probe_info )
16841715 _annotate_openephys_probe (probe , probe_info )
16851716
16861717 chans_saved = get_saved_channel_indices_from_openephys_settings (settings_file , stream_name = stream_name )
0 commit comments