Coverage for o2/util/bit_mask_helper.py: 96%

99 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-05-16 11:18 +0000

1from functools import reduce 

2from typing import Optional 

3 

4 

5def bitmask_to_string(bitmask: int, pad_left: int = 24) -> str: 

6 """Convert a bitmask to a string of 1s and 0s. 

7 

8 Pads the left side with 0s to the specified length. 

9 """ 

10 return bin(bitmask)[2:].zfill(pad_left) 

11 

12 

13def string_to_bitmask(bitmask: str) -> int: 

14 """Convert a string of 1s and 0s to a bitmask.""" 

15 return int(bitmask, 2) 

16 

17 

18def bitmask_to_array(bitmask: int, pad_left: int = 24) -> list[int]: 

19 """Convert a bitmask to an array of integers.""" 

20 return [int(i) for i in bitmask_to_string(bitmask, pad_left)] 

21 

22 

23def array_to_bitmask(bitmask: list[int]) -> int: 

24 """Convert an array of integers to a bitmask.""" 

25 return string_to_bitmask("".join(map(str, bitmask))) 

26 

27 

28def get_ranges_from_bitmask(bitmask: int) -> list[tuple[int, int]]: 

29 """Get the ranges of 1s in a bitmask.""" 

30 bitmask_str = bitmask_to_string(bitmask) 

31 ranges: list[tuple[int, int]] = [] 

32 start = None 

33 for i in range(len(bitmask_str)): 

34 if bitmask_str[i] == "1" and start is None: 

35 start = i 

36 elif bitmask_str[i] == "0" and start is not None: 

37 ranges.append((start, i)) 

38 start = None 

39 if start is not None: 

40 ranges.append((start, len(bitmask_str))) 

41 return ranges 

42 

43 

44def has_overlap(bitmask_a: int, bitmask_b: int) -> bool: 

45 """Check if two bitmasks have an overlap.""" 

46 return bitmask_a & bitmask_b != 0 

47 

48 

49def any_has_overlap(bitmasks: list[int]) -> bool: 

50 """Check if any of the bitmasks have an overlap with any other.""" 

51 acc = 0 

52 for bitmask in bitmasks: 

53 if acc & bitmask != 0: 

54 return True 

55 acc |= bitmask 

56 return False 

57 

58 

59def find_most_frequent_overlap(bitmasks: list[int], min_size: int = 1) -> Optional[tuple[int, int, int]]: 

60 """Find the most frequent overlap in a list of bitmasks. 

61 

62 1. Convert bitmasks to bitarrays 

63 2. Add all bitarrays together 

64 3. Iterate over the resulting array and find the most frequent overlap of size >= min_size 

65 4. Return start and end of the overlap 

66 

67 returns frequency of overlap, start, end; With [start, end) being the range of the overlap. 

68 If no overlap is found, returns the longest range of 1s >= min_size (if any). 

69 """ 

70 if len(bitmasks) == 0: 

71 return None 

72 bitarrays = [bitmask_to_array(bitmask) for bitmask in bitmasks] 

73 summed_bitarray = reduce(lambda a, b: [x + y for x, y in zip(a, b)], bitarrays) 

74 

75 current_value = 0 

76 current_start = 0 

77 current_length = 0 

78 

79 max_start = None 

80 max_end = None 

81 max_value = 0 

82 max_length = 0 

83 

84 current_sufficient_start = 0 

85 current_sufficient_length = 0 

86 

87 max_sufficient_start = None 

88 max_sufficient_end = None 

89 max_sufficient_length = 0 

90 

91 for i, value in enumerate(summed_bitarray): 

92 if value > 0: 

93 if current_sufficient_length == 0: 

94 current_sufficient_start = i 

95 current_sufficient_length += 1 

96 else: 

97 if current_sufficient_length >= min_size and current_sufficient_length > max_sufficient_length: 

98 max_sufficient_start = current_sufficient_start 

99 max_sufficient_end = i 

100 max_sufficient_length = current_sufficient_length 

101 current_sufficient_start = i 

102 current_sufficient_length = 0 

103 

104 if value == current_value: 

105 current_length += 1 

106 else: 

107 # Check if the current range meets the requirements 

108 if current_length >= min_size and ( 

109 (current_value == max_value and current_length > max_length) or (current_value > max_value) 

110 ): 

111 max_start = current_start 

112 # End is exclusive 

113 max_end = current_start + current_length 

114 max_length = current_length 

115 max_value = current_value 

116 

117 # Reset for new value 

118 current_value = value 

119 current_start = i 

120 current_length = 1 

121 

122 # Final check after loop for sufficient length 

123 if current_sufficient_length >= min_size and current_sufficient_length > max_sufficient_length: 

124 max_sufficient_start = current_sufficient_start 

125 max_sufficient_end = len(summed_bitarray) 

126 max_sufficient_length = current_sufficient_length 

127 

128 # Final check after loop for max length 

129 if current_length >= min_size and ( 

130 (current_value == max_value and current_length > max_length) or (current_value > max_value) 

131 ): 

132 max_start = current_start 

133 # End is exclusive 

134 max_end = current_start + current_length 

135 

136 if max_value > 0 and max_start is not None and max_end is not None: 

137 return max_value, max_start, max_end 

138 elif max_sufficient_start is not None and max_sufficient_end is not None: 

139 return 1, max_sufficient_start, max_sufficient_end 

140 else: 

141 return None 

142 

143 

144def find_mixed_ranges_in_bitmask( 

145 bitmask: int, 

146 length: int, 

147 start_index: int, 

148 max_start_index: int, 

149 pad_left: int = 24, 

150) -> list[tuple[int, int]]: 

151 """In the given bitmask, find all (incl. overlapping) ranges of 1s and 0s. 

152 

153 The ranges need to be between start_index and max_start_index and contain 

154 exactly `length` 1s. The 1s do NOT need to be consecutive. Meaning the sequence 

155 will be at least length long, but due to intermediate 0s may be as long 

156 as max_start_index-start_index. 

157 

158 NOTE: The returned range starts on the left, e.g., the left-most bit is 0. 

159 The ranges are non-inclusive for the end index. 

160 

161 Args: 

162 ---- 

163 bitmask (int): The input bitmask, treated as a binary sequence. 

164 length (int): Exact number of `1`s required in the range. 

165 start_index (int): The starting index of the search. 

166 max_start_index (int): The maximum starting index of the search. 

167 pad_left (int): The number of bits in the bitmask. Defaults to 24. 

168 

169 Returns: 

170 ------- 

171 list[tuple[int, int]]: List of tuples where each tuple represents the start (inclusive) and end 

172 (non-inclusive) indices of a valid range. 

173 

174 """ 

175 result = [] 

176 bitmask_length = pad_left 

177 

178 for i in range(start_index, min(max_start_index, bitmask_length - 1) + 1): 

179 ones = 0 

180 range_start = i 

181 

182 # Skip starting bits that are 0 -> we don't get 0-prefixed ranges 

183 if not bitmask & (1 << (bitmask_length - i - 1)): # -1, because it's inclusive & 0-indexed 

184 continue 

185 

186 for j in range(i, bitmask_length): 

187 if bitmask & (1 << (bitmask_length - j - 1)): # Check if the bit is 1 

188 ones += 1 

189 

190 if ones == length: 

191 result.append((range_start, j + 1)) 

192 break 

193 

194 if ones > length: 

195 break 

196 

197 return result