Skip to content

Commit 8b42f05

Browse files
authored
Merge branch 'master' into update-copyright
2 parents 3bfd4f8 + c034591 commit 8b42f05

16 files changed

+331
-112
lines changed

.github/workflows/io-test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ jobs:
1818
strategy:
1919
fail-fast: true
2020
matrix:
21-
python-version: ['3.9', '3.12']
21+
python-version: ['3.9', '3.13']
2222
defaults:
2323
# by default run in bash mode (required for conda usage)
2424
run:

environment_testing.yml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,3 @@ channels:
44
dependencies:
55
- datalad
66
- pip
7-
# temporary have this here for IO testing while we decide how to deal with
8-
# external packages not 2.0 ready
9-
- numpy=1.26.4

neo/core/analogsignal.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,10 @@ def __new__(
200200
"""
201201
if copy is not None:
202202
raise ValueError(
203-
"`copy` is now deprecated in Neo due to removal in NumPy 2.0 and will be removed in 0.15.0."
203+
"`copy` is now deprecated in Neo due to removal in Quantites to support Numpy 2.0. "
204+
"In order to facilitate the deprecation copy can be set to None but will raise an "
205+
"error if set to True/False since this will silently do nothing. This argument will be completely "
206+
"removed in Neo 0.15.0. Please update your code base as necessary."
204207
)
205208

206209
signal = cls._rescale(signal, units=units)
@@ -210,7 +213,7 @@ def __new__(
210213
obj.shape = (-1, 1)
211214

212215
if t_start is None:
213-
raise ValueError("t_start cannot be None")
216+
raise ValueError("`t_start` cannot be None")
214217
obj._t_start = t_start
215218

216219
obj._sampling_rate = _get_sampling_rate(sampling_rate, sampling_period)

neo/core/imagesequence.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,10 @@ def __new__(
124124

125125
if copy is not None:
126126
raise ValueError(
127-
"`copy` is now deprecated in Neo due to removal in NumPy 2.0 and will be removed in 0.15.0."
127+
"`copy` is now deprecated in Neo due to removal in Quantites to support Numpy 2.0. "
128+
"In order to facilitate the deprecation copy can be set to None but will raise an "
129+
"error if set to True/False since this will silently do nothing. This argument will be completely "
130+
"removed in Neo 0.15.0. Please update your code base as necessary."
128131
)
129132

130133
if spatial_scale is None:

neo/core/irregularlysampledsignal.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,10 @@ def __new__(
174174

175175
if copy is not None:
176176
raise ValueError(
177-
"`copy` is now deprecated in Neo due to removal in NumPy 2.0 and will be removed in 0.15.0."
177+
"`copy` is now deprecated in Neo due to removal in Quantites to support Numpy 2.0. "
178+
"In order to facilitate the deprecation copy can be set to None but will raise an "
179+
"error if set to True/False since this will silently do nothing. This argument will be completely "
180+
"removed in Neo 0.15.0. Please update your code base as necessary."
178181
)
179182

180183
signal = cls._rescale(signal, units=units)

neo/core/spiketrain.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,12 @@ def normalize_times_array(times, units=None, dtype=None, copy=None):
194194
"""
195195

196196
if copy is not None:
197-
raise ValueError("`copy` is now deprecated in Neo due to removal in NumPy 2.0 and will be removed in 0.15.0.")
197+
raise ValueError(
198+
"`copy` is now deprecated in Neo due to removal in Quantites to support Numpy 2.0. "
199+
"In order to facilitate the deprecation copy can be set to None but will raise an "
200+
"error if set to True/False since this will silently do nothing. This argument will be completely "
201+
"removed in Neo 0.15.0. Please update your code base as necessary."
202+
)
198203

199204
if dtype is None:
200205
if not hasattr(times, "dtype"):
@@ -352,7 +357,10 @@ def __new__(
352357
"""
353358
if copy is not None:
354359
raise ValueError(
355-
"`copy` is now deprecated in Neo due to removal in NumPy 2.0 and will be removed in 0.15.0."
360+
"`copy` is now deprecated in Neo due to removal in Quantites to support Numpy 2.0. "
361+
"In order to facilitate the deprecation copy can be set to None but will raise an "
362+
"error if set to True/False since this will silently do nothing. This argument will be completely "
363+
"removed in Neo 0.15.0. Please update your code base as necessary."
356364
)
357365

358366
if len(times) != 0 and waveforms is not None and len(times) != waveforms.shape[0]:

neo/io/klustakwikio.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ def _load_spike_times(self, fetfilename):
197197
names.append("spike_time")
198198

199199
# Load into recarray
200-
data = np.recfromtxt(fetfilename, names=names, skip_header=1, delimiter=" ")
200+
data = np.genfromtxt(fetfilename, names=names, skip_header=1, delimiter=" ")
201201

202202
# get features
203203
features = np.array([data[f"fet{n}"] for n in range(nbFeatures)])

neo/rawio/blackrockrawio.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,7 @@ def _parse_header(self):
473473
segment_mask = ev_ids == data_bl
474474
if data[segment_mask].size > 0:
475475
t = data[segment_mask][-1]["timestamp"] / self.__nev_basic_header["timestamp_resolution"]
476+
476477
max_nev_time = max(max_nev_time, t)
477478
if max_nev_time > t_stop:
478479
t_stop = max_nev_time
@@ -680,7 +681,8 @@ def _get_timestamp_slice(self, timestamp, seg_index, t_start, t_stop):
680681
if t_start is None:
681682
t_start = self._seg_t_starts[seg_index]
682683
if t_stop is None:
683-
t_stop = self._seg_t_stops[seg_index]
684+
t_stop = self._seg_t_stops[seg_index] + 1 / float(
685+
self.__nev_basic_header['timestamp_resolution'])
684686

685687
if t_start is None:
686688
ind_start = None
@@ -713,10 +715,16 @@ def _get_spike_raw_waveforms(self, block_index, seg_index, unit_index, t_start,
713715
)
714716
unit_spikes = all_spikes[mask]
715717

716-
wf_dtype = self.__nev_params("waveform_dtypes")[channel_id]
717-
wf_size = self.__nev_params("waveform_size")[channel_id]
718+
wf_dtype = self.__nev_params('waveform_dtypes')[channel_id]
719+
wf_size = self.__nev_params('waveform_size')[channel_id]
720+
wf_byte_size = np.dtype(wf_dtype).itemsize * wf_size
721+
722+
dt1 = [
723+
('extra', 'S{}'.format(unit_spikes['waveform'].dtype.itemsize - wf_byte_size)),
724+
('ch_waveform', 'S{}'.format(wf_byte_size))]
725+
726+
waveforms = unit_spikes['waveform'].view(dt1)['ch_waveform'].flatten().view(wf_dtype)
718727

719-
waveforms = unit_spikes["waveform"].flatten().view(wf_dtype)
720728
waveforms = waveforms.reshape(int(unit_spikes.size), 1, int(wf_size))
721729

722730
timestamp = unit_spikes["timestamp"]
@@ -1357,7 +1365,7 @@ def __match_nsx_and_nev_segment_ids(self, nsx_nb):
13571365

13581366
# Show warning if spikes do not fit any segment (+- 1 sampling 'tick')
13591367
# Spike should belong to segment before
1360-
mask_outside = (ev_ids == i) & (data["timestamp"] < int(seg["timestamp"]) - nsx_offset - nsx_period)
1368+
mask_outside = (ev_ids == i) & (data["timestamp"] < int(seg["timestamp"]) - int(nsx_offset) - int(nsx_period))
13611369

13621370
if len(data[mask_outside]) > 0:
13631371
warnings.warn(f"Spikes outside any segment. Detected on segment #{i}")
@@ -1987,6 +1995,7 @@ def __get_nsx_param_variant_a(self, nsx_nb):
19871995
else:
19881996
units = "uV"
19891997

1998+
19901999
nsx_parameters = {
19912000
"nb_data_points": int(
19922001
(self.__get_file_size(filename) - bytes_in_headers)
@@ -1995,8 +2004,8 @@ def __get_nsx_param_variant_a(self, nsx_nb):
19952004
),
19962005
"labels": labels,
19972006
"units": np.array([units] * self.__nsx_basic_header[nsx_nb]["channel_count"]),
1998-
"min_analog_val": -1 * np.array(dig_factor),
1999-
"max_analog_val": np.array(dig_factor),
2007+
"min_analog_val": -1 * np.array(dig_factor, dtype="float"),
2008+
"max_analog_val": np.array(dig_factor, dtype="float"),
20002009
"min_digital_val": np.array([-1000] * self.__nsx_basic_header[nsx_nb]["channel_count"]),
20012010
"max_digital_val": np.array([1000] * self.__nsx_basic_header[nsx_nb]["channel_count"]),
20022011
"timestamp_resolution": 30000,

neo/rawio/micromedrawio.py

Lines changed: 80 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ def __init__(self, filename=""):
5252

5353
def _parse_header(self):
5454

55-
self._buffer_descriptions = {0: {0: {}}}
5655

5756
with open(self.filename, "rb") as fid:
5857
f = StructFile(fid)
@@ -67,6 +66,7 @@ def _parse_header(self):
6766
rec_datetime = datetime.datetime(year + 1900, month, day, hour, minute, sec)
6867

6968
Data_Start_Offset, Num_Chan, Multiplexer, Rate_Min, Bytes = f.read_f("IHHHH", offset=138)
69+
sig_dtype = "u" + str(Bytes)
7070

7171
# header version
7272
(header_version,) = f.read_f("b", offset=175)
@@ -99,25 +99,37 @@ def _parse_header(self):
9999
if zname != zname2.decode("ascii").strip(" "):
100100
raise NeoReadWriteError("expected the zone name to match")
101101

102-
# raw signals memmap
103-
sig_dtype = "u" + str(Bytes)
104-
signal_shape = get_memmap_shape(self.filename, sig_dtype, num_channels=Num_Chan, offset=Data_Start_Offset)
105-
buffer_id = "0"
106-
stream_id = "0"
107-
self._buffer_descriptions[0][0][buffer_id] = {
108-
"type": "raw",
109-
"file_path": str(self.filename),
110-
"dtype": sig_dtype,
111-
"order": "C",
112-
"file_offset": 0,
113-
"shape": signal_shape,
114-
}
102+
103+
# "TRONCA" zone define segments
104+
zname2, pos, length = zones["TRONCA"]
105+
f.seek(pos)
106+
# this number avoid a infinite loop in case of corrupted TRONCA zone (seg_start!=0 and trace_offset!=0)
107+
max_segments = 100
108+
self.info_segments = []
109+
for i in range(max_segments):
110+
# 4 bytes u4 each
111+
seg_start = int(np.frombuffer(f.read(4), dtype="u4")[0])
112+
trace_offset = int(np.frombuffer(f.read(4), dtype="u4")[0])
113+
if seg_start == 0 and trace_offset == 0:
114+
break
115+
else:
116+
self.info_segments.append((seg_start, trace_offset))
117+
118+
if len(self.info_segments) == 0:
119+
# one unique segment = general case
120+
self.info_segments.append((0, 0))
121+
122+
nb_segment = len(self.info_segments)
115123

116124
# Reading Code Info
117125
zname2, pos, length = zones["ORDER"]
118126
f.seek(pos)
119127
code = np.frombuffer(f.read(Num_Chan * 2), dtype="u2")
120128

129+
# unique stream and buffer
130+
buffer_id = "0"
131+
stream_id = "0"
132+
121133
units_code = {-1: "nV", 0: "uV", 1: "mV", 2: 1, 100: "percent", 101: "dimensionless", 102: "dimensionless"}
122134
signal_channels = []
123135
sig_grounds = []
@@ -140,10 +152,8 @@ def _parse_header(self):
140152
(sampling_rate,) = f.read_f("H")
141153
sampling_rate *= Rate_Min
142154
chan_id = str(c)
155+
signal_channels.append((chan_name, chan_id, sampling_rate, sig_dtype, units, gain, offset, stream_id, buffer_id))
143156

144-
signal_channels.append(
145-
(chan_name, chan_id, sampling_rate, sig_dtype, units, gain, offset, stream_id, buffer_id)
146-
)
147157

148158
signal_channels = np.array(signal_channels, dtype=_signal_channel_dtype)
149159

@@ -155,6 +165,32 @@ def _parse_header(self):
155165
raise NeoReadWriteError("The sampling rates must be the same across signal channels")
156166
self._sampling_rate = float(np.unique(signal_channels["sampling_rate"])[0])
157167

168+
# memmap traces buffer
169+
full_signal_shape = get_memmap_shape(self.filename, sig_dtype, num_channels=Num_Chan, offset=Data_Start_Offset)
170+
seg_limits = [trace_offset for seg_start, trace_offset in self.info_segments] + [full_signal_shape[0]]
171+
self._t_starts = []
172+
self._buffer_descriptions = {0 :{}}
173+
for seg_index in range(nb_segment):
174+
seg_start, trace_offset = self.info_segments[seg_index]
175+
self._t_starts.append(seg_start / self._sampling_rate)
176+
177+
start = seg_limits[seg_index]
178+
stop = seg_limits[seg_index + 1]
179+
180+
shape = (stop - start, Num_Chan)
181+
file_offset = Data_Start_Offset + ( start * np.dtype(sig_dtype).itemsize * Num_Chan)
182+
self._buffer_descriptions[0][seg_index] = {}
183+
self._buffer_descriptions[0][seg_index][buffer_id] = {
184+
"type" : "raw",
185+
"file_path" : str(self.filename),
186+
"dtype" : sig_dtype,
187+
"order": "C",
188+
"file_offset" : file_offset,
189+
"shape" : shape,
190+
}
191+
192+
193+
158194
# Event channels
159195
event_channels = []
160196
event_channels.append(("Trigger", "", "event"))
@@ -176,13 +212,18 @@ def _parse_header(self):
176212
dtype = np.dtype(ev_dtype)
177213
rawevent = np.memmap(self.filename, dtype=dtype, mode="r", offset=pos, shape=length // dtype.itemsize)
178214

179-
keep = (
180-
(rawevent["start"] >= rawevent["start"][0])
181-
& (rawevent["start"] < signal_shape[0])
182-
& (rawevent["start"] != 0)
183-
)
184-
rawevent = rawevent[keep]
185-
self._raw_events.append(rawevent)
215+
# important : all events timing are related to the first segment t_start
216+
self._raw_events.append([])
217+
for seg_index in range(nb_segment):
218+
left_lim = seg_limits[seg_index]
219+
right_lim = seg_limits[seg_index + 1]
220+
keep = (
221+
(rawevent["start"] >= left_lim)
222+
& (rawevent["start"] < right_lim)
223+
& (rawevent["start"] != 0)
224+
)
225+
self._raw_events[-1].append(rawevent[keep])
226+
186227

187228
# No spikes
188229
spike_channels = []
@@ -191,7 +232,7 @@ def _parse_header(self):
191232
# fille into header dict
192233
self.header = {}
193234
self.header["nb_block"] = 1
194-
self.header["nb_segment"] = [1]
235+
self.header["nb_segment"] = [nb_segment]
195236
self.header["signal_buffers"] = signal_buffers
196237
self.header["signal_streams"] = signal_streams
197238
self.header["signal_channels"] = signal_channels
@@ -216,38 +257,40 @@ def _source_name(self):
216257
return self.filename
217258

218259
def _segment_t_start(self, block_index, seg_index):
219-
return 0.0
260+
return self._t_starts[seg_index]
220261

221262
def _segment_t_stop(self, block_index, seg_index):
222-
sig_size = self.get_signal_size(block_index, seg_index, 0)
223-
t_stop = sig_size / self._sampling_rate
224-
return t_stop
263+
duration = self.get_signal_size(block_index, seg_index, stream_index=0) / self._sampling_rate
264+
return duration + self.segment_t_start(block_index, seg_index)
225265

226266
def _get_signal_t_start(self, block_index, seg_index, stream_index):
227-
if stream_index != 0:
228-
raise ValueError("`stream_index` must be 0")
229-
return 0.0
267+
assert stream_index == 0
268+
return self._t_starts[seg_index]
230269

231270
def _spike_count(self, block_index, seg_index, unit_index):
232271
return 0
233272

234273
def _event_count(self, block_index, seg_index, event_channel_index):
235-
n = self._raw_events[event_channel_index].size
274+
n = self._raw_events[event_channel_index][seg_index].size
236275
return n
237276

238277
def _get_event_timestamps(self, block_index, seg_index, event_channel_index, t_start, t_stop):
239278

240-
raw_event = self._raw_events[event_channel_index]
279+
raw_event = self._raw_events[event_channel_index][seg_index]
280+
281+
# important : all events timing are related to the first segment t_start
282+
seg_start0, _ = self.info_segments[0]
241283

242284
if t_start is not None:
243-
keep = raw_event["start"] >= int(t_start * self._sampling_rate)
285+
keep = raw_event["start"] + seg_start0 >= int(t_start * self._sampling_rate)
244286
raw_event = raw_event[keep]
245287

246288
if t_stop is not None:
247-
keep = raw_event["start"] <= int(t_stop * self._sampling_rate)
289+
keep = raw_event["start"] + seg_start0 <= int(t_stop * self._sampling_rate)
248290
raw_event = raw_event[keep]
249291

250-
timestamp = raw_event["start"]
292+
timestamp = raw_event["start"] + seg_start0
293+
251294
if event_channel_index < 2:
252295
durations = None
253296
else:

0 commit comments

Comments
 (0)