@@ -187,12 +187,6 @@ def __init__(
187187
188188 # These dictionaries are used internally to map the file specification
189189 # revision of the nsx and nev files to one of the reading routines
190- self ._nsx_params = {
191- "2.1" : self ._get_nsx_param_spec_v21 ,
192- "2.2" : self ._get_nsx_param_spec_v22_30 ,
193- "2.3" : self ._get_nsx_param_spec_v22_30 ,
194- "3.0" : self ._get_nsx_param_spec_v22_30 ,
195- }
196190 # NEV
197191 self ._waveform_size = {
198192 "2.1" : self ._get_waveform_size_spec_v21 ,
@@ -399,14 +393,8 @@ def _parse_header(self):
399393 if spec_version in ["2.2" , "2.3" , "3.0" ]:
400394 ext_header = self ._nsx_ext_header [nsx_nb ]
401395 elif spec_version == "2.1" :
402- ext_header = []
403- keys = ["labels" , "units" , "min_analog_val" , "max_analog_val" , "min_digital_val" , "max_digital_val" ]
404- params = self ._nsx_params [spec_version ](nsx_nb )
405- for i in range (len (params ["labels" ])):
406- d = {}
407- for key in keys :
408- d [key ] = params [key ][i ]
409- ext_header .append (d )
396+ # v2.1 has no extended headers - construct from NEV digitization factors
397+ ext_header = self ._build_nsx_v21_ext_header (nsx_nb )
410398
411399 if len (ext_header ) > 0 :
412400 # in blackrock : one stream per buffer so same id
@@ -455,7 +443,7 @@ def _parse_header(self):
455443 if "timestamp_resolution" in self ._nsx_basic_header [nsx_nb ].dtype .names :
456444 ts_res = self ._nsx_basic_header [nsx_nb ]["timestamp_resolution" ]
457445 elif spec == "2.1" :
458- ts_res = self . _nsx_params [ spec ]( nsx_nb )[ "timestamp_resolution" ]
446+ ts_res = 30_000 # v2.1 always uses 30kHz timestamp resolution
459447 else :
460448 ts_res = 30_000
461449 period = self ._nsx_basic_header [nsx_nb ]["period" ]
@@ -1053,12 +1041,15 @@ def _parse_nsx_data_v21(self, nsx_nb):
10531041 # Create file memmap
10541042 file_memmap = np .memmap (filename , dtype = 'uint8' , mode = 'r' )
10551043
1056- # Get parameters
1057- params = self ._nsx_params ["2.1" ](nsx_nb )
1044+ # Calculate header size and data points for v2.1
10581045 channels = int (self ._nsx_basic_header [nsx_nb ]["channel_count" ])
1059- num_samples = int (params ["nb_data_points" ])
1060- offset = int (params ["bytes_in_headers" ])
1061-
1046+ bytes_in_headers = (
1047+ self ._nsx_basic_header [nsx_nb ].dtype .itemsize
1048+ + self ._nsx_ext_header [nsx_nb ].dtype .itemsize * channels
1049+ )
1050+ filesize = self ._get_file_size (filename )
1051+ num_samples = int ((filesize - bytes_in_headers ) / (2 * channels ) - 1 )
1052+ offset = bytes_in_headers
10621053 # Create data view into memmap
10631054 data = np .ndarray (
10641055 shape = (num_samples , channels ),
@@ -1330,6 +1321,52 @@ def _segment_nsx_data(self, data_blocks_dict, nsx_nb):
13301321
13311322 return segments
13321323
1324+ def _build_nsx_v21_ext_header (self , nsx_nb ):
1325+ """
1326+ Build extended header structure for v2.1 NSX files.
1327+
1328+ v2.1 NSX files don't have extended headers with analog/digital ranges.
1329+ We estimate these from the digitization factor in the NEV file.
1330+ dig_factor = max_analog_val / max_digital_val
1331+ We set max_digital_val = 1000, so max_analog_val = dig_factor
1332+ dig_factor is in nV, so units are uV.
1333+
1334+ Information from Kian Torab, Blackrock Microsystems.
1335+ """
1336+ ext_header = []
1337+
1338+ for i , elid in enumerate (self ._nsx_ext_header [nsx_nb ]["electrode_id" ]):
1339+ # Get digitization factor from NEV
1340+ if self ._avail_files ["nev" ]:
1341+ # Workaround for DigitalFactor overflow in buggy Cerebus systems
1342+ # Fix from NPMK toolbox (openNEV, line 464, git rev d0a25eac)
1343+ dig_factor = self ._nev_params ("digitization_factor" )[elid ]
1344+ if dig_factor == 21516 :
1345+ dig_factor = 152592.547
1346+ units = "uV"
1347+ else :
1348+ dig_factor = float ("nan" )
1349+ units = ""
1350+ if i == 0 : # Only warn once
1351+ warnings .warn ("Cannot rescale to voltage, raw data will be returned." , UserWarning )
1352+
1353+ # Generate label
1354+ if elid < 129 :
1355+ label = f"chan{ elid } "
1356+ else :
1357+ label = f"ainp{ (elid - 129 + 1 )} "
1358+
1359+ ext_header .append ({
1360+ "labels" : label ,
1361+ "units" : units ,
1362+ "min_analog_val" : - float (dig_factor ),
1363+ "max_analog_val" : float (dig_factor ),
1364+ "min_digital_val" : - 1000 ,
1365+ "max_digital_val" : 1000 ,
1366+ })
1367+
1368+ return ext_header
1369+
13331370 def _read_nev_header (self , spec , filename ):
13341371 """
13351372 Extract nev header information for any specification version.
@@ -1607,9 +1644,8 @@ def _match_nsx_and_nev_segment_ids(self, nsx_nb):
16071644 if len (data [mask_after_seg ]) > 0 :
16081645 # Warning if spikes are after last segment
16091646 if i == len (list_nonempty_nsx_segments ) - 1 :
1610- timestamp_resolution = self ._nsx_params [self ._nsx_spec [nsx_nb ]](
1611- "timestamp_resolution" , nsx_nb
1612- )
1647+ # Get timestamp resolution from header (available for v2.2+)
1648+ timestamp_resolution = self ._nsx_basic_header [nsx_nb ]["timestamp_resolution" ]
16131649 time_after_seg = (
16141650 data [mask_after_seg ]["timestamp" ][- 1 ] - end_of_current_nsx_seg
16151651 ) / timestamp_resolution
@@ -1828,96 +1864,6 @@ def _get_left_sweep_waveforms(self):
18281864
18291865 return wf_left_sweep
18301866
1831- def _get_nsx_param_spec_v21 (self , nsx_nb ):
1832- """
1833- Returns parameter (param_name) for a given nsx (nsx_nb) for file spec
1834- 2.1.
1835- """
1836- # Here, min/max_analog_val and min/max_digital_val are not available in
1837- # the nsx, so that we must estimate these parameters from the
1838- # digitization factor of the nev (information by Kian Torab, Blackrock
1839- # Microsystems). Here dig_factor=max_analog_val/max_digital_val. We set
1840- # max_digital_val to 1000, and max_analog_val=dig_factor. dig_factor is
1841- # given in nV by definition, so the units turn out to be uV.
1842- labels = []
1843- dig_factor = []
1844- for elid in self ._nsx_ext_header [nsx_nb ]["electrode_id" ]:
1845- if self ._avail_files ["nev" ]:
1846- # This is a workaround for the DigitalFactor overflow in NEV
1847- # files recorded with buggy Cerebus system.
1848- # Fix taken from: NMPK toolbox by Blackrock,
1849- # file openNEV, line 464,
1850- # git rev. d0a25eac902704a3a29fa5dfd3aed0744f4733ed
1851- df = self ._nev_params ("digitization_factor" )[elid ]
1852- if df == 21516 :
1853- df = 152592.547
1854- dig_factor .append (df )
1855- else :
1856- dig_factor .append (float ("nan" ))
1857-
1858- if elid < 129 :
1859- labels .append (f"chan{ elid } " )
1860- else :
1861- labels .append (f"ainp{ (elid - 129 + 1 )} " )
1862-
1863- filename = "." .join ([self ._filenames ["nsx" ], f"ns{ nsx_nb } " ])
1864-
1865- bytes_in_headers = (
1866- self ._nsx_basic_header [nsx_nb ].dtype .itemsize
1867- + self ._nsx_ext_header [nsx_nb ].dtype .itemsize * self ._nsx_basic_header [nsx_nb ]["channel_count" ]
1868- )
1869-
1870- if np .isnan (dig_factor [0 ]):
1871- units = ""
1872- warnings .warn ("Cannot rescale to voltage, raw data will be returned." , UserWarning )
1873- else :
1874- units = "uV"
1875-
1876- nsx_parameters = {
1877- "nb_data_points" : int (
1878- (int (self ._get_file_size (filename )) - int (bytes_in_headers ))
1879- / int (2 * self ._nsx_basic_header [nsx_nb ]["channel_count" ])
1880- - 1
1881- ),
1882- "labels" : labels ,
1883- "units" : np .array ([units ] * self ._nsx_basic_header [nsx_nb ]["channel_count" ]),
1884- "min_analog_val" : - 1 * np .array (dig_factor , dtype = "float" ),
1885- "max_analog_val" : np .array (dig_factor , dtype = "float" ),
1886- "min_digital_val" : np .array ([- 1000 ] * self ._nsx_basic_header [nsx_nb ]["channel_count" ]),
1887- "max_digital_val" : np .array ([1000 ] * self ._nsx_basic_header [nsx_nb ]["channel_count" ]),
1888- "timestamp_resolution" : 30000 ,
1889- "bytes_in_headers" : bytes_in_headers ,
1890- "sampling_rate" : 30000 / self ._nsx_basic_header [nsx_nb ]["period" ] * pq .Hz ,
1891- "time_unit" : pq .CompoundUnit (f"1.0/{ 30000 / self ._nsx_basic_header [nsx_nb ]['period' ]} *s" ),
1892- }
1893-
1894- # Returns complete dictionary because then it does not need to be called so often
1895- return nsx_parameters
1896-
1897- def _get_nsx_param_spec_v22_30 (self , param_name , nsx_nb ):
1898- """
1899- Returns parameter (param_name) for a given nsx (nsx_nb) for file spec
1900- 2.2 and 2.3.
1901- """
1902- nsx_parameters = {
1903- "labels" : self ._nsx_ext_header [nsx_nb ]["electrode_label" ],
1904- "units" : self ._nsx_ext_header [nsx_nb ]["units" ],
1905- "min_analog_val" : self ._nsx_ext_header [nsx_nb ]["min_analog_val" ],
1906- "max_analog_val" : self ._nsx_ext_header [nsx_nb ]["max_analog_val" ],
1907- "min_digital_val" : self ._nsx_ext_header [nsx_nb ]["min_digital_val" ],
1908- "max_digital_val" : self ._nsx_ext_header [nsx_nb ]["max_digital_val" ],
1909- "timestamp_resolution" : self ._nsx_basic_header [nsx_nb ]["timestamp_resolution" ],
1910- "bytes_in_headers" : self ._nsx_basic_header [nsx_nb ]["bytes_in_headers" ],
1911- "sampling_rate" : self ._nsx_basic_header [nsx_nb ]["timestamp_resolution" ]
1912- / self ._nsx_basic_header [nsx_nb ]["period" ]
1913- * pq .Hz ,
1914- "time_unit" : pq .CompoundUnit (
1915- f"1.0/{ self ._nsx_basic_header [nsx_nb ]['timestamp_resolution' ] / self ._nsx_basic_header [nsx_nb ]['period' ]} *s"
1916- ),
1917- }
1918-
1919- return nsx_parameters [param_name ]
1920-
19211867 def _get_nonneural_evdicts_spec_v21_22 (self , data ):
19221868 """
19231869 Defines event types and the necessary parameters to extract them from
@@ -2630,7 +2576,7 @@ def _is_set(self, flag, pos):
26302576 "2.1" : None ,
26312577 # Versions 2.2+ use data block headers with timestamp size based on major version
26322578 "2.2" : [("header_flag" , "uint8" ), ("timestamp" , "uint32" ), ("nb_data_points" , "uint32" )],
2633- "2.3" : [("header_flag" , "uint8" ), ("timestamp" , "uint32" ), ("nb_data_points" , "uint32" )],
2579+ "2.3" : [("header_flag" , "uint8" ), ("timestamp" , "uint32" ), ("nb_data_points" , "uint32" )],
26342580 "3.0" : [("header_flag" , "uint8" ), ("timestamp" , "uint64" ), ("nb_data_points" , "uint32" )],
26352581 # PTP variant has a completely different structure with samples embedded
26362582 "3.0-ptp" : lambda channel_count : [
0 commit comments