timelined_array 0.0.2__tar.gz → 0.0.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {timelined_array-0.0.2 → timelined_array-0.0.3}/PKG-INFO +1 -1
- {timelined_array-0.0.2 → timelined_array-0.0.3}/pyproject.toml +1 -1
- timelined_array-0.0.3/src/timelined_array/__init__.py +3 -0
- timelined_array-0.0.3/src/timelined_array/time.py +752 -0
- timelined_array-0.0.2/src/timelined_array/__init__.py +0 -3
- timelined_array-0.0.2/src/timelined_array/time.py +0 -356
- {timelined_array-0.0.2 → timelined_array-0.0.3}/LICENSE +0 -0
- {timelined_array-0.0.2 → timelined_array-0.0.3}/README.md +0 -0
- {timelined_array-0.0.2 → timelined_array-0.0.3}/tests/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: timelined_array
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.3
|
|
4
4
|
Summary: Manage easily 1 or multidimensionnal samples numpy arrays that are time related. Extends numpy without removing any of it's abilities on such arrays.
|
|
5
5
|
Author-Email: Timothe Jost <timothe.jost@wanadoo.fr>
|
|
6
6
|
License: MIT
|
|
@@ -0,0 +1,752 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
from logging import getLogger
|
|
5
|
+
from typing import Tuple, List, Protocol, Type
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
logger = getLogger("timelined_array")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TimeCompatibleProtocol(Protocol):
|
|
12
|
+
|
|
13
|
+
time_dimension: int
|
|
14
|
+
timeline: "Timeline"
|
|
15
|
+
|
|
16
|
+
def __getitem__(self, index) -> np.ndarray: ...
|
|
17
|
+
|
|
18
|
+
def __array__(self) -> np.ndarray: ...
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def shape(self) -> Tuple[int]: ...
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def ndim(self) -> int: ...
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def itime(self) -> "TimeIndexer": ...
|
|
28
|
+
|
|
29
|
+
def _get_array_cls(self) -> "Type": ...
|
|
30
|
+
|
|
31
|
+
def transpose(self): ...
|
|
32
|
+
|
|
33
|
+
def _finish_axis_removing_operation(self, result, axis): ...
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Timeline(np.ndarray):
|
|
37
|
+
def __new__(cls, input_array, uniform_space=False):
|
|
38
|
+
|
|
39
|
+
if uniform_space:
|
|
40
|
+
# if we want a uniformly spaced timeline from start to stop of the current timeline.
|
|
41
|
+
obj = Timeline._uniformize(input_array)
|
|
42
|
+
else:
|
|
43
|
+
if isinstance(input_array, Timeline):
|
|
44
|
+
return input_array
|
|
45
|
+
obj = np.asarray(input_array).view(cls)
|
|
46
|
+
|
|
47
|
+
return obj
|
|
48
|
+
|
|
49
|
+
def __array_finalize__(self, obj):
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
def __setstate__(self, state):
|
|
53
|
+
# try:
|
|
54
|
+
# super().__setstate__(state[0:-2]) # old deserializer
|
|
55
|
+
# except TypeError:
|
|
56
|
+
super().__setstate__(state) # new one
|
|
57
|
+
|
|
58
|
+
def __contains__(self, time_value):
|
|
59
|
+
return self.min() <= time_value <= self.max()
|
|
60
|
+
|
|
61
|
+
def max(self):
|
|
62
|
+
return super().max().item()
|
|
63
|
+
|
|
64
|
+
def min(self):
|
|
65
|
+
return super().min().item()
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def _uniformize(cls, timeline):
|
|
69
|
+
raise NotImplementedError("Upcoming function")
|
|
70
|
+
# obj = np.linspace(input_array[0], input_array[1], len(input_array)).view(cls)
|
|
71
|
+
# TODO : do numpy.interp(np.arange(0, len(a), 1.5), np.arange(0, len(a)), a)
|
|
72
|
+
# interp to get a fixed number of points ?
|
|
73
|
+
|
|
74
|
+
def uniformize(self):
|
|
75
|
+
self[:] = self._uniformize(self)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class TimeIndexer:
|
|
79
|
+
"""The time indexer indexes by default from >= to the time start, and strictly < to time stop"""
|
|
80
|
+
|
|
81
|
+
def __init__(self, array: TimeCompatibleProtocol):
|
|
82
|
+
self.array = array
|
|
83
|
+
|
|
84
|
+
def seconds_to_index(self, index):
|
|
85
|
+
# argument index may be a slice or a scalar. Units of index should be in second. Returns a slice as index
|
|
86
|
+
# this is sort of a wrapper for get_iindex that does the heavy lifting.
|
|
87
|
+
# this function just makes sure to pass arguments to it corectly depending
|
|
88
|
+
# on if the time index is a single value or a slice.
|
|
89
|
+
if isinstance(index, slice):
|
|
90
|
+
return self.get_iindex(index.start, index.stop, index.step)
|
|
91
|
+
else:
|
|
92
|
+
return self.get_iindex(sec_start=index).start
|
|
93
|
+
|
|
94
|
+
def _insert_time_index(self, time_index):
|
|
95
|
+
# put the integer value at the position of time index at the right position
|
|
96
|
+
# (time_dimension) in the tuple of all sliced dimensions
|
|
97
|
+
full_index = [slice(None)] * len(self.array.shape)
|
|
98
|
+
full_index[self.array.time_dimension] = time_index
|
|
99
|
+
|
|
100
|
+
return tuple(full_index)
|
|
101
|
+
|
|
102
|
+
def __getitem__(self, index) -> "TimelinedArray | MaskedTimelinedArray | np.ndarray":
|
|
103
|
+
if hasattr(index, "__iter__"):
|
|
104
|
+
# if not isinstance(index,(int,float,slice,np.integer,np.floating)):
|
|
105
|
+
raise ValueError(
|
|
106
|
+
"Isec allow only indexing on time dimension. Index must be either int, float or slice, not iterable"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
iindex_time = self.seconds_to_index(index)
|
|
110
|
+
full_iindex = self._insert_time_index(iindex_time)
|
|
111
|
+
# print("new full index : ",iindex_time)
|
|
112
|
+
logger.debug(f"About to index over time with iindex_time {iindex_time} and full_iindex {full_iindex}")
|
|
113
|
+
return self.array[full_iindex]
|
|
114
|
+
|
|
115
|
+
def get_iindex(self, sec_start=None, sec_stop=None, sec_step=None): # every value here is in seconds
|
|
116
|
+
# converts a time index (follows a slice syntax, but in time units) to integer units
|
|
117
|
+
timeline_max_step = np.absolute(np.diff(self.array.timeline)).max() * 2
|
|
118
|
+
|
|
119
|
+
if sec_start is None:
|
|
120
|
+
start = 0
|
|
121
|
+
else:
|
|
122
|
+
if sec_start >= self.array.timeline[0]:
|
|
123
|
+
start = np.argmax(self.array.timeline >= sec_start)
|
|
124
|
+
else:
|
|
125
|
+
start = 0
|
|
126
|
+
|
|
127
|
+
if abs(self.array.timeline[start] - sec_start) > timeline_max_step:
|
|
128
|
+
raise IndexError(
|
|
129
|
+
f"The start time value {sec_start} you searched for is not in the timeline of this array "
|
|
130
|
+
f"(timeline starts at {self.array.timeline[0]}, allowed jitter = {timeline_max_step} :"
|
|
131
|
+
" +/- 2 times the max step between two timeline points"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
if sec_stop is None:
|
|
135
|
+
stop = len(self.array.timeline)
|
|
136
|
+
# elif sec_stop < 0 : Here we allowed for negative indexing but as timeline can have negative values
|
|
137
|
+
# , i removed this posibility
|
|
138
|
+
# stop = np.argmin(self.array.timeline<self.array.timeline[-1]+sec_stop)
|
|
139
|
+
else:
|
|
140
|
+
if sec_stop < self.array.timeline[-1]:
|
|
141
|
+
stop = np.argmin(self.array.timeline < sec_stop)
|
|
142
|
+
else:
|
|
143
|
+
stop = len(self.array.timeline) - 1
|
|
144
|
+
|
|
145
|
+
if abs(self.array.timeline[stop] - sec_stop) > timeline_max_step:
|
|
146
|
+
raise IndexError(
|
|
147
|
+
f"The end time value {sec_stop} you searched for is not in the timeline of this array "
|
|
148
|
+
f"(timeline ends at {self.array.timeline[-1]} , allowed jitter = {timeline_max_step} : "
|
|
149
|
+
"+/- 2 times the max step between two timeline points"
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
if sec_step is None:
|
|
153
|
+
step = 1
|
|
154
|
+
else:
|
|
155
|
+
step = int(np.round(sec_step / self.array.timeline.step))
|
|
156
|
+
if step < 1:
|
|
157
|
+
step = 1
|
|
158
|
+
return slice(start, stop, step)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class TimeMixin:
|
|
162
|
+
|
|
163
|
+
time_dimension: int
|
|
164
|
+
timeline: Timeline
|
|
165
|
+
|
|
166
|
+
def _time_dimension_in_axis(self, axis: int | Tuple[int] | None) -> bool:
|
|
167
|
+
if (
|
|
168
|
+
axis is None
|
|
169
|
+
or axis == self.time_dimension
|
|
170
|
+
or (isinstance(axis, (list, tuple)) and self.time_dimension in axis)
|
|
171
|
+
):
|
|
172
|
+
return True
|
|
173
|
+
return False
|
|
174
|
+
|
|
175
|
+
def _get_time_dimension_after_axis_removal(self, axis_removed) -> int:
|
|
176
|
+
if not isinstance(axis_removed, tuple):
|
|
177
|
+
axis_removed = (axis_removed,)
|
|
178
|
+
axis_removed = sorted(axis_removed)
|
|
179
|
+
|
|
180
|
+
final_time_dimension = self.time_dimension
|
|
181
|
+
for axis in axis_removed:
|
|
182
|
+
if axis < self.time_dimension:
|
|
183
|
+
final_time_dimension -= 1
|
|
184
|
+
elif axis == self.time_dimension:
|
|
185
|
+
raise ValueError("The time dimension would simply be discarded after axis removal")
|
|
186
|
+
|
|
187
|
+
return final_time_dimension
|
|
188
|
+
|
|
189
|
+
def _get_advanced_indexed_times(self, index):
|
|
190
|
+
|
|
191
|
+
index = np.asarray(index)
|
|
192
|
+
|
|
193
|
+
if index.size == 1:
|
|
194
|
+
# only in case of an array containing a single value, we perform slice indexing from a numpy array
|
|
195
|
+
index = index.item()
|
|
196
|
+
return self._get_slice_indexed_times(index)
|
|
197
|
+
|
|
198
|
+
else:
|
|
199
|
+
# in that case, this is a boolean selection, to filter the array,
|
|
200
|
+
# or an int selection, to filter and/or reorder the array
|
|
201
|
+
if index.dtype == bool or index.dtype == int:
|
|
202
|
+
|
|
203
|
+
# if it's boolean selecting on dimensions including the time_dim, we drop the timeline
|
|
204
|
+
if len(index.shape) > self.time_dimension:
|
|
205
|
+
# if time dimension is the first one, we filter the time dim in the same way we do for the array
|
|
206
|
+
# and we keep the time_dimension
|
|
207
|
+
if self.time_dimension == 0:
|
|
208
|
+
return index, self.timeline[index], self.time_dimension
|
|
209
|
+
|
|
210
|
+
# TimelinedArray(
|
|
211
|
+
# super().__getitem__(index),
|
|
212
|
+
# timeline=self.timeline[index],
|
|
213
|
+
# time_dimension=self.time_dimension,
|
|
214
|
+
# )
|
|
215
|
+
# if it has dimensions before the time dimension, then we don't know what "sub" filter to use
|
|
216
|
+
# to filter the time dimension, so we skip and return a standard array
|
|
217
|
+
else:
|
|
218
|
+
return index, None, None
|
|
219
|
+
|
|
220
|
+
# else we return the filtered array, keeping time_dimension and timeline as is,
|
|
221
|
+
# as they should be untouched
|
|
222
|
+
else:
|
|
223
|
+
return index, self.timeline, self.time_dimension
|
|
224
|
+
|
|
225
|
+
# TimelinedArray(super().__getitem__(index), timeline=self.timeline, time_dimension=self.time_dimension)
|
|
226
|
+
else:
|
|
227
|
+
raise ValueError(
|
|
228
|
+
"Cannot use advanced indexing with arrays " "that are not composed of either booleans or integers"
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
def _get_slice_indexed_times(self, index):
|
|
232
|
+
|
|
233
|
+
# this will store the new time_dimension axis in the newly formed array
|
|
234
|
+
final_time_dimension = self.time_dimension
|
|
235
|
+
# this is to store the time_dimension axis to use in the index, after we searched if we have np.newaxes in it
|
|
236
|
+
time_dimension_in_index = self.time_dimension
|
|
237
|
+
|
|
238
|
+
# if we reach here, we know we are indexing either with :
|
|
239
|
+
# an int for the index, a tuple of ints, a slice or a tuple of slices
|
|
240
|
+
# to ease our way, we make the single int a tuple first :
|
|
241
|
+
if not isinstance(index, tuple):
|
|
242
|
+
index = (index,)
|
|
243
|
+
|
|
244
|
+
# as we can add np.newaxes dynamically, we need to parse the index in a loop to defined the behaviour to adopt
|
|
245
|
+
for dimension in range(len(index)):
|
|
246
|
+
# as long as we are looking at indexing after the time_dimension, we don't care,
|
|
247
|
+
# because standard numpy indexing will occur without changing anything about the timeline nor time_dimension
|
|
248
|
+
if dimension > time_dimension_in_index:
|
|
249
|
+
continue
|
|
250
|
+
|
|
251
|
+
# np.newaxis is a placeholder for None
|
|
252
|
+
if index[dimension] is None:
|
|
253
|
+
# in that case, it means a dimension was added before time_dimension, so we will shift it by one.
|
|
254
|
+
final_time_dimension += 1
|
|
255
|
+
# we also will look at values for time dimension here to apply to timeline later
|
|
256
|
+
time_dimension_in_index += 1
|
|
257
|
+
|
|
258
|
+
# a index at time_dimension or after, is a single integer
|
|
259
|
+
elif isinstance(index[dimension], (int, np.integer)):
|
|
260
|
+
|
|
261
|
+
# if the time dimension index itself is an integer,
|
|
262
|
+
# we loose time related information and return a standard numpy array
|
|
263
|
+
if dimension == time_dimension_in_index:
|
|
264
|
+
return index, None, None
|
|
265
|
+
# np.array(self).__getitem__(index)
|
|
266
|
+
|
|
267
|
+
# otherwise the dimension removed is below time_dimension,
|
|
268
|
+
# and in that case, we decrease it's position in the final array
|
|
269
|
+
# (not in the index, e.g. time_dimension_in_index, as this will still be used to get how to crop it)
|
|
270
|
+
final_time_dimension -= 1
|
|
271
|
+
|
|
272
|
+
# note that if index is a slice, we siply let the normal indexing occur,
|
|
273
|
+
# as it doesn't add or remove dimensions
|
|
274
|
+
|
|
275
|
+
# if a part of the index was destined to reshape the time dimension,
|
|
276
|
+
# we apply this reshaping to timeline too.
|
|
277
|
+
|
|
278
|
+
final_timeline = (
|
|
279
|
+
self.timeline[index[time_dimension_in_index]] if len(index) > time_dimension_in_index else self.timeline
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
return index, final_timeline, final_time_dimension
|
|
283
|
+
|
|
284
|
+
def _get_indexed_times(self, index: int | Tuple[int] | slice | Tuple[slice] | List | np.ndarray):
|
|
285
|
+
|
|
286
|
+
# if index is an array or a list, we do advanced indexing.
|
|
287
|
+
if isinstance(index, (np.ndarray, list)):
|
|
288
|
+
return self._get_advanced_indexed_times(index)
|
|
289
|
+
# otherwise, if single element, (int, slice) or tuple or these, we do regular indexing.
|
|
290
|
+
return self._get_slice_indexed_times(index)
|
|
291
|
+
|
|
292
|
+
@staticmethod
|
|
293
|
+
def _is_single_element(obj):
|
|
294
|
+
return obj.shape == ()
|
|
295
|
+
|
|
296
|
+
def _finish_axis_removing_operation(self, result: TimeCompatibleProtocol, axis: int | Tuple[int] | None):
|
|
297
|
+
if not isinstance(result, np.ndarray):
|
|
298
|
+
return result
|
|
299
|
+
if not self._is_single_element(result):
|
|
300
|
+
return result.item()
|
|
301
|
+
if self._time_dimension_in_axis(axis):
|
|
302
|
+
return np.asarray(result)
|
|
303
|
+
result.time_dimension = self._get_time_dimension_after_axis_removal(axis)
|
|
304
|
+
return result
|
|
305
|
+
|
|
306
|
+
# # REDUCE and SETSTATE are used to instanciate the array from and to a pickled serialized object.
|
|
307
|
+
# # We only need to store and retrieve time_dimension and timeline on top of the array's data
|
|
308
|
+
def __reduce__(self):
|
|
309
|
+
# Get the parent's __reduce__ tuple
|
|
310
|
+
pickled_state = super().__reduce__()
|
|
311
|
+
# Create our own tuple to pass to __setstate__
|
|
312
|
+
new_state = pickled_state[2] + (self.timeline, self.time_dimension) # type: ignore
|
|
313
|
+
|
|
314
|
+
# self.logger.debug(f"Reduced to : time_dimension={self.time_dimension}. Array shape is : {new_state}")
|
|
315
|
+
# Return a tuple that replaces the parent's __setstate__ tuple with our own
|
|
316
|
+
return (pickled_state[0], pickled_state[1], new_state)
|
|
317
|
+
|
|
318
|
+
def __setstate__(self: TimeCompatibleProtocol, state):
|
|
319
|
+
self.timeline = state[-2] # Set the info attribute
|
|
320
|
+
self.time_dimension = state[-1]
|
|
321
|
+
|
|
322
|
+
# Call the parent's __setstate__ with the other tuple elements.
|
|
323
|
+
super().__setstate__(state[0:-2])
|
|
324
|
+
|
|
325
|
+
def __hash__(self):
|
|
326
|
+
return hash((self.__array__(), self.timeline)) # type: ignore
|
|
327
|
+
|
|
328
|
+
def _get_array_cls(self) -> TimeCompatibleProtocol:
|
|
329
|
+
valid_types = [TimelinedArray, TimelinedArray]
|
|
330
|
+
for vtype in valid_types:
|
|
331
|
+
if isinstance(self, vtype):
|
|
332
|
+
return vtype # type: ignore
|
|
333
|
+
return np.ndarray # type: ignore
|
|
334
|
+
|
|
335
|
+
@property
|
|
336
|
+
def array_info(self: TimeCompatibleProtocol):
|
|
337
|
+
return (
|
|
338
|
+
f"{type(self).__name__} of shape {self.shape}, time_dimension {self.time_dimension} "
|
|
339
|
+
f"and timeline shape {self.timeline.shape}"
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
@property
|
|
343
|
+
def itime(self: TimeCompatibleProtocol):
|
|
344
|
+
return TimeIndexer(self)
|
|
345
|
+
|
|
346
|
+
@property
|
|
347
|
+
def isec(self: TimeCompatibleProtocol):
|
|
348
|
+
return self.itime
|
|
349
|
+
|
|
350
|
+
def align_trace(self, start: float, element_nb: int):
|
|
351
|
+
"""Aligns the timelined array by making it start from a timepoint in time-units (synchronizing)
|
|
352
|
+
and cutting the array N elements after the start point.
|
|
353
|
+
|
|
354
|
+
Args:
|
|
355
|
+
start (float): Start point, in time-units. (usually seconds) Time index based, so can be float or integer.
|
|
356
|
+
element_nb (int): Cuts the returned array at 'element_nb' amount of elements, after the starting point.
|
|
357
|
+
It is item index based, not time index based, so it must necessarily be an integer.
|
|
358
|
+
|
|
359
|
+
Returns:
|
|
360
|
+
TimelinedArray: The synchronized and cut arrray.
|
|
361
|
+
"""
|
|
362
|
+
|
|
363
|
+
return self.itime[start:][:element_nb] # type: ignore
|
|
364
|
+
|
|
365
|
+
def swapaxes(self: TimeCompatibleProtocol, axis1: int, axis2: int):
|
|
366
|
+
# we re-instanciate a TimelinedArray with view instead of the full constructor : faster
|
|
367
|
+
cls = self._get_array_cls()
|
|
368
|
+
|
|
369
|
+
swapped_array: TimeCompatibleProtocol = np.swapaxes(np.asarray(self), axis1, axis2).view(cls) # type: ignore
|
|
370
|
+
swapped_array.timeline = self.timeline
|
|
371
|
+
|
|
372
|
+
if axis1 == self.time_dimension:
|
|
373
|
+
swapped_array.time_dimension = axis2
|
|
374
|
+
elif axis2 == self.time_dimension:
|
|
375
|
+
swapped_array.time_dimension = axis1
|
|
376
|
+
else:
|
|
377
|
+
swapped_array.time_dimension = self.time_dimension
|
|
378
|
+
|
|
379
|
+
# TimelinedArray.time_dimension and TimelinedArray.timeline are set. good to go
|
|
380
|
+
return swapped_array
|
|
381
|
+
|
|
382
|
+
def transpose(self: TimeCompatibleProtocol, *axes):
|
|
383
|
+
if not axes:
|
|
384
|
+
axes = tuple(range(self.ndim))[::-1]
|
|
385
|
+
|
|
386
|
+
cls = self._get_array_cls()
|
|
387
|
+
|
|
388
|
+
# we re-instanciate a TimelinedArray with view instead of the full constructor : faster
|
|
389
|
+
transposed_array: TimeCompatibleProtocol = np.transpose(np.asarray(self), axes).view(cls) # type: ignore
|
|
390
|
+
transposed_array.timeline = self.timeline
|
|
391
|
+
|
|
392
|
+
if self.time_dimension in axes:
|
|
393
|
+
transposed_array.time_dimension = axes.index(self.time_dimension)
|
|
394
|
+
else:
|
|
395
|
+
transposed_array.time_dimension = self.time_dimension
|
|
396
|
+
|
|
397
|
+
# TimelinedArray.time_dimension and TimelinedArray.timeline are set. good to go
|
|
398
|
+
return transposed_array
|
|
399
|
+
|
|
400
|
+
@property
|
|
401
|
+
def T(self: TimeCompatibleProtocol):
|
|
402
|
+
return self.transpose()
|
|
403
|
+
|
|
404
|
+
def moveaxis(self: TimeCompatibleProtocol, source: int | Tuple[int], destination: int | Tuple[int]):
|
|
405
|
+
if isinstance(source, int):
|
|
406
|
+
source = (source,)
|
|
407
|
+
if isinstance(destination, int):
|
|
408
|
+
destination = (destination,)
|
|
409
|
+
|
|
410
|
+
cls = self._get_array_cls()
|
|
411
|
+
|
|
412
|
+
# we re-instanciate a TimelinedArray with view instead of the full constructor : faster
|
|
413
|
+
moved_array: TimeCompatibleProtocol = np.moveaxis(np.asarray(self), source, destination).view(
|
|
414
|
+
cls
|
|
415
|
+
) # type: ignore
|
|
416
|
+
moved_array.timeline = self.timeline
|
|
417
|
+
moved_array.time_dimension = self.time_dimension
|
|
418
|
+
|
|
419
|
+
if self.time_dimension in source:
|
|
420
|
+
index_in_source = source.index(self.time_dimension)
|
|
421
|
+
moved_array.time_dimension = destination[index_in_source]
|
|
422
|
+
else:
|
|
423
|
+
for src, dest in zip(source, destination):
|
|
424
|
+
if src < self.time_dimension and dest >= self.time_dimension:
|
|
425
|
+
moved_array.time_dimension -= 1
|
|
426
|
+
elif src > self.time_dimension and dest <= self.time_dimension:
|
|
427
|
+
moved_array.time_dimension += 1
|
|
428
|
+
|
|
429
|
+
# TimelinedArray.time_dimension and TimelinedArray.timeline are set. good to go
|
|
430
|
+
return moved_array
|
|
431
|
+
|
|
432
|
+
def rollaxis(self: TimeCompatibleProtocol, axis: int, start: int = 0):
|
|
433
|
+
# we re-instanciate a TimelinedArray with view instead of the full constructor : faster
|
|
434
|
+
cls = self._get_array_cls()
|
|
435
|
+
|
|
436
|
+
rolled_array: TimeCompatibleProtocol = np.rollaxis(np.asarray(self), axis, start).view(cls) # type: ignore
|
|
437
|
+
rolled_array.timeline = self.timeline
|
|
438
|
+
rolled_array.time_dimension = self.time_dimension
|
|
439
|
+
|
|
440
|
+
if axis < self.time_dimension:
|
|
441
|
+
if start <= axis:
|
|
442
|
+
rolled_array.time_dimension += 1
|
|
443
|
+
elif start <= self.time_dimension:
|
|
444
|
+
rolled_array.time_dimension -= 1
|
|
445
|
+
elif axis == self.time_dimension:
|
|
446
|
+
rolled_array.time_dimension = start
|
|
447
|
+
else:
|
|
448
|
+
if start <= self.time_dimension:
|
|
449
|
+
rolled_array.time_dimension -= 1
|
|
450
|
+
elif start > self.time_dimension:
|
|
451
|
+
rolled_array.time_dimension += 1
|
|
452
|
+
|
|
453
|
+
# TimelinedArray.time_dimension and TimelinedArray.timeline are set. good to go
|
|
454
|
+
return rolled_array
|
|
455
|
+
|
|
456
|
+
def mean(self: TimeCompatibleProtocol, axis: int | Tuple[int] | None = None, dtype=None, out=None, keepdims=False):
|
|
457
|
+
result = super().mean(axis=axis, dtype=dtype, out=out, keepdims=keepdims)
|
|
458
|
+
return self._finish_axis_removing_operation(result, axis)
|
|
459
|
+
|
|
460
|
+
# Override other reduction methods similarly if needed
|
|
461
|
+
def sum(self: TimeCompatibleProtocol, axis: int | Tuple[int] | None = None, dtype=None, out=None, keepdims=False):
|
|
462
|
+
result = super().sum(axis=axis, dtype=dtype, out=out, keepdims=keepdims)
|
|
463
|
+
return self._finish_axis_removing_operation(result, axis)
|
|
464
|
+
|
|
465
|
+
def std(
|
|
466
|
+
self: TimeCompatibleProtocol, axis: int | Tuple[int] | None = None, dtype=None, out=None, ddof=0, keepdims=False
|
|
467
|
+
):
|
|
468
|
+
result = super().std(axis=axis, dtype=dtype, out=out, ddof=ddof, keepdims=keepdims)
|
|
469
|
+
return self._finish_axis_removing_operation(result, axis)
|
|
470
|
+
|
|
471
|
+
def var(
|
|
472
|
+
self: TimeCompatibleProtocol, axis: int | Tuple[int] | None = None, dtype=None, out=None, ddof=0, keepdims=False
|
|
473
|
+
):
|
|
474
|
+
result = super().var(axis=axis, dtype=dtype, out=out, ddof=ddof, keepdims=keepdims)
|
|
475
|
+
return self._finish_axis_removing_operation(result, axis)
|
|
476
|
+
|
|
477
|
+
def rebase_timeline(self, at=0):
|
|
478
|
+
# returns a modified version of the array, with the first element of the array to time zero,
|
|
479
|
+
# and shift the rest accordingly
|
|
480
|
+
cls = self._get_array_cls()
|
|
481
|
+
return cls(self, timeline=self.timeline - self.timeline[at]) # type: ignore
|
|
482
|
+
|
|
483
|
+
def offset_timeline(self, offset):
|
|
484
|
+
# returns a modified version of the array, where we set time of all elements
|
|
485
|
+
# in array at a fix offset relative to their current value.
|
|
486
|
+
cls = self._get_array_cls()
|
|
487
|
+
return cls(self, timeline=self.timeline + offset) # type: ignore
|
|
488
|
+
|
|
489
|
+
@property
|
|
490
|
+
def pack(self):
|
|
491
|
+
return TimePacker(self)
|
|
492
|
+
|
|
493
|
+
def sec_max(self):
|
|
494
|
+
# get maximum time
|
|
495
|
+
return self.timeline.max()
|
|
496
|
+
|
|
497
|
+
def sec_min(self):
|
|
498
|
+
# get minimum time
|
|
499
|
+
return self.timeline.min()
|
|
500
|
+
|
|
501
|
+
@staticmethod
|
|
502
|
+
def extract_time_from_data(data, timeline=None, time_dimension=None, uniform_space=False):
|
|
503
|
+
_unpacking = False
|
|
504
|
+
# if timeline not explicitely passed as arg, we try to pick up the timeline of the input_array.
|
|
505
|
+
# will rise after if input_array is not a timelined_array
|
|
506
|
+
if timeline is None:
|
|
507
|
+
timeline = getattr(data, "timeline", None)
|
|
508
|
+
|
|
509
|
+
if timeline is None:
|
|
510
|
+
# if arguments are an uniform list of timelined array
|
|
511
|
+
# (often use to make mean and std of synchonized timelines), we pick up the first one.
|
|
512
|
+
for element in data:
|
|
513
|
+
timeline = getattr(element, "timeline", None)
|
|
514
|
+
_unpacking = True
|
|
515
|
+
break
|
|
516
|
+
|
|
517
|
+
if timeline is None:
|
|
518
|
+
raise ValueError("timeline must be supplied if the input_array is not a TimelinedArray")
|
|
519
|
+
|
|
520
|
+
if time_dimension is None: # same thing for the time dimension.
|
|
521
|
+
time_dimension = getattr(data, "time_dimension", None)
|
|
522
|
+
|
|
523
|
+
if time_dimension is None:
|
|
524
|
+
# if arguments are an uniform list of timelined array
|
|
525
|
+
# (often use to make mean and std of synchonized timelines), we pick up the first one.
|
|
526
|
+
# but it also means default numpy packing will set the new dimension as dimension 0.
|
|
527
|
+
# As such, the current time dimension will have to be the time dimension of the listed elements,
|
|
528
|
+
# +1 (a.k.a. shifted one dimension deeper)
|
|
529
|
+
|
|
530
|
+
for element in data:
|
|
531
|
+
time_dimension = getattr(element, "time_dimension", None) + 1
|
|
532
|
+
_unpacking = True
|
|
533
|
+
break
|
|
534
|
+
else:
|
|
535
|
+
time_dimension = 0
|
|
536
|
+
|
|
537
|
+
if time_dimension is None:
|
|
538
|
+
time_dimension = 0
|
|
539
|
+
|
|
540
|
+
if not isinstance(time_dimension, int):
|
|
541
|
+
raise ValueError("time_dimension must be an integer")
|
|
542
|
+
|
|
543
|
+
timeline = Timeline(timeline, uniform_space=uniform_space)
|
|
544
|
+
|
|
545
|
+
if _unpacking:
|
|
546
|
+
logger.debug(f"We are unpacking {type(data)} data")
|
|
547
|
+
if not isinstance(data, np.ndarray) or len(data.shape) <= time_dimension: # type: ignore
|
|
548
|
+
data = np.stack(data) # type: ignore
|
|
549
|
+
|
|
550
|
+
return data, timeline, time_dimension
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
class TimePacker:
|
|
554
|
+
def __init__(self, array):
|
|
555
|
+
self.array = array
|
|
556
|
+
|
|
557
|
+
def __iter__(self):
|
|
558
|
+
return iter((self.array.timeline, self.array.__array__()))
|
|
559
|
+
|
|
560
|
+
|
|
561
|
+
class TimelinedArray(TimeMixin, np.ndarray, TimeCompatibleProtocol):
|
|
562
|
+
"""
|
|
563
|
+
The TimelinedArray class is a subclass of the numpy.ndarray class, which represents a multi-dimensional
|
|
564
|
+
array of homogeneous data. This class adds additional functionality
|
|
565
|
+
for working with arrays that have a time dimension, specifically:
|
|
566
|
+
|
|
567
|
+
It defines a Timeline class, which is also a subclass of numpy.ndarray, and represents a timeline associated
|
|
568
|
+
with the array. The Timeline class has several methods, including:
|
|
569
|
+
arange_timeline: This method takes a timeline array and creates an evenly spaced timeline based
|
|
570
|
+
on the start and stop time of the original timeline.
|
|
571
|
+
timeline_step: This method returns the average time difference between each consecutive value in the timeline.
|
|
572
|
+
|
|
573
|
+
TimelinedArrayIndexer class, which has several methods, including:
|
|
574
|
+
seconds_to_index: This method converts time in seconds to index value.
|
|
575
|
+
get_iindex: This method converts time in seconds to a slice object representing time.
|
|
576
|
+
|
|
577
|
+
__new__ : This method is used to creates a new instance of the TimelinedArray class. It takes several optional
|
|
578
|
+
arguments: timeline, time_dimension, arange_timeline, and timeline_is_arranged.
|
|
579
|
+
It creates a TimelinedArrayIndexer object with the input array,
|
|
580
|
+
and assigns the supplied timeline and dimension properties.
|
|
581
|
+
|
|
582
|
+
It defines an indexer to access the TimelinedArray as if it was indexed by time instead of index
|
|
583
|
+
It also adds an attribute time_dimension , and timeline_is_arranged to the class, which are used to keep track of
|
|
584
|
+
the time dimension of the array and whether the timeline is arranged or not.
|
|
585
|
+
It enables accessing the array with time instead of index, and it also tries to keep track of the time dimension
|
|
586
|
+
and the timeline, so it can be used to correct indexed time.
|
|
587
|
+
|
|
588
|
+
Example :
|
|
589
|
+
...
|
|
590
|
+
|
|
591
|
+
"""
|
|
592
|
+
|
|
593
|
+
def __new__(cls, data, timeline=None, time_dimension: int | None = None, uniform_space=False) -> "TimelinedArray":
|
|
594
|
+
|
|
595
|
+
data, timeline, time_dimension = TimeMixin.extract_time_from_data(
|
|
596
|
+
data, timeline=timeline, time_dimension=time_dimension, uniform_space=uniform_space
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
# if np.isscalar(timeline):
|
|
600
|
+
# logger.debug(f"Scalar timeline found. Timeline is {timeline}")
|
|
601
|
+
# return np.asarray(input_array) # type: ignore
|
|
602
|
+
|
|
603
|
+
# instanciate the np array as a view, as per numpy documentation on how to make ndarray child classes
|
|
604
|
+
obj = np.asarray(data).view(cls)
|
|
605
|
+
|
|
606
|
+
if obj.shape[time_dimension] != len(timeline):
|
|
607
|
+
raise ValueError(
|
|
608
|
+
"timeline object and the shape of time_dimension of the input_array must be equal. "
|
|
609
|
+
f"They are : {len(timeline)} and {obj.shape[time_dimension]}"
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
obj.timeline = timeline
|
|
613
|
+
obj.time_dimension = time_dimension
|
|
614
|
+
return obj
|
|
615
|
+
|
|
616
|
+
def __array_finalize__(self, obj):
|
|
617
|
+
super().__array_finalize__(obj)
|
|
618
|
+
if obj is None:
|
|
619
|
+
return
|
|
620
|
+
self.timeline = getattr(obj, "timeline", Timeline([]))
|
|
621
|
+
self.time_dimension = getattr(obj, "time_dimension", 0)
|
|
622
|
+
|
|
623
|
+
def __array_wrap__(self, out_arr, context=None):
|
|
624
|
+
if context is not None:
|
|
625
|
+
logger.debug(f"wrapping array after ufunc {context[0].__name__}")
|
|
626
|
+
output = super().__array_wrap__(out_arr, context)
|
|
627
|
+
if len(output.shape) < len(self.shape):
|
|
628
|
+
logger.debug(f"shape reduced from : {self.shape} to : {output.shape}. outarray was : {out_arr.shape}")
|
|
629
|
+
return output
|
|
630
|
+
|
|
631
|
+
def __array_function__(self, func, types, args, kwargs):
|
|
632
|
+
logger.debug(f"intercepting array before function {func.__name__}")
|
|
633
|
+
return super().__array_function__(func, types, args, kwargs)
|
|
634
|
+
|
|
635
|
+
def __getitem__(
|
|
636
|
+
self, index: int | Tuple[int] | slice | Tuple[slice] | List | np.ndarray
|
|
637
|
+
) -> "TimelinedArray | np.ndarray":
|
|
638
|
+
|
|
639
|
+
index, final_timeline, final_time_dimension = self._get_indexed_times(index)
|
|
640
|
+
|
|
641
|
+
if final_timeline is None or final_time_dimension is None:
|
|
642
|
+
return np.array(self).__getitem__(index)
|
|
643
|
+
|
|
644
|
+
indexed_result = super().__getitem__(index)
|
|
645
|
+
|
|
646
|
+
logger.debug(
|
|
647
|
+
f"Current object : {self.array_info}.\n"
|
|
648
|
+
f"Newly indexed object : {type(indexed_result).__name__} of shape {indexed_result.shape}, "
|
|
649
|
+
f"time_dimension {final_time_dimension} and timeline shape {final_timeline.shape}"
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
return TimelinedArray(indexed_result, timeline=final_timeline, time_dimension=final_time_dimension)
|
|
653
|
+
|
|
654
|
+
# __repr__ and __str__ ARE OVERRIDEN TO AVOID HORRIBLE PERFORMANCE WHEN PRINTING
|
|
655
|
+
# DUE TO CUSTOM __GETITEM__ PRE-CHECKS WITH RECURSIVE NATIVE NUMPY REPR
|
|
656
|
+
def __repr__(self):
|
|
657
|
+
return type(self).__name__ + np.array(self).__repr__()[5:]
|
|
658
|
+
|
|
659
|
+
def __str__(self):
|
|
660
|
+
return type(self).__name__ + np.array(self).__str__()
|
|
661
|
+
|
|
662
|
+
@staticmethod
|
|
663
|
+
def align_from_iterable(iterable) -> "TimelinedArray":
|
|
664
|
+
start = min([item.timeline.min() for item in iterable])
|
|
665
|
+
maxlen = min([len(item.isec[start:]) for item in iterable])
|
|
666
|
+
|
|
667
|
+
aligned_arrays = []
|
|
668
|
+
for index, item in enumerate(iterable):
|
|
669
|
+
aligned_arrays.append(item.align_trace(start, maxlen))
|
|
670
|
+
|
|
671
|
+
return TimelinedArray(aligned_arrays)
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
class MaskedTimelinedArray(TimeMixin, np.ma.MaskedArray, TimeCompatibleProtocol):
|
|
675
|
+
def __new__(
|
|
676
|
+
cls,
|
|
677
|
+
data,
|
|
678
|
+
mask=np.ma.nomask,
|
|
679
|
+
dtype=None,
|
|
680
|
+
copy=False,
|
|
681
|
+
fill_value=None,
|
|
682
|
+
keep_mask=True,
|
|
683
|
+
hard_mask=False,
|
|
684
|
+
shrink=True,
|
|
685
|
+
timeline=None,
|
|
686
|
+
time_dimension=None,
|
|
687
|
+
uniform_space=False,
|
|
688
|
+
**kwargs,
|
|
689
|
+
):
|
|
690
|
+
|
|
691
|
+
_, timeline, time_dimension = TimeMixin.extract_time_from_data(
|
|
692
|
+
data, timeline=timeline, time_dimension=time_dimension, uniform_space=uniform_space
|
|
693
|
+
)
|
|
694
|
+
|
|
695
|
+
obj = super().__new__(
|
|
696
|
+
cls,
|
|
697
|
+
data,
|
|
698
|
+
mask=mask,
|
|
699
|
+
dtype=dtype,
|
|
700
|
+
copy=copy,
|
|
701
|
+
fill_value=fill_value,
|
|
702
|
+
keep_mask=keep_mask,
|
|
703
|
+
hard_mask=hard_mask,
|
|
704
|
+
shrink=shrink,
|
|
705
|
+
**kwargs,
|
|
706
|
+
)
|
|
707
|
+
|
|
708
|
+
obj.timeline = timeline
|
|
709
|
+
obj.time_dimension = time_dimension
|
|
710
|
+
return obj
|
|
711
|
+
|
|
712
|
+
def __array_finalize__(self, obj):
|
|
713
|
+
super().__array_finalize__(obj)
|
|
714
|
+
if obj is None:
|
|
715
|
+
return
|
|
716
|
+
self.timeline = getattr(obj, "timeline", Timeline([]))
|
|
717
|
+
self.time_dimension = getattr(obj, "time_dimension", 0)
|
|
718
|
+
|
|
719
|
+
def __getitem__(
|
|
720
|
+
self, index: int | Tuple[int] | slice | Tuple[slice] | List | np.ndarray
|
|
721
|
+
) -> "MaskedTimelinedArray | np.ma.MaskedArray":
|
|
722
|
+
|
|
723
|
+
index, final_timeline, final_time_dimension = self._get_indexed_times(index)
|
|
724
|
+
|
|
725
|
+
if final_timeline is None or final_time_dimension is None:
|
|
726
|
+
return np.ma.MaskedArray(data=np.asarray(self), mask=self.mask, fill_value=self.fill_value).__getitem__(
|
|
727
|
+
index
|
|
728
|
+
)
|
|
729
|
+
|
|
730
|
+
indexed_result = super().__getitem__(index)
|
|
731
|
+
|
|
732
|
+
logger.debug(
|
|
733
|
+
f"Current object : {self.array_info}.\n"
|
|
734
|
+
f"Newly indexed object : {type(indexed_result).__name__} of shape {indexed_result.shape}, "
|
|
735
|
+
f"time_dimension {final_time_dimension} and timeline shape {final_timeline.shape}"
|
|
736
|
+
)
|
|
737
|
+
|
|
738
|
+
return MaskedTimelinedArray(indexed_result, timeline=final_timeline, time_dimension=final_time_dimension)
|
|
739
|
+
|
|
740
|
+
|
|
741
|
+
class Seconds(float):
|
|
742
|
+
def to_index(self, fs):
|
|
743
|
+
"""_summary_
|
|
744
|
+
|
|
745
|
+
Args:
|
|
746
|
+
fs (float or int): Sampling frequency in Hertz (samples per second)
|
|
747
|
+
|
|
748
|
+
Returns:
|
|
749
|
+
int: The samples index that this second corresponds to,
|
|
750
|
+
(if sample 0 is at 0 second) in an uniformly spaced time array.
|
|
751
|
+
"""
|
|
752
|
+
return int(self * fs)
|
|
@@ -1,356 +0,0 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
|
|
3
|
-
import numpy as np
|
|
4
|
-
|
|
5
|
-
# TODO : do numpy.interp(np.arange(0, len(a), 1.5), np.arange(0, len(a)), a) interp to get a fixed number of points ?
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
class TimelinedArray(np.ndarray):
|
|
9
|
-
"""
|
|
10
|
-
The TimelinedArray class is a subclass of the numpy.ndarray class, which represents a multi-dimensional
|
|
11
|
-
array of homogeneous data. This class adds additional functionality
|
|
12
|
-
for working with arrays that have a time dimension, specifically:
|
|
13
|
-
|
|
14
|
-
It defines a Timeline class, which is also a subclass of numpy.ndarray, and represents a timeline associated
|
|
15
|
-
with the array. The Timeline class has several methods, including:
|
|
16
|
-
arange_timeline: This method takes a timeline array and creates an evenly spaced timeline based
|
|
17
|
-
on the start and stop time of the original timeline.
|
|
18
|
-
timeline_step: This method returns the average time difference between each consecutive value in the timeline.
|
|
19
|
-
|
|
20
|
-
TimelinedArrayIndexer class, which has several methods, including:
|
|
21
|
-
seconds_to_index: This method converts time in seconds to index value.
|
|
22
|
-
get_iindex: This method converts time in seconds to a slice object representing time.
|
|
23
|
-
|
|
24
|
-
__new__ : This method is used to creates a new instance of the TimelinedArray class. It takes several optional
|
|
25
|
-
arguments: timeline, time_dimension, arange_timeline, and timeline_is_arranged.
|
|
26
|
-
It creates a TimelinedArrayIndexer object with the input array,
|
|
27
|
-
and assigns the supplied timeline and dimension properties.
|
|
28
|
-
|
|
29
|
-
It defines an indexer to access the TimelinedArray as if it was indexed by time instead of index
|
|
30
|
-
It also adds an attribute time_dimension , and timeline_is_arranged to the class, which are used to keep track of
|
|
31
|
-
the time dimension of the array and whether the timeline is arranged or not.
|
|
32
|
-
It enables accessing the array with time instead of index, and it also tries to keep track of the time dimension
|
|
33
|
-
and the timeline, so it can be used to correct indexed time.
|
|
34
|
-
|
|
35
|
-
Example :
|
|
36
|
-
...
|
|
37
|
-
|
|
38
|
-
"""
|
|
39
|
-
|
|
40
|
-
time_dimension: int
|
|
41
|
-
timeline: np.ndarray
|
|
42
|
-
|
|
43
|
-
class TA_Timeline(np.ndarray):
|
|
44
|
-
def __new__(cls, input_array, uniform_space=False):
|
|
45
|
-
|
|
46
|
-
if uniform_space:
|
|
47
|
-
# if we want a uniformly spaced timeline from start to stop of the current timeline.
|
|
48
|
-
obj = np.linspace(input_array[0], input_array[1], len(input_array)).view(cls)
|
|
49
|
-
|
|
50
|
-
else:
|
|
51
|
-
if isinstance(input_array, TimelinedArray.TA_Timeline):
|
|
52
|
-
return input_array
|
|
53
|
-
obj = np.asarray(input_array).view(cls)
|
|
54
|
-
|
|
55
|
-
return obj
|
|
56
|
-
|
|
57
|
-
def __array_finalize__(self, obj):
|
|
58
|
-
pass
|
|
59
|
-
|
|
60
|
-
def __setstate__(self, state):
|
|
61
|
-
try:
|
|
62
|
-
super().__setstate__(state[0:-2]) # old deserializer
|
|
63
|
-
except TypeError:
|
|
64
|
-
super().__setstate__(state) # new one
|
|
65
|
-
|
|
66
|
-
def __contains__(self, time_value):
|
|
67
|
-
if time_value >= self.min() and time_value <= self.max():
|
|
68
|
-
return True
|
|
69
|
-
return False
|
|
70
|
-
|
|
71
|
-
def max(self):
|
|
72
|
-
return super().max().item()
|
|
73
|
-
|
|
74
|
-
def min(self):
|
|
75
|
-
return super().min().item()
|
|
76
|
-
|
|
77
|
-
class TA_Isec_Indexer:
|
|
78
|
-
|
|
79
|
-
def __init__(self, array):
|
|
80
|
-
self.array = array
|
|
81
|
-
|
|
82
|
-
def seconds_to_index(self, index):
|
|
83
|
-
# argument index may be a slice or a scalar. Units of index should be in second. Returns a slice as index
|
|
84
|
-
# this is sort of a wrapper for get_iindex that does the heavy lifting.
|
|
85
|
-
# this function just makes sure to pass arguments to it corectly depending
|
|
86
|
-
# on if the time index is a single value or a slice.
|
|
87
|
-
if isinstance(index, slice):
|
|
88
|
-
return self.get_iindex(index.start, index.stop, index.step)
|
|
89
|
-
else:
|
|
90
|
-
return self.get_iindex(sec_start=index).start
|
|
91
|
-
|
|
92
|
-
def _insert_time_index(self, time_index):
|
|
93
|
-
# put the integer value at the position of time index at the right position
|
|
94
|
-
# (time_dimension) in the tuple of all sliced dimensions
|
|
95
|
-
full_index = [slice(None)] * len(self.array.shape)
|
|
96
|
-
full_index[self.array.time_dimension] = time_index
|
|
97
|
-
|
|
98
|
-
return tuple(full_index)
|
|
99
|
-
|
|
100
|
-
def __getitem__(self, index):
|
|
101
|
-
if hasattr(index, "__iter__"):
|
|
102
|
-
# if not isinstance(index,(int,float,slice,np.integer,np.floating)):
|
|
103
|
-
raise ValueError(
|
|
104
|
-
"Isec allow only indexing on time dimension. Index must be either int, float or slice, not iterable"
|
|
105
|
-
)
|
|
106
|
-
|
|
107
|
-
iindex_time = self.seconds_to_index(index)
|
|
108
|
-
full_iindex = self._insert_time_index(iindex_time)
|
|
109
|
-
# print("new full index : ",iindex_time)
|
|
110
|
-
return self.array[full_iindex]
|
|
111
|
-
|
|
112
|
-
def get_iindex(self, sec_start=None, sec_stop=None, sec_step=None): # every value here is in seconds
|
|
113
|
-
# converts a time index (follows a slice syntax, but in time units) to integer units
|
|
114
|
-
timeline_max_step = np.absolute(np.diff(self.array.timeline)).max() * 2
|
|
115
|
-
|
|
116
|
-
if sec_start is None:
|
|
117
|
-
start = 0
|
|
118
|
-
else:
|
|
119
|
-
if sec_start >= self.array.timeline[0]:
|
|
120
|
-
start = np.argmax(self.array.timeline >= sec_start)
|
|
121
|
-
else:
|
|
122
|
-
start = 0
|
|
123
|
-
|
|
124
|
-
if abs(self.array.timeline[start] - sec_start) > timeline_max_step:
|
|
125
|
-
raise IndexError(
|
|
126
|
-
f"The start time value {sec_start} you searched for is not in the timeline of this array "
|
|
127
|
-
f"(timeline starts at {self.array.timeline[0]}, allowed jitter = {timeline_max_step} :"
|
|
128
|
-
" +/- 2 times the max step between two timeline points"
|
|
129
|
-
)
|
|
130
|
-
|
|
131
|
-
if sec_stop is None:
|
|
132
|
-
stop = len(self.array.timeline)
|
|
133
|
-
# elif sec_stop < 0 : Here we allowed for negative indexing but as timeline can have negative values
|
|
134
|
-
# , i removed this posibility
|
|
135
|
-
# stop = np.argmin(self.array.timeline<self.array.timeline[-1]+sec_stop)
|
|
136
|
-
else:
|
|
137
|
-
if sec_stop < self.array.timeline[-1]:
|
|
138
|
-
stop = np.argmin(self.array.timeline < sec_stop)
|
|
139
|
-
else:
|
|
140
|
-
stop = len(self.array.timeline) - 1
|
|
141
|
-
|
|
142
|
-
if abs(self.array.timeline[stop] - sec_stop) > timeline_max_step:
|
|
143
|
-
raise IndexError(
|
|
144
|
-
f"The end time value {sec_stop} you searched for is not in the timeline of this array "
|
|
145
|
-
f"(timeline ends at {self.array.timeline[-1]} , allowed jitter = {timeline_max_step} : "
|
|
146
|
-
"+/- 2 times the max step between two timeline points"
|
|
147
|
-
)
|
|
148
|
-
|
|
149
|
-
if sec_step is None:
|
|
150
|
-
step = 1
|
|
151
|
-
else:
|
|
152
|
-
step = int(np.round(sec_step / self.array.timeline.step))
|
|
153
|
-
if step < 1:
|
|
154
|
-
step = 1
|
|
155
|
-
return slice(start, stop, step)
|
|
156
|
-
|
|
157
|
-
class TA_Packer:
|
|
158
|
-
def __init__(self, array):
|
|
159
|
-
self.array = array
|
|
160
|
-
|
|
161
|
-
def __iter__(self):
|
|
162
|
-
return iter((self.array.timeline, self.array.__array__()))
|
|
163
|
-
|
|
164
|
-
def __new__(cls, input_array, timeline=None, time_dimension: int | None = None, uniform_space=False):
|
|
165
|
-
|
|
166
|
-
_unpacking = False
|
|
167
|
-
if (
|
|
168
|
-
timeline is None
|
|
169
|
-
): # if timeline not explicitely passed as arg, we try to pick up the timeline of the input_array.
|
|
170
|
-
# will rise after if input_array is not a timelined_array
|
|
171
|
-
try:
|
|
172
|
-
timeline = input_array.timeline
|
|
173
|
-
except AttributeError:
|
|
174
|
-
try: # if arguments are an uniform list of timelined array
|
|
175
|
-
# (often use to make mean and std of synchonized timelines), we pick up the first one.
|
|
176
|
-
for element in input_array:
|
|
177
|
-
timeline = element.timeline
|
|
178
|
-
_unpacking = True
|
|
179
|
-
break
|
|
180
|
-
except AttributeError:
|
|
181
|
-
raise ValueError("timeline must be supplied if the input_array is not a TimelinedArray")
|
|
182
|
-
|
|
183
|
-
if time_dimension is None: # same thing for the time dimension.
|
|
184
|
-
try:
|
|
185
|
-
time_dimension = int(input_array.time_dimension)
|
|
186
|
-
except AttributeError:
|
|
187
|
-
try: # if arguments are an uniform list of timelined array
|
|
188
|
-
# (often use to make mean and std of synchonized timelines), we pick up the first one.
|
|
189
|
-
# but it also means default numpy packing will set the new dimension as dimension 0.
|
|
190
|
-
# As such, the current time dimension will have to be the time dimension of the listed elements,
|
|
191
|
-
# +1 (a.k.a. shifted one dimension deeper)
|
|
192
|
-
for element in input_array:
|
|
193
|
-
time_dimension = int(element.time_dimension) + 1
|
|
194
|
-
_unpacking = True
|
|
195
|
-
break
|
|
196
|
-
else:
|
|
197
|
-
time_dimension = 0
|
|
198
|
-
except AttributeError:
|
|
199
|
-
time_dimension = 0
|
|
200
|
-
|
|
201
|
-
timeline = TimelinedArray.TA_Timeline(timeline, uniform_space=uniform_space)
|
|
202
|
-
|
|
203
|
-
if _unpacking and len(input_array.shape) <= time_dimension: # type: ignore
|
|
204
|
-
input_array = np.stack(input_array)
|
|
205
|
-
|
|
206
|
-
obj = np.asarray(input_array).view(
|
|
207
|
-
cls
|
|
208
|
-
) # instanciate the np array as a view, as per numpy documentation on how to make ndarray child classes
|
|
209
|
-
|
|
210
|
-
if obj.shape[time_dimension] != len(timeline):
|
|
211
|
-
raise ValueError(
|
|
212
|
-
"timeline object and the shape of time_dimension of the input_array must be equal. "
|
|
213
|
-
f"They are : {len(timeline)} and {obj.shape[time_dimension]}"
|
|
214
|
-
)
|
|
215
|
-
|
|
216
|
-
obj.timeline = timeline
|
|
217
|
-
obj.time_dimension = time_dimension
|
|
218
|
-
return obj
|
|
219
|
-
|
|
220
|
-
def __array_finalize__(self, obj):
|
|
221
|
-
if obj is None:
|
|
222
|
-
return
|
|
223
|
-
# else, we can reassign attributes of the old array, after a transformation for example .mean, etc
|
|
224
|
-
# TODO : change here to include safechecks to change the timeline if the array changed.
|
|
225
|
-
# maybe using original_shape as a memo of the shape before transormation,
|
|
226
|
-
# to find out wich dimension was reduced ?
|
|
227
|
-
self.timeline = getattr(obj, "timeline", np.array([]))
|
|
228
|
-
self.time_dimension = getattr(obj, "time_dimension", 0)
|
|
229
|
-
|
|
230
|
-
def rebase_timeline(self):
|
|
231
|
-
# returns a modified version of the array, with the first element of the array to time zero,
|
|
232
|
-
# and shift the rest accordingly
|
|
233
|
-
return TimelinedArray(self, timeline=self.timeline - self.timeline[0])
|
|
234
|
-
|
|
235
|
-
def offset_timeline(self, offset):
|
|
236
|
-
# returns a modified version of the array, where we set time of all elements
|
|
237
|
-
# in array at a fix offset relative to their current value.
|
|
238
|
-
return TimelinedArray(self, timeline=self.timeline + offset)
|
|
239
|
-
|
|
240
|
-
@property
|
|
241
|
-
def pack(self):
|
|
242
|
-
return TimelinedArray.TA_Packer(self)
|
|
243
|
-
|
|
244
|
-
def __getitem__(self, index):
|
|
245
|
-
# we re-implement the index getting method of numpy, to crop the timeline with the array,
|
|
246
|
-
# when the array is asked to be cropped.
|
|
247
|
-
|
|
248
|
-
# first we must check wether the time dimension has evolved (less/more dimensions)
|
|
249
|
-
# if index is not iterable, then we affect only dimension 0
|
|
250
|
-
# we define a placeholder as a tuple just for code flexibility to all cases
|
|
251
|
-
|
|
252
|
-
# just in case index is a numpy array with one element, we get it as a single element and not an array
|
|
253
|
-
# as an array has __iter__ but a single element array has no __len__, so the next code would break
|
|
254
|
-
if isinstance(index, np.ndarray):
|
|
255
|
-
if index.size == 1:
|
|
256
|
-
index = index.item()
|
|
257
|
-
|
|
258
|
-
_index = index if hasattr(index, "__iter__") else (index,)
|
|
259
|
-
_time_dimension = self.time_dimension
|
|
260
|
-
_time_dimension_in_index = self.time_dimension
|
|
261
|
-
|
|
262
|
-
for dimension in range(len(_index)):
|
|
263
|
-
if dimension > _time_dimension_in_index:
|
|
264
|
-
continue # if the _index[dimension] changes above time dimension,
|
|
265
|
-
# we don't care as it will not affect it's position.
|
|
266
|
-
if _index[dimension] is None: # np.newaxis is a placeholder for None. a None in an index == a new axis.
|
|
267
|
-
# in that case, it means a dimension was added before time_dimension, so we will shift it by one.
|
|
268
|
-
_time_dimension += 1
|
|
269
|
-
_time_dimension_in_index += (
|
|
270
|
-
1 # we also will look at values for time dimension here to apply to timeline later
|
|
271
|
-
)
|
|
272
|
-
elif isinstance(_index[dimension], (int, np.integer)):
|
|
273
|
-
if dimension == _time_dimension:
|
|
274
|
-
# if the time dimension IS an integer, the use will loose time anyway
|
|
275
|
-
# (getting a single point in time)
|
|
276
|
-
# so in that case, we stop further thinking in term of TimelinedArray and return a standard array,
|
|
277
|
-
# indexed as wanted.
|
|
278
|
-
return np.array(self).__getitem__(index)
|
|
279
|
-
# otherwise it is below, so in that case, if one dimension is removed,
|
|
280
|
-
# bu selecting an single value or that dimension, we substract one to time dimension.
|
|
281
|
-
_time_dimension -= 1
|
|
282
|
-
# here we don't remove one to _time_dimension_in_index.
|
|
283
|
-
# #This -1 removal is for the new array, but the current array still has some somension here that will
|
|
284
|
-
# be indexed,and we need to know that value for timeline.
|
|
285
|
-
|
|
286
|
-
# (index, _time_dimension, _time_dimension_in_index)
|
|
287
|
-
|
|
288
|
-
if len(_index) >= _time_dimension_in_index + 1:
|
|
289
|
-
# if a part of the index was destined to reshape the time dimension,
|
|
290
|
-
# we apply this reshaping to timeline too.
|
|
291
|
-
_timeline = self.timeline[_index[_time_dimension_in_index]]
|
|
292
|
-
else:
|
|
293
|
-
_timeline = self.timeline # if time_dimension is not part of index, then timeline doesn't change.
|
|
294
|
-
|
|
295
|
-
return TimelinedArray(super().__getitem__(index), timeline=_timeline, time_dimension=_time_dimension)
|
|
296
|
-
|
|
297
|
-
@property
|
|
298
|
-
def isec(self):
|
|
299
|
-
"""Allows to use my_array.isec[12.5] to get element closes to time 12.5 in the array's timeline.
|
|
300
|
-
Works with spans of time, and uses item based index for dimensions that are not a time_dimension
|
|
301
|
-
|
|
302
|
-
Returns:
|
|
303
|
-
TA_Isec_Indexer: the objects that can index on the TimelinedArray,
|
|
304
|
-
and returns a TimelinedArray or standard nparray upon __getitem__ call.
|
|
305
|
-
(Standard ndarray array if the time dimension collapses to size 1)
|
|
306
|
-
"""
|
|
307
|
-
return TimelinedArray.TA_Isec_Indexer(self)
|
|
308
|
-
|
|
309
|
-
def sec_max(self):
|
|
310
|
-
# get maximum time
|
|
311
|
-
return self.timeline.max()
|
|
312
|
-
|
|
313
|
-
def sec_min(self):
|
|
314
|
-
# get minimum time
|
|
315
|
-
return self.timeline.min()
|
|
316
|
-
|
|
317
|
-
def __reduce__(self):
|
|
318
|
-
# Get the parent's __reduce__ tuple
|
|
319
|
-
pickled_state = super().__reduce__()
|
|
320
|
-
# Create our own tuple to pass to __setstate__
|
|
321
|
-
new_state = pickled_state[2] + (self.timeline, self.time_dimension) # type: ignore
|
|
322
|
-
|
|
323
|
-
# Return a tuple that replaces the parent's __setstate__ tuple with our own
|
|
324
|
-
return (pickled_state[0], pickled_state[1], new_state)
|
|
325
|
-
|
|
326
|
-
def __setstate__(self, state):
|
|
327
|
-
self.timeline = state[-2] # Set the info attribute
|
|
328
|
-
self.time_dimension = state[-1]
|
|
329
|
-
# Call the parent's __setstate__ with the other tuple elements.
|
|
330
|
-
super().__setstate__(state[0:-2])
|
|
331
|
-
|
|
332
|
-
def __hash__(self):
|
|
333
|
-
return hash((self.__array__(), self.timeline))
|
|
334
|
-
|
|
335
|
-
# THESE ARE OVERRIDEN TO AVOID HORRIBLE PERFORMANCE WHEN PRINTING
|
|
336
|
-
# DUE TO CUSTOM __GETITEM__ PRE-CHECKS WITH RECURSIVE NATIVE NUMPY REPR
|
|
337
|
-
def __repr__(self):
|
|
338
|
-
return type(self).__name__ + np.array(self).__repr__()[5:]
|
|
339
|
-
|
|
340
|
-
def __str__(self):
|
|
341
|
-
return type(self).__name__ + np.array(self).__str__()
|
|
342
|
-
|
|
343
|
-
def align_trace(self, start: float, element_nb: int):
|
|
344
|
-
"""Aligns the timelined array by making it start from a timepoint in time-units (synchronizing)
|
|
345
|
-
and cutting the array N elements after the start point.
|
|
346
|
-
|
|
347
|
-
Args:
|
|
348
|
-
start (float): Start point, in time-units. (usually seconds) Time index based, so can be float or integer.
|
|
349
|
-
element_nb (int): Cuts the returned array at 'element_nb' amount of elements, after the starting point.
|
|
350
|
-
It is item index based, not time index based, so it must necessarily be an integer.
|
|
351
|
-
|
|
352
|
-
Returns:
|
|
353
|
-
TimelinedArray: The synchronized and cut arrray.
|
|
354
|
-
"""
|
|
355
|
-
|
|
356
|
-
return self.isec[start:][:element_nb]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|