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 float
s is the
truncation of decimal digits. For FixedPoint
s, 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
Function |
Argument Type |
Truncated Digits |
Rounds Towards |
---|---|---|---|
binary |
\(-\infty\) |
||
decimal |
\(-\infty\) |
||
binary |
\(-\infty\) |
||
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')
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.