Coverage for src/foapy/ma/_intervals.py: 99%
70 statements
« prev ^ index » next coverage.py v7.10.3, created at 2025-08-16 11:33 +0000
« prev ^ index » next coverage.py v7.10.3, created at 2025-08-16 11:33 +0000
1import numpy as np
2from numpy import ma
4from foapy import binding as binding_enum
5from foapy import mode as mode_enum
6from foapy.exceptions import InconsistentOrderException, Not1DArrayException
9def intervals(X, binding, mode):
10 """
11 Finding array of array of intervals of the uniform
12 sequences in the given input sequence
14 Parameters
15 ----------
16 X: masked_array
17 Array to get intervals.
19 binding: int
20 binding.start = 1 - Intervals are extracted from left to right.
21 binding.end = 2 – Intervals are extracted from right to left.
23 mode: int
24 mode.lossy = 1 - Both interval from the start of the sequence
25 to the first element occurrence and interval from the
26 last element occurrence to the end of the sequence are not taken into account.
28 mode.normal = 2 - Interval from the start of the sequence to the
29 first occurrence of the element or interval from the last occurrence
30 of the element to the end of the sequence is taken into account.
32 mode.cycle = 3 - Interval from the start of the sequence to the first
33 element occurrence
34 and interval from the last element occurrence to the end of the
35 sequence are summed
36 into one interval (as if sequence was cyclic). Interval is
37 placed either in the
38 beginning of intervals array (in case of binding to the
39 beginning) or in the end.
41 mode.redundant = 4 - Both interval from start of the sequence
42 to the first element
43 occurrence and the interval from the last element occurrence
44 to the end of the
45 sequence are taken into account. Their placement in results
46 array is determined
47 by the binding.
49 Returns
50 -------
51 result: array or Exception.
52 Exception if not d1 array or wrong mask, array otherwise.
54 Examples
55 --------
57 ----1----
58 >>> import foapy.ma as ma
59 >>> a = [2, 4, 2, 2, 4]
60 >>> b = ma.intervals(X, binding.start, mode.lossy)
61 >>> b
62 [
63 [5],
64 [1, 4],
65 [],
66 []
67 ]
69 ----2----
70 >>> import foapy.ma as ma
71 >>> a = [2, 4, 2, 2, 4]
72 >>> b = ma.intervals(X, binding.end, mode.lossy)
73 >>> b
74 [
75 [5],
76 [1, 4],
77 [],
78 []
79 ]
81 ----3----
82 >>> import foapy.ma as ma
83 >>> a = [2, 4, 2, 2, 4]
84 >>> b = ma.intervals(X, binding.start, mode.normal)
85 >>> b
86 [
87 [1, 2, 1],
88 [2, 3]
89 ]
91 ----4----
92 >>> import foapy.ma as ma
93 >>> a = [2, 4, 2, 2, 4]
94 >>> b = ma.intervals(X, binding.end, mode.normal)
95 >>> b
96 [
97 [2, 1, 2],
98 [3, 1]
99 ]
101 ----5----
102 >>> import foapy.ma as ma
103 >>> a = [2, 4, 2, 2, 4]
104 >>> b = ma.intervals(X, binding.start, mode.cycle)
105 >>> b
106 [
107 [2, 2, 1],
108 [2, 3]
109 ]
111 ----6----
112 >>> import foapy.ma as ma
113 >>> a = [2, 4, 2, 2, 4]
114 >>> b = ma.intervals(X, binding.end, mode.cycle)
115 >>> b
116 [
117 [2, 1, 2],
118 [3, 2]
119 ]
121 ----7----
122 >>> import foapy.ma as ma
123 >>> a = [2, 4, 2, 2, 4]
124 >>> b = ma.intervals(X, binding.start, mode.redunant)
125 >>> b
126 [
127 [1, 2, 1, 2],
128 [2, 3, 1]
129 ]
131 ----8----
132 >>> import foapy.ma as ma
133 >>> a = [2, 4, 2, 2, 4]
134 >>> b = ma.intervals(X, binding.end, mode.redunant)
135 >>> b
136 [
137 [1, 2, 1, 2],
138 [2, 3, 1]
139 ]
141 ----9----
142 >>> import foapy.ma as ma
143 >>> a = ['a', 'b', 'c', 'a', 'b', 'c', 'c', 'c', 'b', 'a', 'c', 'b', 'c']
144 >>> mask = [0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0]
145 >>> masked_a = ma.masked_array(a, mask)
146 >>> b = intervals(X, binding.end, mode.redunant)
147 >>> b
148 Exception
150 ----10----
151 >>> import foapy.ma as ma
152 >>> a = [[2, 2, 2], [2, 2, 2]]
153 >>> mask = [[0, 0, 0], [0, 0, 0]]
154 >>> masked_a = ma.masked_array(a, mask)
155 >>> b = ma.intervals(X, binding.end, mode.redunant)
156 >>> b
157 Exception
158 """
160 # Validate binding
161 if binding not in {binding_enum.start, binding_enum.end}:
162 raise ValueError(
163 {"message": "Invalid binding value. Use binding.start or binding.end."}
164 )
166 # Validate mode
167 valid_modes = {
168 mode_enum.lossy,
169 mode_enum.normal,
170 mode_enum.cycle,
171 mode_enum.redundant,
172 }
173 if mode not in valid_modes:
174 raise ValueError(
175 {"message": "Invalid mode value. Use mode.lossy,normal,cycle or redundant."}
176 )
177 # ex.:
178 # ar = ['a', 'c', 'c', 'e', 'd', 'a']
180 power = X.shape[0]
181 if power == 0:
182 return np.array([])
184 if len(X.shape) != 2:
185 message = f"Incorrect array form. Expected d2 array, exists {len(X.shape)}"
186 raise Not1DArrayException({"message": message})
188 length = X.shape[1]
190 mask = ma.getmaskarray(X)
192 verify_data = X[~mask]
193 verify_positions = np.transpose(np.argwhere(~mask))
194 verify = np.empty(verify_data.shape[0], dtype=bool)
195 verify[:1] = False
196 verify[1:] = np.logical_xor(
197 verify_positions[0, 1:] != verify_positions[0, :-1],
198 verify_data[1:] != verify_data[:-1],
199 )
201 if not np.all(~verify):
202 failed_indexes = verify_positions[0, verify]
203 i = X[failed_indexes[0]]
204 raise InconsistentOrderException(
205 {"message": f"Elements {i} have wrong appearance"}
206 )
208 extended_mask = np.empty((power, length + 1), dtype=bool)
209 if binding == binding_enum.end:
210 extended_mask[:, :-1] = ~mask[::-1, ::-1]
211 else:
212 extended_mask[:, :-1] = ~mask
213 extended_mask[:, -1] = np.any(extended_mask[:, :-1], axis=1)
215 positions = np.transpose(np.argwhere(extended_mask))
217 border_indexes = np.empty(positions.shape[1] + 1, dtype=bool)
218 border_indexes[:1] = True
219 border_indexes[1:-1] = positions[0, 1:] != positions[0, :-1]
220 border_indexes[-1:] = True
222 first_indexes = np.argwhere(border_indexes[:-1]).ravel()
223 last_indexes = np.argwhere(border_indexes[1:]).ravel()
225 indecies = np.zeros(positions.shape[1], dtype=int)
226 indecies[1:] = positions[1, 1:] - positions[1, :-1]
227 delta = indecies[last_indexes] if mode == mode_enum.cycle else 1
228 indecies[first_indexes] = positions[1][first_indexes] + delta
230 split_boarders = np.zeros(power * 2, dtype=int)
232 if mode == mode_enum.lossy:
233 split_boarders[positions[0][last_indexes[:1]] * 2] = first_indexes[:1] + 1
234 split_boarders[positions[0][last_indexes[1:]] * 2] = (
235 np.fmax(last_indexes[:-1], first_indexes[1:]) + 1
236 )
237 split_boarders[positions[0][last_indexes] * 2 + 1] = last_indexes
238 elif mode == mode_enum.normal:
239 split_boarders[positions[0][last_indexes[:1]] * 2] = 0
240 split_boarders[positions[0][last_indexes[1:]] * 2] = last_indexes[:-1] + 1
241 split_boarders[positions[0][last_indexes] * 2 + 1] = last_indexes
242 elif mode == mode_enum.cycle:
243 split_boarders[positions[0][last_indexes[:1]] * 2] = 0
244 split_boarders[positions[0][last_indexes[1:]] * 2] = last_indexes[:-1] + 1
245 split_boarders[positions[0][last_indexes] * 2 + 1] = last_indexes
246 elif mode == mode_enum.redundant: 246 ↛ 251line 246 didn't jump to line 251 because the condition on line 246 was always true
247 split_boarders[positions[0][last_indexes[:1]] * 2] = 0
248 split_boarders[positions[0][last_indexes[1:]] * 2] = 0
249 split_boarders[positions[0][last_indexes] * 2 + 1] = last_indexes + 1
251 preserve_previous = np.frompyfunc(lambda x, y: x if y == 0 else y, 2, 1)
252 split_boarders = preserve_previous.accumulate(split_boarders)
253 if binding == binding_enum.end:
254 split_boarders[:-1] = np.diff(split_boarders)
255 split_boarders[-1:] = len(indecies) - split_boarders[-1]
256 split_boarders = np.cumsum(split_boarders[::-1])
257 indecies = indecies[::-1]
259 indecies = np.array_split(indecies, split_boarders)
260 return indecies[1:-1:2]