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

1import numpy as np 

2from numpy import ma 

3 

4from foapy import binding as binding_enum 

5from foapy import mode as mode_enum 

6from foapy.exceptions import InconsistentOrderException, Not1DArrayException 

7 

8 

9def intervals(X, binding, mode): 

10 """ 

11 Finding array of array of intervals of the uniform 

12 sequences in the given input sequence 

13 

14 Parameters 

15 ---------- 

16 X: masked_array 

17 Array to get intervals. 

18 

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. 

22 

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. 

27 

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. 

31 

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. 

40 

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. 

48 

49 Returns 

50 ------- 

51 result: array or Exception. 

52 Exception if not d1 array or wrong mask, array otherwise. 

53 

54 Examples 

55 -------- 

56 

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 ] 

68 

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 ] 

80 

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 ] 

90 

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 ] 

100 

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 ] 

110 

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 ] 

120 

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 ] 

130 

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 ] 

140 

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 

149 

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 """ 

159 

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 ) 

165 

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'] 

179 

180 power = X.shape[0] 

181 if power == 0: 

182 return np.array([]) 

183 

184 if len(X.shape) != 2: 

185 message = f"Incorrect array form. Expected d2 array, exists {len(X.shape)}" 

186 raise Not1DArrayException({"message": message}) 

187 

188 length = X.shape[1] 

189 

190 mask = ma.getmaskarray(X) 

191 

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 ) 

200 

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 ) 

207 

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) 

214 

215 positions = np.transpose(np.argwhere(extended_mask)) 

216 

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 

221 

222 first_indexes = np.argwhere(border_indexes[:-1]).ravel() 

223 last_indexes = np.argwhere(border_indexes[1:]).ravel() 

224 

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 

229 

230 split_boarders = np.zeros(power * 2, dtype=int) 

231 

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 

250 

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] 

258 

259 indecies = np.array_split(indecies, split_boarders) 

260 return indecies[1:-1:2]