Coverage for hdl_registers/field/numerical_interpretation.py: 96%

137 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-04-29 06:41 +0000

1# -------------------------------------------------------------------------------------------------- 

2# Copyright (c) Lukas Vik. All rights reserved. 

3# 

4# This file is part of the hdl-registers project, an HDL register generator fast enough to run 

5# in real time. 

6# https://hdl-registers.com 

7# https://github.com/hdl-registers/hdl-registers 

8# -------------------------------------------------------------------------------------------------- 

9 

10from __future__ import annotations 

11 

12from abc import ABC, abstractmethod 

13 

14 

15def from_unsigned_binary( 

16 num_bits: int, 

17 value: int, 

18 num_integer_bits: int | None = None, 

19 num_fractional_bits: int = 0, 

20 is_signed: bool = False, 

21) -> int | float: 

22 """ 

23 Convert from a fixed-point unsigned binary value to one of 

24 

25 * unsigned integer (Python ``int``) 

26 * signed integer (Python ``int``) 

27 * unsigned floating-point (Python ``float``) 

28 * signed floating-point (Python ``float``) 

29 

30 Signed types use two's complement representation for the binary value. 

31 Sources: 

32 

33 * https://en.wikibooks.org/wiki/Floating_Point/Fixed-Point_Numbers 

34 * https://vhdlguru.blogspot.com/2010/03/fixed-point-operations-in-vhdl-tutorial.html 

35 

36 Arguments: 

37 num_bits: Width of the field. 

38 value: Unsigned binary integer representation of the value. 

39 num_integer_bits: The number of integer bits in the fixed-point ``value``. 

40 Might be negative if this is a fixed-point word with only fractional bits. 

41 num_fractional_bits: The number of fractional bits in the fixed-point ``value``. 

42 Might be negative if this is a fixed-point word with only integer bits. 

43 is_signed: If ``True``, the MSB of the ``value`` will be treated as a sign bit. 

44 Enables the handling of negative result values. 

45 

46 Return: 

47 Native Python representation of the ``value``. 

48 Will be a ``float`` if ``num_fractional_bits`` is non-zero, otherwise it will be an ``int``. 

49 """ 

50 num_integer_bits = num_bits if num_integer_bits is None else num_integer_bits 

51 

52 if num_integer_bits + num_fractional_bits != num_bits: 

53 raise ValueError("Inconsistent bit width") 

54 

55 result: int | float = value * 2**-num_fractional_bits 

56 

57 if is_signed: 

58 sign_bit = value & (1 << (num_bits - 1)) 

59 if sign_bit != 0: 

60 # If sign bit is set, compute negative value. 

61 result -= 2**num_integer_bits 

62 

63 return result 

64 

65 

66def to_unsigned_binary( 

67 num_bits: int, 

68 value: float, 

69 num_integer_bits: int | None = None, 

70 num_fractional_bits: int = 0, 

71 is_signed: bool = False, 

72) -> int: 

73 """ 

74 Convert from one of 

75 

76 * unsigned integer (Python ``int``) 

77 * signed integer (Python ``int``) 

78 * unsigned floating-point (Python ``float``) 

79 * signed floating-point (Python ``float``) 

80 

81 into an unsigned binary. 

82 Signed types use two's complement representation for the binary value. 

83 Sources: 

84 

85 * https://en.wikibooks.org/wiki/Floating_Point/Fixed-Point_Numbers 

86 * https://vhdlguru.blogspot.com/2010/03/fixed-point-operations-in-vhdl-tutorial.html 

87 

88 Arguments: 

89 num_bits: Width of the field. 

90 value: Native numeric Python representation of the value. 

91 If ``num_fractional_bits`` is non-zero the value is expected to be a ``float``, 

92 otherwise an ``int`` is expected. 

93 num_integer_bits: The number of integer bits in the target fixed-point word. 

94 Might be negative if this is a fixed-point word with only fractional bits. 

95 num_fractional_bits: The number of fractional bits in the target fixed-point word. 

96 Might be negative if this is a fixed-point word with only integer bits. 

97 is_signed: Enables the handling of negative input ``value``. 

98 If ``True``, the MSB of the result word will be treated as a sign bit. 

99 

100 Return: 

101 Unsigned binary integer representation of the value. 

102 Potentially rounded, if the input ``value`` is a floating-point number. 

103 """ 

