timelined_array 0.1.1__tar.gz → 0.1.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.1.1 → timelined_array-0.1.3}/PKG-INFO +9 -6
- {timelined_array-0.1.1 → timelined_array-0.1.3}/README.md +3 -1
- timelined_array-0.1.3/pyproject.toml +73 -0
- timelined_array-0.1.3/pyproject.toml.orig +64 -0
- timelined_array-0.1.3/src/timelined_array/__init__.py +5 -0
- {timelined_array-0.1.1 → timelined_array-0.1.3}/src/timelined_array/time.py +181 -100
- timelined_array-0.1.1/LICENSE +0 -21
- timelined_array-0.1.1/pyproject.toml +0 -90
- timelined_array-0.1.1/src/timelined_array/__init__.py +0 -3
- timelined_array-0.1.1/tests/__init__.py +0 -0
- timelined_array-0.1.1/tests/test_timelined_array.py +0 -267
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
2
|
Name: timelined_array
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.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
|
-
Author
|
|
5
|
+
Author: Timothe Jost
|
|
6
|
+
Author-email: Timothe Jost <44769559+JostTim@users.noreply.github.com>
|
|
6
7
|
License: MIT
|
|
8
|
+
Requires-Dist: numpy>=2
|
|
9
|
+
Requires-Python: >=3.11
|
|
7
10
|
Project-URL: Homepage, https://pypi.org/project/timelined_array/
|
|
8
11
|
Project-URL: Source, https://github.com/JostTim/timelined_array
|
|
9
|
-
Requires-Python: >=3.11
|
|
10
|
-
Requires-Dist: numpy>=2
|
|
11
12
|
Description-Content-Type: text/markdown
|
|
12
13
|
|
|
13
14
|
# timelined_array
|
|
@@ -16,7 +17,9 @@ Description-Content-Type: text/markdown
|
|
|
16
17
|

|
|
17
18
|

|
|
18
19
|

|
|
19
|
-

