Coverage for ccsds124/bitvector.py: 100%

135 statements  

« prev     ^ index     » next       coverage.py v7.14.3, created at 2026-06-29 15:01 +0000

1""" 

2Fixed-length bit vector implementation using 32-bit words. 

3 

4This module provides fixed-length bit vector operations optimized for 

5CCSDS 124.0-B-1 compression. Uses 32-bit words with big-endian byte packing to 

6match ESA/ESOC reference implementation. 

7 

8Bit Numbering Convention (CCSDS 124.0-B-1 Section 1.6.1): 

9- Bit 0 = MSB (Most Significant Bit, transmitted first) 

10- Bit N-1 = LSB (Least Significant Bit) 

11 

12MicroPython compatible - no typing module imports. 

13""" 

14 

15 

16class BitVector: 

17 """Fixed-length bit vector using 32-bit word storage.""" 

18 

19 def __init__(self, num_bits: int) -> None: 

20 """ 

21 Initialize a bit vector with specified length. 

22 

23 Args: 

24 num_bits: Number of bits (must be > 0) 

25 

26 Raises: 

27 ValueError: If num_bits <= 0 

28 """ 

29 if num_bits <= 0: 

30 raise ValueError("num_bits must be positive") 

31 

32 self.length = num_bits 

33 # Calculate number of 32-bit words needed 

34 num_bytes = (num_bits + 7) // 8 

35 self.num_words = (num_bytes + 3) // 4 

36 

37 # Initialize all words to zero 

38 self._data = [0] * self.num_words 

39 

40 def zero(self) -> None: 

41 """Set all bits to zero.""" 

42 for i in range(self.num_words): 

43 self._data[i] = 0 

44 

45 def copy(self) -> "BitVector": 

46 """ 

47 Create a copy of this bit vector. 

48 

49 Returns: 

50 New BitVector with same contents 

51 """ 

52 result = BitVector(self.length) 

53 for i in range(self.num_words): 

54 result._data[i] = self._data[i] 

55 return result 

56 

57 def copy_from(self, other: "BitVector") -> None: 

58 """ 

59 Copy contents from another bit vector into this one. 

60 

61 Args: 

62 other: Source bit vector to copy from 

63 """ 

64 num_words = min(self.num_words, other.num_words) 

65 for i in range(num_words): 

66 self._data[i] = other._data[i] 

67 

68 def get_bit(self, pos: int) -> int: 

69 """ 

70 Get bit value at position. 

71 

72 Args: 

73 pos: Bit position (0 = MSB, length-1 = LSB) 

74 

75 Returns: 

76 Bit value (0 or 1) 

77 

78 Raises: 

79 IndexError: If pos is out of range 

80 """ 

81 if pos < 0 or pos >= self.length: 

82 raise IndexError(f"Bit position {pos} out of range [0, {self.length})") 

83 

84 # Calculate byte and bit within byte 

85 byte_index = pos // 8 

86 bit_in_byte = pos % 8 

87 

88 # Calculate word index and byte position within word 

89 word_index = byte_index // 4 

90 byte_in_word = byte_index % 4 

91 

92 # Big-endian: byte 0 is at bits 24-31, byte 1 at 16-23, etc. 

93 # MSB-first indexing: bit 0 is the MSB (leftmost) within each byte 

94 bit_in_word = ((3 - byte_in_word) * 8) + (7 - bit_in_byte) 

95 

96 return (self._data[word_index] >> bit_in_word) & 1 

97 

98 def set_bit(self, pos: int, value: int) -> None: 

99 """ 

100 Set bit value at position. 

101 

102 Args: 

103 pos: Bit position (0 = MSB, length-1 = LSB) 

104 value: Bit value (0 or non-zero for 1) 

105 

106 Raises: 

107 IndexError: If pos is out of range 

108 """ 

109 if pos < 0 or pos >= self.length: 

110 raise IndexError(f"Bit position {pos} out of range [0, {self.length})") 

