timelined_array 0.0.12__tar.gz → 0.0.13__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: timelined_array
3
- Version: 0.0.12
3
+ Version: 0.0.13
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
@@ -12,6 +12,10 @@ Description-Content-Type: text/markdown
12
12
 
13
13
  # timelined_array
14
14
 
15
+ ![Publish Status](https://github.com/JostTim/timelined_array/actions/workflows/publish.yml/badge.svg)
16
+ ![Test Status](https://github.com/JostTim/timelined_array/actions/workflows/test.yml/badge.svg)
17
+ [![codecov](https://codecov.io/gh/JostTim/timelined_array/branch/main/graph/badge.svg)](https://codecov.io/gh/JostTim/timelined_array)
18
+
15
19
  ## Overview
16
20
  The TimelinedArray package provides a set of classes and utilities for working with time-indexed arrays. It extends the functionality of NumPy arrays to include time-based indexing and operations, making it easier to work with time-series data.
17
21
 
@@ -1,5 +1,9 @@
1
1
  # timelined_array
2
2
 
3
+ ![Publish Status](https://github.com/JostTim/timelined_array/actions/workflows/publish.yml/badge.svg)
4
+ ![Test Status](https://github.com/JostTim/timelined_array/actions/workflows/test.yml/badge.svg)
5
+ [![codecov](https://codecov.io/gh/JostTim/timelined_array/branch/main/graph/badge.svg)](https://codecov.io/gh/JostTim/timelined_array)
6
+
3
7
  ## Overview
4
8
  The TimelinedArray package provides a set of classes and utilities for working with time-indexed arrays. It extends the functionality of NumPy arrays to include time-based indexing and operations, making it easier to work with time-series data.
5
9
 
@@ -10,7 +10,7 @@ dependencies = [
10
10
  requires-python = ">=3.11"
11
11
  readme = "README.md"
12
12
  dynamic = []
13
- version = "0.0.12"
13
+ version = "0.0.13"
14
14
 
15
15
  [project.license]
16
16
  text = "MIT"
@@ -31,3 +31,15 @@ distribution = true
31
31
  [tool.pdm.version]
32
32
  source = "file"
33
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 html --cov --junitxml=pytest_results.xml"
43
+ testpaths = [
44
+ "tests",
45
+ ]
@@ -1,3 +1,3 @@
1
- __version__ = "0.0.12"
1
+ __version__ = "0.0.13"
2
2
 
3
3
  from .time import TimelinedArray, MaskedTimelinedArray, Timeline
@@ -260,7 +260,8 @@ class TimeIndexer:
260
260
  iindex_time = self.time_to_index(index)
261
261
  full_iindex = self._insert_time_index(iindex_time)
262
262
  # print("new full index : ",iindex_time)
263
- logger.debug(f"About to index over time with iindex_time {iindex_time} and full_iindex {full_iindex}")
263
+ logger.debug(
264
+ f"About to index over time with iindex_time {iindex_time} and full_iindex {full_iindex}")
264
265
  return self.array[full_iindex]
265
266
 
266
267
  def get_iindex(self, sec_start=None, sec_stop=None, sec_step=None):
@@ -370,7 +371,8 @@ class TimeMixin:
370
371
  if axis < self.time_dimension:
371
372
  final_time_dimension -= 1
372
373
  elif axis == self.time_dimension:
373
- raise ValueError("The time dimension would simply be discarded after axis removal")
374
+ raise ValueError(
375
+ "The time dimension would simply be discarded after axis removal")
374
376
 
375
377
  return final_time_dimension
376
378
 
@@ -480,7 +482,8 @@ class TimeMixin:
480
482
  # we apply this reshaping to timeline too.
481
483
 
482
484
  final_timeline = (
483
- self.timeline[index[time_dimension_in_index]] if len(index) > time_dimension_in_index else self.timeline
485
+ self.timeline[index[time_dimension_in_index]] if len(
486
+ index) > time_dimension_in_index else self.timeline
484
487
  )
485
488
 
486
489
  return index, final_timeline, final_time_dimension
@@ -533,7 +536,8 @@ class TimeMixin:
533
536
  return result.item()
534
537
  if self._time_dimension_in_axis(axis):
535
538
  return np.asarray(result)
536
- result.time_dimension = self._get_time_dimension_after_axis_removal(axis)
539
+ result.time_dimension = self._get_time_dimension_after_axis_removal(
540
+ axis)
537
541
  return result
538
542
 
539
543
  # # REDUCE and SETSTATE are used to instanciate the array from and to a pickled serialized object.
@@ -545,7 +549,8 @@ class TimeMixin:
545
549
  # Get the parent's __reduce__ tuple
546
550
  pickled_state = super().__reduce__()
547
551
  # Create our own tuple to pass to __setstate__
548
- new_state = pickled_state[2] + (self.timeline, self.time_dimension) # type: ignore
552
+ # type: ignore
553
+ new_state = pickled_state[2] + (self.timeline, self.time_dimension)
549
554
 
550
555
  # self.logger.debug(f"Reduced to : time_dimension={self.time_dimension}. Array shape is : {new_state}")
551
556
  # Return a tuple that replaces the parent's __setstate__ tuple with our own
@@ -656,7 +661,8 @@ class TimeMixin:
656
661
  indexer.append(slice(None))
657
662
  indexer = tuple(indexer)
658
663
 
659
- shift_area = self.itime.__getitem__(period) if time_period else self.__getitem__(period)
664
+ shift_area = self.itime.__getitem__(
665
+ period) if time_period else self.__getitem__(period)
660
666
 
661
667
  # if not this : we lost a dimension because we sliced one axis to a single element, no need to no .mean
662
668
  if not len(shift_area.shape) < len(self.shape):
@@ -678,7 +684,8 @@ class TimeMixin:
678
684
  # we re-instanciate a TimelinedArray with view instead of the full constructor : faster
679
685
  cls = self._get_array_cls()
680
686
 
681
- swapped_array: TimeCompatibleProtocol = np.swapaxes(np.asarray(self), axis1, axis2).view(cls) # type: ignore
687
+ swapped_array: TimeCompatibleProtocol = np.swapaxes(
688
+ np.asarray(self), axis1, axis2).view(cls) # type: ignore
682
689
  swapped_array.timeline = self.timeline
683
690
 
684
691
  if axis1 == self.time_dimension:
@@ -707,7 +714,8 @@ class TimeMixin:
707
714
  cls = self._get_array_cls()
708
715
 
709
716
  # we re-instanciate a TimelinedArray with view instead of the full constructor : faster
710
- transposed_array: TimeCompatibleProtocol = np.transpose(np.asarray(self), axes).view(cls) # type: ignore
717
+ transposed_array: TimeCompatibleProtocol = np.transpose(
718
+ np.asarray(self), axes).view(cls) # type: ignore
711
719
  transposed_array.timeline = self.timeline
712
720
 
713
721
  if self.time_dimension in axes:
@@ -780,24 +788,36 @@ class TimeMixin:
780
788
  # we re-instanciate a TimelinedArray with view instead of the full constructor : faster
781
789
  cls = self._get_array_cls()
782
790
 
783
- rolled_array: TimeCompatibleProtocol = np.rollaxis(np.asarray(self), axis, start).view(cls) # type: ignore
791
+ rolled_array: TimeCompatibleProtocol = np.rollaxis(
792
+ np.asarray(self), axis, start).view(cls) # type: ignore
793
+
794
+ # reinject timeline as is
784
795
  rolled_array.timeline = self.timeline
785
- rolled_array.time_dimension = self.time_dimension
786
-
787
- if axis < self.time_dimension:
788
- if start <= axis:
789
- rolled_array.time_dimension += 1
790
- elif start <= self.time_dimension:
791
- rolled_array.time_dimension -= 1
792
- elif axis == self.time_dimension:
793
- rolled_array.time_dimension = start
794
- else:
795
- if start <= self.time_dimension:
796
- rolled_array.time_dimension -= 1
797
- elif start > self.time_dimension:
798
- rolled_array.time_dimension += 1
799
796
 
800
- # TimelinedArray.time_dimension and TimelinedArray.timeline are set. good to go
797
+ # then fix the time_position according to the rolled axis
798
+
799
+ def rollaxis_mapping(shape, axis, start=0):
800
+ n = len(shape)
801
+ if axis < 0:
802
+ axis += n
803
+ if start < 0:
804
+ start += n
805
+ if not (0 <= axis < n and 0 <= start <= n):
806
+ raise ValueError("axis and start must be within valid range")
807
+ new_order = list(range(n))
808
+
809
+ axis_value = new_order.pop(axis)
810
+
811
+ if start > axis:
812
+ new_order.insert(start - 1, axis_value)
813
+ else:
814
+ new_order.insert(start, axis_value)
815
+ mapping = {i: new_order.index(i) for i in range(n)}
816
+ return mapping
817
+
818
+ rolled_array.time_dimension = rollaxis_mapping(
819
+ self.shape, axis, start)[self.time_dimension]
820
+
801
821
  return rolled_array
802
822
 
803
823
  def mean(
@@ -915,7 +935,8 @@ class TimeMixin:
915
935
  # returns a modified version of the array, with the first element of the array to time zero,
916
936
  # and shift the rest accordingly
917
937
  cls = self._get_array_cls()
918
- return cls(self, timeline=self.timeline - self.timeline[at]) # type: ignore
938
+ # type: ignore
939
+ return cls(self, timeline=self.timeline - self.timeline[at])
919
940
 
920
941
  def offset_timeline(self, offset):
921
942
  """Returns a modified version of the array with time offset.
@@ -983,7 +1004,8 @@ class TimeMixin:
983
1004
  break
984
1005
 
985
1006
  if timeline is None:
986
- raise ValueError("timeline must be supplied if the input_array is not a TimelinedArray")
1007
+ raise ValueError(
1008
+ "timeline must be supplied if the input_array is not a TimelinedArray")
987
1009
 
988
1010
  if time_dimension is None: # same thing for the time dimension.
989
1011
  time_dimension = getattr(data, "time_dimension", None)
@@ -1134,7 +1156,8 @@ class TimelinedArray(TimeMixin, np.ndarray, TimeCompatibleProtocol):
1134
1156
  logger.debug(f"wrapping array after ufunc {context[0].__name__}")
1135
1157
  output = super().__array_wrap__(out_arr, context)
1136
1158
  if len(output.shape) < len(self.shape):
1137
- logger.debug(f"shape reduced from : {self.shape} to : {output.shape}. outarray was : {out_arr.shape}")
1159
+ logger.debug(
1160
+ f"shape reduced from : {self.shape} to : {output.shape}. outarray was : {out_arr.shape}")
1138
1161
  return output
1139
1162
 
1140
1163
  def __array_function__(self, func, types, args, kwargs):
@@ -1166,7 +1189,8 @@ class TimelinedArray(TimeMixin, np.ndarray, TimeCompatibleProtocol):
1166
1189
  TimelinedArray | np.ndarray: Indexed result based on the provided index.
1167
1190
  """
1168
1191
 
1169
- index, final_timeline, final_time_dimension = self._get_indexed_times(index)
1192
+ index, final_timeline, final_time_dimension = self._get_indexed_times(
1193
+ index)
1170
1194
 
1171
1195
  if final_timeline is None or final_time_dimension is None:
1172
1196
  return np.asarray(self).__getitem__(index)
@@ -1302,7 +1326,8 @@ class MaskedTimelinedArray(TimeMixin, np.ma.MaskedArray, TimeCompatibleProtocol)
1302
1326
  MaskedTimelinedArray | np.ma.MaskedArray: The masked array or MaskedTimelinedArray based on the index.
1303
1327
  """
1304
1328
 
1305
- index, final_timeline, final_time_dimension = self._get_indexed_times(index)
1329
+ index, final_timeline, final_time_dimension = self._get_indexed_times(
1330
+ index)
1306
1331
 
1307
1332
  if final_timeline is None or final_time_dimension is None:
1308
1333
  return np.ma.MaskedArray(data=np.asarray(self), mask=self.mask, fill_value=self.fill_value).__getitem__(
File without changes
@@ -0,0 +1,171 @@
1
+ import numpy as np
2
+ import pytest
3
+ from timelined_array import TimelinedArray, MaskedTimelinedArray
4
+ from timelined_array.time import (Timeline,
5
+ StartBoundary,
6
+ StoptBoundary,
7
+ EdgePolicy,
8
+ TimeIndexer,
9
+ TimeMixin,
10
+ TimePacker,
11
+ Seconds)
12
+
13
+
14
+ @pytest.fixture
15
+ def timelined_array_1D():
16
+ return TimelinedArray(np.random.rand(50),
17
+ timeline=np.arange(50),
18
+ time_dimension=0)
19
+
20
+
21
+ @pytest.fixture
22
+ def timelined_array_3D():
23
+ return TimelinedArray(np.random.rand(25, 50, 75),
24
+ timeline=np.arange(50),
25
+ time_dimension=1)
26
+
27
+
28
+ def get_axis_parameters_3D():
29
+ # affected_axis, time_dimension, shape
30
+ # if None, we expect an AttributeError
31
+ return [
32
+ (0, 0, (50, 75)),
33
+ (1, None, (25, 75)),
34
+ (2, 1, (25, 50)),
35
+ ((0, 2), 0, (50,)),
36
+ (None, None, None)
37
+ ]
38
+
39
+
40
+ @pytest.mark.parametrize("shape, timeline_dimension", [
41
+ ((12,), 0),
42
+ ((10, 30, 50), 2),
43
+ ((29, 34, 61, 15, 4), 0)
44
+ ])
45
+ def test_ta_array_shape(shape, timeline_dimension):
46
+ ta_array = TimelinedArray(np.random.rand(*shape),
47
+ timeline=np.arange(
48
+ shape[timeline_dimension]),
49
+ time_dimension=timeline_dimension)
50
+
51
+ assert ta_array.shape == shape
52
+ assert ta_array.timeline.shape == (shape[timeline_dimension],)
53
+
54
+
55
+ def test_timeline_creation():
56
+ timeline = Timeline(np.arange(10))
57
+ assert isinstance(timeline, Timeline)
58
+ assert timeline.max() == 9
59
+ assert timeline.min() == 0
60
+
61
+
62
+ def test_start_boundary():
63
+ assert StartBoundary.inclusive.value(5, 5)
64
+ assert not StartBoundary.exclusive.value(5, 5)
65
+
66
+
67
+ def test_stop_boundary():
68
+ assert StoptBoundary.inclusive.value(5, 5)
69
+ assert not StoptBoundary.exclusive.value(5, 5)
70
+
71
+
72
+ def test_edge_policy():
73
+ assert EdgePolicy.start.value == StartBoundary
74
+ assert EdgePolicy.stop.value == StoptBoundary
75
+
76
+
77
+ def test_time_indexer(timelined_array_1D):
78
+ indexer = TimeIndexer(timelined_array_1D)
79
+ assert indexer.time_to_index(5) == 5
80
+ assert indexer.time_to_index(slice(2, 5)) == slice(2, 5, 1)
81
+
82
+
83
+ def test_time_mixin_methods(timelined_array_1D):
84
+ assert timelined_array_1D.sec_max() == 49
85
+ assert timelined_array_1D.sec_min() == 0
86
+
87
+
88
+ def test_ta_transpose(timelined_array_3D):
89
+ transposed = timelined_array_3D.transpose(2, 0, 1)
90
+ assert transposed.shape == (75, 25, 50)
91
+ assert transposed.time_dimension == 2
92
+
93
+
94
+ def test_ta_swapaxes(timelined_array_3D):
95
+ swapped = timelined_array_3D.swapaxes(1, 2)
96
+ assert swapped.shape == (25, 75, 50)
97
+ assert swapped.time_dimension == 2
98
+
99
+
100
+ @pytest.mark.parametrize("rolled_axis, end_position, expected_time_position, expected_shape", [
101
+ (0, 2, 0, (50, 25, 75)),
102
+ (2, 1, 2, (25, 75, 50)),
103
+ (1, 3, 2, (25, 75, 50))
104
+ ])
105
+ def test_ta_rollaxis(rolled_axis,
106
+ end_position,
107
+ expected_time_position,
108
+ expected_shape,
109
+ timelined_array_3D):
110
+ # roll back the axis
111
+ rolled = timelined_array_3D.rollaxis(rolled_axis, end_position)
112
+ assert rolled.shape == expected_shape
113
+ assert rolled.time_dimension == expected_time_position
114
+
115
+
116
+ def axis_affecting_functions_blueprint(timelined_array_3D,
117
+ func,
118
+ affected_axis,
119
+ time_dimension,
120
+ shape):
121
+ result = func(timelined_array_3D, axis=affected_axis)
122
+ if time_dimension is None:
123
+ with pytest.raises(AttributeError):
124
+ result.time_dimension
125
+ else:
126
+ assert result.time_dimension == time_dimension
127
+
128
+ if shape is None:
129
+ with pytest.raises(AttributeError):
130
+ result.shape
131
+ else:
132
+ assert result.shape == shape
133
+
134
+
135
+ @pytest.mark.parametrize("affected_axis, time_dimension, shape", get_axis_parameters_3D())
136
+ def test_ta_mean(timelined_array_3D, affected_axis, time_dimension, shape):
137
+ axis_affecting_functions_blueprint(timelined_array_3D, np.mean,
138
+ affected_axis, time_dimension, shape)
139
+
140
+
141
+ @pytest.mark.parametrize("affected_axis, time_dimension, shape", get_axis_parameters_3D())
142
+ def test_ta_sum(timelined_array_3D, affected_axis, time_dimension, shape):
143
+ axis_affecting_functions_blueprint(timelined_array_3D, np.sum,
144
+ affected_axis, time_dimension, shape)
145
+
146
+
147
+ @pytest.mark.parametrize("affected_axis, time_dimension, shape", get_axis_parameters_3D())
148
+ def test_ta_std(timelined_array_3D, affected_axis, time_dimension, shape):
149
+ axis_affecting_functions_blueprint(timelined_array_3D, np.std,
150
+ affected_axis, time_dimension, shape)
151
+
152
+
153
+ @pytest.mark.parametrize("affected_axis, time_dimension, shape", get_axis_parameters_3D())
154
+ def test_ta_var(timelined_array_3D, affected_axis, time_dimension, shape):
155
+ axis_affecting_functions_blueprint(timelined_array_3D, np.var,
156
+ affected_axis, time_dimension, shape)
157
+
158
+
159
+ def test_masked_ta_creation():
160
+ data = np.random.rand(10, 10)
161
+ mask = data > 0.5
162
+ masked_ta = MaskedTimelinedArray(
163
+ data, mask=mask, timeline=np.arange(10), time_dimension=0)
164
+ assert isinstance(masked_ta, MaskedTimelinedArray)
165
+ assert masked_ta.shape == (10, 10)
166
+ assert masked_ta.time_dimension == 0
167
+
168
+
169
+ def test_seconds_to_index():
170
+ sec = Seconds(10)
171
+ assert sec.to_index(2) == 20
@@ -1,111 +0,0 @@
1
- import unittest
2
- import numpy as np
3
- from numpy.testing import assert_array_equal
4
- from timelined_array.time import TimeIndexer, Timeline, TimelinedArray, MaskedTimelinedArray
5
-
6
-
7
- class TestTimeline(unittest.TestCase):
8
- def test_timeline_creation(self):
9
- timeline = Timeline([0, 1, 2, 3, 4])
10
- self.assertEqual(timeline.max(), 4)
11
- self.assertEqual(timeline.min(), 0)
12
-
13
- def test_timeline_uniformize(self):
14
- with self.assertRaises(NotImplementedError):
15
- Timeline._uniformize([0, 1, 2, 3, 4])
16
-
17
- def test_timeline_step(self):
18
- timeline = Timeline([0, 1, 2, 3, 4])
19
- self.assertEqual(timeline.step, 1)
20
-
21
- def test_timeline_max_step(self):
22
- timeline = Timeline([0, 1, 2, 3, 4])
23
- self.assertEqual(timeline.max_step, 2)
24
-
25
-
26
- class TestTimeIndexer(unittest.TestCase):
27
- def setUp(self):
28
- self.timeline = Timeline([0, 1, 2, 3, 4])
29
- self.array = TimelinedArray(np.random.rand(5, 5), timeline=self.timeline, time_dimension=0)
30
- self.indexer = TimeIndexer(self.array)
31
-
32
- def test_time_to_index(self):
33
- self.assertEqual(self.indexer.time_to_index(2), 2)
34
-
35
- def test_get_iindex(self):
36
- self.assertEqual(self.indexer.get_iindex(1, 3), slice(1, 3, 1))
37
-
38
-
39
- class TestTimelinedArray(unittest.TestCase):
40
- def setUp(self):
41
- self.timeline = Timeline([0, 1, 2, 3, 4])
42
- self.array = TimelinedArray(np.random.rand(5, 5), timeline=self.timeline, time_dimension=0)
43
-
44
- def test_array_creation(self):
45
- self.assertEqual(self.array.shape, (5, 5))
46
- self.assertEqual(self.array.timeline.max(), 4)
47
- self.assertEqual(self.array.time_dimension, 0)
48
-
49
- def test_array_indexing(self):
50
- indexed_array = self.array[1:3]
51
- self.assertEqual(indexed_array.shape, (2, 5))
52
- self.assertEqual(indexed_array.timeline.min(), 1)
53
- self.assertEqual(indexed_array.timeline.max(), 2)
54
-
55
- def test_array_transpose(self):
56
- transposed_array = self.array.transpose()
57
- self.assertEqual(transposed_array.shape, (5, 5))
58
- self.assertEqual(transposed_array.time_dimension, 1)
59
-
60
- def test_array_mean(self):
61
- mean_value = self.array.mean(axis=0)
62
- self.assertEqual(mean_value.shape, (5,))
63
-
64
-
65
- class TestMaskedTimelinedArray(unittest.TestCase):
66
- def setUp(self):
67
- self.timeline = Timeline([0, 1, 2, 3, 4])
68
- self.data = np.random.rand(5, 5)
69
- self.mask = np.zeros((5, 5), dtype=bool)
70
- self.mask[0, 0] = True
71
- self.array = MaskedTimelinedArray(self.data, mask=self.mask, timeline=self.timeline, time_dimension=0)
72
-
73
- def test_masked_array_creation(self):
74
- self.assertEqual(self.array.shape, (5, 5))
75
- self.assertEqual(self.array.timeline.max(), 4)
76
- self.assertEqual(self.array.time_dimension, 0)
77
- self.assertTrue(self.array.mask[0, 0])
78
-
79
- def test_masked_array_indexing(self):
80
- indexed_array = self.array[1:3]
81
- self.assertEqual(indexed_array.shape, (2, 5))
82
- self.assertEqual(indexed_array.timeline.min(), 1)
83
- self.assertEqual(indexed_array.timeline.max(), 2)
84
-
85
-
86
- class TestMultidimensionnalIndexing(unittest.TestCase):
87
- def setUp(self):
88
- dimension = (15, 150, 200, 200)
89
- y_dim_scale = 2
90
- prime = [
91
- [
92
- (
93
- np.array(
94
- [
95
- np.roll(np.linspace(phase, phase * 2, num=dimension[3], endpoint=True), shift=r)
96
- for r in np.arange(0, dimension[2] * y_dim_scale, y_dim_scale)
97
- ]
98
- )
99
- * t
100
- )
101
- for t in np.sin(np.linspace(phase, phase + 2, num=dimension[1]))
102
- ]
103
- for phase in np.linspace(1, dimension[0], num=dimension[0], endpoint=True)
104
- ]
105
- prime = np.array(prime)
106
- sin = np.sin(prime)
107
- self.array = TimelinedArray(sin, time_dimension=1, timeline=np.linspace(-2, 7, num=300))
108
-
109
-
110
- if __name__ == "__main__":
111
- unittest.main()