104 num_integer_bits = num_bits if num_integer_bits is None else num_integer_bits 

105 

106 if num_integer_bits + num_fractional_bits != num_bits: 

107 raise ValueError("Inconsistent bit width") 

108 

109 binary_value: int = round(value * 2**num_fractional_bits) 

110 if binary_value < 0: 

111 if is_signed: 

112 binary_value += 1 << num_bits 

113 else: 

114 raise ValueError("Attempting to convert negative value to unsigned") 

115 

116 return binary_value 

117 

118 

119class NumericalInterpretation(ABC): 

120 """ 

121 Represents different modes used when interpreting a field of bits as a numeric value. 

122 Contains metadata, helper methods, etc. 

123 """ 

124 

125 # Width of the field in number of bits. 

126 bit_width: int 

127 

128 @property 

129 def name(self) -> str: 

130 """ 

131 Short name that describes the numerical interpretation. 

132 E.g. "Unsigned". 

133 We might add bit widths to this in the future. 

134 """ 

135 return self.__class__.__name__ 

136 

137 @property 

138 @abstractmethod 

139 def is_signed(self) -> bool: 

140 """ 

141 Is the field signed (two's complement)? 

142 """ 

143 

144 @property 

145 @abstractmethod 

146 def min_value(self) -> int | float: 

147 """ 

148 Minimum representable value for this field, in its native numeric representation. 

149 

150 Return type is the native Python representation of the value, which depends on the subclass. 

151 If the subclass has a non-zero number of fractional bits, the value will be a ``float``. 

152 If not, it will be an ``int``. 

153 """ 

154 

155 @property 

156 @abstractmethod 

157 def max_value(self) -> int | float: 

158 """ 

159 Maximum representable value for this field, in its native numeric representation. 

160 

161 Return type is the native Python representation of the value, which depends on the subclass. 

162 If the subclass has a non-zero number of fractional bits, the value will be a ``float``. 

163 If not, it will be an ``int``. 

164 """ 

165 

166 @abstractmethod 

167 def convert_from_unsigned_binary(self, unsigned_binary: int) -> int | float: 

168 """ 

169 Convert from the unsigned binary integer representation of a field, 

170 into the native value of the field. 

171 

172 Arguments: 

173 unsigned_binary: Unsigned binary integer representation of the field. 

174 """ 

175 

176 @abstractmethod 

177 def convert_to_unsigned_binary(self, value: float) -> int: 

178 """ 

179 Convert from the native value of the field, into the 

180 unsigned binary integer representation which can be written to a register. 

181 

182 Arguments: 

183 value: Native Python representation of the field value. 

184 

185 Return: 

186 Unsigned binary integer representation of the field. 

187 """ 

188 

189 @abstractmethod 

190 def __repr__(self) -> str: 

191 pass 

192 

193 def _check_native_value_in_range(self, value: float) -> None: 

194 """ 

195 Raise an exception if the given field value is not within the allowed range. 

196 Note that this is the native field value, not the raw binary value. 

197 

198 Arguments: 

199 value: Native Python representation of the field value. 

200 """ 

201 if isinstance(value, float) and (not value.is_integer()) and (not isinstance(self, Fixed)): 

202 raise ValueError( 

203 f"Fractional value passed to non-fractional numerical type. Got: {value}." 

204 ) 

205 

206 min_ = self.min_value 

207 max_ = self.max_value 

208 if not min_ <= value <= max_: 

209 raise ValueError( 

210 f"Value: {value} out of range of {self.bit_width}-bit ({min_}, {max_})." 

211 ) 

212 

213 def _check_unsigned_binary_value_in_range(self, value: float) -> None: 

214 """ 

215 Raise an exception if the given unsigned binary value does not fit in the field. 

216 

217 Arguments: 

218 value: Unsigned binary representation of the field value. 

219 """ 

220 max_ = 2**self.bit_width - 1 

221 if not 0 <= value <= max_: 

222 raise ValueError(f"Value: {value} out of range of {self.bit_width}-bit (0, {max_}).") 

223 

224 

225class Unsigned(NumericalInterpretation): 

226 """ 

227 Unsigned integer. 

228 """ 

229 

230 is_signed: bool = False 

231 

232 def __init__(self, bit_width: int) -> None: 

233 self.bit_width = bit_width 

234 

235 @property 

236 def min_value(self) -> int: 

237 return 0 

238 

239 @property 

