Bit Resizing

Attention

The code examples on this page change betwee the function version and the method version of the resizing operation. For example, y1, and y2 in the following example are equivalent, and x remains unchanged:

>>> (x := FixedPoint(-1.5)).qformat
'Q2.1'
>>> y1 = FixedPoint(x)
>>> y1.resize(10, 10)

>>> y2 = resize(x, 10, 10)

>>> print(f'{x.qformat=}\n{y1.qformat=}\n{y2.qformat=}')
x.qformat='Q2.1'
y1.qformat='Q10.10'
y2.qformat='Q10.10'

The function will operate on a copy of the given object, so the original object is not modified.

resize

Using FixedPoint.resize() or resize(), the fractional and integer bit width can grow or shrink. The bit widths are modified based on the position of the binary point, so as long as overflow or rounding does not occur, the value does not change.

>>> from fixedpoint import FixedPoint
>>> def show(A):
...     print(f"{A: <+5.2f}  ", end="")       # float
...     print(f"{A:>5q}  ", end="")           # Q format
...     print(f"{A:0{A.m+(A.m-1)//4}_bm}." if A.m else ".", end="")
...     print(f"{A:0{A.n}_bn}" if A.n else "") # Show binary point

>>> neg_signed   = FixedPoint(-2, 1, 4, 2)
>>> pos_signed   = FixedPoint(+2, 1, 4, 2)
>>> pos_unsigned = FixedPoint(+2, 0, 4, 2)

>>> show(neg_signed)
-2.00   Q4.2  1110.00

>>> show(pos_signed)
+2.00   Q4.2  0010.00

>>> show(pos_unsigned)
+2.00  UQ4.2  0010.00

Increasing the integer bit width of a negative FixedPoint will sign-extend:

>>> x = resize(neg_signed, 7, 2)
>>> show(x)
-2.00   Q7.2  111_1110.00

Increasing the integer bit width of a positive FixedPoint (sign or unsigned) will pad with zeros:

>>> x = resize(pos_signed, 7, 2)
>>> show(x)
+2.00   Q7.2  000_0010.00

>>> y = resize(pos_unsigned, 7, 2)
>>> show(y)
+2.00  UQ7.2  000_0010.00

Since fractional bits always have a positive weight (by virtue of the fact that the Q format does not allow for a non-positive integer bit width for signed numbers), increasing the fractional bit width of any (signed or unsigned) FixedPoint will pad with zeros:

>>> show(resize(neg_signed, 4, 5))
-2.00   Q4.5  1110.0_0000

>>> show(resize(pos_signed, 4, 5))
+2.00   Q4.5  0010.0_0000

>>> show(resize(pos_unsigned, 4, 5))
+2.00  UQ4.5  0010.0_0000

Decreasing the integer bit width below the minimum number of bits required to represent it will result in overflow:

>>> neg_signed.resize(1, 5)
Traceback (most recent call last):
    ...
fixedpoint.FixedPointOverflowError: [SN1] Overflow in format Q4.5.

Override the overflow_alert property by setting the alert argument to the desired alert level. Override the overflow property by setting the overflow argument. These overrides only take effect inside the function/method; the original property setting is restored after resizing.

>>> show(resize(neg_signed, 1, 5, alert='warning', overflow='wrap'))
WARNING [SN10]: Overflow in format Q4.5.
WARNING [SN10]: Wrapped minimum.
+0.00   Q1.5  0.0_0000

>>> show(resize(pos_signed, 1, 2, alert='warning'))
WARNING [SN11]: Overflow in format Q4.2.
WARNING [SN11]: Clamped to maximum.
+0.75   Q1.2  0.11

>>> show(resize(pos_unsigned, 1, 0, alert='ignore'))
+1.00  UQ1.0  1.

Decreasing the fractional bit width below the minimum number of bits required to represent it will result in rounding. Override the rounding property by setting the rounding argument. This override only takes effect inside the function/method; the original property setting is restored after resizing.

>>> neg_signed.rounding
'convergent'
>>> show(resize(neg_signed | 0b11, 3, 1))
-1.00   Q3.1  111.0

>>> show(resize(pos_signed | 0b11, 4, 1, rounding='down'))
+2.50   Q4.1  0010.1
>>> pos_signed.rounding
'convergent'

>>> show(resize(pos_unsigned | 0b11, 2, 0, 'out'))
+3.00  UQ2.0  11.
>>> pos_unsigned.rounding
'nearest'

Rounding can potentially cause overflow if the integer portion of the FixedPoint is already at its maximum. Only certain rounding schemes can cause this.

>>> signed_maxed = FixedPoint('0xF', 1, 3, 2) # convergent rounding
>>> show(signed_maxed)
+3.75   Q3.2  011.11

