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
« 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# --------------------------------------------------------------------------------------------------
10from __future__ import annotations
12from abc import ABC, abstractmethod
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
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: 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: 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
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 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")
116 return binary_value
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 """
125 # Width of the field in number of bits.
126 bit_width: int
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__
137 @property
138 @abstractmethod
139 def is_signed(self) -> bool:
140 """
141 Is the field signed (two's complement)?
142 """
144 @property
145 @abstractmethod
146 def min_value(self) -> int | float:
147 """
148 Minimum representable value for this field, in its native numeric representation.
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 """
155 @property
156 @abstractmethod
157 def max_value(self) -> int | float:
158 """
159 Maximum representable value for this field, in its native numeric representation.
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 """
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.
172 Arguments:
173 unsigned_binary: Unsigned binary integer representation of the field.
174 """
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.
182 Arguments:
183 value: Native Python representation of the field value.
185 Return:
186 Unsigned binary integer representation of the field.
187 """
189 @abstractmethod
190 def __repr__(self) -> str:
191 pass
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.
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 )
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 )
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.
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_}).")
225class Unsigned(NumericalInterpretation):
226 """
227 Unsigned integer.
228 """
230 is_signed: bool = False
232 def __init__(self, bit_width: int) -> None:
233 self.bit_width = bit_width
235 @property
236 def min_value(self) -> int:
237 return 0
239 @property
240 def max_value(self) -> int:
241 result: int = 2**self.bit_width - 1
242 return result
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
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)
259 def __repr__(self) -> str:
260 return f"""{self.__class__.__name__}(\
261bit_width={self.bit_width},\
262)"""
265class Signed(NumericalInterpretation):
266 """
267 Two's complement signed integer format.
268 """
270 is_signed: bool = True
272 def __init__(self, bit_width: int) -> None:
273 self.bit_width = bit_width
275 @property
276 def min_value(self) -> int:
277 result: int = -(2 ** (self.bit_width - 1))
278 return result
280 @property
281 def max_value(self) -> int:
282 result: int = 2 ** (self.bit_width - 1) - 1
283 return result
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 )
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)
304 def __repr__(self) -> str:
305 return f"""{self.__class__.__name__}(\
306bit_width={self.bit_width},\
307)"""
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.
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.
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")
329 self.max_bit_index = max_bit_index
330 self.min_bit_index = min_bit_index
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
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 )
343 @property
344 def is_signed(self) -> bool:
345 return self._is_signed
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)
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)
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 )
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 )
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)"""
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.
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.
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".
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)
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.
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)
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.
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.
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".
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)
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.
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)