111 

112 # Calculate byte and bit within byte 

113 byte_index = pos // 8 

114 bit_in_byte = pos % 8 

115 

116 # Calculate word index and byte position within word 

117 word_index = byte_index // 4 

118 byte_in_word = byte_index % 4 

119 

120 # Big-endian: byte 0 is at bits 24-31, byte 1 at 16-23, etc. 

121 # MSB-first indexing: bit 0 is the MSB (leftmost) within each byte 

122 bit_in_word = ((3 - byte_in_word) * 8) + (7 - bit_in_byte) 

123 

124 if value: 

125 # Set bit to 1 

126 self._data[word_index] |= 1 << bit_in_word 

127 else: 

128 # Clear bit to 0 

129 self._data[word_index] &= ~(1 << bit_in_word) 

130 

131 def from_bytes(self, data: bytes) -> None: 

132 """ 

133 Load bit vector from bytes. 

134 

135 Args: 

136 data: Bytes to load (big-endian) 

137 """ 

138 # Zero the array first 

139 self.zero() 

140 

141 # Pack bytes into 32-bit words (big-endian) 

142 j = 4 # Counter for bytes within word (4, 3, 2, 1) 

143 bytes_to_int = 0 

144 current_word = 0 

145 

146 for byte in data: 

147 j -= 1 

148 bytes_to_int |= byte << (j * 8) 

149 

150 if j == 0: 

151 # Word complete - store it 

152 self._data[current_word] = bytes_to_int 

153 current_word += 1 

154 bytes_to_int = 0 

155 j = 4 

156 

157 # Handle incomplete final word 

158 if j < 4: 

159 self._data[current_word] = bytes_to_int 

160 

161 def to_bytes(self) -> bytes: 

162 """ 

163 Convert bit vector to bytes. 

164 

165 Returns: 

166 Bytes representation (big-endian) 

167 """ 

168 expected_bytes = (self.length + 7) // 8 

169 result = bytearray(expected_bytes) 

170 

171 byte_index = 0 

172 for word_index in range(self.num_words): 

173 word = self._data[word_index] 

174 

175 # Extract up to 4 bytes from this word 

176 for j in range(3, -1, -1): 

177 if byte_index >= expected_bytes: 

178 break 

179 result[byte_index] = (word >> (j * 8)) & 0xFF 

180 byte_index += 1 

181 

182 return bytes(result) 

183 

184 def xor(self, a: "BitVector", b: "BitVector | None" = None) -> "BitVector": 

185 """ 

186 Compute XOR of bit vectors. 

187 

188 Two calling conventions: 

189 - result = v.xor(other) - returns new vector with v XOR other 

190 - v.xor(a, b) - stores a XOR b into v (in-place) 

191 

192 Args: 

193 a: First operand (or only operand if b is None) 

194 b: Second operand (optional) 

195 

196 Returns: 

197 New BitVector if b is None, else self 

198 """ 

199 if b is None: 

200 # Old API: return self XOR a 

201 result = BitVector(self.length) 

202 num_words = min(self.num_words, a.num_words) 

203 for i in range(num_words): 

204 result._data[i] = self._data[i] ^ a._data[i] 

205 return result 

206 else: 

207 # New API: self = a XOR b 

208 num_words = min(self.num_words, a.num_words, b.num_words) 

209 for i in range(num_words): 

210 self._data[i] = a._data[i] ^ b._data[i] 

211 return self 

212 

213 def or_(self, a: "BitVector", b: "BitVector | None" = None) -> "BitVector": 

214 """ 

215 Compute OR of bit vectors. 

216 

217 Two calling conventions: 

218 - result = v.or_(other) - returns new vector with v OR other 

219 - v.or_(a, b) - stores a OR b into v (in-place) 

220 

221 Args: 

222 a: First operand (or only operand if b is None) 

223 b: Second operand (optional) 

224 

225 Returns: 

226 New BitVector if b is None, else self 

227 """ 

