@@ -261,7 +261,7 @@ def _parse_header(self):
261261
262262 # read nev headers
263263 nev_filename = f"{ self ._filenames ['nev' ]} .nev"
264- self ._nev_basic_header , self ._nev_ext_header = self ._read_nev_header (self ._nev_spec , nev_filename )
264+ self ._nev_basic_header , self ._nev_ext_header = self ._read_nev_header (self ._nev_spec )
265265
266266 self .nev_data = self ._read_nev_data (self ._nev_spec , nev_filename )
267267 spikes , spike_segment_ids = self .nev_data ["Spikes" ]
@@ -461,7 +461,7 @@ def _parse_header(self):
461461 if "timestamps_memmap_kwargs" in seg_header :
462462 # FileSpec 3.0 with PTP -- read first/last timestamps on demand
463463 ts_kw = seg_header ["timestamps_memmap_kwargs" ]
464- fid = self ._get_nsx_fid (nsx_nb )
464+ fid = self ._get_nsx_file_handle (nsx_nb )
465465 ts_array = self ._create_mmap_view (
466466 fid = fid ,
467467 dtype = ts_kw ["dtype" ],
@@ -727,20 +727,43 @@ def _create_mmap_view(fid, dtype, offset, num_samples, num_channels=None, packet
727727 strides = strides ,
728728 )
729729
730- def _get_nsx_fid (self , nsx_nb ):
731- """Open NSX file on demand and cache the file descriptor for reuse."""
732- if not hasattr (self , "_nsx_fids " ):
733- self ._nsx_fids = {}
734- if nsx_nb not in self ._nsx_fids :
730+ def _get_nsx_file_handle (self , nsx_nb ):
731+ """Open NSX file on demand and cache the file handle for reuse."""
732+ if not hasattr (self , "_nsx_file_handles_dict " ):
733+ self ._nsx_file_handles_dict = {}
734+ if nsx_nb not in self ._nsx_file_handles_dict :
735735 filename = f"{ self ._filenames ['nsx' ]} .ns{ nsx_nb } "
736- self ._nsx_fids [nsx_nb ] = open (filename , "rb" )
737- return self ._nsx_fids [nsx_nb ]
736+ self ._nsx_file_handles_dict [nsx_nb ] = open (filename , "rb" )
737+ return self ._nsx_file_handles_dict [nsx_nb ]
738+
739+ def _get_nev_file_handle (self ):
740+ """Open NEV file on demand and cache the file handle for reuse."""
741+ if not hasattr (self , "_nev_file_handle" ):
742+ filename = f"{ self ._filenames ['nev' ]} .nev"
743+ self ._nev_file_handle = open (filename , "rb" )
744+ return self ._nev_file_handle
745+
746+ def __del__ (self ):
747+ if hasattr (self , "_nsx_file_handles_dict" ):
748+ for file_handle in self ._nsx_file_handles_dict .values ():
749+ file_handle .close ()
750+ del self ._nsx_file_handles_dict
751+ if hasattr (self , "_nev_file_handle" ):
752+ self ._nev_file_handle .close ()
753+ del self ._nev_file_handle
754+
755+ def __getstate__ (self ):
756+ # Open files cannot be pickled. The getters above reopen them on the first read after unpickling.
757+ state = self .__dict__ .copy ()
758+ state .pop ("_nsx_file_handles_dict" , None )
759+ state .pop ("_nev_file_handle" , None )
760+ return state
738761
739762 def _get_analogsignal_chunk (self , block_index , seg_index , i_start , i_stop , stream_index , channel_indexes ):
740763 stream_id = self .header ["signal_streams" ][stream_index ]["id" ]
741764 nsx_nb = int (stream_id )
742765 seg = self ._nsx_data_header [nsx_nb ][seg_index ]
743- fid = self ._get_nsx_fid (nsx_nb )
766+ fid = self ._get_nsx_file_handle (nsx_nb )
744767 memmap_kwargs = seg ["memmap_kwargs" ]
745768 channels = int (self ._nsx_basic_header [nsx_nb ]["channel_count" ])
746769 data = self ._create_mmap_view (
@@ -809,7 +832,7 @@ def _get_blackrock_timestamps(self, block_index, seg_index, i_start, i_stop, str
809832 # PTP format: read timestamps on demand via strided mmap view
810833 if "timestamps_memmap_kwargs" in seg :
811834 ts_kw = seg ["timestamps_memmap_kwargs" ]
812- fid = self ._get_nsx_fid (nsx_nb )
835+ fid = self ._get_nsx_file_handle (nsx_nb )
813836 timestamps = self ._create_mmap_view (
814837 fid = fid ,
815838 dtype = ts_kw ["dtype" ],
@@ -969,13 +992,13 @@ def _extract_nsx_file_spec(self, nsx_nb):
969992 """
970993 Extract file specification from an .nsx file.
971994 """
972- filename = f"{ self ._filenames ['nsx' ]} .ns{ nsx_nb } "
973-
974995 # Header structure of files specification 2.2 and higher. For files 2.1
975996 # and lower, the entries ver_major and ver_minor are not supported.
976997 dt0 = [("file_id" , "S8" ), ("ver_major" , "uint8" ), ("ver_minor" , "uint8" )]
977998
978- nsx_file_id = np .fromfile (filename , count = 1 , dtype = dt0 )[0 ]
999+ fid = self ._get_nsx_file_handle (nsx_nb )
1000+ fid .seek (0 )
1001+ nsx_file_id = np .frombuffer (fid .read (np .dtype (dt0 ).itemsize ), dtype = dt0 , count = 1 )[0 ]
9791002 if nsx_file_id ["file_id" ].decode () == "NEURALSG" :
9801003 spec = "2.1"
9811004 elif nsx_file_id ["file_id" ].decode () in ["NEURALCD" , "BRSMPGRP" ]:
@@ -989,12 +1012,13 @@ def _extract_nev_file_spec(self):
9891012 """
9901013 Extract file specification from an .nev file
9911014 """
992- filename = f"{ self ._filenames ['nev' ]} .nev"
9931015 # Header structure of files specification 2.2 and higher. For files 2.1
9941016 # and lower, the entries ver_major and ver_minor are not supported.
9951017 dt0 = [("file_id" , "S8" ), ("ver_major" , "uint8" ), ("ver_minor" , "uint8" )]
9961018
997- nev_file_id = np .fromfile (filename , count = 1 , dtype = dt0 )[0 ]
1019+ fid = self ._get_nev_file_handle ()
1020+ fid .seek (0 )
1021+ nev_file_id = np .frombuffer (fid .read (np .dtype (dt0 ).itemsize ), dtype = dt0 , count = 1 )[0 ]
9981022 if nev_file_id ["file_id" ].decode () in ["NEURALEV" , "BREVENTS" ]:
9991023 spec = f"{ nev_file_id ['ver_major' ]} .{ nev_file_id ['ver_minor' ]} "
10001024 else :
@@ -1020,18 +1044,25 @@ def _read_nsx_header(self, spec, nsx_nb):
10201044 nsx_ext_header : numpy memmap
10211045 Extended header information
10221046 """
1023- # Construct filename
1024- filename = f"{ self ._filenames ['nsx' ]} .ns{ nsx_nb } "
1047+ fid = self ._get_nsx_file_handle (nsx_nb )
10251048
10261049 # Get basic header structure for this spec
10271050 basic_header_dtype = NSX_BASIC_HEADER_TYPES [spec ]
1028- nsx_basic_header = np .fromfile (filename , count = 1 , dtype = basic_header_dtype )[0 ]
1051+ fid .seek (0 )
1052+ nsx_basic_header = np .frombuffer (
1053+ fid .read (np .dtype (basic_header_dtype ).itemsize ), dtype = basic_header_dtype , count = 1
1054+ )[0 ]
10291055
10301056 # Get extended header structure for this spec
10311057 ext_header_dtype = NSX_EXT_HEADER_TYPES [spec ]
10321058 offset_dt0 = np .dtype (basic_header_dtype ).itemsize
10331059 channel_count = int (nsx_basic_header ["channel_count" ])
1034- nsx_ext_header = np .memmap (filename , shape = channel_count , offset = offset_dt0 , dtype = ext_header_dtype , mode = "r" )
1060+ fid .seek (offset_dt0 )
1061+ nsx_ext_header = np .frombuffer (
1062+ fid .read (channel_count * np .dtype (ext_header_dtype ).itemsize ),
1063+ dtype = ext_header_dtype ,
1064+ count = channel_count ,
1065+ )
10351066
10361067 return nsx_basic_header , nsx_ext_header
10371068
@@ -1048,14 +1079,16 @@ def _read_nsx_dataheader(self, spec, nsx_nb, offset):
10481079 offset : int
10491080 Offset position in the file
10501081 """
1051- filename = f"{ self ._filenames ['nsx' ]} .ns{ nsx_nb } "
1052-
10531082 # Get data header structure for this spec
10541083 data_header_dtype = NSX_DATA_HEADER_TYPES [spec ]
10551084 if data_header_dtype is None :
10561085 return None # v2.1 has no data headers
10571086
1058- nsx_basic_header = np .memmap (filename , dtype = data_header_dtype , shape = 1 , offset = offset , mode = "r" )[0 ]
1087+ fid = self ._get_nsx_file_handle (nsx_nb )
1088+ fid .seek (offset )
1089+ nsx_basic_header = np .frombuffer (
1090+ fid .read (np .dtype (data_header_dtype ).itemsize ), dtype = data_header_dtype , count = 1
1091+ )[0 ]
10591092
10601093 return nsx_basic_header
10611094
@@ -1309,7 +1342,7 @@ def _parse_nsx_data_v30_ptp(self, nsx_nb):
13091342 # Verify this is truly PTP: the first packet should carry exactly one sample.
13101343 # PTP files are homogeneous (every packet holds a single sample), so the first
13111344 # packet is diagnostic for the whole file and we avoid mapping all of it.
1312- fid = self ._get_nsx_fid (nsx_nb )
1345+ fid = self ._get_nsx_file_handle (nsx_nb )
13131346 fid .seek (header_size )
13141347 first_packet = np .frombuffer (fid .read (ptp_dtype .itemsize ), dtype = ptp_dtype , count = 1 )[0 ]
13151348 if first_packet ["num_data_points" ] != 1 :
@@ -1445,7 +1478,7 @@ def _segment_nsx_data(self, parsed_data_headers, nsx_nb):
14451478 samples_kw = block_info ["memmap_kwargs" ]
14461479
14471480 # Read timestamps via strided mmap view for gap detection
1448- fid = self ._get_nsx_fid (nsx_nb )
1481+ fid = self ._get_nsx_file_handle (nsx_nb )
14491482 timestamps = self ._create_mmap_view (
14501483 fid = fid ,
14511484 dtype = ts_kw ["dtype" ],
@@ -1586,16 +1619,14 @@ def _build_nsx_v21_ext_header(self, nsx_nb):
15861619
15871620 return ext_header
15881621
1589- def _read_nev_header (self , spec , filename ):
1622+ def _read_nev_header (self , spec ):
15901623 """
15911624 Extract nev header information for any specification version.
15921625
15931626 Parameters
15941627 ----------
15951628 spec : str
15961629 The specification version (e.g., "2.1", "2.2", "2.3", "3.0")
1597- filename : str
1598- The NEV filename to read from
15991630
16001631 Returns
16011632 -------
@@ -1637,15 +1668,18 @@ def _read_nev_header(self, spec, filename):
16371668 ("nb_ext_headers" , "uint32" ),
16381669 ]
16391670
1640- nev_basic_header = np .fromfile (filename , count = 1 , dtype = dt0 )[0 ]
1671+ fid = self ._get_nev_file_handle ()
1672+ fid .seek (0 )
1673+ nev_basic_header = np .frombuffer (fid .read (np .dtype (dt0 ).itemsize ), dtype = dt0 , count = 1 )[0 ]
16411674
16421675 shape = nev_basic_header ["nb_ext_headers" ]
16431676 offset_dt0 = np .dtype (dt0 ).itemsize
16441677
16451678 # This is the common structure of the beginning of extended headers
16461679 dt1 = [("packet_id" , "S8" ), ("info_field" , "S24" )]
16471680
1648- raw_ext_header = np .memmap (filename , offset = offset_dt0 , dtype = dt1 , shape = shape , mode = "r" )
1681+ fid .seek (offset_dt0 )
1682+ raw_ext_header = np .frombuffer (fid .read (int (shape ) * np .dtype (dt1 ).itemsize ), dtype = dt1 , count = int (shape ))
16491683
16501684 # Get extended header types for this spec
16511685 header_types = NEV_EXT_HEADER_TYPES_BY_SPEC [spec ]
0 commit comments