240 def max_value(self) -> int: 

241 result: int = 2**self.bit_width - 1 

242 return result 

243 

244 def convert_from_unsigned_binary(self, unsigned_binary: int) -> int: 

245 self._check_unsigned_binary_value_in_range(unsigned_binary) 

246 return unsigned_binary 

247 

248 def convert_to_unsigned_binary(self, value: float) -> int: 

249 """ 

250 Note that the argument is of type ``float`` (which according to Python typing 

251 means that either ``int`` or ``float`` values can be passed). 

252 This is to keep the same API as the others. 

253 However since this numerical interpretation has no fractional bits, the value provided 

254 must be an integer. 

255 """ 

256 self._check_native_value_in_range(value) 

257 return int(value) 

258 

259 def __repr__(self) -> str: 

260 return f"""{self.__class__.__name__}(\ 

261bit_width={self.bit_width},\ 

262)""" 

263 

264 

265class Signed(NumericalInterpretation): 

266 """ 

267 Two's complement signed integer format. 

268 """ 

269 

270 is_signed: bool = True 

271 

272 def __init__(self, bit_width: int) -> None: 

273 self.bit_width = bit_width 

274 

275 @property 

276 def min_value(self) -> int: 

277 result: int = -(2 ** (self.bit_width - 1)) 

278 return result 

279 

280 @property 

281 def max_value(self) -> int: 

282 result: int = 2 ** (self.bit_width - 1) - 1 

283 return result 

284 

285 def convert_from_unsigned_binary(self, unsigned_binary: int) -> int: 

286 self._check_unsigned_binary_value_in_range(unsigned_binary) 

287 return int( 

288 from_unsigned_binary( 

289 num_bits=self.bit_width, value=unsigned_binary, is_signed=self.is_signed 

290 ) 

291 ) 

292 

293 def convert_to_unsigned_binary(self, value: float) -> int: 

294 """ 

295 Note that the argument is of type ``float`` (which according to Python typing 

296 means that either ``int`` or ``float`` values can be passed). 

297 This is to keep the same API as the others. 

298 However since this numerical interpretation has no fractional bits, the value provided 

299 must be an integer. 

300 """ 

301 self._check_native_value_in_range(value) 

302 return to_unsigned_binary(num_bits=self.bit_width, value=value, is_signed=self.is_signed) 

303 

304 def __repr__(self) -> str: 

305 return f"""{self.__class__.__name__}(\ 

306bit_width={self.bit_width},\ 

307)""" 

308 

309 

310class Fixed(NumericalInterpretation, ABC): 

311 def __init__(self, is_signed: bool, max_bit_index: int, min_bit_index: int) -> None: 

312 """ 

313 Abstract baseclass for fixed-point fields. 

314 

315 The bit_index arguments indicates the position of the decimal point in 

316 relation to the number expressed by the field. The decimal point is 

317 between the "0" and "-1" bit index. If the bit_index argument is 

318 negative, then the number represented by the field will include a 

319 fractional portion. 

320 

321 Arguments: 

322 is_signed: Is the field signed (two's complement)? 

323 max_bit_index: Position of the upper bit relative to the decimal point. 

324 min_bit_index: Position of the lower bit relative to the decimal point. 

325 """ 

326 if max_bit_index < min_bit_index: 

327 raise ValueError("max_bit_index must be >= min_bit_index") 

328 

329 self.max_bit_index = max_bit_index 

330 self.min_bit_index = min_bit_index 

331 

332 self.integer_bit_width = self.max_bit_index + 1 

333 self.fraction_bit_width = -self.min_bit_index 

334 # The total width. 

335 # Note that this the same property name as the 'Unsigned' and 'Signed' classes. 

336 self.bit_width = self.integer_bit_width + self.fraction_bit_width 

337 

338 self._is_signed = is_signed 

339 self._integer = ( 

340 Signed(bit_width=self.bit_width) if is_signed else Unsigned(bit_width=self.bit_width) 

341 ) 

342 

343 @property 

344 def is_signed(self) -> bool: 

345 return self._is_signed 

346 

347 @property 

348 def min_value(self) -> float: 

349 min_integer_value = self._integer.min_value 

350 min_integer_binary = self._integer.convert_to_unsigned_binary(min_integer_value) 

351 return self.convert_from_unsigned_binary(min_integer_binary) 

352 

353 @property 

354 def max_value(self) -> float: 