228 if b is None: 

229 # Old API: return self OR a 

230 result = BitVector(self.length) 

231 num_words = min(self.num_words, a.num_words) 

232 for i in range(num_words): 

233 result._data[i] = self._data[i] | a._data[i] 

234 return result 

235 else: 

236 # New API: self = a OR b 

237 num_words = min(self.num_words, a.num_words, b.num_words) 

238 for i in range(num_words): 

239 self._data[i] = a._data[i] | b._data[i] 

240 return self 

241 

242 def and_(self, other: "BitVector") -> "BitVector": 

243 """ 

244 Compute AND with another bit vector. 

245 

246 Args: 

247 other: Other bit vector 

248 

249 Returns: 

250 New BitVector with result 

251 """ 

252 result = BitVector(self.length) 

253 num_words = min(self.num_words, other.num_words) 

254 

255 for i in range(num_words): 

256 result._data[i] = self._data[i] & other._data[i] 

257 

258 return result 

259 

260 def not_(self) -> "BitVector": 

261 """ 

262 Compute NOT (bitwise inversion). 

263 

264 Returns: 

265 New BitVector with inverted bits 

266 """ 

267 result = BitVector(self.length) 

268 

269 for i in range(self.num_words): 

270 result._data[i] = ~self._data[i] & 0xFFFFFFFF 

271 

272 # Mask off unused bits in last word 

273 if self.num_words > 0: 

274 num_bytes = (self.length + 7) // 8 

275 bytes_in_last_word = ((num_bytes - 1) % 4) + 1 

276 bits_in_last_byte = self.length - ((num_bytes - 1) * 8) 

277 

278 # Create mask for valid bits in big-endian word. 

279 # MSB-first: valid bits in a partial last byte are at the HIGH 

280 # end of the byte, so mask 0xFF << (8 - bits) (GOTCHAS #19). 

281 mask = 0 

282 for byte in range(bytes_in_last_word): 

283 if byte == bytes_in_last_word - 1: 

284 byte_mask = (0xFF << (8 - bits_in_last_byte)) & 0xFF 

285 else: 

286 byte_mask = 0xFF 

287 shift_amt = (3 - byte) * 8 

288 mask |= byte_mask << shift_amt 

289 

290 result._data[self.num_words - 1] &= mask 

291 

292 return result 

293 

294 def left_shift(self) -> "BitVector": 

295 """ 

296 Compute left shift by 1 position. 

297 

298 Left shift moves bits toward MSB (lower indices). 

299 MSB is lost, LSB becomes 0. 

300 

301 Returns: 

302 New BitVector with shifted bits 

303 """ 

304 result = BitVector(self.length) 

305 

306 # Word-level left shift (big-endian: MSB in high bits of first word) 

307 # Shift each word left by 1, carry MSB from next word 

308 carry = 0 

309 for i in range(self.num_words - 1, -1, -1): 

310 word = self._data[i] 

311 result._data[i] = ((word << 1) | carry) & 0xFFFFFFFF 

312 carry = (word >> 31) & 1 

313 

314 return result 

315 

316 def hamming_weight(self) -> int: 

317 """ 

318 Count number of 1 bits. 

319 

320 Returns: 

321 Number of bits set to 1 

322 """ 

323 count = 0 

324 for word in self._data: 

325 # Fast popcount using bit manipulation 

326 n = word 

327 while n: 

328 count += n & 1 

329 n >>= 1 

330 return count 

331 

332 def equals(self, other: "BitVector") -> bool: 

333 """ 

334 Check equality with another bit vector. 

335 

336 Args: 

337 other: Other bit vector 

338 

339 Returns: 

340 True if equal, False otherwise 

341 """ 

342 if self.length != other.length: 

343 return False 

344 

345 for i in range(self.num_words): 

346 if self._data[i] != other._data[i]: 

347 return False 

348 

349 return True