diff --git a/neo/rawio/baserawio.py b/neo/rawio/baserawio.py index 7b5d8b768..7c9b7b237 100644 --- a/neo/rawio/baserawio.py +++ b/neo/rawio/baserawio.py @@ -1577,7 +1577,9 @@ def __init__(self, *arg, **kwargs): def _get_signal_size(self, block_index, seg_index, stream_index): buffer_id = self.header["signal_streams"][stream_index]["buffer_id"] buffer_desc = self.get_analogsignal_buffer_description(block_index, seg_index, buffer_id) - # some hdf5 revert teh buffer + # time_axis indicates which dimension is time: + # time_axis=0: shape is (time, channels) + # time_axis=1: shape is (channels, time) time_axis = buffer_desc.get("time_axis", 0) return buffer_desc["shape"][time_axis] @@ -1598,34 +1600,67 @@ def _get_analogsignal_chunk( buffer_desc = self.get_analogsignal_buffer_description(block_index, seg_index, buffer_id) + # Get time_axis to determine which dimension is time + time_axis = buffer_desc.get("time_axis", 0) + i_start = i_start or 0 - i_stop = i_stop or buffer_desc["shape"][0] + i_stop = i_stop or buffer_desc["shape"][time_axis] if buffer_desc["type"] == "raw": - # open files on demand and keep reference to opened file - if not hasattr(self, "_memmap_analogsignal_buffers"): - self._memmap_analogsignal_buffers = {} - if block_index not in self._memmap_analogsignal_buffers: - self._memmap_analogsignal_buffers[block_index] = {} - if seg_index not in self._memmap_analogsignal_buffers[block_index]: - self._memmap_analogsignal_buffers[block_index][seg_index] = {} - if buffer_id not in self._memmap_analogsignal_buffers[block_index][seg_index]: - fid = open(buffer_desc["file_path"], mode="rb") - self._memmap_analogsignal_buffers[block_index][seg_index][buffer_id] = fid - else: - fid = self._memmap_analogsignal_buffers[block_index][seg_index][buffer_id] + if time_axis == 0: + # MULTIPLEXED: time_axis=0 means (time, channels) layout + # open files on demand and keep reference to opened file + if not hasattr(self, "_memmap_analogsignal_buffers"): + self._memmap_analogsignal_buffers = {} + if block_index not in self._memmap_analogsignal_buffers: + self._memmap_analogsignal_buffers[block_index] = {} + if seg_index not in self._memmap_analogsignal_buffers[block_index]: + self._memmap_analogsignal_buffers[block_index][seg_index] = {} + if buffer_id not in self._memmap_analogsignal_buffers[block_index][seg_index]: + fid = open(buffer_desc["file_path"], mode="rb") + self._memmap_analogsignal_buffers[block_index][seg_index][buffer_id] = fid + else: + fid = self._memmap_analogsignal_buffers[block_index][seg_index][buffer_id] + + num_channels = buffer_desc["shape"][1] + + raw_sigs = get_memmap_chunk_from_opened_file( + fid, + num_channels, + i_start, + i_stop, + np.dtype(buffer_desc["dtype"]), + file_offset=buffer_desc["file_offset"], + ) - num_channels = buffer_desc["shape"][1] + elif time_axis == 1: + # VECTORIZED: time_axis=1 means shape is (channels, time) + # Data is stored as [all_samples_ch1, all_samples_ch2, ...] + dtype = np.dtype(buffer_desc["dtype"]) + num_channels = buffer_desc["shape"][0] # shape is (channels, time) + num_samples = i_stop - i_start + total_samples_per_channel = buffer_desc["shape"][1] # shape is (channels, time) + + # Determine which channels to read + if channel_indexes is None: + chan_indices = np.arange(num_channels) + else: + chan_indices = np.asarray(channel_indexes) + + raw_sigs = np.empty((num_samples, len(chan_indices)), dtype=dtype) + + for i, chan_idx in enumerate(chan_indices): + offset = buffer_desc["file_offset"] + chan_idx * total_samples_per_channel * dtype.itemsize + channel_data = np.memmap(buffer_desc["file_path"], dtype=dtype, mode='r', + offset=offset, shape=(total_samples_per_channel,)) + raw_sigs[:, i] = channel_data[i_start:i_stop] + + # Channel slicing already done above, so skip later channel_indexes slicing + channel_indexes = None - raw_sigs = get_memmap_chunk_from_opened_file( - fid, - num_channels, - i_start, - i_stop, - np.dtype(buffer_desc["dtype"]), - file_offset=buffer_desc["file_offset"], - ) + else: + raise ValueError(f"time_axis must be 0 or 1, got {time_axis}") elif buffer_desc["type"] == "hdf5": diff --git a/neo/rawio/brainvisionrawio.py b/neo/rawio/brainvisionrawio.py index ca25e7141..601381470 100644 --- a/neo/rawio/brainvisionrawio.py +++ b/neo/rawio/brainvisionrawio.py @@ -62,9 +62,12 @@ def _parse_header(self): raise NeoReadWriteError( f"Only `BINARY` format has been implemented. Current Data Format is {vhdr_header['Common Infos']['DataFormat']}" ) - if vhdr_header["Common Infos"]["DataOrientation"] != "MULTIPLEXED": + + # Store the data orientation for later use in reading + self._data_orientation = vhdr_header["Common Infos"]["DataOrientation"] + if self._data_orientation not in ("MULTIPLEXED", "VECTORIZED"): raise NeoReadWriteError( - f"Only `MULTIPLEXED` is implemented. Current Orientation is {vhdr_header['Common Infos']['DataOrientation']}" + f"Data orientation must be either `MULTIPLEXED` or `VECTORIZED`. Current Orientation is {self._data_orientation}" ) nb_channel = int(vhdr_header["Common Infos"]["NumberOfChannels"]) @@ -87,7 +90,19 @@ def _parse_header(self): buffer_id = "0" self._buffer_descriptions = {0: {0: {}}} self._stream_buffer_slice = {} - shape = get_memmap_shape(binary_filename, sig_dtype, num_channels=nb_channel, offset=0) + + # time_axis indicates data layout: 0 for MULTIPLEXED (time, channels), 1 for VECTORIZED (channels, time) + time_axis = 0 if self._data_orientation == "MULTIPLEXED" else 1 + + # Get shape - always returns (num_samples, num_channels) + temp_shape = get_memmap_shape(binary_filename, sig_dtype, num_channels=nb_channel, offset=0) + + # For consistency with HDF5 pattern: when time_axis=1, shape should be (channels, time) + if time_axis == 1: + shape = (temp_shape[1], temp_shape[0]) # (num_channels, num_samples) + else: + shape = temp_shape # (num_samples, num_channels) + self._buffer_descriptions[0][0][buffer_id] = { "type": "raw", "file_path": binary_filename, @@ -95,6 +110,7 @@ def _parse_header(self): "order": "C", "file_offset": 0, "shape": shape, + "time_axis": time_axis, } self._stream_buffer_slice[stream_id] = None