Skip to content

Commit 3408073

Browse files
authored
Merge branch 'master' into authors
2 parents 356254d + 6f4a97c commit 3408073

File tree

11 files changed

+97
-64
lines changed

11 files changed

+97
-64
lines changed

neo/io/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@
167167
.. autoclass:: neo.io.MaxwellIO
168168
169169
.. autoattribute:: extensions
170-
170+
171171
.. autoclass:: neo.io.MedIO
172172
173173
.. autoattribute:: extensions

neo/rawio/axonrawio.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
strings section:
2424
[uModifierNameIndex, uCreatorNameIndex, uProtocolPathIndex, lFileComment, lADCCChannelNames, lADCUnitsIndex
2525
lDACChannelNameIndex, lDACUnitIndex, lDACFilePath, nLeakSubtractADC]
26-
['', 'Clampex', '', 'C:/path/protocol.pro', 'some comment', 'IN 0', 'mV', 'IN 1', 'mV', 'Cmd 0', 'pA',
26+
['', 'Clampex', '', 'C:/path/protocol.pro', 'some comment', 'IN 0', 'mV', 'IN 1', 'mV', 'Cmd 0', 'pA',
2727
'Cmd 1', 'pA', 'Cmd 2', 'mV', 'Cmd 3', 'mV']
2828
2929
Information on abf 1 and 2 formats is available here:

neo/rawio/intanrawio.py

Lines changed: 56 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
* http://intantech.com/files/Intan_RHD2000_data_file_formats.pdf
1717
* http://intantech.com/files/Intan_RHS2000_data_file_formats.pdf
1818
19-
19+
2020
Author: Samuel Garcia (Initial), Zach McKenzie & Heberto Mayorquin (Updates)
2121
2222
"""
@@ -531,6 +531,57 @@ def _demultiplex_digital_data(self, raw_digital_data, channel_ids, i_start, i_st
531531

532532
return output
533533

534+
def get_intan_timestamps(self, i_start=None, i_stop=None):
535+
"""
536+
Retrieves the sample indices from the Intan raw data within a specified range.
537+
538+
Note that sample indices are called timestamps in the Intan format but they are
539+
in fact just sample indices. This function extracts the sample index timestamps
540+
from Intan files, which represent relative time points in sample units (not absolute time).
541+
These indices can be particularly useful when working with recordings that have discontinuities.
542+
543+
Parameters
544+
----------
545+
i_start : int, optional
546+
The starting index from which to retrieve sample indices. If None, starts from 0.
547+
i_stop : int, optional
548+
The stopping index up to which to retrieve sample indices (exclusive).
549+
If None, retrieves all available indices from i_start onward.
550+
551+
Returns
552+
-------
553+
timestamps : ndarray
554+
The flattened array of sample indices within the specified range.
555+
556+
Notes
557+
-----
558+
- Sample indices can be converted to seconds by dividing by the sampling rate of the amplifier stream.
559+
- The function automatically handles different file formats:
560+
* header-attached: Timestamps are extracted directly from the timestamp field
561+
* one-file-per-signal: Timestamps are read from the timestamp stream
562+
* one-file-per-channel: Timestamps are read from the first channel in the timestamp stream
563+
- When recordings have discontinuities (indicated by the `discontinuous_timestamps`
564+
attribute being True), these indices allow for proper temporal alignment of the data.
565+
"""
566+
if i_start is None:
567+
i_start = 0
568+
569+
# Get the timestamps based on file format
570+
if self.file_format == "header-attached":
571+
timestamps = self._raw_data["timestamp"]
572+
elif self.file_format == "one-file-per-signal":
573+
timestamps = self._raw_data["timestamp"]
574+
elif self.file_format == "one-file-per-channel":
575+
timestamps = self._raw_data["timestamp"][0]
576+
577+
# TODO if possible ensure that timestamps memmaps are always of correct shape to avoid memory copy here.
578+
timestamps = timestamps.flatten() if timestamps.ndim > 1 else timestamps
579+
580+
if i_stop is None:
581+
return timestamps[i_start:]
582+
else:
583+
return timestamps[i_start:i_stop]
584+
534585
def _assert_timestamp_continuity(self):
535586
"""
536587
Asserts the continuity of timestamps in the data.
@@ -545,26 +596,11 @@ def _assert_timestamp_continuity(self):
545596
NeoReadWriteError
546597
If timestamps are not continuous and `ignore_integrity_checks` is False.
547598
The error message includes a table detailing the discontinuities found.
548-
549-
Notes
550-
-----
551-
The method extracts timestamps from the raw data based on the file format:
552-
553-
* **header-attached:** Timestamps are extracted from a 'timestamp' field in the raw data.
554-
* **one-file-per-signal:** Timestamps are taken from the last stream.
555-
* **one-file-per-channel:** Timestamps are retrieved from the first channel of the last stream.
556599
"""
557600
# check timestamp continuity
558-
if self.file_format == "header-attached":
559-
timestamp = self._raw_data["timestamp"].flatten()
560-
561-
# timestamps are always last stream for headerless binary files
562-
elif self.file_format == "one-file-per-signal":
563-
timestamp = self._raw_data["timestamp"]
564-
elif self.file_format == "one-file-per-channel":
565-
timestamp = self._raw_data["timestamp"][0]
601+
timestamps = self.get_intan_timestamps()
566602

567-
discontinuous_timestamps = np.diff(timestamp) != 1
603+
discontinuous_timestamps = np.diff(timestamps) != 1
568604
timestamps_are_not_contiguous = np.any(discontinuous_timestamps)
569605
if timestamps_are_not_contiguous:
570606
# Mark a flag that can be checked after parsing the header to see if the timestamps are continuous or not
@@ -582,8 +618,8 @@ def _assert_timestamp_continuity(self):
582618

583619
amplifier_sampling_rate = self._global_info["sampling_rate"]
584620
for discontinuity_index in np.where(discontinuous_timestamps)[0]:
585-
prev_ts = timestamp[discontinuity_index]
586-
next_ts = timestamp[discontinuity_index + 1]
621+
prev_ts = timestamps[discontinuity_index]
622+
next_ts = timestamps[discontinuity_index + 1]
587623
time_diff = (next_ts - prev_ts) / amplifier_sampling_rate
588624

589625
error_msg += (

neo/rawio/medrawio.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""
22
Class for reading MED (Multiscale Electrophysiology Data) Format.
3-
3+
44
Uses the dhn-med-py python package, created by Dark Horse Neuro, Inc.
55
66
Authors: Dan Crepeau, Matt Stead

neo/rawio/neuronexusrawio.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@
66
* The *.xdat.json metadata file
77
* The *_data.xdat binary file of all raw data
88
* The *_timestamps.xdat binary file of the timestamp data
9-
9+
1010
Based on sample data is appears that the binary file is always a float32 format
1111
Other information can be found within the metadata json file
1212
1313
1414
The metadata file has a pretty complicated structure as far as I can tell
15-
a lot of which is dedicated to probe information, which won't be handle at the
15+
a lot of which is dedicated to probe information, which won't be handle at the
1616
the Neo level.
1717
1818
It appears that the metadata['status'] provides most of the information necessary

neo/rawio/openephysbinaryrawio.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -292,16 +292,14 @@ def _parse_header(self):
292292
rising_indices.extend(rising)
293293
falling_indices.extend(falling)
294294

295-
rising_indices = np.array(rising_indices)
296-
falling_indices = np.array(falling_indices)
295+
rising_indices = np.array(rising_indices, dtype=np.int64)
296+
falling_indices = np.array(falling_indices, dtype=np.int64)
297297

298298
# Sort the indices to maintain chronological order
299299
sorted_order = np.argsort(rising_indices)
300300
rising_indices = rising_indices[sorted_order]
301301
falling_indices = falling_indices[sorted_order]
302302

303-
durations = None
304-
# if len(rising_indices) == len(falling_indices):
305303
durations = timestamps[falling_indices] - timestamps[rising_indices]
306304
if not self._use_direct_evt_timestamps:
307305
timestamps = timestamps / info["sample_rate"]

neo/rawio/tdtrawio.py

Lines changed: 30 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -205,28 +205,37 @@ def _parse_header(self):
205205
keep = info_channel_groups["TankEvType"] & EVTYPE_MASK == EVTYPE_STREAM
206206
missing_sev_channels = []
207207
for stream_index, info in enumerate(info_channel_groups[keep]):
208+
stream_index = int(stream_index) # This transforms numpy scalar to python native int
208209
self._sig_sample_per_chunk[stream_index] = info["NumPoints"]
209210

210-
stream_name = str(info["StoreName"])
211+
stream_name_bytes = info["StoreName"]
212+
stream_name = info["StoreName"].decode("utf-8")
211213
stream_id = f"{stream_index}"
212214
buffer_id = ""
213215
signal_streams.append((stream_name, stream_id, buffer_id))
214216

215-
for c in range(info["NumChan"]):
217+
for channel_index in range(info["NumChan"]):
216218
global_chan_index = len(signal_channels)
217-
chan_id = c + 1 # several StoreName can have same chan_id: this is ok
219+
chan_id = channel_index + 1
218220

219221
# loop over segment to get sampling_rate/data_index/data_buffer
220222
sampling_rate = None
221223
dtype = None
222224
for seg_index, segment_name in enumerate(segment_names):
223225
# get data index
224226
tsq = self._tsq[seg_index]
225-
mask = (
226-
(tsq["evtype"] & EVTYPE_MASK == EVTYPE_STREAM)
227-
& (tsq["evname"] == info["StoreName"])
228-
& (tsq["channel"] == chan_id)
229-
)
227+
# Filter TSQ events to find all data chunks belonging to the current stream and channel
228+
# This identifies which parts of the TEV/SEV files contain our signal data
229+
is_stream_event = (
230+
tsq["evtype"] & EVTYPE_MASK
231+
) == EVTYPE_STREAM # Get only stream events (continuous data)
232+
matches_store_name = (
233+
tsq["evname"] == stream_name_bytes
234+
) # Match the 4-char store name (e.g., 'RSn1')
235+
matches_channel = tsq["channel"] == chan_id # Match the specific channel number
236+
237+
# Combine all conditions - we want events that satisfy all three criteria
238+
mask = is_stream_event & matches_store_name & matches_channel
230239
data_index = tsq[mask].copy()
231240
self._sigs_index[seg_index][global_chan_index] = data_index
232241

@@ -252,11 +261,11 @@ def _parse_header(self):
252261
# sampling_rate and dtype
253262
if len(data_index):
254263
_sampling_rate = float(data_index["frequency"][0])
255-
_dtype = data_formats[data_index["dataformat"][0]]
264+
_dtype = data_formats_map[data_index["dataformat"][0]]
256265
else:
257266
# if no signal present use dummy values
258267
_sampling_rate = 1.0
259-
_dtype = int
268+
_dtype = "int"
260269
if sampling_rate is None:
261270
sampling_rate = _sampling_rate
262271
dtype = _dtype
@@ -277,17 +286,15 @@ def _parse_header(self):
277286
# path = self.dirname / segment_name
278287
if self.tdt_block_mode == "multi":
279288
# for multi block datasets the names of sev files are fixed
280-
store = info["StoreName"].decode("ascii")
281-
sev_stem = f"{tankname}_{segment_name}_{store}_ch{chan_id}"
289+
sev_stem = f"{tankname}_{segment_name}_{stream_name}_ch{chan_id}"
282290
sev_filename = (path / sev_stem).with_suffix(".sev")
283291
else:
284-
# for single block datasets the exact name of sev files in not known
292+
# for single block datasets the exact name of sev files is not known
285293
sev_regex = f"*_[cC]h{chan_id}.sev"
286294
sev_filename = list(self.dirname.parent.glob(str(sev_regex)))
287295
# in case multiple sev files are found, try to find the one for current stream
288296
if len(sev_filename) > 1:
289-
store = info["StoreName"].decode("ascii")
290-
sev_regex = f"*_{store}_Ch{chan_id}.sev"
297+
sev_regex = f"*_{stream_name}_Ch{chan_id}.sev"
291298
sev_filename = list(self.dirname.parent.glob(str(sev_regex)))
292299

293300
# in case non or multiple sev files are found for current stream + channel
@@ -305,14 +312,14 @@ def _parse_header(self):
305312
raise NeoReadWriteError("no TEV nor SEV data to read")
306313
self._sigs_data_buf[seg_index][global_chan_index] = data
307314

308-
chan_name = f"{info['StoreName']} {c + 1}"
315+
channel_name = f"{stream_name} {channel_index + 1}"
309316
sampling_rate = sampling_rate
310317
units = "uV" # see https://github.com/NeuralEnsemble/python-neo/issues/1369
311318
gain = 1.0
312319
offset = 0.0
313320
buffer_id = ""
314321
signal_channels.append(
315-
(chan_name, str(chan_id), sampling_rate, dtype, units, gain, offset, stream_id, buffer_id)
322+
(channel_name, str(chan_id), sampling_rate, dtype, units, gain, offset, stream_id, buffer_id)
316323
)
317324

318325
if missing_sev_channels:
@@ -354,7 +361,7 @@ def _parse_header(self):
354361
)
355362

356363
self._waveforms_size.append(info["NumPoints"])
357-
self._waveforms_dtype.append(np.dtype(data_formats[info["DataFormat"]]))
364+
self._waveforms_dtype.append(np.dtype(data_formats_map[info["DataFormat"]]))
358365

359366
spike_channels = np.array(spike_channels, dtype=_spike_channel_dtype)
360367

@@ -417,13 +424,13 @@ def _get_analogsignal_chunk(self, block_index, seg_index, i_start, i_stop, strea
417424
global_chan_indexes = global_chan_indexes[channel_indexes]
418425
signal_channels = signal_channels[channel_indexes]
419426

420-
dt = self._sig_dtype_by_group[stream_index]
421-
raw_signals = np.zeros((i_stop - i_start, signal_channels.size), dtype=dt)
427+
dtype = self._sig_dtype_by_group[stream_index]
428+
raw_signals = np.zeros((i_stop - i_start, signal_channels.size), dtype=dtype)
422429

423430
sample_per_chunk = self._sig_sample_per_chunk[stream_index]
424431
bl0 = i_start // sample_per_chunk
425432
bl1 = int(np.ceil(i_stop / sample_per_chunk))
426-
chunk_nb_bytes = sample_per_chunk * dt.itemsize
433+
chunk_nb_bytes = sample_per_chunk * dtype.itemsize
427434

428435
for c, global_index in enumerate(global_chan_indexes):
429436
data_index = self._sigs_index[seg_index][global_index]
@@ -434,7 +441,7 @@ def _get_analogsignal_chunk(self, block_index, seg_index, i_start, i_stop, strea
434441
for bl in range(bl0, bl1):
435442
ind0 = data_index[bl]["offset"]
436443
ind1 = ind0 + chunk_nb_bytes
437-
data = data_buf[ind0:ind1].view(dt)
444+
data = data_buf[ind0:ind1].view(dtype)
438445

439446
if bl == bl1 - 1:
440447
# right border
@@ -620,7 +627,7 @@ def read_tbk(tbk_filename):
620627
EVMARK_STARTBLOCK = int("0001", 16) # 1
621628
EVMARK_STOPBLOCK = int("0002", 16) # 2
622629

623-
data_formats = {
630+
data_formats_map = {
624631
0: "float32",
625632
1: "int32",
626633
2: "int16",

neo/test/iotest/test_openephysbinaryio.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
"""
2-
3-
"""
1+
""" """
42

53
import unittest
64

neo/test/iotest/test_openephysio.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
"""
2-
3-
"""
1+
""" """
42

53
import unittest
64

neo/test/rawiotest/test_brainvisionrawio.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
"""
2-
3-
"""
1+
""" """
42

53
import unittest
64

0 commit comments

Comments
 (0)