Coverage for src/foapy/core/_intervals.py: 98%
46 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-17 20:45 +0000
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-17 20:45 +0000
1import numpy as np
2from numpy import ndarray
4from foapy.core import binding as constants_binding
5from foapy.core import mode as constants_mode
8def intervals(X, binding: int, mode: int) -> ndarray:
9 """
10 Function to extract intervals from a sequence.
12 An interval is defined as the distance between consecutive occurrences
13 of the similar elements in the sequence, with boundary intervals counted as positions
14 from sequence edges to first/last occurrence. The intervals are extracted
15 based on the specified binding direction ([start][foapy.binding.start] or [end][foapy.binding.end])
16 and mode ([normal][foapy.mode.normal], [lossy][foapy.mode.lossy], [cycle][foapy.mode.cycle], or [redundant][foapy.mode.redundant]).
18 Example how intervals are extracted from the sequence with **"binding.start"** and **"mode.normal"**.
20 | **X** | b | a | b | c | b |
21 |:-------------:|:------:|:------:|:------:|:------:|:-----:|
22 | b | 1 | -> | 2 | -> | 2 |
23 | a | -> | 2 | | | |
24 | c | -> | -> | -> | 4 | |
25 | **intervals** | **1** | **2** | **2** | **4** | **2** |
28 Parameters
29 ----------
30 X: array_like
31 Array to exctact an intervals from. Must be a 1-dimensional array.
32 binding: int
33 [start][foapy.binding.start] = 1 - Intervals are extracted from left to right.
34 [end][foapy.binding.end] = 2 – Intervals are extracted from right to left.
35 mode: int
36 Mode handling the intervals at the sequence boundaries:
38 [lossy][foapy.mode.lossy] = 1 - Both interval from the start of the sequence
39 to the first element occurrence and interval from the
40 last element occurrence to the end of the sequence
41 are not taken into account.
43 [normal][foapy.mode.normal] = 2 - Interval from the start of the sequence to
44 the first occurrence of the element
45 (in case of binding to the beginning)
46 or interval from the last occurrence of the element to
47 the end of the sequence
48 (in case of binding to the end) is taken into account.
50 [cycle][foapy.mode.cycle] = 3 - Interval from the start of the sequence to
51 the first element occurrence
52 and interval from the last element occurrence to the
53 end of the sequence are summed
54 into one interval (as if sequence was cyclic).
55 Interval is placed either in the beginning of
56 intervals array (in case of binding to the beginning)
57 or in the end (in case of binding to the end).
59 [redundant][foapy.mode.redundant] = 4 - Both interval from start of the sequence
60 to the first element occurrence and the interval from
61 the last element occurrence to the end of the
62 sequence are taken into account. Their placement in results
63 array is determined by the binding.
65 Returns
66 -------
67 order : ndarray
68 Intervals extracted from the sequence
70 Raises
71 -------
72 Not1DArrayException
73 When X parameter is not a 1-dimensional array
75 ValueError
76 When binding or mode is not valid
78 Examples
79 --------
81 Get intervals from a sequence binding to [start][foapy.binding.start] and mode [normal][foapy.mode.normal].
83 ``` py linenums="1"
84 import foapy
86 source = ['a', 'b', 'a', 'c', 'a', 'd']
87 intervals = foapy.intervals(source, foapy.binding.start, foapy.mode.normal)
88 print(intervals)
89 # [1 2 2 3 2 5]
90 ```
92 Get intervals from a emprty sequence.
93 ``` py linenums="1"
94 import foapy
96 source = []
97 intervals = foapy.intervals(source, foapy.binding.start, foapy.mode.normal)
98 print(intervals)
99 # []
100 ```
102 Getting an intervals of an array with more than 1 dimension is not allowed.
103 ``` py linenums="1"
104 import foapy
105 source = [[1, 2], [3, 4]]
106 intervals = foapy.intervals(source, foapy.binding.start, foapy.mode.normal)
107 # Not1DArrayException:
108 # {'message': 'Incorrect array form. Expected d1 array, exists 2'}
109 ```
110 """ # noqa: E501
112 # Validate binding
113 if binding not in {constants_binding.start, constants_binding.end}:
114 raise ValueError(
115 {"message": "Invalid binding value. Use binding.start or binding.end."}
116 )
118 # Validate mode
119 valid_modes = [
120 constants_mode.lossy,
121 constants_mode.normal,
122 constants_mode.cycle,
123 constants_mode.redundant,
124 ]
125 if mode not in valid_modes:
126 raise ValueError(
127 {"message": "Invalid mode value. Use mode.lossy,normal,cycle or redundant."}
128 )
130 ar = np.asanyarray(X)
132 if ar.shape == (0,):
133 return []
135 if binding == constants_binding.end:
136 ar = ar[::-1]
138 perm = ar.argsort(kind="mergesort")
140 mask_shape = ar.shape
141 mask = np.empty(mask_shape[0] + 1, dtype=bool)
142 mask[:1] = True
143 mask[1:-1] = ar[perm[1:]] != ar[perm[:-1]]
144 mask[-1:] = True # or mask[-1] = True
146 first_mask = mask[:-1]
147 last_mask = mask[1:]
149 intervals = np.empty(ar.shape, dtype=np.intp)
150 intervals[1:] = perm[1:] - perm[:-1]
152 delta = len(ar) - perm[last_mask] if mode == constants_mode.cycle else 1
153 intervals[first_mask] = perm[first_mask] + delta
155 inverse_perm = np.empty(ar.shape, dtype=np.intp)
156 inverse_perm[perm] = np.arange(ar.shape[0])
158 if mode == constants_mode.lossy:
159 intervals[first_mask] = 0
160 intervals = intervals[inverse_perm]
161 result = intervals[intervals != 0]
162 elif mode == constants_mode.normal:
163 result = intervals[inverse_perm]
164 elif mode == constants_mode.cycle:
165 result = intervals[inverse_perm]
166 elif mode == constants_mode.redundant: 166 ↛ 172line 166 didn't jump to line 172 because the condition on line 166 was always true
167 result = intervals[inverse_perm]
168 redundant_intervals = len(ar) - perm[last_mask]
169 if binding == constants_binding.end:
170 redundant_intervals = redundant_intervals[::-1]
171 result = np.concatenate((result, redundant_intervals))
172 if binding == constants_binding.end:
173 result = result[::-1]
175 return result