Coverage for hdl_registers/field/numerical_interpretation.py: 96%
132 statements
« prev ^ index » next coverage.py v7.6.8, created at 2024-12-01 20:50 +0000
« prev ^ index » next coverage.py v7.6.8, created at 2024-12-01 20:50 +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# --------------------------------------------------------------------------------------------------
10# Standard libraries
11from abc import ABC, abstractmethod
12from typing import Optional, Union
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
25 * unsigned integer (Python ``int``)
26 * signed integer (Python ``int``)
27 * unsigned floating-point (Python ``float``)
28 * signed floating-point (Python ``float``)
30 Signed types use two's complement representation for the binary value.
31 Sources:
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
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.
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
52 if num_integer_bits + num_fractional_bits != num_bits:
53 raise ValueError("Inconsistent bit width")
55 result: Union[int, float] = value * 2**-num_fractional_bits
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
63 return result
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
76 * unsigned integer (Python ``int``)
77 * signed integer (Python ``int``)
78 * unsigned floating-point (Python ``float``)
79 * signed floating-point (Python ``float``)
81 into an unsigned binary.
82 Signed types use two's complement representation for the binary value.
83 Sources:
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
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.
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
106 if num_integer_bits + num_fractional_bits != num_bits:
107 raise ValueError("Inconsistent bit width")
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")
116 return binary_value
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 """
125 # Width of the field in number of bits.
126 bit_width: int
128 @property
129 @abstractmethod
130 def is_signed(self) -> bool:
131 """
132 Is the field signed (two's complement)?
133 """
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.
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 """
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.
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 """
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.
163 Arguments:
164 unsigned_binary: Unsigned binary integer representation of the field.
165 """
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.
173 Arguments:
174 value: Native Python representation of the field value.
176 Return:
177 Unsigned binary integer representation of the field.
178 """
180 @abstractmethod
181 def __repr__(self) -> str:
182 pass
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.
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 )
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.
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_}).")
211class Unsigned(NumericalInterpretation):
212 """
213 Unsigned integer.
214 """
216 is_signed: bool = False
218 def __init__(self, bit_width: int):
219 self.bit_width = bit_width
221 @property
222 def min_value(self) -> int:
223 return 0
225 @property
226 def max_value(self) -> int:
227 result: int = 2**self.bit_width - 1
228 return result
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
234 def convert_to_unsigned_binary(self, value: float) -> int:
235 self._check_native_value_in_range(value)
236 return round(value)
238 def __repr__(self) -> str:
239 return f"""{self.__class__.__name__}(\
240bit_width={self.bit_width},\
241)"""
244class Signed(NumericalInterpretation):
245 """
246 Two's complement signed integer format.
247 """
249 is_signed: bool = True
251 def __init__(self, bit_width: int):
252 self.bit_width = bit_width
254 @property
255 def min_value(self) -> int:
256 result: int = -(2 ** (self.bit_width - 1))
257 return result
259 @property
260 def max_value(self) -> int:
261 result: int = 2 ** (self.bit_width - 1) - 1
262 return result
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 )
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)
276 def __repr__(self) -> str:
277 return f"""{self.__class__.__name__}(\
278bit_width={self.bit_width},\
279)"""
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.
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.
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")
301 self.max_bit_index = max_bit_index
302 self.min_bit_index = min_bit_index
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
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 )
315 @property
316 def is_signed(self) -> bool:
317 return self._is_signed
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)
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)
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 )
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 )
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)"""
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.
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.
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".
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)
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.
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)
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.
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.
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".
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)
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.
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)