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

132 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2024-09-07 20:51 +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 

10# Standard libraries 

11from abc import ABC, abstractmethod 

12from typing import Optional, Union 

13 

14 

15def from_unsigned_binary( 

16 num_bits: int, 

17 value: int, 

18 num_integer_bits: Optional[int] = None, 

19 num_fractional_bits: int = 0, 

20 is_signed: bool = False, 

21) -> Union[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: Union[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: Union[int, float], 

69 num_integer_bits: Optional[int] = 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 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 This class 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 @abstractmethod 

130 def is_signed(self) -> bool: 

131 """ 

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

133 """ 

134 

135 @property 

136 @abstractmethod 

137 def min_value(self) -> Union[int, float]: 

138 """ 

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

140 

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

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

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

144 """ 

145 

146 @property 

147 @abstractmethod 

148 def max_value(self) -> Union[int, float]: 

149 """ 

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

151 

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

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

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

155 """ 

156 

157 @abstractmethod 

158 def convert_from_unsigned_binary(self, unsigned_binary: int) -> Union[int, float]: 

159 """ 

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

161 into the native value of the field. 

162 

163 Arguments: 

164 unsigned_binary: Unsigned binary integer representation of the field. 

165 """ 

166 

167 @abstractmethod 

168 def convert_to_unsigned_binary(self, value: Union[int, float]) -> int: 

169 """ 

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

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

172 

173 Arguments: 

174 value: Native Python representation of the field value. 

175 

176 Return: 

177 Unsigned binary integer representation of the field. 

178 """ 

179 

180 @abstractmethod 

181 def __repr__(self) -> str: 

182 pass 

183 

184 def _check_native_value_in_range(self, value: Union[int, float]) -> None: 

185 """ 

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

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

188 

189 Arguments: 

190 value: Native Python representation of the field value. 

191 """ 

192 min_ = self.min_value 

193 max_ = self.max_value 

194 if not min_ <= value <= max_: 

195 raise ValueError( 

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

197 ) 

198 

199 def _check_unsigned_binary_value_in_range(self, value: Union[int, float]) -> None: 

200 """ 

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

202 

203 Arguments: 

204 value: Unsigned binary representation of the field value. 

205 """ 

206 max_ = 2**self.bit_width - 1 

207 if not 0 <= value <= max_: 

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

209 

210 

211class Unsigned(NumericalInterpretation): 

212 """ 

213 Unsigned integer. 

214 """ 

215 

216 is_signed: bool = False 

217 

218 def __init__(self, bit_width: int): 

219 self.bit_width = bit_width 

220 

221 @property 

222 def min_value(self) -> int: 

223 return 0 

224 

225 @property 

226 def max_value(self) -> int: 

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

228 return result 

229 

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

231 self._check_unsigned_binary_value_in_range(unsigned_binary) 

232 return unsigned_binary 

233 

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

235 self._check_native_value_in_range(value) 

236 return round(value) 

237 

238 def __repr__(self) -> str: 

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

240bit_width={self.bit_width},\ 

241)""" 

242 

243 

244class Signed(NumericalInterpretation): 

245 """ 

246 Two's complement signed integer format. 

247 """ 

248 

249 is_signed: bool = True 

250 

251 def __init__(self, bit_width: int): 

252 self.bit_width = bit_width 

253 

254 @property 

255 def min_value(self) -> int: 

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

257 return result 

258 

259 @property 

260 def max_value(self) -> int: 

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

262 return result 

263 

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

265 self._check_unsigned_binary_value_in_range(unsigned_binary) 

266 return int( 

267 from_unsigned_binary( 

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

269 ) 

270 ) 

271 

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

273 self._check_native_value_in_range(value) 

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

275 

276 def __repr__(self) -> str: 

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

278bit_width={self.bit_width},\ 

279)""" 

280 

281 

282class Fixed(NumericalInterpretation, ABC): 

283 def __init__(self, is_signed: bool, max_bit_index: int, min_bit_index: int): 

284 """ 

285 Abstract baseclass for fixed-point fields. 

286 

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

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

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

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

291 fractional portion. 

292 

293 Arguments: 

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

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

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

297 """ 

298 if max_bit_index < min_bit_index: 

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

300 

301 self.max_bit_index = max_bit_index 

302 self.min_bit_index = min_bit_index 

303 

304 self.integer_bit_width = self.max_bit_index + 1 

305 self.fraction_bit_width = -self.min_bit_index 

306 # The total width. 

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

308 self.bit_width = self.integer_bit_width + self.fraction_bit_width 

309 

310 self._is_signed = is_signed 

311 self._integer = ( 

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

313 ) 

314 

315 @property 

316 def is_signed(self) -> bool: 

317 return self._is_signed 

318 

319 @property 

320 def min_value(self) -> float: 

321 min_integer_value = self._integer.min_value 

322 min_integer_binary = self._integer.convert_to_unsigned_binary(min_integer_value) 

323 return self.convert_from_unsigned_binary(min_integer_binary) 

324 

325 @property 

326 def max_value(self) -> float: 

327 max_integer_value = self._integer.max_value 

328 max_integer_binary = self._integer.convert_to_unsigned_binary(max_integer_value) 

329 return self.convert_from_unsigned_binary(max_integer_binary) 

330 

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

332 self._check_unsigned_binary_value_in_range(unsigned_binary) 

333 return from_unsigned_binary( 

334 num_bits=self.bit_width, 

335 value=unsigned_binary, 

336 num_integer_bits=self.integer_bit_width, 

337 num_fractional_bits=self.fraction_bit_width, 

338 is_signed=self.is_signed, 

339 ) 

340 

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

342 self._check_native_value_in_range(value) 

343 return to_unsigned_binary( 

344 num_bits=self.bit_width, 

345 value=value, 

346 num_integer_bits=self.integer_bit_width, 

347 num_fractional_bits=self.fraction_bit_width, 

348 is_signed=self.is_signed, 

349 ) 

350 

351 def __repr__(self) -> str: 

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

353max_bit_index={self.max_bit_index},\ 

354min_bit_index={self.min_bit_index},\ 

355)""" 

356 

357 

358class UnsignedFixedPoint(Fixed): 

359 def __init__(self, max_bit_index: int, min_bit_index: int): 

360 """ 

361 Unsigned fixed point format to represent fractional values. 

362 

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

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

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

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

367 fractional portion. 

368 

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

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

371 

372 Arguments: 

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

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

375 """ 

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

377 

378 @classmethod 

379 def from_bit_widths( 

380 cls, integer_bit_width: int, fraction_bit_width: int 

381 ) -> "UnsignedFixedPoint": 

382 """ 

383 Create instance via the respective fixed point bit widths. 

384 

385 Arguments: 

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

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

388 field value. 

389 """ 

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

391 

392 

393class SignedFixedPoint(Fixed): 

394 def __init__(self, max_bit_index: int, min_bit_index: int): 

395 """ 

396 Signed fixed point format to represent fractional values. 

397 Signed integer uses two's complement representation. 

398 

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

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

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

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

403 fractional portion. 

404 

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

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

407 

408 Arguments: 

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

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

411 """ 

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

413 

414 @classmethod 

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

416 """ 

417 Create instance via the respective fixed point bit widths. 

418 

419 Arguments: 

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

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

422 field value. 

423 """ 

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