>>> show(resize(signed_maxed, 3, 0, alert='warning', overflow='wrap'))
WARNING [SN20]: Convergent round to Q3.0 causes overflow.
WARNING [SN20]: Wrapped maximum.
-4.00   Q3.0  100.

When resizing, fractional bits are resized first, followed by integer bits. This could cause issues if (for example) the number being resized originally has 0 integer bits, and you are resizing to 0 fractional bits:

>>> orig = FixedPoint(0.25, rounding='up')
>>> show(orig)
+0.25  UQ0.2  .01

>>> orig.resize(4, 0) # resize to Q4.0
Traceback (most recent call last):
    ...
ValueError: Word size (integer and fractional) must be positive.

In this case, you’ll need to manually resize the integer bit width first, then the fractional:

>>> orig.m = 4
>>> orig.n = 0
>>> show(orig)
+1.00  UQ4.0  0001.

trim

Using FixedPoint.trim() or trim() will remove superfluous/insignificant bits.

>>> neg_signed   = FixedPoint(-2, 1, 4, 2)
>>> pos_signed   = FixedPoint(+2, 1, 4, 2)
>>> pos_unsigned = FixedPoint(+2, 0, 4, 2)

>>> show(neg_signed)
-2.00   Q4.2  1110.00
>>> show(trim(neg_signed))
-2.00   Q2.0  10.

>>> show(pos_signed)
+2.00   Q4.2  0010.00
>>> show(trim(pos_signed))
+2.00   Q3.0  010.

>>> show(pos_unsigned)
+2.00  UQ4.2  0010.00
>>> show(trim(pos_unsigned))
+2.00  UQ2.0  10.

You can opt to trim off only fractional or integer bits by setting fracs or int, respectively, to True.

>>> neg_signed.trim(ints=True)
>>> show(neg_signed)
-2.00   Q2.2  10.00

>>> pos_signed.trim(fracs=True)
>>> show(pos_signed)
+2.00   Q4.0  0010.

>>> pos_unsigned.trim(True, True) # same as pos_unsigned.trim()
>>> show(pos_unsigned)
+2.00  UQ2.0  10.

Zero is always trimmed to 1 integer bit and 0 fractional bits.

>>> signed = FixedPoint(0, 1, 4, 4)
>>> signed_no_frac = FixedPoint(0, 1, 4, 0)
>>> unsigned_no_int = FixedPoint(0, 0, 0, 4)

>>> show(signed)
+0.00   Q4.4  0000.0000
>>> show(trim(signed))
+0.00   Q1.0  0.

>>> show(signed_no_frac)
+0.00   Q4.0  0000.
>>> show(trim(signed_no_frac))
+0.00   Q1.0  0.

>>> show(unsigned_no_int)
+0.00  UQ0.4  .0000
>>> show(trim(unsigned_no_int))
+0.00  UQ1.0  0.

Rounding

See the Initialization page for numerical examples on various rounding schemes. The items described here warrant more information than what those examples show.

Default rounding

When a FixedPoint is instantiated, a rounding scheme (whether defaulted or explicitly specified) is determined.

The FixedPoint.round() method and built-in round() function use the inherent rounding scheme.

>>> x = FixedPoint(1/3, n=24)
>>> x.rounding
'nearest'
>>> show(x)
+0.33  UQ0.24  .0101_0101_0101_0101_0101_0101

>>> show(round(x, 4))
+0.31  UQ0.4  .0101

>>> x.rounding = 'up'
>>> x.round(7)
>>> show(x)
+0.34  UQ0.7  .010_1011

Additionally, when shrinking the fractional bit width (via FixedPoint.n), the default rounding scheme is used.

>>> x.rounding = 'in'
>>> x.n = 3
>>> show(x)
+0.25  UQ0.3  .010

math.floor()

When given a float, math.floor() will round towards \(-\infty\) and return an int type.

>>> import math
>>> x = math.floor(1/3)
>>> x, type(x)
(0, <class 'int'>)

>>> y = math.floor(-1/2)
>>> y, type(y)
(-1, <class 'int'>)

Using math.floor() on a FixedPoint will produce the same result, but will not modify the fractional bit width. It simply sets all fractional bits to 0.

>>> x = FixedPoint(-2**-5)
>>> y = math.floor(x)
>>> show(x); show(y)
-0.03   Q1.5  1.1_1111
-1.00   Q1.5  1.0_0000

The FixedPoint.round_down() method is similar to math.floor(), but instead will change the fractional bit width. A bit mask can accomplish the same thing as math.floor() if importing math is not desired.

>>> show(x); show(x & ~(2**x.n - 1))
-0.03   Q1.5  1.1_1111
-1.00   Q1.5  1.0_0000

math.ceil()

When given a float, math.ceil() will round towards \(+\infty\) and return an int type.

>>> import math
>>> x = math.ceil(1/3)
>>> x, type(x)
(1, <class 'int'>)

