timelined_array 0.0.13__tar.gz → 0.1.0__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,21 +1,25 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: timelined_array
3
- Version: 0.0.13
3
+ Version: 0.1.0
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-Email: Timothe Jost <timothe.jost@wanadoo.fr>
5
+ Author-Email: Timothe Jost <44769559+JostTim@users.noreply.github.com>
6
6
  License: MIT
7
7
  Project-URL: Homepage, https://pypi.org/project/timelined_array/
8
8
  Project-URL: Source, https://github.com/JostTim/timelined_array
9
9
  Requires-Python: >=3.11
10
- Requires-Dist: numpy
10
+ Requires-Dist: numpy>=2
11
11
  Description-Content-Type: text/markdown
12
12
 
13
13
  # timelined_array
14
14
 
15
+ ![Python](https://img.shields.io/badge/python-3.11-blue.svg)
16
+ [![PyPI version](https://badge.fury.io/py/timelined_array.svg)](https://badge.fury.io/py/timelined_array)
15
17
  ![Publish Status](https://github.com/JostTim/timelined_array/actions/workflows/publish.yml/badge.svg)
16
18
  ![Test Status](https://github.com/JostTim/timelined_array/actions/workflows/test.yml/badge.svg)
17
19
  [![codecov](https://codecov.io/gh/JostTim/timelined_array/branch/main/graph/badge.svg)](https://codecov.io/gh/JostTim/timelined_array)
18
20
 
21
+ <!-- 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.-->
22
+
19
23
  ## Overview
20
24
  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.
21
25
 
@@ -1,9 +1,13 @@
1
1
  # timelined_array
2
2
 
3
+ ![Python](https://img.shields.io/badge/python-3.11-blue.svg)
4
+ [![PyPI version](https://badge.fury.io/py/timelined_array.svg)](https://badge.fury.io/py/timelined_array)
3
5
  ![Publish Status](https://github.com/JostTim/timelined_array/actions/workflows/publish.yml/badge.svg)
4
6
  ![Test Status](https://github.com/JostTim/timelined_array/actions/workflows/test.yml/badge.svg)
5
7
  [![codecov](https://codecov.io/gh/JostTim/timelined_array/branch/main/graph/badge.svg)](https://codecov.io/gh/JostTim/timelined_array)
6
8
 
9
+ <!-- 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.-->
10
+
7
11
  ## Overview
8
12
  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.
9
13
 
@@ -0,0 +1,84 @@
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.0"
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 --junitxml=pytest_results.xml"
43
+ testpaths = [
44
+ "tests",
45
+ ]
46
+
47
+ [tool.black]
48
+ preview = true
49
+ line-length = 120
50
+
51
+ [tool.flake8]
52
+ max-line-length = 120
53
+ ignore = [
54
+ "F401",
55
+ "F403",
56
+ "F841",
57
+ "E401",
58
+ "E265",
59
+ "E704",
60
+ "E266",
61
+ "E203",
62
+ "E712",
63
+ "W503",
64
+ "W605",
65
+ ]
66
+
67
+ [tool.pyright]
68
+ reportGeneralTypeIssues = "information"
69
+ reportOptionalMemberAccess = "information"
70
+ reportMissingImports = "information"
71
+ reportMissingModuleSource = "information"
72
+ reportInvalidStringEscapeSequence = "information"
73
+ reportOptionalOperand = "none"
74
+ reportOptionalSubscript = "none"
75
+ reportOptionalIterable = "none"
76
+ reportAttributeAccessIssue = "information"
77
+ typeCheckingMode = "basic"
78
+ include = [
79
+ "src",
80
+ "tests",
81
+ ]
82
+ exclude = [
83
+ "**/__pycache__",
84
+ ]
@@ -1,3 +1,3 @@
1
- __version__ = "0.0.13"
1
+ __version__ = "0.1.0"
2
2
 
3
3
  from .time import TimelinedArray, MaskedTimelinedArray, Timeline
@@ -5,7 +5,9 @@ from logging import getLogger
5
5
  from enum import Enum
6
6
  import operator
7
7
 
8
- from typing import Tuple, List, Protocol, Type, Callable, Any
8
+ from numpy.typing import NDArray
9
+ from types import MethodType
10
+ from typing import Tuple, List, Protocol, Type, Callable, Any, Optional
9
11
 
10
12
  OperatorType = Callable[[Any, Any], bool]
11
13
 
@@ -177,7 +179,7 @@ class TimeIndexer:
177
179
  _start_operation: OperatorType
178
180
  _stop_operation: OperatorType
179
181
 
180
- def __init__(self, array: TimeCompatibleProtocol, start="inclusive", stop="exclusive"):
182
+ def __init__(self, array: "BaseTimeArray", start="inclusive", stop="exclusive"):
181
183
  self.array = array
182
184
  self.set_edge_policy(start, stop)
183
185
 
@@ -260,8 +262,7 @@ class TimeIndexer:
260
262
  iindex_time = self.time_to_index(index)
261
263
  full_iindex = self._insert_time_index(iindex_time)
262
264
  # print("new full index : ",iindex_time)
263
- logger.debug(
264
- f"About to index over time with iindex_time {iindex_time} and full_iindex {full_iindex}")
265
+ logger.debug(f"About to index over time with iindex_time {iindex_time} and full_iindex {full_iindex}")
265
266
  return self.array[full_iindex]
266
267
 
267
268
  def get_iindex(self, sec_start=None, sec_stop=None, sec_step=None):
@@ -331,6 +332,11 @@ class TimeMixin:
331
332
  start_policy = "inclusive"
332
333
  stop_policy = "exclusive"
333
334
 
335
+ # ndarray inherited
336
+ ndim: int
337
+ shape: Tuple[int, ...]
338
+ __sub__: Callable
339
+
334
340
  def _time_dimension_in_axis(self, axis: int | Tuple[int, ...] | None) -> bool:
335
341
  """Check if the time dimension is present in the specified axis.
336
342
 
@@ -371,8 +377,7 @@ class TimeMixin:
371
377
  if axis < self.time_dimension:
372
378
  final_time_dimension -= 1
373
379
  elif axis == self.time_dimension:
374
- raise ValueError(
375
- "The time dimension would simply be discarded after axis removal")
380
+ raise ValueError("The time dimension would simply be discarded after axis removal")
376
381
 
377
382
  return final_time_dimension
378
383
 
@@ -482,8 +487,7 @@ class TimeMixin:
482
487
  # we apply this reshaping to timeline too.
483
488
 
484
489
  final_timeline = (
485
- self.timeline[index[time_dimension_in_index]] if len(
486
- index) > time_dimension_in_index else self.timeline
490
+ self.timeline[index[time_dimension_in_index]] if len(index) > time_dimension_in_index else self.timeline
487
491
  )
488
492
 
489
493
  return index, final_timeline, final_time_dimension
@@ -505,7 +509,7 @@ class TimeMixin:
505
509
  return self._get_slice_indexed_times(index)
506
510
 
507
511
  @staticmethod
508
- def _is_single_element(obj):
512
+ def _is_single_element(obj: np.ndarray):
509
513
  """Check if the input object is a single element.
510
514
 
511
515
  Args:
@@ -519,27 +523,29 @@ class TimeMixin:
519
523
 
520
524
  def _finish_axis_removing_operation(
521
525
  self, result: "TimelinedArray| MaskedTimelinedArray | np.ndarray | int | float", axis: int | Tuple[int, ...]
522
- ) -> "TimelinedArray| MaskedTimelinedArray | np.ndarray | int | float":
526
+ ) -> "TimelinedArray| MaskedTimelinedArray | np.ndarray | int | float | str":
523
527
  """Finish axis removing operation.
524
528
 
525
529
  Args:
526
- result (TimeCompatibleProtocol): The result of the operation.
530
+ result (BaseTimeArray): The result of the operation.
527
531
  axis (int | Tuple[int, ...] | None): The axis or axes to remove.
528
532
 
529
533
  Returns:
530
- TimeCompatibleProtocol: The result after finishing the axis removing operation.
534
+ BaseTimeArray: The result after finishing the axis removing operation.
531
535
  """
532
536
 
533
- if not isinstance(result, np.ndarray):
537
+ if not isinstance(result, np.ndarray) or not isinstance(result, TimeMixin):
534
538
  return result
535
539
  if self._is_single_element(result):
536
540
  return result.item()
537
541
  if self._time_dimension_in_axis(axis):
538
542
  return np.asarray(result)
539
- result.time_dimension = self._get_time_dimension_after_axis_removal(
540
- axis)
543
+ result.time_dimension = self._get_time_dimension_after_axis_removal(axis)
541
544
  return result
542
545
 
546
+
547
+ class BaseTimeArray(TimeMixin, np.ndarray):
548
+
543
549
  # # REDUCE and SETSTATE are used to instanciate the array from and to a pickled serialized object.
544
550
  # # We only need to store and retrieve time_dimension and timeline on top of the array's data
545
551
  def __reduce__(self):
@@ -547,7 +553,7 @@ class TimeMixin:
547
553
  object with additional attributes 'timeline' and 'time_dimension'."""
548
554
 
549
555
  # Get the parent's __reduce__ tuple
550
- pickled_state = super().__reduce__()
556
+ pickled_state: Tuple[Any, Any, tuple] = super().__reduce__() # type: ignore
551
557
  # Create our own tuple to pass to __setstate__
552
558
  # type: ignore
553
559
  new_state = pickled_state[2] + (self.timeline, self.time_dimension)
@@ -556,11 +562,11 @@ class TimeMixin:
556
562
  # Return a tuple that replaces the parent's __setstate__ tuple with our own
557
563
  return (pickled_state[0], pickled_state[1], new_state)
558
564
 
559
- def __setstate__(self: TimeCompatibleProtocol, state):
565
+ def __setstate__(self: "BaseTimeArray", state):
560
566
  """Set the state of the object using the provided state tuple.
561
567
 
562
568
  Args:
563
- self (TimeCompatibleProtocol): The TimeCompatibleProtocol object.
569
+ self (BaseTimeArray): The BaseTimeArray object.
564
570
  state: The state tuple containing information to set the object's attributes.
565
571
 
566
572
  Returns:
@@ -582,11 +588,11 @@ class TimeMixin:
582
588
 
583
589
  return hash((self.__array__(), self.timeline)) # type: ignore
584
590
 
585
- def _get_array_cls(self) -> TimeCompatibleProtocol:
591
+ def _get_array_cls(self) -> "BaseTimeArray":
586
592
  """Return the class of the array that is compatible with time operations.
587
593
 
588
594
  Returns:
589
- TimeCompatibleProtocol: The class of the array that is compatible with time operations.
595
+ BaseTimeArray: The class of the array that is compatible with time operations.
590
596
  """
591
597
 
592
598
  valid_types = [TimelinedArray, TimelinedArray]
@@ -596,7 +602,7 @@ class TimeMixin:
596
602
  return np.ndarray # type: ignore
597
603
 
598
604
  @property
599
- def array_info(self: TimeCompatibleProtocol):
605
+ def array_info(self: "BaseTimeArray"):
600
606
  """Return information about the array.
601
607
 
602
608
  Returns:
@@ -609,11 +615,12 @@ class TimeMixin:
609
615
  )
610
616
 
611
617
  @property
612
- def itime(self: TimeCompatibleProtocol):
613
- """Return a TimeIndexer object based on the given TimeCompatibleProtocol object."""
618
+ def itime(self):
619
+ """Return a TimeIndexer object based on the given BaseTimeArray object."""
614
620
 
615
621
  return TimeIndexer(self, self.start_policy, self.stop_policy)
616
622
 
623
+ # backward compatibility
617
624
  isec = itime
618
625
 
619
626
  def align_trace(self, start: float, element_nb: int):
@@ -661,16 +668,21 @@ class TimeMixin:
661
668
  indexer.append(slice(None))
662
669
  indexer = tuple(indexer)
663
670
 
664
- shift_area = self.itime.__getitem__(
665
- period) if time_period else self.__getitem__(period)
671
+ shift_area = self.itime.__getitem__(period) if time_period else self.__getitem__(period)
666
672
 
667
673
  # if not this : we lost a dimension because we sliced one axis to a single element, no need to no .mean
668
674
  if not len(shift_area.shape) < len(self.shape):
669
675
  shift_area = shift_area.mean(axis=axis)
670
676
 
677
+ if not isinstance(shift_area, np.ndarray):
678
+ raise NotImplementedError(
679
+ "Returning shift_values() of the array when shift area is a scalar value (due to .mean()) "
680
+ "is not yet implemented"
681
+ )
682
+
671
683
  return self - np.repeat(shift_area.__getitem__(tuple(indexer)), self.shape[axis], axis=axis)
672
684
 
673
- def swapaxes(self: TimeCompatibleProtocol, axis1: int, axis2: int):
685
+ def swapaxes(self: "BaseTimeArray", axis1: int, axis2: int):
674
686
  """Swap the two specified axes of the TimelinedArray.
675
687
 
676
688
  Args:
@@ -678,14 +690,13 @@ class TimeMixin:
678
690
  axis2 (int): The second axis to be swapped.
679
691
 
680
692
  Returns:
681
- TimeCompatibleProtocol: A new TimelinedArray with the specified axes swapped.
693
+ BaseTimeArray: A new TimelinedArray with the specified axes swapped.
682
694
  """
683
695
 
684
696
  # we re-instanciate a TimelinedArray with view instead of the full constructor : faster
685
697
  cls = self._get_array_cls()
686
698
 
687
- swapped_array: TimeCompatibleProtocol = np.swapaxes(
688
- np.asarray(self), axis1, axis2).view(cls) # type: ignore
699
+ swapped_array: BaseTimeArray = np.swapaxes(np.asarray(self), axis1, axis2).view(cls) # type: ignore
689
700
  swapped_array.timeline = self.timeline
690
701
 
691
702
  if axis1 == self.time_dimension:
@@ -698,14 +709,14 @@ class TimeMixin:
698
709
  # TimelinedArray.time_dimension and TimelinedArray.timeline are set. good to go
699
710
  return swapped_array
700
711
 
701
- def transpose(self: TimeCompatibleProtocol, *axes):
712
+ def transpose(self, *axes):
702
713
  """Transpose the array along the specified axes.
703
714
 
704
715
  Args:
705
716
  *axes: The axes to transpose the array along. If not provided, transposes the array in reverse order.
706
717
 
707
718
  Returns:
708
- TimeCompatibleProtocol: The transposed array with updated timeline and time dimension.
719
+ BaseTimeArray: The transposed array with updated timeline and time dimension.
709
720
  """
710
721
 
711
722
  if not axes:
@@ -714,8 +725,7 @@ class TimeMixin:
714
725
  cls = self._get_array_cls()
715
726
 
716
727
  # we re-instanciate a TimelinedArray with view instead of the full constructor : faster
717
- transposed_array: TimeCompatibleProtocol = np.transpose(
718
- np.asarray(self), axes).view(cls) # type: ignore
728
+ transposed_array: BaseTimeArray = np.transpose(np.asarray(self), axes).view(cls) # type: ignore
719
729
  transposed_array.timeline = self.timeline
720
730
 
721
731
  if self.time_dimension in axes:
@@ -727,12 +737,12 @@ class TimeMixin:
727
737
  return transposed_array
728
738
 
729
739
  @property
730
- def T(self: TimeCompatibleProtocol):
740
+ def T(self):
731
741
  """Transposes the object using the transpose method."""
732
742
 
733
743
  return self.transpose()
734
744
 
735
- def moveaxis(self: TimeCompatibleProtocol, source: int | Tuple[int, ...], destination: int | Tuple[int, ...]):
745
+ def moveaxis(self: "BaseTimeArray", source: int | Tuple[int, ...], destination: int | Tuple[int, ...]):
736
746
  """Move the axis of the array to new positions.
737
747
 
738
748
  Args:
@@ -740,7 +750,7 @@ class TimeMixin:
740
750
  destination (int or Tuple[int, ...]): The destination position(s) to move the axis to.
741
751
 
742
752
  Returns:
743
- TimeCompatibleProtocol: A new array with the axis moved to the specified destination.
753
+ BaseTimeArray: A new array with the axis moved to the specified destination.
744
754
 
745
755
  Note:
746
756
  This method re-instantiates a TimelinedArray with a view instead of the full
@@ -755,9 +765,7 @@ class TimeMixin:
755
765
  cls = self._get_array_cls()
756
766
 
757
767
  # we re-instanciate a TimelinedArray with view instead of the full constructor : faster
758
- moved_array: TimeCompatibleProtocol = np.moveaxis(np.asarray(self), source, destination).view(
759
- cls
760
- ) # type: ignore
768
+ moved_array: BaseTimeArray = np.moveaxis(np.asarray(self), source, destination).view(cls) # type: ignore
761
769
  moved_array.timeline = self.timeline
762
770
  moved_array.time_dimension = self.time_dimension
763
771
 
@@ -774,7 +782,7 @@ class TimeMixin:
774
782
  # TimelinedArray.time_dimension and TimelinedArray.timeline are set. good to go
775
783
  return moved_array
776
784
 
777
- def rollaxis(self: TimeCompatibleProtocol, axis: int, start: int = 0):
785
+ def rollaxis(self: "BaseTimeArray", axis: int, start: int = 0):
778
786
  """Roll the axis of the TimelinedArray.
779
787
 
780
788
  Args:
@@ -782,14 +790,13 @@ class TimeMixin:
782
790
  start (int, optional): The position where the axis is placed. Defaults to 0.
783
791
 
784
792
  Returns:
785
- TimeCompatibleProtocol: A TimelinedArray with the rolled axis.
793
+ BaseTimeArray: A TimelinedArray with the rolled axis.
786
794
  """
787
795
 
788
796
  # we re-instanciate a TimelinedArray with view instead of the full constructor : faster
789
797
  cls = self._get_array_cls()
790
798
 
791
- rolled_array: TimeCompatibleProtocol = np.rollaxis(
792
- np.asarray(self), axis, start).view(cls) # type: ignore
799
+ rolled_array: BaseTimeArray = np.rollaxis(np.asarray(self), axis, start).view(cls) # type: ignore
793
800
 
794
801
  # reinject timeline as is
795
802
  rolled_array.timeline = self.timeline
@@ -815,14 +822,14 @@ class TimeMixin:
815
822
  mapping = {i: new_order.index(i) for i in range(n)}
816
823
  return mapping
817
824
 
818
- rolled_array.time_dimension = rollaxis_mapping(
819
- self.shape, axis, start)[self.time_dimension]
825
+ rolled_array.time_dimension = rollaxis_mapping(self.shape, axis, start)[self.time_dimension]
820
826
 
821
827
  return rolled_array
822
828
 
823
- def mean(
824
- self: TimeCompatibleProtocol, axis: int | Tuple[int, ...] | None = None, dtype=None, out=None, keepdims=False
825
- ):
829
+ def all_axes(self):
830
+ return tuple([i for i in range(self.ndim)])
831
+
832
+ def mean(self, axis: int | Tuple[int, ...] | None = None, dtype=None, out=None, keepdims=False):
826
833
  """Calculates the mean along the specified axis.
827
834
 
828
835
  Args:
@@ -835,14 +842,13 @@ class TimeMixin:
835
842
  Returns:
836
843
  ndarray: Mean of the input array along the specified axis.
837
844
  """
838
-
845
+ if axis is None:
846
+ axis = self.all_axes()
839
847
  result = super().mean(axis=axis, dtype=dtype, out=out, keepdims=keepdims)
840
848
  return self._finish_axis_removing_operation(result, axis)
841
849
 
842
850
  # Override other reduction methods similarly if needed
843
- def sum(
844
- self: TimeCompatibleProtocol, axis: int | Tuple[int, ...] | None = None, dtype=None, out=None, keepdims=False
845
- ):
851
+ def sum(self, axis: int | Tuple[int, ...] | None = None, dtype=None, out=None, keepdims=False):
846
852
  """Calculate the sum along the specified axis.
847
853
 
848
854
  Args:
@@ -861,12 +867,13 @@ class TimeMixin:
861
867
  Returns:
862
868
  The sum of the input array along the specified axis.
863
869
  """
864
-
870
+ if axis is None:
871
+ axis = self.all_axes()
865
872
  result = super().sum(axis=axis, dtype=dtype, out=out, keepdims=keepdims)
866
873
  return self._finish_axis_removing_operation(result, axis)
867
874
 
868
875
  def std(
869
- self: TimeCompatibleProtocol,
876
+ self: "BaseTimeArray",
870
877
  axis: int | Tuple[int, ...] | None = None,
871
878
  dtype=None,
872
879
  out=None,
@@ -889,12 +896,13 @@ class TimeMixin:
889
896
  ndarray: A new array containing the standard deviation
890
897
  of elements along the specified axis after removing the axis.
891
898
  """
892
-
899
+ if axis is None:
900
+ axis = self.all_axes()
893
901
  result = super().std(axis=axis, dtype=dtype, out=out, ddof=ddof, keepdims=keepdims)
894
902
  return self._finish_axis_removing_operation(result, axis)
895
903
 
896
904
  def var(
897
- self: TimeCompatibleProtocol,
905
+ self: "BaseTimeArray",
898
906
  axis: int | Tuple[int, ...] | None = None,
899
907
  dtype=None,
900
908
  out=None,
@@ -904,7 +912,7 @@ class TimeMixin:
904
912
  """Calculate the variance along the specified axis.
905
913
 
906
914
  Args:
907
- self (TimeCompatibleProtocol): The input data.
915
+ self (BaseTimeArray): The input data.
908
916
  axis (int | Tuple[int, ...] | None): Axis or axes along which the variance is computed.
909
917
  The default is to compute the variance of the flattened array.
910
918
  dtype: Data-type of the result. If not provided, the data-type of the input is used.
@@ -918,7 +926,8 @@ class TimeMixin:
918
926
  Returns:
919
927
  ndarray: A new array containing the variance of the input array along the specified axis.
920
928
  """
921
-
929
+ if axis is None:
930
+ axis = self.all_axes()
922
931
  result = super().var(axis=axis, dtype=dtype, out=out, ddof=ddof, keepdims=keepdims)
923
932
  return self._finish_axis_removing_operation(result, axis)
924
933
 
@@ -936,7 +945,7 @@ class TimeMixin:
936
945
  # and shift the rest accordingly
937
946
  cls = self._get_array_cls()
938
947
  # type: ignore
939
- return cls(self, timeline=self.timeline - self.timeline[at])
948
+ return cls(self, timeline=self.timeline - self.timeline[at]) # type: ignore
940
949
 
941
950
  def offset_timeline(self, offset):
942
951
  """Returns a modified version of the array with time offset.
@@ -1004,8 +1013,7 @@ class TimeMixin:
1004
1013
  break
1005
1014
 
1006
1015
  if timeline is None:
1007
- raise ValueError(
1008
- "timeline must be supplied if the input_array is not a TimelinedArray")
1016
+ raise ValueError("timeline must be supplied if the input_array is not a TimelinedArray")
1009
1017
 
1010
1018
  if time_dimension is None: # same thing for the time dimension.
1011
1019
  time_dimension = getattr(data, "time_dimension", None)
@@ -1053,7 +1061,7 @@ class TimePacker:
1053
1061
  return iter((self.array.timeline, self.array.__array__()))
1054
1062
 
1055
1063
 
1056
- class TimelinedArray(TimeMixin, np.ndarray, TimeCompatibleProtocol):
1064
+ class TimelinedArray(BaseTimeArray):
1057
1065
  """
1058
1066
  The TimelinedArray class is a subclass of the numpy.ndarray class, which represents a multi-dimensional
1059
1067
  array of homogeneous data. This class adds additional functionality
@@ -1085,9 +1093,16 @@ class TimelinedArray(TimeMixin, np.ndarray, TimeCompatibleProtocol):
1085
1093
 
1086
1094
  """
1087
1095
 
1096
+ # backward compatibility
1088
1097
  TA_Timeline = Timeline
1089
1098
 
1090
- def __new__(cls, data, timeline=None, time_dimension: int | None = None, uniform_space=False) -> "TimelinedArray":
1099
+ def __new__(
1100
+ cls,
1101
+ data,
1102
+ timeline: Optional[Timeline | np.ndarray | list] = None,
1103
+ time_dimension: int | None = None,
1104
+ uniform_space=False,
1105
+ ) -> "TimelinedArray":
1091
1106
  """Create a new TimelinedArray object from the input data.
1092
1107
 
1093
1108
  Args:
@@ -1100,7 +1115,7 @@ class TimelinedArray(TimeMixin, np.ndarray, TimeCompatibleProtocol):
1100
1115
  TimelinedArray: A new TimelinedArray object.
1101
1116
  """
1102
1117
 
1103
- data, timeline, time_dimension = TimeMixin.extract_time_from_data(
1118
+ data, timeline, time_dimension = BaseTimeArray.extract_time_from_data(
1104
1119
  data, timeline=timeline, time_dimension=time_dimension, uniform_space=uniform_space
1105
1120
  )
1106
1121
 
@@ -1137,7 +1152,12 @@ class TimelinedArray(TimeMixin, np.ndarray, TimeCompatibleProtocol):
1137
1152
  self.timeline = getattr(obj, "timeline", Timeline([]))
1138
1153
  self.time_dimension = getattr(obj, "time_dimension", 0)
1139
1154
 
1140
- def __array_wrap__(self, out_arr, context=None):
1155
+ def __array_wrap__(
1156
+ self,
1157
+ out_arr,
1158
+ context=None,
1159
+ return_scalar=False,
1160
+ ):
1141
1161
  """Wrap the output array after a ufunc operation.
1142
1162
 
1143
1163
  Args:
@@ -1154,10 +1174,9 @@ class TimelinedArray(TimeMixin, np.ndarray, TimeCompatibleProtocol):
1154
1174
 
1155
1175
  if context is not None:
1156
1176
  logger.debug(f"wrapping array after ufunc {context[0].__name__}")
1157
- output = super().__array_wrap__(out_arr, context)
1177
+ output = super().__array_wrap__(out_arr, context, return_scalar)
1158
1178
  if len(output.shape) < len(self.shape):
1159
- logger.debug(
1160
- f"shape reduced from : {self.shape} to : {output.shape}. outarray was : {out_arr.shape}")
1179
+ logger.debug(f"shape reduced from : {self.shape} to : {output.shape}. outarray was : {out_arr.shape}")
1161
1180
  return output
1162
1181
 
1163
1182
  def __array_function__(self, func, types, args, kwargs):
@@ -1189,8 +1208,7 @@ class TimelinedArray(TimeMixin, np.ndarray, TimeCompatibleProtocol):
1189
1208
  TimelinedArray | np.ndarray: Indexed result based on the provided index.
1190
1209
  """
1191
1210
 
1192
- index, final_timeline, final_time_dimension = self._get_indexed_times(
1193
- index)
1211
+ index, final_timeline, final_time_dimension = self._get_indexed_times(index)
1194
1212
 
1195
1213
  if final_timeline is None or final_time_dimension is None:
1196
1214
  return np.asarray(self).__getitem__(index)
@@ -1240,19 +1258,19 @@ class TimelinedArray(TimeMixin, np.ndarray, TimeCompatibleProtocol):
1240
1258
  return TimelinedArray(aligned_arrays)
1241
1259
 
1242
1260
 
1243
- class MaskedTimelinedArray(TimeMixin, np.ma.MaskedArray, TimeCompatibleProtocol):
1261
+ class MaskedTimelinedArray(np.ma.MaskedArray, BaseTimeArray):
1244
1262
  def __new__(
1245
1263
  cls,
1246
1264
  data,
1247
- mask=np.ma.nomask,
1265
+ mask: NDArray[np.bool_] | np.bool_ | bool | np.ma.MaskedArray = np.ma.nomask,
1248
1266
  dtype=None,
1249
1267
  copy=False,
1250
1268
  fill_value=None,
1251
1269
  keep_mask=True,
1252
1270
  hard_mask=False,
1253
1271
  shrink=True,
1254
- timeline=None,
1255
- time_dimension=None,
1272
+ timeline: Optional[Timeline | np.ndarray | list] | None = None,
1273
+ time_dimension: Optional[int] = None,
1256
1274
  uniform_space=False,
1257
1275
  **kwargs,
1258
1276
  ):
@@ -1277,7 +1295,7 @@ class MaskedTimelinedArray(TimeMixin, np.ma.MaskedArray, TimeCompatibleProtocol)
1277
1295
  An instance of the class with the specified parameters.
1278
1296
  """
1279
1297
 
1280
- _, timeline, time_dimension = TimeMixin.extract_time_from_data(
1298
+ _, timeline, time_dimension = BaseTimeArray.extract_time_from_data(
1281
1299
  data, timeline=timeline, time_dimension=time_dimension, uniform_space=uniform_space
1282
1300
  )
1283
1301
 
@@ -1326,8 +1344,7 @@ class MaskedTimelinedArray(TimeMixin, np.ma.MaskedArray, TimeCompatibleProtocol)
1326
1344
  MaskedTimelinedArray | np.ma.MaskedArray: The masked array or MaskedTimelinedArray based on the index.
1327
1345
  """
1328
1346
 
1329
- index, final_timeline, final_time_dimension = self._get_indexed_times(
1330
- index)
1347
+ index, final_timeline, final_time_dimension = self._get_indexed_times(index)
1331
1348
 
1332
1349
  if final_timeline is None or final_time_dimension is None:
1333
1350
  return np.ma.MaskedArray(data=np.asarray(self), mask=self.mask, fill_value=self.fill_value).__getitem__(
@@ -1,52 +1,49 @@
1
1
  import numpy as np
2
2
  import pytest
3
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)
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
12
16
 
13
17
 
14
18
  @pytest.fixture
15
19
  def timelined_array_1D():
16
- return TimelinedArray(np.random.rand(50),
17
- timeline=np.arange(50),
18
- time_dimension=0)
20
+ return TimelinedArray(np.random.rand(50), timeline=np.arange(50), time_dimension=0)
19
21
 
20
22
 
21
23
  @pytest.fixture
22
24
  def timelined_array_3D():
23
- return TimelinedArray(np.random.rand(25, 50, 75),
24
- timeline=np.arange(50),
25
- time_dimension=1)
25
+ return TimelinedArray(np.random.rand(25, 50, 75), timeline=np.arange(50), time_dimension=1)
26
+
27
+
28
+ @pytest.fixture
29
+ def pickle_path():
30
+ file_path = Path("serialized_test_3D_ta_array.pickle")
31
+ yield file_path
32
+ if file_path.exists():
33
+ file_path.unlink(missing_ok=True)
26
34
 
27
35
 
28
36
  def get_axis_parameters_3D():
29
37
  # affected_axis, time_dimension, shape
30
38
  # 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
- ])
39
+ return [(0, 0, (50, 75)), (1, None, (25, 75)), (2, 1, (25, 50)), ((0, 2), 0, (50,)), (None, None, None)]
40
+
41
+
42
+ @pytest.mark.parametrize("shape, timeline_dimension", [((12,), 0), ((10, 30, 50), 2), ((29, 34, 61, 15, 4), 0)])
45
43
  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)
44
+ ta_array = TimelinedArray(
45
+ np.random.rand(*shape), timeline=np.arange(shape[timeline_dimension]), time_dimension=timeline_dimension
46
+ )
50
47
 
51
48
  assert ta_array.shape == shape
52
49
  assert ta_array.timeline.shape == (shape[timeline_dimension],)
@@ -58,6 +55,25 @@ def test_timeline_creation():
58
55
  assert timeline.max() == 9
59
56
  assert timeline.min() == 0
60
57
 
58
+ with pytest.raises(NotImplementedError):
59
+ timeline = Timeline(np.random.rand(10), uniform_space=True)
60
+ # assert np.unique(np.diff(timeline)).size == 1
61
+
62
+
63
+ def test_pickle_unpickle(timelined_array_3D, pickle_path):
64
+ # Pickle the numpy array
65
+ with open(pickle_path, "wb") as f:
66
+ pickle.dump(timelined_array_3D, f)
67
+
68
+ # Unpickle the numpy array
69
+ with open(pickle_path, "rb") as f:
70
+ unserialized_3D_array = pickle.load(f)
71
+
72
+ # Assert that the original and unpickled arrays are the same
73
+ np.testing.assert_array_equal(timelined_array_3D, unserialized_3D_array)
74
+ np.testing.assert_array_equal(timelined_array_3D.timeline, unserialized_3D_array.timeline)
75
+ assert timelined_array_3D.time_dimension == unserialized_3D_array.time_dimension
76
+
61
77
 
62
78
  def test_start_boundary():
63
79
  assert StartBoundary.inclusive.value(5, 5)
@@ -97,27 +113,18 @@ def test_ta_swapaxes(timelined_array_3D):
97
113
  assert swapped.time_dimension == 2
98
114
 
99
115
 
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):
116
+ @pytest.mark.parametrize(
117
+ "rolled_axis, end_position, expected_time_position, expected_shape",
118
+ [(0, 2, 0, (50, 25, 75)), (2, 1, 2, (25, 75, 50)), (1, 3, 2, (25, 75, 50))],
119
+ )
120
+ def test_ta_rollaxis(rolled_axis, end_position, expected_time_position, expected_shape, timelined_array_3D):
110
121
  # roll back the axis
111
122
  rolled = timelined_array_3D.rollaxis(rolled_axis, end_position)
112
123
  assert rolled.shape == expected_shape
113
124
  assert rolled.time_dimension == expected_time_position
114
125
 
115
126
 
116
- def axis_affecting_functions_blueprint(timelined_array_3D,
117
- func,
118
- affected_axis,
119
- time_dimension,
120
- shape):
127
+ def axis_affecting_functions_blueprint(timelined_array_3D, func, affected_axis, time_dimension, shape):
121
128
  result = func(timelined_array_3D, axis=affected_axis)
122
129
  if time_dimension is None:
123
130
  with pytest.raises(AttributeError):
@@ -134,33 +141,28 @@ def axis_affecting_functions_blueprint(timelined_array_3D,
134
141
 
135
142
  @pytest.mark.parametrize("affected_axis, time_dimension, shape", get_axis_parameters_3D())
136
143
  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)
144
+ axis_affecting_functions_blueprint(timelined_array_3D, np.mean, affected_axis, time_dimension, shape)
139
145
 
140
146
 
141
147
  @pytest.mark.parametrize("affected_axis, time_dimension, shape", get_axis_parameters_3D())
142
148
  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)
149
+ axis_affecting_functions_blueprint(timelined_array_3D, np.sum, affected_axis, time_dimension, shape)
145
150
 
146
151
 
147
152
  @pytest.mark.parametrize("affected_axis, time_dimension, shape", get_axis_parameters_3D())
148
153
  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)
154
+ axis_affecting_functions_blueprint(timelined_array_3D, np.std, affected_axis, time_dimension, shape)
151
155
 
152
156
 
153
157
  @pytest.mark.parametrize("affected_axis, time_dimension, shape", get_axis_parameters_3D())
154
158
  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)
159
+ axis_affecting_functions_blueprint(timelined_array_3D, np.var, affected_axis, time_dimension, shape)
157
160
 
158
161
 
159
162
  def test_masked_ta_creation():
160
163
  data = np.random.rand(10, 10)
161
164
  mask = data > 0.5
162
- masked_ta = MaskedTimelinedArray(
163
- data, mask=mask, timeline=np.arange(10), time_dimension=0)
165
+ masked_ta = MaskedTimelinedArray(data, mask=mask, timeline=np.arange(10), time_dimension=0)
164
166
  assert isinstance(masked_ta, MaskedTimelinedArray)
165
167
  assert masked_ta.shape == (10, 10)
166
168
  assert masked_ta.time_dimension == 0
@@ -1,45 +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 = "timothe.jost@wanadoo.fr" },
6
- ]
7
- dependencies = [
8
- "numpy",
9
- ]
10
- requires-python = ">=3.11"
11
- readme = "README.md"
12
- dynamic = []
13
- version = "0.0.13"
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 html --cov --junitxml=pytest_results.xml"
43
- testpaths = [
44
- "tests",
45
- ]