Skip to content

Commit 42788f4

Browse files
authored
Merge pull request #1940 from h-mayorquin/use_read_methods_on_blackrock_headers
Blackrock: read headers through an open file instead of `np.fromfile` and `np.memmap`
2 parents ec9c50a + 4f6c737 commit 42788f4

1 file changed

Lines changed: 64 additions & 30 deletions

File tree

‎neo/rawio/blackrockrawio.py‎

Lines changed: 64 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)