355 max_integer_value = self._integer.max_value 

356 max_integer_binary = self._integer.convert_to_unsigned_binary(max_integer_value) 

357 return self.convert_from_unsigned_binary(max_integer_binary) 

358 

359 def convert_from_unsigned_binary(self, unsigned_binary: int) -> float: 

360 self._check_unsigned_binary_value_in_range(unsigned_binary) 

361 return from_unsigned_binary( 

362 num_bits=self.bit_width, 

363 value=unsigned_binary, 

364 num_integer_bits=self.integer_bit_width, 

365 num_fractional_bits=self.fraction_bit_width, 

366 is_signed=self.is_signed, 

367 ) 

368 

369 def convert_to_unsigned_binary(self, value: float) -> int: 

370 self._check_native_value_in_range(value) 

371 return to_unsigned_binary( 

372 num_bits=self.bit_width, 

373 value=value, 

374 num_integer_bits=self.integer_bit_width, 

375 num_fractional_bits=self.fraction_bit_width, 

376 is_signed=self.is_signed, 

377 ) 

378 

379 def __repr__(self) -> str: 

380 return f"""{self.__class__.__name__}(\ 

381max_bit_index={self.max_bit_index},\ 

382min_bit_index={self.min_bit_index},\ 

383)""" 

384 

385 

386class UnsignedFixedPoint(Fixed): 

387 def __init__(self, max_bit_index: int, min_bit_index: int) -> None: 

388 """ 

389 Unsigned fixed point format to represent fractional values. 

390 

391 The bit_index arguments indicates the position of the decimal point in 

392 relation to the number expressed by the field. The decimal point is 

393 between the "0" and "-1" bit index. If the bit_index argument is 

394 negative, then the number represented by the field will include a 

395 fractional portion. 

396 

397 e.g. ufixed (4 downto -5) specifies unsigned fixed point, 10 bits wide, 

398 with 5 bits of decimal. Consequently y = 6.5 = "00110.10000". 

399 

400 Arguments: 

401 max_bit_index: Position of the upper bit relative to the decimal point. 

402 min_bit_index: Position of the lower bit relative to the decimal point. 

403 """ 

404 super().__init__(is_signed=False, max_bit_index=max_bit_index, min_bit_index=min_bit_index) 

405 

406 @classmethod 

407 def from_bit_widths(cls, integer_bit_width: int, fraction_bit_width: int) -> UnsignedFixedPoint: 

408 """ 

409 Create instance via the respective fixed point bit widths. 

410 

411 Arguments: 

412 integer_bit_width: The number of bits assigned to the integer part of the field value. 

413 fraction_bit_width: The number of bits assigned to the fractional part of the 

414 field value. 

415 """ 

416 return cls(max_bit_index=integer_bit_width - 1, min_bit_index=-fraction_bit_width) 

417 

418 

419class SignedFixedPoint(Fixed): 

420 def __init__(self, max_bit_index: int, min_bit_index: int) -> None: 

421 """ 

422 Signed fixed point format to represent fractional values. 

423 Signed integer uses two's complement representation. 

424 

425 The bit_index arguments indicates the position of the decimal point in 

426 relation to the number expressed by the field. The decimal point is 

427 between the "0" and "-1" bit index. If the bit_index argument is 

428 negative, then the number represented by the field will include a 

429 fractional portion. 

430 

431 e.g. sfixed (4 downto -5) specifies signed fixed point, 10 bits wide, 

432 with 5 bits of decimal. Consequently y = -6.5 = "11001.10000". 

433 

434 Arguments: 

435 max_bit_index: Position of the upper bit relative to the decimal point. 

436 min_bit_index: Position of the lower bit relative to the decimal point. 

437 """ 

438 super().__init__(is_signed=True, max_bit_index=max_bit_index, min_bit_index=min_bit_index) 

439 

440 @classmethod 

441 def from_bit_widths(cls, integer_bit_width: int, fraction_bit_width: int) -> SignedFixedPoint: 

442 """ 

443 Create instance via the respective fixed point bit widths. 

444 

445 Arguments: 

446 integer_bit_width: The number of bits assigned to the integer part of the field value. 

447 fraction_bit_width: The number of bits assigned to the fractional part of the 

448 field value. 

449 """ 

450 return cls(max_bit_index=integer_bit_width - 1, min_bit_index=-fraction_bit_width)