>>> y = math.ceil(-1/2)
>>> y, type(y)
(0, <class 'int'>)

Using math.ceil() on a FixedPoint produces the same result. Note that this can cause overflow.

>>> x = FixedPoint(2**-5, signed=True, overflow_alert='warning')
>>> y = math.ceil(x)
WARNING [SN2]: Rounding up to Q1.0 causes overflow.
WARNING [SN2]: Clamped to maximum.
>>> show(x); show(y)
+0.03   Q1.5  0.0_0001
+0.00   Q1.0  0.

Because the fractional bit width is changed to 0, unsigned numbers with no no integer bits will raise an exception.

>>> x = FixedPoint(2**-5)
>>> show(x)
+0.03  UQ0.5  .0_0001

>>> math.ceil(x)
Traceback (most recent call last):
    ...
ValueError: Word size (integer and fractional) must be positive.

math.trunc()

When given a float, math.trunc() will round towards 0 (truncating decimal digits) and return an int type.

>>> import math
>>> x = math.trunc(0.333333333333333333)
>>> x, type(x)
(0, <class 'int'>)

>>> y = math.trunc(-0.5)
>>> y, type(y)
(0, <class 'int'>)

The truncation that math.trunc() performs on floats is the truncation of decimal digits. For FixedPoints, binary digits are truncated, effectively flooring the number. Thus the only difference between math.floor() and math.trunc() is that the latter leaves no fractional bits in the return value.

Attention

Table 7 Comparison of floor/truncation rounding on floats/FixedPoints

Function

Argument Type

Truncated Digits

Rounds Towards

math.floor()

FixedPoint

binary

\(-\infty\)

float

decimal

\(-\infty\)

math.trunc()

FixedPoint

binary

\(-\infty\)

float

decimal

0

>>> x = FixedPoint(-2**-5, signed=1)
>>> y = math.trunc(x)
>>> show(x); show(y)
-0.03   Q1.5  1.1_1111
-1.00   Q1.0  1.

Because the fractional bit width is changed to 0, unsigned numbers with no no integer bits will have the integer bit width set to 1.

>>> x = FixedPoint(2**-5)
>>> show(x)
+0.03  UQ0.5  .0_0001

>>> show(math.trunc(x))
+0.00  UQ1.0  0.

Rounding-induced overflow

The following rounding schemes can cause overflow under the right circumstances:

This is because each of these schemes can increase the value of a number toward \(+\infty\) which can overflow into the integer bits. One possible workaround (if clamping/wrapping is not desired) is to manually change the integer bit width via FixedPoint.m before rounding:

>>> x = FixedPoint("0b1111", 0, 2, 2)
>>> x.round_out(1)
Traceback (most recent call last):
    ...
fixedpoint.FixedPointOverflowError: [SN1] Rounding out to UQ2.1 causes overflow.

>>> x.m += 1
>>> x.round_out(1)
>>> x.qformat, bin(x)
('UQ3.1', '0b1000')

Overflow-safe rounding

The following rounding schemes cannot possibly cause overflow:

Rounding beyond the fractional bit width

There may be times when you want to round away bits beyond the fractional bit width (e.g., keep the most significant 18 bits of a Q24.18 number and round off the rest). This can be done with FixedPoint.keep_msbs() or keep_msbs().

>>> x = FixedPoint('0x15555555555', 1, 24, 18, rounding='up')
>>> y = keep_msbs(x, 18, 0)
>>> print(f'{x: <6q} {x:_b}\n{y: <6q} {y:_b}')
Q24.18 1_0101_0101_0101_0101_0101_0101_0101_0101_0101_0101
Q18.0  1_0101_0101_0101_0110

Bit widths are calculated with respect to the MSb. This is not the same as y = resize(x, 18, 0) where the bit widths are calculated with respect to the current binary point position.

Overflow Handling

See the Initialization page for numerical examples on various overflow handling schemes. The items described here warrant more information than what those examples show.

Clamping/wrapping below 0 integer bits

There may be times when you want to remove MSbs and still perform clamping ( e.g. keep the least significant 18 bits of a Q18.24 number but clamp/wrap the entire value). This can be done with the FixedPoint.keep_lsbs() or keep_lsbs().

>>> x = FixedPoint('0x15555555555', 1, 18, 24)
>>> y = keep_lsbs(x, 18, 0, overflow='clamp', alert='ignore')
>>> print(f'{x: <6q} {x:_b}\n{y: <6q} {y:_b}')
Q18.24 1_0101_0101_0101_0101_0101_0101_0101_0101_0101_0101
Q18.0  1_1111_1111_1111_1111

Bit widths are calculated with respect to the LSb. This is not the same as y = resize(x, 18, 0) where the bit widths are calculated with respect to the current binary point position.