Skip to content

Move Blackrock nsx handling closer to the buffer API pattern#1820

Open
h-mayorquin wants to merge 2 commits into
NeuralEnsemble:masterfrom
h-mayorquin:add_buffer_api_like_behavior_for_blackrock
Open

Move Blackrock nsx handling closer to the buffer API pattern#1820
h-mayorquin wants to merge 2 commits into
NeuralEnsemble:masterfrom
h-mayorquin:add_buffer_api_like_behavior_for_blackrock

Conversation

@h-mayorquin

Copy link
Copy Markdown
Contributor

PR #1513 introduced BaseRawWithBufferApiIO, a base class that stores lightweight buffer descriptions (file paths, offsets, dtypes) instead of persistent np.memmap objects, creating short-lived mmap views on demand through cached file descriptors. This PR moves BlackrockRawIO toward that same pattern. We cannot fully adopt BaseRawWithBufferApiIO because: (a) Blackrock's standard format (v2.2/2.3/3.0) stores multiple data blocks with explicit headers in a single file, requiring block-aware offset tracking that the base class's one-buffer-per-stream model does not support; (b) the PTP variant interleaves per-sample timestamps with sample data in fixed-size packets, requiring strided mmap access (np.ndarray with custom strides) rather than contiguous views; and (c) the segmentation logic for PTP files needs to read timestamps from the file to detect gaps, which couples parsing and segmentation in a way the base class does not allow. However, we can adopt the core idea: replace stored memmaps with on-demand creation.

This has the added benefit of reducing the number of memmaps that are open at any time and we have seen that this pattern is both 1) more performant memory wise, 2) less likely to lead to leaks. Importantly, this PR does not change any behavior of the reader, it is an internal optimization only. It lays the groundwork for a follow-up PR adding gap detection on the standard format.

@h-mayorquin h-mayorquin self-assigned this Feb 17, 2026
@h-mayorquin h-mayorquin changed the title Blackrock nsx closer to the buffer API pattern Move Blackrock nsx handling closer to the buffer API pattern Feb 17, 2026
@alejoe91 alejoe91 added this to the 0.14.5 milestone Mar 25, 2026
@alejoe91 alejoe91 modified the milestones: 0.14.5, 0.14.6 Jun 24, 2026

@zm711 zm711 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initial round of comments. I'm not as familiar with this reader so might need a bit more explanation in general @h-mayorquin.

strides=strides,
)

def _get_nsx_fid(self, nsx_nb):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you walk me through how this construction is going to happen? Shouldn't we grab all nsx_fids at one time to construct them. This calls and caches the fids at it goes right? creating the dict on the first call?

"""

import datetime
import mmap

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the benefit of mmap vs np.memmap?

memmap_data = self.nsx_datas[nsx_nb][seg_index]
seg = self._nsx_data_header[nsx_nb][seg_index]
fid = self._get_nsx_fid(nsx_nb)
kw = seg["memmap_kwargs"]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we get a better variable name for readability?

# Extract data and timestamps from structured array
data = file_memmap["samples"]
timestamps = file_memmap["timestamps"]
del temp_memmap

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are we deleting the temp_memmap twice?

"timestamp": timestamps, # Use singular for backward compatibility
"nb_data_points": data.shape[0],
segments.append({
"timestamp": block_info["timestamps"], # Use singular for backward compatibility

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we know what this backward compatibility is?

ev_ids[ev_ids == j] -= 1
# Remap nev segment ids: shift down all ids above the removed segment
if self._avail_files["nev"]:
for _key, (data, ev_ids) in self.nev_data.items():

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where is _key even being used?


# Check all nonempty nsX segments
for i, seg in enumerate(list_nonempty_nsx_segments[:]):
for i, seg in enumerate(nonempty_nsx_segments[:]):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you're already changing names of stuff want to change i into something more interpretable for future us?

keep_seg = True
for nsx_nb in self.nsx_to_load:
length = self.nsx_datas[nsx_nb][data_bl].shape[0]
length = self._nsx_data_header[nsx_nb][seg_index]["nb_data_points"]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something I keep seeing. Why didn't the original implementation use this nb_data_points value? It seems to have been present before. They seem to want to use the shape of the data. Is it possible that the dict value is ever off from the actual data shape?

data_header = self._nsx_data_header[nsx_nb].pop(j)
self._nsx_data_header[nsx_nb][j - 1] = data_header
# Remove empty segments in reverse order to preserve indices
for seg_index in reversed(empty_indices):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice switch to reversed

# Remap nev segment ids: shift down all ids above the removed segment
if self._avail_files["nev"]:
for _key, (data, ev_ids) in self.nev_data.items():
ev_ids[ev_ids > seg_index] -= 1

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was the other bit I was getting a lost at. So you check for > seg_index, but they seem to be only checking that j is equal. So let me double check that I understand correctly. Previously they were doing a nested loop with the j and the ev_ids and then basically it would iterate one value per loop. What you are doing is doing only one loop through the data and changing all applicable values during that one loop, right? If that's not the correct intuition than I might need you to explain this bit to me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants