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

1import numpy as np 

2from numpy import ndarray 

3 

4from foapy.core import binding as constants_binding 

5from foapy.core import mode as constants_mode 

6 

7 

8def intervals(X, binding: int, mode: int) -> ndarray: 

9 """ 

10 Function to extract intervals from a sequence. 

11 

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]). 

17 

18 Example how intervals are extracted from the sequence with **"binding.start"** and **"mode.normal"**. 

19 

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

26 

27 

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: 

37 

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. 

42 

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. 

49 

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). 

58 

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. 

64 

65 Returns 

66 ------- 

67 order : ndarray 

68 Intervals extracted from the sequence 

69 

70 Raises 

71 ------- 

72 Not1DArrayException 

73 When X parameter is not a 1-dimensional array 

74 

75 ValueError 

76 When binding or mode is not valid 

77 

78 Examples 

79 -------- 

80 

81 Get intervals from a sequence binding to [start][foapy.binding.start] and mode [normal][foapy.mode.normal]. 

82 

83 ``` py linenums="1" 

84 import foapy 

85 

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

91 

92 Get intervals from a emprty sequence. 

93 ``` py linenums="1" 

94 import foapy 

95 

96 source = [] 

97 intervals = foapy.intervals(source, foapy.binding.start, foapy.mode.normal) 

98 print(intervals) 

99 # [] 

100 ``` 

101 

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 

111 

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 ) 

117 

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 ) 

129 

130 ar = np.asanyarray(X) 

131 

132 if ar.shape == (0,): 

133 return [] 

134 

135 if binding == constants_binding.end: 

136 ar = ar[::-1] 

137 

138 perm = ar.argsort(kind="mergesort") 

139 

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 

145 

146 first_mask = mask[:-1] 

147 last_mask = mask[1:] 

148 

149 intervals = np.empty(ar.shape, dtype=np.intp) 

150 intervals[1:] = perm[1:] - perm[:-1] 

151 

152 delta = len(ar) - perm[last_mask] if mode == constants_mode.cycle else 1 

153 intervals[first_mask] = perm[first_mask] + delta 

154 

155 inverse_perm = np.empty(ar.shape, dtype=np.intp) 

156 inverse_perm[perm] = np.arange(ar.shape[0]) 

157 

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] 

174 

175 return result