1313import warnings
1414from packaging .version import parse
1515import json
16-
1716import numpy as np
1817
1918from .probe import Probe
2019from .utils import import_safely
2120
2221# Map imDatPrb_pn (probe number) to imDatPrb_type (probe type) when the latter is missing
22+ # ONLY needed for `read_imro` function
2323probe_part_number_to_probe_type = {
2424 # for old version without a probe number we assume NP1.0
2525 None : "0" ,
5959 "NP1300" : "1300" , # Opto probe
6060}
6161
62- probe_type_to_probe_part_number = {v : k for k , v in probe_part_number_to_probe_type .items ()}
63-
62+ # Map from imro format to ProbeInterface naming conventions
6463imro_field_to_pi_field = {
6564 "ap_gain" : "ap_gains" ,
6665 "ap_hipas_flt" : "ap_hp_filters" ,
7675 "bankB" : "bankB" ,
7776}
7877
78+ # Map from ProbeInterface to ProbeTable naming conventions
7979pi_to_pt_names = {
8080 "x_pitch" : "electrode_pitch_horz_um" ,
8181 "y_pitch" : "electrode_pitch_vert_um" ,
9191 "tip_length_um" : "tip_length_um" ,
9292}
9393
94+ def get_probe_length (probe_part_number : str ) -> int :
95+ """
96+ Returns the length of a given probe. We assume a length of
97+ 1cm (10_000 microns) by default.
98+
99+ Parameters
100+ ----------
101+ probe_part_number : str
102+ The part number of the probe e.g. 'NP2013'.
103+
104+ Returns
105+ -------
106+ probe_length : int
107+ Lenth of full probe (microns)
108+ """
109+
110+ probe_length = 10_000
111+
112+ return probe_length
113+
94114
95115def make_npx_description (probe_part_number ):
96116 """
@@ -109,21 +129,15 @@ def make_npx_description(probe_part_number):
109129 Dictionary containing metadata about NeuroPixels probes using ProbeInterface syntax.
110130 """
111131
112- is_phase3a = False
113- # These are all prototype NP1.0 probes, not contained in ProbeTable
114- if probe_part_number in ["PRB_1_4_0480_1" , "PRB_1_4_0480_1_C" , "PRB_1_2_0480_2" , None ]:
115- if probe_part_number is None :
116- is_phase3a = True
117- probe_part_number = "NP1010"
118-
119132 probe_features_filepath = Path (__file__ ).absolute ().parent / Path ("resources/probe_features.json" )
120133 probe_features = json .load (open (probe_features_filepath , "r" ))
134+
135+ # We use `pt` and `pi` as shorthand for `ProbeTable` and `ProbeInterface` throughout this function
121136 pt_metadata = probe_features ["neuropixels_probes" ].get (probe_part_number )
137+ pi_metadata = {}
122138
123139 if pt_metadata is None :
124- raise ValueError (f"Probe type { probe_part_number } not supported." )
125-
126- pi_metadata = {}
140+ raise ValueError (f"Probe part number { probe_part_number } not supported." )
127141
128142 # Extract most of the metadata
129143 for pi_name , pt_name in pi_to_pt_names .items ():
@@ -159,14 +173,10 @@ def make_npx_description(probe_part_number):
159173 # Read the imro table formats to find out which fields the imro tables contain
160174 imro_table_format_type = pt_metadata ["imro_table_format_type" ]
161175 imro_table_fields = probe_features ["z_imro_formats" ][imro_table_format_type + "_elm_flds" ]
162-
176+
163177 # parse the imro_table_fields, which look like (value value value ...)
164178 list_of_imro_fields = imro_table_fields .replace ("(" , "" ).replace (")" , "" ).split (" " )
165179
166- # The Phase3a probe does not contain the `ap_hipas_flt` imro table field.
167- if is_phase3a :
168- list_of_imro_fields .remove ("ap_hipas_flt" )
169-
170180 pi_imro_fields = []
171181 for imro_field in list_of_imro_fields :
172182 pi_imro_fields .append (imro_field_to_pi_field [imro_field ])
@@ -175,10 +185,12 @@ def make_npx_description(probe_part_number):
175185 # Construct probe contour, for styling the probe
176186 shank_width = float (pt_metadata ["shank_width_um" ])
177187 tip_length = float (pt_metadata ["tip_length_um" ])
178- probe_length = 10_000
188+
189+ probe_length = get_probe_length (probe_part_number )
179190 pi_metadata ["contour_description" ] = get_probe_contour_vertices (shank_width , tip_length , probe_length )
180191
181- # Get the mux table
192+ # Get the mux table. This describes which electrodes are multiplexed together, meaning
193+ # which electrodes are sampled at the same time.
182194 mux_table_format_type = pt_metadata ["mux_table_format_type" ]
183195 mux_information = probe_features ["z_mux_tables" ].get (mux_table_format_type )
184196 pi_metadata ["mux_table_array" ] = make_mux_table_array (mux_information )
@@ -279,7 +291,25 @@ def read_imro(file_path: Union[str, Path]) -> Probe:
279291 assert meta_file .suffix == ".imro" , "'file' should point to the .imro file"
280292 with meta_file .open (mode = "r" ) as f :
281293 imro_str = str (f .read ())
282- return _read_imro_string (imro_str )
294+
295+ imro_table_header_str , * imro_table_values_list , _ = imro_str .strip ().split (")" )
296+ imro_table_header = tuple (map (int , imro_table_header_str [1 :].split ("," )))
297+
298+ if len (imro_table_header ) == 3 :
299+ # In older versions of neuropixel arrays (phase 3A), imro tables were structured differently.
300+ # We use probe_type "0", which maps to probe_part_number NP1010 as a proxy for Phase3a.
301+ imDatPrb_type = "0"
302+ elif len (imro_table_header ) == 2 :
303+ imDatPrb_type , _ = imro_table_header
304+ else :
305+ raise ValueError (f"read_imro error, the header has a strange length: { imro_table_header } " )
306+ imDatPrb_type = str (imDatPrb_type )
307+
308+ for probe_part_number , probe_type in probe_part_number_to_probe_type .items ():
309+ if imDatPrb_type == probe_type :
310+ imDatPrb_pn = probe_part_number
311+
312+ return _read_imro_string (imro_str , imDatPrb_pn )
283313
284314
285315def _make_npx_probe_from_description (probe_description , elec_ids , shank_ids ):
@@ -369,30 +399,19 @@ def _read_imro_string(imro_str: str, imDatPrb_pn: Optional[str] = None) -> Probe
369399 https://billkarsh.github.io/SpikeGLX/help/imroTables/
370400
371401 """
372- imro_table_header_str , * imro_table_values_list , _ = imro_str .strip ().split (")" )
373- imro_table_header = tuple (map (int , imro_table_header_str [1 :].split ("," )))
374402
375- imDatPrb_type = None
376- if imDatPrb_pn is None :
377- if len (imro_table_header ) == 3 :
378- # In older versions of neuropixel arrays (phase 3A), imro tables were structured differently.
379- probe_serial_number , probe_option , num_contact = imro_table_header
380- imDatPrb_type = "Phase3a"
381- imDatPrb_pn = None
382- elif len (imro_table_header ) == 2 :
383- imDatPrb_type , num_contact = imro_table_header
384- imDatPrb_type = str (imDatPrb_type )
385- imDatPrb_pn = probe_type_to_probe_part_number [imDatPrb_type ]
386- else :
387- raise ValueError (f"read_imro error, the header has a strange length: { imro_table_header } " )
403+ probe_type_num_chans , * imro_table_values_list , _ = imro_str .strip ().split (")" )
404+
405+ # probe_type_num_chans looks like f"({probe_type},{num_chans}"
406+ probe_type = probe_type_num_chans .split (',' )[0 ][1 :]
388407
389408 probe_description = make_npx_description (imDatPrb_pn )
390409
391410 fields = probe_description ["fields_in_imro_table" ]
392411 contact_info = {k : [] for k in fields }
393412 for field_values_str in imro_table_values_list : # Imro table values look like '(value, value, value, ... '
413+ # Split them by space to get int('value'), int('value'), int('value'), ...)
394414 values = tuple (map (int , field_values_str [1 :].split (" " )))
395- # Split them by space to get (int('value'), int('value'), int('value'), ...)
396415 for field , field_value in zip (fields , values ):
397416 contact_info [field ].append (field_value )
398417
@@ -416,12 +435,12 @@ def _read_imro_string(imro_str: str, imDatPrb_pn: Optional[str] = None) -> Probe
416435
417436 # this is scalar annotations
418437 probe .annotate (
419- probe_type = imDatPrb_type ,
438+ probe_type = probe_type ,
420439 )
421440
422441 # this is vector annotations
423442 vector_properties = ("channel_ids" , "banks" , "references" , "ap_gains" , "lf_gains" , "ap_hp_filters" )
424- vector_properties_available = {k : v for k , v in contact_info .items () if k in vector_properties }
443+ vector_properties_available = {k : v for k , v in contact_info .items () if ( k in vector_properties ) and ( len ( v ) > 0 ) }
425444 probe .annotate_contacts (** vector_properties_available )
426445
427446 return probe
@@ -494,12 +513,6 @@ def read_spikeglx(file: str | Path) -> Probe:
494513
495514 The shape is auto generated as a shank.
496515
497- Now reads:
498- * NP0.0 (=phase3A)
499- * NP1.0 (=phase3B2)
500- * NP2.0 with 4 shank
501- * NP1.0-NHP
502-
503516 Parameters
504517 ----------
505518 file : Path or str
@@ -530,6 +543,10 @@ def read_spikeglx(file: str | Path) -> Probe:
530543 imDatPrb_slot = meta .get ("imDatPrb_slot" , None )
531544 imDatPrb_part_number = meta .get ("imDatPrb_pn" , None )
532545
546+ # Only Phase3a probe has "imProbeOpt". Map this to NP10101.
547+ if meta .get ("imProbeOpt" ) is not None :
548+ imDatPrb_pn = "NP1010"
549+
533550 probe = _read_imro_string (imro_str = imro_table , imDatPrb_pn = imDatPrb_pn )
534551
535552 # add serial number and other annotations
0 commit comments