|
|
20
|
+
[](https://codecov.io/gh/JostTim/timelined_array)
|
|
21
|
+

|
|
22
|
+
|
|
20
23
|
|
|
21
24
|
|
|
22
25
|
<!-- eventually, add ?cache-bust=1 to badges url above, and update the value of cache-bust randomly at git commits (or based on the commit name) so that we always see the latest badge produced, and not the cached badge in the browser.-->
|
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|

|
|
5
5
|

|
|
6
6
|

|
|
7
|
-

|
|
7
|
+
[](https://codecov.io/gh/JostTim/timelined_array)
|
|
8
|
+

|
|
9
|
+
|
|
8
10
|
|
|
9
11
|
|
|
10
12
|
<!-- eventually, add ?cache-bust=1 to badges url above, and update the value of cache-bust randomly at git commits (or based on the commit name) so that we always see the latest badge produced, and not the cached badge in the browser.-->
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "timelined_array"
|
|
3
|
+
description = "Manage easily 1 or multidimensionnal samples numpy arrays that are time related. Extends numpy without removing any of it's abilities on such arrays."
|
|
4
|
+
version = "0.1.3"
|
|
5
|
+
dependencies = ["numpy>=2"]
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
|
|
9
|
+
[[project.authors]]
|
|
10
|
+
name = "Timothe Jost"
|
|
11
|
+
email = "44769559+JostTim@users.noreply.github.com"
|
|
12
|
+
|
|
13
|
+
[project.license]
|
|
14
|
+
text = "MIT"
|
|
15
|
+
|
|
16
|
+
[project.urls]
|
|
17
|
+
Homepage = "https://pypi.org/project/timelined_array/"
|
|
18
|
+
Source = "https://github.com/JostTim/timelined_array"
|
|
19
|
+
|
|
20
|
+
[dependency-groups]
|
|
21
|
+
dev = [
|
|
22
|
+
"pytest>=9.1.1",
|
|
23
|
+
"pytest-cov>=7.1.0",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[build-system]
|
|
27
|
+
requires = ["uv_build>=0.12.5,<0.13.0"]
|
|
28
|
+
build-backend = "uv_build"
|
|
29
|
+
|
|
30
|
+
[tool.uv]
|
|
31
|
+
package = true
|
|
32
|
+
|
|
33
|
+
[tool.ty.rules]
|
|
34
|
+
invalid-method-override = "ignore"
|
|
35
|
+
|
|
36
|
+
[tool.ruff]
|
|
37
|
+
ignore = [
|
|
38
|
+
"B010",
|
|
39
|
+
"PIE796",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
[tool.pytest.ini_options]
|
|
43
|
+
addopts = "-ra -q --pdbcls=IPython.terminal.debugger:TerminalPdb --cov-report xml --cov=timelined_array --junitxml=pytest_results.xml --cov-config=pyproject.toml"
|
|
44
|
+
testpaths = ["tests"]
|
|
45
|
+
|
|
46
|
+
[tool.coverage.run]
|
|
47
|
+
omit = [
|
|
48
|
+
".vscode/*",
|
|
49
|
+
".tests/*",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
[tool.bumpversion]
|
|
53
|
+
current_version = "0.1.3"
|
|
54
|
+
parse = '(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)'
|
|
55
|
+
serialize = ["{major}.{minor}.{patch}"]
|
|
56
|
+
search = "{current_version}"
|
|
57
|
+
replace = "{new_version}"
|
|
58
|
+
regex = false
|
|
59
|
+
ignore_missing_version = false
|
|
60
|
+
tag = false
|
|
61
|
+
sign_tags = false
|
|
62
|
+
tag_name = "v{new_version}"
|
|
63
|
+
tag_message = "Bump version: {current_version} → {new_version}"
|
|
64
|
+
allow_dirty = true
|
|
65
|
+
commit = false
|
|
66
|
+
message = "Bump version: {current_version} → {new_version}"
|
|
67
|
+
commit_args = ""
|
|
68
|
+
|
|
69
|
+
[[tool.bumpversion.files]]
|
|
70
|
+
filename = "pyproject.toml"
|
|
71
|
+
|
|
72
|
+
[[tool.bumpversion.files]]
|
|
73
|
+
filename = "src/timelined_array/__init__.py"
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "timelined_array"
|
|
3
|
+
description = "Manage easily 1 or multidimensionnal samples numpy arrays that are time related. Extends numpy without removing any of it's abilities on such arrays."
|
|
4
|
+
authors = [
|
|
5
|
+
{ name = "Timothe Jost", email = "44769559+JostTim@users.noreply.github.com" },
|
|
6
|
+
]
|
|
7
|
+
version = "0.1.3"
|
|
8
|
+
dependencies = ["numpy>=2"]
|
|
9
|
+
requires-python = ">=3.11"
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
|
|
13
|
+
[project.urls]
|
|
14
|
+
"Homepage" = "https://pypi.org/project/timelined_array/"
|
|
15
|
+
"Source" = "https://github.com/JostTim/timelined_array"
|
|
16
|
+
|
|
17
|
+
[dependency-groups]
|
|
18
|
+
dev = [
|
|
19
|
+
"pytest>=9.1.1",
|
|
20
|
+
"pytest-cov>=7.1.0"
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[build-system]
|
|
24
|
+
requires = ["uv_build>=0.12.5,<0.13.0"]
|
|
25
|
+
build-backend = "uv_build"
|
|
26
|
+
|
|
27
|
+
[tool.uv]
|
|
28
|
+
package = true
|
|
29
|
+
|
|
30
|
+
[tool.ty.rules]
|
|
31
|
+
invalid-method-override = "ignore"
|
|
32
|
+
|
|
33
|
+
[tool.ruff]
|
|
34
|
+
ignore = ["B010", "PIE796"]
|
|
35
|
+
|
|
36
|
+
[tool.pytest.ini_options]
|
|
37
|
+
addopts = "-ra -q --pdbcls=IPython.terminal.debugger:TerminalPdb --cov-report xml --cov=timelined_array --junitxml=pytest_results.xml --cov-config=pyproject.toml"
|
|
38
|
+
testpaths = ["tests"]
|
|
39
|
+
|
|
40
|
+
[tool.coverage.run]
|
|
41
|
+
omit = [".vscode/*", ".tests/*"]
|
|
42
|
+
|
|
43
|
+
[tool.bumpversion]
|
|
44
|
+
current_version = "0.1.3"
|
|
45
|
+
parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
|
|
46
|
+
serialize = ["{major}.{minor}.{patch}"]
|
|
47
|
+
search = "{current_version}"
|
|
48
|
+
replace = "{new_version}"
|
|
49
|
+
regex = false
|
|
50
|
+
ignore_missing_version = false
|
|
51
|
+
tag = false
|
|
52
|
+
sign_tags = false
|
|
53
|
+
tag_name = "v{new_version}"
|
|
54
|
+
tag_message = "Bump version: {current_version} → {new_version}"
|
|
55
|
+
allow_dirty = true
|
|
56
|
+
commit = false
|
|
57
|
+
message = "Bump version: {current_version} → {new_version}"
|
|
58
|
+
commit_args = ""
|
|
59
|
+
|
|
60
|
+
[[tool.bumpversion.files]]
|
|
61
|
+
filename = "pyproject.toml"
|
|
62
|
+
|
|
63
|
+
[[tool.bumpversion.files]]
|
|
64
|
+
filename = "src/timelined_array/__init__.py"
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
|
|
3
|
-
import numpy as np
|
|
4
|
-
from logging import getLogger
|
|
5
|
-
from enum import Enum
|
|
6
1
|
import operator
|
|
2
|
+
from collections.abc import Callable, Sequence
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from logging import getLogger
|
|
5
|
+
from typing import Any, Literal, Protocol, Self, TypeAlias, TypeVar
|
|
7
6
|
|
|
7
|
+
import numpy as np
|
|
8
|
+
import numpy.typing as npt
|
|
8
9
|
from numpy.typing import NDArray
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
|
|
11
|
+
Tp = TypeVar("Tp", bound="TimelinedArray")
|
|
11
12
|
|
|
12
13
|
OperatorType = Callable[[Any, Any], bool]
|
|
13
14
|
|
|
@@ -17,7 +18,6 @@ logger = getLogger("timelined_array")
|
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
class TimeCompatibleProtocol(Protocol):
|
|
20
|
-
|
|
21
21
|
time_dimension: int
|
|
22
22
|
timeline: "Timeline"
|
|
23
23
|
|
|
@@ -26,7 +26,7 @@ class TimeCompatibleProtocol(Protocol):
|
|
|
26
26
|
def __array__(self) -> np.ndarray: ...
|
|
27
27
|
|
|
28
28
|
@property
|
|
29
|
-
def shape(self) ->
|
|
29
|
+
def shape(self) -> tuple[int, ...]: ...
|
|
30
30
|
|
|
31
31
|
@property
|
|
32
32
|
def ndim(self) -> int: ...
|
|
@@ -34,7 +34,7 @@ class TimeCompatibleProtocol(Protocol):
|
|
|
34
34
|
@property
|
|
35
35
|
def itime(self) -> "TimeIndexer": ...
|
|
36
36
|
|
|
37
|
-
def _get_array_cls(self) -> "
|
|
37
|
+
def _get_array_cls(self) -> "type": ...
|
|
38
38
|
|
|
39
39
|
def transpose(self): ...
|
|
40
40
|
|
|
@@ -76,8 +76,6 @@ class Timeline(np.ndarray):
|
|
|
76
76
|
None
|
|
77
77
|
"""
|
|
78
78
|
|
|
79
|
-
pass
|
|
80
|
-
|
|
81
79
|
def __setstate__(self, state):
|
|
82
80
|
"""Set the state of the object.
|
|
83
81
|
|
|
@@ -106,7 +104,7 @@ class Timeline(np.ndarray):
|
|
|
106
104
|
return super().min().item()
|
|
107
105
|
|
|
108
106
|
@classmethod
|
|
109
|
-
def _uniformize(cls, timeline):
|
|
107
|
+
def _uniformize(cls: type[Self], timeline: Self) -> Self:
|
|
110
108
|
"""Uniformize the given timeline data.
|
|
111
109
|
|
|
112
110
|
Args:
|
|
@@ -120,10 +118,14 @@ class Timeline(np.ndarray):
|
|
|
120
118
|
None
|
|
121
119
|
"""
|
|
122
120
|
|
|
123
|
-
raise NotImplementedError(
|
|
121
|
+
raise NotImplementedError(
|
|
122
|
+
"Uniformization of space is not yet supported. Upcoming in future versions"
|
|
123
|
+
)
|
|
124
124
|
# obj = np.linspace(input_array[0], input_array[1], len(input_array)).view(cls)
|
|
125
125
|
# TODO : do numpy.interp(np.arange(0, len(a), 1.5), np.arange(0, len(a)), a)
|
|
126
|
-
# interp to get a fixed number of points ?
|
|
126
|
+
# interp on the paretn array ? to get a fixed number of points ?
|
|
127
|
+
# strategy of this is still to make up...
|
|
128
|
+
# (sampling interpolation, with more/less points ? policies /arguments to make up for this)
|
|
127
129
|
|
|
128
130
|
def uniformize(self):
|
|
129
131
|
"""Uniformize the elements of the list using the _uniformize method."""
|
|
@@ -154,23 +156,21 @@ class Timeline(np.ndarray):
|
|
|
154
156
|
return self._max_step * self.max_step_mult
|
|
155
157
|
|
|
156
158
|
|
|
157
|
-
class
|
|
159
|
+
class StartEdgePolicy(Enum):
|
|
158
160
|
inclusive = operator.ge
|
|
159
161
|
exclusive = operator.gt
|
|
160
162
|
inc = operator.ge
|
|
161
163
|
exc = operator.gt
|
|
162
164
|
|
|
163
165
|
|
|
164
|
-
class
|
|
166
|
+
class EndEdgePolicy(Enum):
|
|
165
167
|
inclusive = operator.le
|
|
166
168
|
exclusive = operator.lt
|
|
167
169
|
inc = operator.le
|
|
168
170
|
exc = operator.lt
|
|
169
171
|
|
|
170
172
|
|
|
171
|
-
|
|
172
|
-
start = StartBoundary
|
|
173
|
-
stop = StoptBoundary
|
|
173
|
+
EdgePolicyString: TypeAlias = Literal["inclusive", "exclusive", "inc", "exc"]
|
|
174
174
|
|
|
175
175
|
|
|
176
176
|
class TimeIndexer:
|
|
@@ -179,17 +179,30 @@ class TimeIndexer:
|
|
|
179
179
|
_start_operation: OperatorType
|
|
180
180
|
_stop_operation: OperatorType
|
|
181
181
|
|
|
182
|
-
def __init__(
|
|
182
|
+
def __init__(
|
|
183
|
+
self,
|
|
184
|
+
array: "BaseTimeArray",
|
|
185
|
+
start: EdgePolicyString = "inclusive",
|
|
186
|
+
stop: EdgePolicyString = "exclusive",
|
|
187
|
+
):
|
|
183
188
|
self.array = array
|
|
184
189
|
self.set_edge_policy(start, stop)
|
|
185
190
|
|
|
186
|
-
def set_edge_policy(
|
|
187
|
-
self
|
|
188
|
-
|
|
191
|
+
def set_edge_policy(
|
|
192
|
+
self,
|
|
193
|
+
start: EdgePolicyString = "inclusive",
|
|
194
|
+
stop: EdgePolicyString = "exclusive",
|
|
195
|
+
):
|
|
196
|
+
self._start_operation = StartEdgePolicy[start].value
|
|
197
|
+
self._stop_operation = EndEdgePolicy[stop].value
|
|
189
198
|
return self
|
|
190
199
|
|
|
191
200
|
def time_to_index(
|
|
192
|
-
self,
|
|
201
|
+
self,
|
|
202
|
+
time: float
|
|
203
|
+
| slice
|
|
204
|
+
| tuple[int | float]
|
|
205
|
+
| list[float | int | slice | tuple[int | float]],
|
|
193
206
|
):
|
|
194
207
|
"""Converts time to index based on different input types.
|
|
195
208
|
|
|
@@ -213,11 +226,13 @@ class TimeIndexer:
|
|
|
213
226
|
elif isinstance(time, list):
|
|
214
227
|
return np.array([self.time_to_index(t) for t in time])
|
|
215
228
|
elif isinstance(time, tuple):
|
|
216
|
-
return self.get_iindex(
|
|
229
|
+
return self.get_iindex(
|
|
230
|
+
*[time[i] if len(time) > i else None for i in range(3)]
|
|
231
|
+
)
|
|
217
232
|
elif isinstance(time, (int, float)):
|
|
218
233
|
return self.get_iindex(sec_start=time).start
|
|
219
234
|
else:
|
|
220
|
-
raise
|
|
235
|
+
raise TypeError("Cannot process time to index")
|
|
221
236
|
|
|
222
237
|
seconds_to_index = time_to_index
|
|
223
238
|
|
|
@@ -238,7 +253,9 @@ class TimeIndexer:
|
|
|
238
253
|
|
|
239
254
|
return tuple(full_index)
|
|
240
255
|
|
|
241
|
-
def __getitem__(
|
|
256
|
+
def __getitem__(
|
|
257
|
+
self, index
|
|
258
|
+
) -> "TimelinedArray | MaskedTimelinedArray | np.ndarray":
|
|
242
259
|
"""Get item from TimelinedArray, MaskedTimelinedArray, or np.ndarray based on the given index.
|
|
243
260
|
|
|
244
261
|
Args:
|
|
@@ -262,7 +279,9 @@ class TimeIndexer:
|
|
|
262
279
|
iindex_time = self.time_to_index(index)
|
|
263
280
|
full_iindex = self._insert_time_index(iindex_time)
|
|
264
281
|
# print("new full index : ",iindex_time)
|
|
265
|
-
logger.debug(
|
|
282
|
+
logger.debug(
|
|
283
|
+
f"About to index over time with iindex_time {iindex_time} and full_iindex {full_iindex}"
|
|
284
|
+
)
|
|
266
285
|
return self.array[full_iindex]
|
|
267
286
|
|
|
268
287
|
def get_iindex(self, sec_start=None, sec_stop=None, sec_step=None):
|
|
@@ -291,7 +310,8 @@ class TimeIndexer:
|
|
|
291
310
|
if abs(self.array.timeline[start] - sec_start) > timeline_max_step:
|
|
292
311
|
raise IndexError(
|
|
293
312
|
f"The start time value {sec_start} you searched for is not in the timeline of this array "
|
|
294
|
-
f"(timeline starts at {self.array.timeline[0]}
|
|
313
|
+
f"(timeline starts at {self.array.timeline[0]} and ends at {self.array.timeline[-1]}, "
|
|
314
|
+
f"allowed jitter = {timeline_max_step} : "
|
|
295
315
|
" +/- 2 times the max step between two timeline points"
|
|
296
316
|
)
|
|
297
317
|
|
|
@@ -309,7 +329,8 @@ class TimeIndexer:
|
|
|
309
329
|
if abs(self.array.timeline[stop] - sec_stop) > timeline_max_step:
|
|
310
330
|
raise IndexError(
|
|
311
331
|
f"The end time value {sec_stop} you searched for is not in the timeline of this array "
|
|
312
|
-
f"(timeline
|
|
332
|
+
f"(timeline starts at {self.array.timeline[0]} and ends at {self.array.timeline[-1]}, "
|
|
333
|
+
f"allowed jitter = {timeline_max_step} : "
|
|
313
334
|
"+/- 2 times the max step between two timeline points"
|
|
314
335
|
)
|
|
315
336
|
|
|
@@ -317,8 +338,7 @@ class TimeIndexer:
|
|
|
317
338
|
step = 1
|
|
318
339
|
else:
|
|
319
340
|
step = int(np.round(sec_step / self.array.timeline.step))
|
|
320
|
-
|
|
321
|
-
step = 1
|
|
341
|
+
step = max(step, 1)
|
|
322
342
|
return slice(start, stop, step)
|
|
323
343
|
|
|
324
344
|
def __call__(self, start="inclusive", stop="exclusive"):
|
|
@@ -326,18 +346,17 @@ class TimeIndexer:
|
|
|
326
346
|
|
|
327
347
|
|
|
328
348
|
class TimeMixin:
|
|
329
|
-
|
|
330
349
|
time_dimension: int
|
|
331
350
|
timeline: Timeline
|
|
332
|
-
start_policy = "inclusive"
|
|
333
|
-
stop_policy = "exclusive"
|
|
351
|
+
start_policy: EdgePolicyString = "inclusive"
|
|
352
|
+
stop_policy: EdgePolicyString = "exclusive"
|
|
334
353
|
|
|
335
354
|
# ndarray inherited
|
|
336
355
|
ndim: int
|
|
337
|
-
shape:
|
|
356
|
+
shape: tuple[int, ...]
|
|
338
357
|
__sub__: Callable
|
|
339
358
|
|
|
340
|
-
def _time_dimension_in_axis(self, axis: int |
|
|
359
|
+
def _time_dimension_in_axis(self, axis: int | tuple[int, ...] | None) -> bool:
|
|
341
360
|
"""Check if the time dimension is present in the specified axis.
|
|
342
361
|
|
|
343
362
|
Args:
|
|
@@ -347,13 +366,12 @@ class TimeMixin:
|
|
|
347
366
|
bool: True if the time dimension is present in the axis, False otherwise.
|
|
348
367
|
"""
|
|
349
368
|
|
|
350
|
-
|
|
369
|
+
return bool(
|
|
351
370
|
axis is None
|
|
352
371
|
or axis == self.time_dimension
|
|
353
|
-
or
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
return False
|
|
372
|
+
or isinstance(axis, (list, tuple))
|
|
373
|
+
and self.time_dimension in axis
|
|
374
|
+
)
|
|
357
375
|
|
|
358
376
|
def _get_time_dimension_after_axis_removal(self, axis_removed) -> int:
|
|
359
377
|
"""Return the time dimension after removing specified axis.
|
|
@@ -377,7 +395,9 @@ class TimeMixin:
|
|
|
377
395
|
if axis < self.time_dimension:
|
|
378
396
|
final_time_dimension -= 1
|
|
379
397
|
elif axis == self.time_dimension:
|
|
380
|
-
raise ValueError(
|
|
398
|
+
raise ValueError(
|
|
399
|
+
"The time dimension would simply be discarded after axis removal"
|
|
400
|
+
)
|
|
381
401
|
|
|
382
402
|
return final_time_dimension
|
|
383
403
|
|
|
@@ -402,7 +422,6 @@ class TimeMixin:
|
|
|
402
422
|
# in that case, this is a boolean selection, to filter the array,
|
|
403
423
|
# or an int selection, to filter and/or reorder the array
|
|
404
424
|
if index.dtype == bool or index.dtype == int:
|
|
405
|
-
|
|
406
425
|
# if it's boolean selecting on dimensions including the time_dim, we drop the timeline
|
|
407
426
|
if len(index.shape) > self.time_dimension:
|
|
408
427
|
# if time dimension is the first one, we filter the time dim in the same way we do for the array
|
|
@@ -428,7 +447,8 @@ class TimeMixin:
|
|
|
428
447
|
# TimelinedArray(super().__getitem__(index), timeline=self.timeline, time_dimension=self.time_dimension)
|
|
429
448
|
else:
|
|
430
449
|
raise ValueError(
|
|
431
|
-
"Cannot use advanced indexing with arrays "
|
|
450
|
+
"Cannot use advanced indexing with arrays "
|
|
451
|
+
"that are not composed of either booleans or integers"
|
|
432
452
|
)
|
|
433
453
|
|
|
434
454
|
def _get_slice_indexed_times(self, index):
|
|
@@ -468,7 +488,6 @@ class TimeMixin:
|
|
|
468
488
|
|
|
469
489
|
# a index at time_dimension or after, is a single integer
|
|
470
490
|
elif isinstance(index[dimension], (int, np.integer)):
|
|
471
|
-
|
|
472
491
|
# if the time dimension index itself is an integer,
|
|
473
492
|
# we loose time related information and return a standard numpy array
|
|
474
493
|
if dimension == time_dimension_in_index:
|
|
@@ -487,12 +506,17 @@ class TimeMixin:
|
|
|
487
506
|
# we apply this reshaping to timeline too.
|
|
488
507
|
|
|
489
508
|
final_timeline = (
|
|
490
|
-
self.timeline[index[time_dimension_in_index]]
|
|
509
|
+
self.timeline[index[time_dimension_in_index]]
|
|
510
|
+
if len(index) > time_dimension_in_index
|
|
511
|
+
else self.timeline
|
|
491
512
|
)
|
|
492
513
|
|
|
493
514
|
return index, final_timeline, final_time_dimension
|
|
494
515
|
|
|
495
|
-
def _get_indexed_times(
|
|
516
|
+
def _get_indexed_times(
|
|
517
|
+
self,
|
|
518
|
+
index: int | tuple[int, ...] | slice | tuple[slice, ...] | list | np.ndarray,
|
|
519
|
+
):
|
|
496
520
|
"""Get indexed times based on the provided index.
|
|
497
521
|
|
|
498
522
|
Args:
|
|
@@ -522,8 +546,10 @@ class TimeMixin:
|
|
|
522
546
|
return obj.shape == ()
|
|
523
547
|
|
|
524
548
|
def _finish_axis_removing_operation(
|
|
525
|
-
self
|
|
526
|
-
|
|
549
|
+
self: Self,
|
|
550
|
+
result: "TimelinedArray| MaskedTimelinedArray | np.ndarray | int | float",
|
|
551
|
+
axis: int | tuple[int, ...],
|
|
552
|
+
) -> Self | np.ndarray | float:
|
|
527
553
|
"""Finish axis removing operation.
|
|
528
554
|
|
|
529
555
|
Args:
|
|
@@ -545,7 +571,6 @@ class TimeMixin:
|
|
|
545
571
|
|
|
546
572
|
|
|
547
573
|
class BaseTimeArray(TimeMixin, np.ndarray):
|
|
548
|
-
|
|
549
574
|
# # REDUCE and SETSTATE are used to instanciate the array from and to a pickled serialized object.
|
|
550
575
|
# # We only need to store and retrieve time_dimension and timeline on top of the array's data
|
|
551
576
|
def __reduce__(self):
|
|
@@ -553,9 +578,8 @@ class BaseTimeArray(TimeMixin, np.ndarray):
|
|
|
553
578
|
object with additional attributes 'timeline' and 'time_dimension'."""
|
|
554
579
|
|
|
555
580
|
# Get the parent's __reduce__ tuple
|
|
556
|
-
pickled_state:
|
|
581
|
+
pickled_state: tuple[Any, Any, tuple] = super().__reduce__() # type: ignore
|
|
557
582
|
# Create our own tuple to pass to __setstate__
|
|
558
|
-
# type: ignore
|
|
559
583
|
new_state = pickled_state[2] + (self.timeline, self.time_dimension)
|
|
560
584
|
|
|
561
585
|
# self.logger.debug(f"Reduced to : time_dimension={self.time_dimension}. Array shape is : {new_state}")
|
|
@@ -586,7 +610,7 @@ class BaseTimeArray(TimeMixin, np.ndarray):
|
|
|
586
610
|
int: Hash value of the object.
|
|
587
611
|
"""
|
|
588
612
|
|
|
589
|
-
return hash((self.__array__(), self.timeline))
|
|
613
|
+
return hash((self.__array__(), self.timeline))
|
|
590
614
|
|
|
591
615
|
def _get_array_cls(self) -> "BaseTimeArray":
|
|
592
616
|
"""Return the class of the array that is compatible with time operations.
|
|
@@ -635,12 +659,22 @@ class BaseTimeArray(TimeMixin, np.ndarray):
|
|
|
635
659
|
Returns:
|
|
636
660
|
TimelinedArray: The synchronized and cut arrray.
|
|
637
661
|
"""
|
|
662
|
+
# this slice is to select the number of elements, on the time_dimension
|
|
663
|
+
slices = tuple(
|
|
664
|
+
slice(None) if i != self.time_dimension else slice(None, element_nb)
|
|
665
|
+
for i in range(self.ndim)
|
|
666
|
+
)
|
|
638
667
|
|
|
639
|
-
return self.itime[start:][
|
|
668
|
+
return self.itime[start:][slices]
|
|
640
669
|
|
|
641
670
|
def shift_values(
|
|
642
671
|
self,
|
|
643
|
-
period: int
|
|
672
|
+
period: int
|
|
673
|
+
| tuple[int, ...]
|
|
674
|
+
| slice
|
|
675
|
+
| tuple[slice, ...]
|
|
676
|
+
| list
|
|
677
|
+
| np.ndarray = 0,
|
|
644
678
|
axis=None,
|
|
645
679
|
time_period=True,
|
|
646
680
|
):
|
|
@@ -668,7 +702,9 @@ class BaseTimeArray(TimeMixin, np.ndarray):
|
|
|
668
702
|
indexer.append(slice(None))
|
|
669
703
|
indexer = tuple(indexer)
|
|
670
704
|
|
|
671
|
-
shift_area =
|
|
705
|
+
shift_area = (
|
|
706
|
+
self.itime.__getitem__(period) if time_period else self.__getitem__(period)
|
|
707
|
+
)
|
|
672
708
|
|
|
673
709
|
# if not this : we lost a dimension because we sliced one axis to a single element, no need to no .mean
|
|
674
710
|
if not len(shift_area.shape) < len(self.shape):
|
|
@@ -680,9 +716,11 @@ class BaseTimeArray(TimeMixin, np.ndarray):
|
|
|
680
716
|
"is not yet implemented"
|
|
681
717
|
)
|
|
682
718
|
|
|
683
|
-
return self - np.repeat(
|
|
719
|
+
return self - np.repeat(
|
|
720
|
+
shift_area.__getitem__(tuple(indexer)), self.shape[axis], axis=axis
|
|
721
|
+
)
|
|
684
722
|
|
|
685
|
-
def swapaxes(self:
|
|
723
|
+
def swapaxes(self: Self, axis1: int, axis2: int) -> Self:
|
|
686
724
|
"""Swap the two specified axes of the TimelinedArray.
|
|
687
725
|
|
|
688
726
|
Args:
|
|
@@ -694,9 +732,9 @@ class BaseTimeArray(TimeMixin, np.ndarray):
|
|
|
694
732
|
"""
|
|
695
733
|
|
|
696
734
|
# we re-instanciate a TimelinedArray with view instead of the full constructor : faster
|
|
697
|
-
cls = self._get_array_cls()
|
|
735
|
+
cls: type[Self] = self._get_array_cls() # type: ignore
|
|
698
736
|
|
|
699
|
-
swapped_array
|
|
737
|
+
swapped_array = np.swapaxes(np.asarray(self), axis1, axis2).view(cls)
|
|
700
738
|
swapped_array.timeline = self.timeline
|
|
701
739
|
|
|
702
740
|
if axis1 == self.time_dimension:
|
|
@@ -742,7 +780,11 @@ class BaseTimeArray(TimeMixin, np.ndarray):
|
|
|
742
780
|
|
|
743
781
|
return self.transpose()
|
|
744
782
|
|
|
745
|
-
def moveaxis(
|
|
783
|
+
def moveaxis(
|
|
784
|
+
self: "BaseTimeArray",
|
|
785
|
+
source: int | tuple[int, ...],
|
|
786
|
+
destination: int | tuple[int, ...],
|
|
787
|
+
):
|
|
746
788
|
"""Move the axis of the array to new positions.
|
|
747
789
|
|
|
748
790
|
Args:
|
|
@@ -765,7 +807,9 @@ class BaseTimeArray(TimeMixin, np.ndarray):
|
|
|
765
807
|
cls = self._get_array_cls()
|
|
766
808
|
|
|
767
809
|
# we re-instanciate a TimelinedArray with view instead of the full constructor : faster
|
|
768
|
-
moved_array: BaseTimeArray = np.moveaxis(
|
|
810
|
+
moved_array: BaseTimeArray = np.moveaxis(
|
|
811
|
+
np.asarray(self), source, destination
|
|
812
|
+
).view(cls) # type: ignore
|
|
769
813
|
moved_array.timeline = self.timeline
|
|
770
814
|
moved_array.time_dimension = self.time_dimension
|
|
771
815
|
|
|
@@ -796,7 +840,9 @@ class BaseTimeArray(TimeMixin, np.ndarray):
|
|
|
796
840
|
# we re-instanciate a TimelinedArray with view instead of the full constructor : faster
|
|
797
841
|
cls = self._get_array_cls()
|
|
798
842
|
|
|
799
|
-
rolled_array: BaseTimeArray = np.rollaxis(np.asarray(self), axis, start).view(
|
|
843
|
+
rolled_array: BaseTimeArray = np.rollaxis(np.asarray(self), axis, start).view(
|
|
844
|
+
cls
|
|
845
|
+
) # type: ignore
|
|
800
846
|
|
|
801
847
|
# reinject timeline as is
|
|
802
848
|
rolled_array.timeline = self.timeline
|
|
@@ -822,14 +868,22 @@ class BaseTimeArray(TimeMixin, np.ndarray):
|
|
|
822
868
|
mapping = {i: new_order.index(i) for i in range(n)}
|
|
823
869
|
return mapping
|
|
824
870
|
|
|
825
|
-
rolled_array.time_dimension = rollaxis_mapping(self.shape, axis, start)[
|
|
871
|
+
rolled_array.time_dimension = rollaxis_mapping(self.shape, axis, start)[
|
|
872
|
+
self.time_dimension
|
|
873
|
+
]
|
|
826
874
|
|
|
827
875
|
return rolled_array
|
|
828
876
|
|
|
829
877
|
def all_axes(self):
|
|
830
878
|
return tuple([i for i in range(self.ndim)])
|
|
831
879
|
|
|
832
|
-
def mean(
|
|
880
|
+
def mean(
|
|
881
|
+
self: Self,
|
|
882
|
+
axis: int | tuple[int, ...] | None = None,
|
|
883
|
+
dtype=None,
|
|
884
|
+
out=None,
|
|
885
|
+
keepdims=False,
|
|
886
|
+
) -> Self | np.ndarray | float:
|
|
833
887
|
"""Calculates the mean along the specified axis.
|
|
834
888
|
|
|
835
889
|
Args:
|
|
@@ -848,7 +902,13 @@ class BaseTimeArray(TimeMixin, np.ndarray):
|
|
|
848
902
|
return self._finish_axis_removing_operation(result, axis)
|
|
849
903
|
|
|
850
904
|
# Override other reduction methods similarly if needed
|
|
851
|
-
def sum(
|
|
905
|
+
def sum(
|
|
906
|
+
self,
|
|
907
|
+
axis: int | tuple[int, ...] | None = None,
|
|
908
|
+
dtype=None,
|
|
909
|
+
out=None,
|
|
910
|
+
keepdims=False,
|
|
911
|
+
) -> Self | np.ndarray | float:
|
|
852
912
|
"""Calculate the sum along the specified axis.
|
|
853
913
|
|
|
854
914
|
Args:
|
|
@@ -874,7 +934,7 @@ class BaseTimeArray(TimeMixin, np.ndarray):
|
|
|
874
934
|
|
|
875
935
|
def std(
|
|
876
936
|
self: "BaseTimeArray",
|
|
877
|
-
axis: int |
|
|
937
|
+
axis: int | tuple[int, ...] | None = None,
|
|
878
938
|
dtype=None,
|
|
879
939
|
out=None,
|
|
880
940
|
ddof=0,
|
|
@@ -898,12 +958,14 @@ class BaseTimeArray(TimeMixin, np.ndarray):
|
|
|
898
958
|
"""
|
|
899
959
|
if axis is None:
|
|
900
960
|
axis = self.all_axes()
|
|
901
|
-
result = super().std(
|
|
961
|
+
result = super().std(
|
|
962
|
+
axis=axis, dtype=dtype, out=out, ddof=ddof, keepdims=keepdims
|
|
963
|
+
)
|
|
902
964
|
return self._finish_axis_removing_operation(result, axis)
|
|
903
965
|
|
|
904
966
|
def var(
|
|
905
967
|
self: "BaseTimeArray",
|
|
906
|
-
axis: int |
|
|
968
|
+
axis: int | tuple[int, ...] | None = None,
|
|
907
969
|
dtype=None,
|
|
908
970
|
out=None,
|
|
909
971
|
ddof=0,
|
|
@@ -928,7 +990,9 @@ class BaseTimeArray(TimeMixin, np.ndarray):
|
|
|
928
990
|
"""
|
|
929
991
|
if axis is None:
|
|
930
992
|
axis = self.all_axes()
|
|
931
|
-
result = super().var(
|
|
993
|
+
result = super().var(
|
|
994
|
+
axis=axis, dtype=dtype, out=out, ddof=ddof, keepdims=keepdims
|
|
995
|
+
)
|
|
932
996
|
return self._finish_axis_removing_operation(result, axis)
|
|
933
997
|
|
|
934
998
|
def rebase_timeline(self, at=0):
|
|
@@ -944,7 +1008,7 @@ class BaseTimeArray(TimeMixin, np.ndarray):
|
|
|
944
1008
|
# returns a modified version of the array, with the first element of the array to time zero,
|
|
945
1009
|
# and shift the rest accordingly
|
|
946
1010
|
cls = self._get_array_cls()
|
|
947
|
-
|
|
1011
|
+
|
|
948
1012
|
return cls(self, timeline=self.timeline - self.timeline[at]) # type: ignore
|
|
949
1013
|
|
|
950
1014
|
def offset_timeline(self, offset):
|
|
@@ -984,7 +1048,9 @@ class BaseTimeArray(TimeMixin, np.ndarray):
|
|
|
984
1048
|
return self.timeline.min()
|
|
985
1049
|
|
|
986
1050
|
@staticmethod
|
|
987
|
-
def extract_time_from_data(
|
|
1051
|
+
def extract_time_from_data(
|
|
1052
|
+
data, timeline=None, time_dimension=None, uniform_space=False
|
|
1053
|
+
) -> tuple[NDArray, Timeline, int]:
|
|
988
1054
|
"""Extracts time-related information from the input data.
|
|
989
1055
|
|
|
990
1056
|
Args:
|
|
@@ -1013,7 +1079,9 @@ class BaseTimeArray(TimeMixin, np.ndarray):
|
|
|
1013
1079
|
break
|
|
1014
1080
|
|
|
1015
1081
|
if timeline is None:
|
|
1016
|
-
raise ValueError(
|
|
1082
|
+
raise ValueError(
|
|
1083
|
+
"timeline must be supplied if the input_array is not a TimelinedArray"
|
|
1084
|
+
)
|
|
1017
1085
|
|
|
1018
1086
|
if time_dimension is None: # same thing for the time dimension.
|
|
1019
1087
|
time_dimension = getattr(data, "time_dimension", None)
|
|
@@ -1039,14 +1107,14 @@ class BaseTimeArray(TimeMixin, np.ndarray):
|
|
|
1039
1107
|
time_dimension = 0
|
|
1040
1108
|
|
|
1041
1109
|
if not isinstance(time_dimension, int):
|
|
1042
|
-
raise
|
|
1110
|
+
raise TypeError("time_dimension must be an integer")
|
|
1043
1111
|
|
|
1044
1112
|
timeline = Timeline(timeline, uniform_space=uniform_space)
|
|
1045
1113
|
|
|
1046
1114
|
if _unpacking:
|
|
1047
1115
|
logger.debug(f"We are unpacking {type(data)} data")
|
|
1048
|
-
if not isinstance(data, np.ndarray) or len(data.shape) <= time_dimension:
|
|
1049
|
-
data = np.stack(data)
|
|
1116
|
+
if not isinstance(data, np.ndarray) or len(data.shape) <= time_dimension:
|
|
1117
|
+
data = np.stack(data)
|
|
1050
1118
|
|
|
1051
1119
|
return data, timeline, time_dimension
|
|
1052
1120
|
|
|
@@ -1097,12 +1165,12 @@ class TimelinedArray(BaseTimeArray):
|
|
|
1097
1165
|
TA_Timeline = Timeline
|
|
1098
1166
|
|
|
1099
1167
|
def __new__(
|
|
1100
|
-
cls,
|
|
1101
|
-
data,
|
|
1102
|
-
timeline:
|
|
1168
|
+
cls: type[Self],
|
|
1169
|
+
data: npt.ArrayLike,
|
|
1170
|
+
timeline: Timeline | np.ndarray | list | None = None,
|
|
1103
1171
|
time_dimension: int | None = None,
|
|
1104
1172
|
uniform_space=False,
|
|
1105
|
-
) ->
|
|
1173
|
+
) -> Self:
|
|
1106
1174
|
"""Create a new TimelinedArray object from the input data.
|
|
1107
1175
|
|
|
1108
1176
|
Args:
|
|
@@ -1116,12 +1184,15 @@ class TimelinedArray(BaseTimeArray):
|
|
|
1116
1184
|
"""
|
|
1117
1185
|
|
|
1118
1186
|
data, timeline, time_dimension = BaseTimeArray.extract_time_from_data(
|
|
1119
|
-
data,
|
|
1187
|
+
data,
|
|
1188
|
+
timeline=timeline,
|
|
1189
|
+
time_dimension=time_dimension,
|
|
1190
|
+
uniform_space=uniform_space,
|
|
1120
1191
|
)
|
|
1121
1192
|
|
|
1122
1193
|
# if np.isscalar(timeline):
|
|
1123
1194
|
# logger.debug(f"Scalar timeline found. Timeline is {timeline}")
|
|
1124
|
-
# return np.asarray(input_array)
|
|
1195
|
+
# return np.asarray(input_array)
|
|
1125
1196
|
|
|
1126
1197
|
# instanciate the np array as a view, as per numpy documentation on how to make ndarray child classes
|
|
1127
1198
|
obj = np.asarray(data).view(cls)
|
|
@@ -1176,7 +1247,9 @@ class TimelinedArray(BaseTimeArray):
|
|
|
1176
1247
|
logger.debug(f"wrapping array after ufunc {context[0].__name__}")
|
|
1177
1248
|
output = super().__array_wrap__(out_arr, context, return_scalar)
|
|
1178
1249
|
if len(output.shape) < len(self.shape):
|
|
1179
|
-
logger.debug(
|
|
1250
|
+
logger.debug(
|
|
1251
|
+
f"shape reduced from : {self.shape} to : {output.shape}. outarray was : {out_arr.shape}"
|
|
1252
|
+
)
|
|
1180
1253
|
return output
|
|
1181
1254
|
|
|
1182
1255
|
def __array_function__(self, func, types, args, kwargs):
|
|
@@ -1197,7 +1270,8 @@ class TimelinedArray(BaseTimeArray):
|
|
|
1197
1270
|
return super().__array_function__(func, types, args, kwargs)
|
|
1198
1271
|
|
|
1199
1272
|
def __getitem__(
|
|
1200
|
-
self,
|
|
1273
|
+
self,
|
|
1274
|
+
index: int | tuple[int, ...] | slice | tuple[slice, ...] | list | np.ndarray,
|
|
1201
1275
|
) -> "TimelinedArray | np.ndarray":
|
|
1202
1276
|
"""Get item from TimelinedArray based on index or slice.
|
|
1203
1277
|
|
|
@@ -1221,7 +1295,9 @@ class TimelinedArray(BaseTimeArray):
|
|
|
1221
1295
|
f"time_dimension {final_time_dimension} and timeline shape {final_timeline.shape}"
|
|
1222
1296
|
)
|
|
1223
1297
|
|
|
1224
|
-
return TimelinedArray(
|
|
1298
|
+
return TimelinedArray(
|
|
1299
|
+
indexed_result, timeline=final_timeline, time_dimension=final_time_dimension
|
|
1300
|
+
)
|
|
1225
1301
|
|
|
1226
1302
|
# __repr__ and __str__ ARE OVERRIDEN TO AVOID HORRIBLE PERFORMANCE WHEN PRINTING
|
|
1227
1303
|
# DUE TO CUSTOM __GETITEM__ PRE-CHECKS WITH RECURSIVE NATIVE NUMPY REPR
|
|
@@ -1238,7 +1314,7 @@ class TimelinedArray(BaseTimeArray):
|
|
|
1238
1314
|
return type(self).__name__ + np.asarray(self).__str__()
|
|
1239
1315
|
|
|
1240
1316
|
@staticmethod
|
|
1241
|
-
def align_from_iterable(iterable) -> "TimelinedArray":
|
|
1317
|
+
def align_from_iterable(iterable: "Sequence[TimelinedArray]") -> "TimelinedArray":
|
|
1242
1318
|
"""Aligns arrays from an iterable based on their timelines.
|
|
1243
1319
|
|
|
1244
1320
|
Args:
|
|
@@ -1260,7 +1336,7 @@ class TimelinedArray(BaseTimeArray):
|
|
|
1260
1336
|
|
|
1261
1337
|
class MaskedTimelinedArray(np.ma.MaskedArray, BaseTimeArray):
|
|
1262
1338
|
def __new__(
|
|
1263
|
-
cls,
|
|
1339
|
+
cls: type[Self],
|
|
1264
1340
|
data,
|
|
1265
1341
|
mask: NDArray[np.bool_] | np.bool_ | bool | np.ma.MaskedArray = np.ma.nomask,
|
|
1266
1342
|
dtype=None,
|
|
@@ -1269,8 +1345,8 @@ class MaskedTimelinedArray(np.ma.MaskedArray, BaseTimeArray):
|
|
|
1269
1345
|
keep_mask=True,
|
|
1270
1346
|
hard_mask=False,
|
|
1271
1347
|
shrink=True,
|
|
1272
|
-
timeline:
|
|
1273
|
-
time_dimension:
|
|
1348
|
+
timeline: Timeline | np.ndarray | list | None = None,
|
|
1349
|
+
time_dimension: int | None = None,
|
|
1274
1350
|
uniform_space=False,
|
|
1275
1351
|
**kwargs,
|
|
1276
1352
|
):
|
|
@@ -1295,8 +1371,11 @@ class MaskedTimelinedArray(np.ma.MaskedArray, BaseTimeArray):
|
|
|
1295
1371
|
An instance of the class with the specified parameters.
|
|
1296
1372
|
"""
|
|
1297
1373
|
|
|
1298
|
-
_, timeline, time_dimension =
|
|
1299
|
-
data,
|
|
1374
|
+
_, timeline, time_dimension = super().extract_time_from_data(
|
|
1375
|
+
data,
|
|
1376
|
+
timeline=timeline,
|
|
1377
|
+
time_dimension=time_dimension,
|
|
1378
|
+
uniform_space=uniform_space,
|
|
1300
1379
|
)
|
|
1301
1380
|
|
|
1302
1381
|
obj = super().__new__(
|
|
@@ -1312,8 +1391,8 @@ class MaskedTimelinedArray(np.ma.MaskedArray, BaseTimeArray):
|
|
|
1312
1391
|
**kwargs,
|
|
1313
1392
|
)
|
|
1314
1393
|
|
|
1315
|
-
obj
|
|
1316
|
-
obj
|
|
1394
|
+
setattr(obj, "timeline", timeline)
|
|
1395
|
+
setattr(obj, "time_dimension", time_dimension)
|
|
1317
1396
|
return obj
|
|
1318
1397
|
|
|
1319
1398
|
def __array_finalize__(self, obj):
|
|
@@ -1333,7 +1412,7 @@ class MaskedTimelinedArray(np.ma.MaskedArray, BaseTimeArray):
|
|
|
1333
1412
|
self.time_dimension = getattr(obj, "time_dimension", 0)
|
|
1334
1413
|
|
|
1335
1414
|
def __getitem__(
|
|
1336
|
-
self, index: int |
|
|
1415
|
+
self, index: int | tuple[int, ...] | slice | tuple[slice] | list | np.ndarray
|
|
1337
1416
|
) -> "MaskedTimelinedArray | np.ma.MaskedArray":
|
|
1338
1417
|
"""Get item from the MaskedTimelinedArray based on the provided index.
|
|
1339
1418
|
|
|
@@ -1347,9 +1426,9 @@ class MaskedTimelinedArray(np.ma.MaskedArray, BaseTimeArray):
|
|
|
1347
1426
|
index, final_timeline, final_time_dimension = self._get_indexed_times(index)
|
|
1348
1427
|
|
|
1349
1428
|
if final_timeline is None or final_time_dimension is None:
|
|
1350
|
-
return np.ma.MaskedArray(
|
|
1351
|
-
|
|
1352
|
-
)
|
|
1429
|
+
return np.ma.MaskedArray(
|
|
1430
|
+
data=np.asarray(self), mask=self.mask, fill_value=self.fill_value
|
|
1431
|
+
).__getitem__(index)
|
|
1353
1432
|
|
|
1354
1433
|
indexed_result = super().__getitem__(index)
|
|
1355
1434
|
|
|
@@ -1359,7 +1438,9 @@ class MaskedTimelinedArray(np.ma.MaskedArray, BaseTimeArray):
|
|
|
1359
1438
|
f"time_dimension {final_time_dimension} and timeline shape {final_timeline.shape}"
|
|
1360
1439
|
)
|
|
1361
1440
|
|
|
1362
|
-
return MaskedTimelinedArray(
|
|
1441
|
+
return MaskedTimelinedArray(
|
|
1442
|
+
indexed_result, timeline=final_timeline, time_dimension=final_time_dimension
|
|
1443
|
+
)
|
|
1363
1444
|
|
|
1364
1445
|
|
|
1365
1446
|
class Seconds(float):
|
timelined_array-0.1.1/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2024 Timothe Jost
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
[project]
|
|
2
|
-
name = "timelined_array"
|
|
3
|
-
description = "Manage easily 1 or multidimensionnal samples numpy arrays that are time related. Extends numpy without removing any of it's abilities on such arrays."
|
|
4
|
-
authors = [
|
|
5
|
-
{ name = "Timothe Jost", email = "44769559+JostTim@users.noreply.github.com" },
|
|
6
|
-
]
|
|
7
|
-
dependencies = [
|
|
8
|
-
"numpy>=2",
|
|
9
|
-
]
|
|
10
|
-
requires-python = ">=3.11"
|
|
11
|
-
readme = "README.md"
|
|
12
|
-
dynamic = []
|
|
13
|
-
version = "0.1.1"
|
|
14
|
-
|
|
15
|
-
[project.license]
|
|
16
|
-
text = "MIT"
|
|
17
|
-
|
|
18
|
-
[project.urls]
|
|
19
|
-
Homepage = "https://pypi.org/project/timelined_array/"
|
|
20
|
-
Source = "https://github.com/JostTim/timelined_array"
|
|
21
|
-
|
|
22
|
-
[build-system]
|
|
23
|
-
requires = [
|
|
24
|
-
"pdm-backend",
|
|
25
|
-
]
|
|
26
|
-
build-backend = "pdm.backend"
|
|
27
|
-
|
|
28
|
-
[tool.pdm]
|
|
29
|
-
distribution = true
|
|
30
|
-
|
|
31
|
-
[tool.pdm.version]
|
|
32
|
-
source = "file"
|
|
33
|
-
path = "src/timelined_array/__init__.py"
|
|
34
|
-
|
|
35
|
-
[tool.pdm.dev-dependencies]
|
|
36
|
-
dev = [
|
|
37
|
-
"pytest>=8.3.2",
|
|
38
|
-
"pytest-cov>=5.0.0",
|
|
39
|
-
]
|
|
40
|
-
|
|
41
|
-
[tool.pytest.ini_options]
|
|
42
|
-
addopts = "--pdbcls=IPython.terminal.debugger:TerminalPdb --cov-report xml --cov=timelined_array --junitxml=pytest_results.xml --cov-config=pyproject.toml"
|
|
43
|
-
testpaths = [
|
|
44
|
-
"tests",
|
|
45
|
-
]
|
|
46
|
-
|
|
47
|
-
[tool.coverage.run]
|
|
48
|
-
omit = [
|
|
49
|
-
".vscode/*",
|
|
50
|
-
".tests/*",
|
|
51
|
-
]
|
|
52
|
-
|
|
53
|
-
[tool.black]
|
|
54
|
-
preview = true
|
|
55
|
-
line-length = 120
|
|
56
|
-
|
|
57
|
-
[tool.flake8]
|
|
58
|
-
max-line-length = 120
|
|
59
|
-
ignore = [
|
|
60
|
-
"F401",
|
|
61
|
-
"F403",
|
|
62
|
-
"F841",
|
|
63
|
-
"E401",
|
|
64
|
-
"E265",
|
|
65
|
-
"E704",
|
|
66
|
-
"E266",
|
|
67
|
-
"E203",
|
|
68
|
-
"E712",
|
|
69
|
-
"W503",
|
|
70
|
-
"W605",
|
|
71
|
-
]
|
|
72
|
-
|
|
73
|
-
[tool.pyright]
|
|
74
|
-
reportGeneralTypeIssues = "information"
|
|
75
|
-
reportOptionalMemberAccess = "information"
|
|
76
|
-
reportMissingImports = "information"
|
|
77
|
-
reportMissingModuleSource = "information"
|
|
78
|
-
reportInvalidStringEscapeSequence = "information"
|
|
79
|
-
reportOptionalOperand = "none"
|
|
80
|
-
reportOptionalSubscript = "none"
|
|
81
|
-
reportOptionalIterable = "none"
|
|
82
|
-
reportAttributeAccessIssue = "information"
|
|
83
|
-
typeCheckingMode = "basic"
|
|
84
|
-
include = [
|
|
85
|
-
"src",
|
|
86
|
-
"tests",
|
|
87
|
-
]
|
|
88
|
-
exclude = [
|
|
89
|
-
"**/__pycache__",
|
|
90
|
-
]
|
|
File without changes
|
|
@@ -1,267 +0,0 @@
|
|
|
1
|
-
import numpy as np
|
|
2
|
-
import pytest
|
|
3
|
-
from timelined_array import TimelinedArray, MaskedTimelinedArray
|
|
4
|
-
from timelined_array.time import (
|
|
5
|
-
Timeline,
|
|
6
|
-
StartBoundary,
|
|
7
|
-
StoptBoundary,
|
|
8
|
-
EdgePolicy,
|
|
9
|
-
TimeIndexer,
|
|
10
|
-
TimeMixin,
|
|
11
|
-
TimePacker,
|
|
12
|
-
Seconds,
|
|
13
|
-
)
|
|
14
|
-
import pickle
|
|
15
|
-
from pathlib import Path
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
@pytest.fixture
|
|
19
|
-
def timelined_array_1D():
|
|
20
|
-
return TimelinedArray(np.random.rand(50), timeline=np.arange(50), time_dimension=0)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
@pytest.fixture
|
|
24
|
-
def timelined_array_3D():
|
|
25
|
-
return TimelinedArray(np.random.rand(25, 50, 75), timeline=np.arange(50), time_dimension=1)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
@pytest.fixture
|
|
29
|
-
def masked_timelined_array_1D():
|
|
30
|
-
data = np.random.rand(50)
|
|
31
|
-
mask = data > 0.5
|
|
32
|
-
return MaskedTimelinedArray(data, mask=mask, timeline=np.arange(50), time_dimension=0)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
@pytest.fixture
|
|
36
|
-
def pickle_path():
|
|
37
|
-
file_path = Path("serialized_test_3D_ta_array.pickle")
|
|
38
|
-
yield file_path
|
|
39
|
-
if file_path.exists():
|
|
40
|
-
file_path.unlink(missing_ok=True)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def test_timelined_array_1D_properties(timelined_array_1D):
|
|
44
|
-
assert timelined_array_1D.time_dimension == 0
|
|
45
|
-
assert timelined_array_1D.timeline.shape == (50,)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def test_timelined_array_3D_properties(timelined_array_3D):
|
|
49
|
-
assert timelined_array_3D.time_dimension == 1
|
|
50
|
-
assert timelined_array_3D.timeline.shape == (50,)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def test_masked_timelined_array_1D_properties(masked_timelined_array_1D):
|
|
54
|
-
assert masked_timelined_array_1D.time_dimension == 0
|
|
55
|
-
assert np.all(masked_timelined_array_1D.mask == (masked_timelined_array_1D.data > 0.5))
|
|
56
|
-
assert masked_timelined_array_1D.timeline.shape == (50,)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
def test_timelined_array_indexing(timelined_array_1D):
|
|
60
|
-
sub_array = timelined_array_1D[10:20]
|
|
61
|
-
assert isinstance(sub_array, TimelinedArray)
|
|
62
|
-
assert sub_array.shape == (10,)
|
|
63
|
-
assert np.array_equal(sub_array.timeline, np.arange(10, 20))
|
|
64
|
-
assert timelined_array_1D[[1]] == timelined_array_1D[1]
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def test_masked_timelined_array_indexing(masked_timelined_array_1D):
|
|
68
|
-
sub_array = masked_timelined_array_1D[10:20]
|
|
69
|
-
assert isinstance(sub_array, MaskedTimelinedArray)
|
|
70
|
-
assert sub_array.shape == (10,)
|
|
71
|
-
assert np.array_equal(sub_array.timeline, np.arange(10, 20))
|
|
72
|
-
assert np.all(sub_array.mask == (sub_array.data > 0.5))
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
def get_axis_parameters_3D():
|
|
76
|
-
# affected_axis, time_dimension, shape
|
|
77
|
-
# if None, we expect an AttributeError
|
|
78
|
-
return [(0, 0, (50, 75)), (1, None, (25, 75)), (2, 1, (25, 50)), ((0, 2), 0, (50,)), (None, None, None)]
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
@pytest.mark.parametrize("shape, timeline_dimension", [((12,), 0), ((10, 30, 50), 2), ((29, 34, 61, 15, 4), 0)])
|
|
82
|
-
def test_ta_array_shape(shape, timeline_dimension):
|
|
83
|
-
ta_array = TimelinedArray(
|
|
84
|
-
np.random.rand(*shape), timeline=np.arange(shape[timeline_dimension]), time_dimension=timeline_dimension
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
assert ta_array.shape == shape
|
|
88
|
-
assert ta_array.timeline.shape == (shape[timeline_dimension],)
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
def test_timeline_creation():
|
|
92
|
-
timeline = Timeline(np.arange(10))
|
|
93
|
-
assert isinstance(timeline, Timeline)
|
|
94
|
-
assert timeline.max() == 9
|
|
95
|
-
assert timeline.min() == 0
|
|
96
|
-
assert 5 in timeline
|
|
97
|
-
assert timeline.step == 1
|
|
98
|
-
|
|
99
|
-
with pytest.raises(NotImplementedError):
|
|
100
|
-
timeline = Timeline(np.random.rand(10), uniform_space=True)
|
|
101
|
-
# assert np.unique(np.diff(timeline)).size == 1
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
def test_timelined_array_creation(timelined_array_1D):
|
|
105
|
-
|
|
106
|
-
with pytest.raises(ValueError):
|
|
107
|
-
array = TimelinedArray(np.array(timelined_array_1D))
|
|
108
|
-
|
|
109
|
-
with pytest.raises(ValueError):
|
|
110
|
-
array = TimelinedArray(np.array(timelined_array_1D), time_dimension=5.5)
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
def test_pickle_unpickle(timelined_array_3D, pickle_path):
|
|
114
|
-
# Pickle the numpy array
|
|
115
|
-
with open(pickle_path, "wb") as f:
|
|
116
|
-
pickle.dump(timelined_array_3D, f)
|
|
117
|
-
|
|
118
|
-
# Unpickle the numpy array
|
|
119
|
-
with open(pickle_path, "rb") as f:
|
|
120
|
-
unserialized_3D_array = pickle.load(f)
|
|
121
|
-
|
|
122
|
-
# Assert that the original and unpickled arrays are the same
|
|
123
|
-
np.testing.assert_array_equal(timelined_array_3D, unserialized_3D_array)
|
|
124
|
-
np.testing.assert_array_equal(timelined_array_3D.timeline, unserialized_3D_array.timeline)
|
|
125
|
-
assert timelined_array_3D.time_dimension == unserialized_3D_array.time_dimension
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
def test_start_boundary():
|
|
129
|
-
assert StartBoundary.inclusive.value(5, 5) # 5 is => than 5
|
|
130
|
-
assert not StartBoundary.exclusive.value(5, 5) # 5 is not > than 5
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
def test_stop_boundary():
|
|
134
|
-
assert StoptBoundary.inclusive.value(5, 5) # 5 is =< than 5
|
|
135
|
-
assert not StoptBoundary.exclusive.value(5, 5) # 5 is not < than 5
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
def test_edge_policy():
|
|
139
|
-
assert EdgePolicy.start.value == StartBoundary
|
|
140
|
-
assert EdgePolicy.stop.value == StoptBoundary
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
def test_time_indexer(timelined_array_1D):
|
|
144
|
-
indexer = TimeIndexer(timelined_array_1D)
|
|
145
|
-
assert indexer.time_to_index(5) == 5
|
|
146
|
-
assert indexer.time_to_index(slice(2, 5)) == slice(2, 5, 1)
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
def test_time_mixin_methods(timelined_array_1D):
|
|
150
|
-
assert timelined_array_1D.sec_max() == 49
|
|
151
|
-
assert timelined_array_1D.sec_min() == 0
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
def test_ta_transpose(timelined_array_3D):
|
|
155
|
-
transposed = timelined_array_3D.transpose(2, 0, 1)
|
|
156
|
-
assert transposed.shape == (75, 25, 50)
|
|
157
|
-
assert transposed.time_dimension == 2
|
|
158
|
-
|
|
159
|
-
transposed = timelined_array_3D.T
|
|
160
|
-
assert transposed.shape == (75, 50, 25)
|
|
161
|
-
assert transposed.time_dimension == 1
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
def test_ta_swapaxes(timelined_array_3D):
|
|
165
|
-
swapped = timelined_array_3D.swapaxes(1, 2)
|
|
166
|
-
assert swapped.shape == (25, 75, 50)
|
|
167
|
-
assert swapped.time_dimension == 2
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
@pytest.mark.parametrize(
|
|
171
|
-
"rolled_axis, end_position, expected_time_position, expected_shape",
|
|
172
|
-
[(0, 2, 0, (50, 25, 75)), (2, 1, 2, (25, 75, 50)), (1, 3, 2, (25, 75, 50))],
|
|
173
|
-
)
|
|
174
|
-
def test_ta_rollaxis(rolled_axis, end_position, expected_time_position, expected_shape, timelined_array_3D):
|
|
175
|
-
# roll back the axis
|
|
176
|
-
rolled = timelined_array_3D.rollaxis(rolled_axis, end_position)
|
|
177
|
-
assert rolled.shape == expected_shape
|
|
178
|
-
assert rolled.time_dimension == expected_time_position
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
def axis_affecting_functions_blueprint(timelined_array_3D, func, affected_axis, time_dimension, shape):
|
|
182
|
-
result = func(timelined_array_3D, axis=affected_axis)
|
|
183
|
-
if time_dimension is None:
|
|
184
|
-
with pytest.raises(AttributeError):
|
|
185
|
-
result.time_dimension
|
|
186
|
-
else:
|
|
187
|
-
assert result.time_dimension == time_dimension
|
|
188
|
-
|
|
189
|
-
if shape is None:
|
|
190
|
-
with pytest.raises(AttributeError):
|
|
191
|
-
result.shape
|
|
192
|
-
else:
|
|
193
|
-
assert result.shape == shape
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
@pytest.mark.parametrize("affected_axis, time_dimension, shape", get_axis_parameters_3D())
|
|
197
|
-
def test_ta_mean(timelined_array_3D, affected_axis, time_dimension, shape):
|
|
198
|
-
axis_affecting_functions_blueprint(timelined_array_3D, np.mean, affected_axis, time_dimension, shape)
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
@pytest.mark.parametrize("affected_axis, time_dimension, shape", get_axis_parameters_3D())
|
|
202
|
-
def test_ta_sum(timelined_array_3D, affected_axis, time_dimension, shape):
|
|
203
|
-
axis_affecting_functions_blueprint(timelined_array_3D, np.sum, affected_axis, time_dimension, shape)
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
@pytest.mark.parametrize("affected_axis, time_dimension, shape", get_axis_parameters_3D())
|
|
207
|
-
def test_ta_std(timelined_array_3D, affected_axis, time_dimension, shape):
|
|
208
|
-
axis_affecting_functions_blueprint(timelined_array_3D, np.std, affected_axis, time_dimension, shape)
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
@pytest.mark.parametrize("affected_axis, time_dimension, shape", get_axis_parameters_3D())
|
|
212
|
-
def test_ta_var(timelined_array_3D, affected_axis, time_dimension, shape):
|
|
213
|
-
axis_affecting_functions_blueprint(timelined_array_3D, np.var, affected_axis, time_dimension, shape)
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
def test_masked_ta_creation():
|
|
217
|
-
data = np.random.rand(10, 10)
|
|
218
|
-
mask = data > 0.5
|
|
219
|
-
masked_ta = MaskedTimelinedArray(data, mask=mask, timeline=np.arange(10), time_dimension=0)
|
|
220
|
-
assert isinstance(masked_ta, MaskedTimelinedArray)
|
|
221
|
-
assert masked_ta.shape == (10, 10)
|
|
222
|
-
assert masked_ta.time_dimension == 0
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
def test_seconds_to_index():
|
|
226
|
-
sec = Seconds(10)
|
|
227
|
-
assert sec.to_index(2) == 20
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
def test_timelined_array_from_iterable(timelined_array_3D):
|
|
231
|
-
array = TimelinedArray.align_from_iterable(timelined_array_3D)
|
|
232
|
-
assert np.all(array == timelined_array_3D)
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
def test_rebase_timelined_array(timelined_array_1D):
|
|
236
|
-
|
|
237
|
-
array = timelined_array_1D.rebase_timeline(at=16)
|
|
238
|
-
assert array.timeline[16] == 0
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
def test_offset_timeline(timelined_array_1D):
|
|
242
|
-
array = timelined_array_1D.offset_timeline(offset=84)
|
|
243
|
-
assert array.timeline[22] == (22 + 84)
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
def test_shift_period(timelined_array_1D):
|
|
247
|
-
|
|
248
|
-
with pytest.raises(NotImplementedError):
|
|
249
|
-
array = timelined_array_1D.shift_values(period=slice(8, 10))
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
def test_moveaxis(timelined_array_3D):
|
|
253
|
-
|
|
254
|
-
array = timelined_array_3D.moveaxis(1, 2)
|
|
255
|
-
assert array.time_dimension == 2
|
|
256
|
-
assert array.shape[2] == 50
|
|
257
|
-
|
|
258
|
-
array = timelined_array_3D.moveaxis(0, 2)
|
|
259
|
-
assert array.time_dimension == 0
|
|
260
|
-
assert array.shape[1] == 75
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
def test_pack(timelined_array_1D):
|
|
264
|
-
|
|
265
|
-
timeline, array = timelined_array_1D.pack
|
|
266
|
-
assert timeline is timelined_array_1D.timeline
|
|
267
|
-
assert np.all(array == timelined_array_1D)
|