from enum import Enum
import numpy as np
from numpy import bitwise_and as np_and
from numpy import bitwise_not as np_not
from . import generalutil as gu
__author__ = "Samuele FAVAZZA"
__copyright__ = "Copyright 2018, Samuele FAVAZZA"
"""Module implementing fix-point arithmentic classes."""
class EFormat(Enum):
"""Enum class to express number base expression"""
HEX = 'hex'
INT = 'int'
BIN = 'bin'
FLOAT = 'float'
class ERoundMethod(Enum):
"""Enum class for round methods."""
SYM_INF = 'SymInf'
SYM_ZERO = 'SymZero'
NON_SYM_POS = 'NonSymPos'
NON_SYM_NEG = 'NonSymNeg'
CONV_EVEN = 'ConvEven'
CONV_ODD = 'ConvOdd'
FLOOR = 'Floor'
CEIL = 'Ceil'
class EOverMethod(Enum):
"""Enum class for overflow methods."""
SAT = 'Sat'
WRAP = 'Wrap'
[docs]class FixFmt:
"""Fix format class
:param signed: indicate whether the representation is signed (True) or unsigned
:param int_bits: number of bits representing the integer part
:param frac_bits: number of bits representing the fractional part
:type signed: bool
:type int_bits: int
:type frac_bits: int
"""
def __init__(self, signed, int_bits, frac_bits):
if int_bits < 0 or frac_bits < 0:
raise ValueError("Integer and fractional sizes must be positive.")
self.signed = gu.check_args(signed, bool)
self.int_bits = gu.check_args(int_bits, int)
self.frac_bits = gu.check_args(frac_bits, int)
def __str__(self):
return "(%s, %s, %s)" % (self.signed, self.int_bits, self.frac_bits)
def __repr__(self):
return """%s
<%s at %s>""" % (self.__str__(), gu.get_class_name(self), hex(id(self)))
@property
def mask(self):
"""Return mask to limit number representation (bit_length*ones).
Ex:
>>> from pyphix import fix
>>> fmt = fix.FixFmt(True, 2, 3)
>>> bin(fmt.mask)
'0b111111'
>>> fmt = fix.FixFmt(False, 1, 12)
>>> bin(fmt.mask)
'0b1111111111111'"""
return 2**self.bit_length - 1
@property
def bit_length(self):
"""Return the number of bits required to represent a number with current fix format."""
return int(self.signed) + self.int_bits + self.frac_bits
def _minmaxvalueformatter(self, value, fmt):
"""Return value in desired expressed format.
:param value: value to format.
:param fmt: desired format
:type value: int
:type fmt: EFormat
:return: formatted input value.
:rtype: float or int or str"""
if fmt is EFormat.BIN:
return _bin2fixstring(value & self.mask, self.bit_length)
if fmt is EFormat.HEX:
return hex(value & self.mask)
if fmt is EFormat.FLOAT:
return value / 2**self.frac_bits
return value
[docs] def maxvalue(self, fmt='float'):
"""Return max representable value by current fix format objext.
:param fmt: format the value is presented, either hex, int, bin, float.
:type fmt: EFormat or str
:return: max representable value.
:rtype: float or int or str"""
# ensure the given fmt is correct
_fmt = gu.check_enum(fmt, EFormat)
maxvalue_int = (1 << (self.bit_length - 1)) - 1 if self.signed else \
(1 << (self.bit_length)) - 1
return self._minmaxvalueformatter(maxvalue_int, _fmt)
[docs] def minvalue(self, fmt='float'):
"""Return min representable value by current fix format objext."""
# ensure the given fmt is correct
_fmt = gu.check_enum(fmt, EFormat)
minvalue_int = -2**(self.bit_length - 1) if self.signed else 0
return self._minmaxvalueformatter(minvalue_int, _fmt)
@property
def fixrange(self):
"""Return the range representable by fix format object as tuple (min, max)."""
return (self.minvalue(), self.maxvalue())
@property
def tuplefmt(self):
"""Return object as a tuple."""
return (self.signed, self.int_bits, self.frac_bits)
@property
def listfmt(self):
"""Return object as a list."""
return [self.signed, self.int_bits, self.frac_bits]
def __contains__(self, elem):
return self.minvalue() <= elem <= self.maxvalue()
[docs]class FixNum:
"""Fixed point number class
+--------------------------------------------------------------------------+
| **Round methods** |
+---------------+----------------------------------------------------------+
| ``SymInf`` | positive numbers tend to +inf, negative numbers to -inf |
+---------------+----------------------------------------------------------+
| ``SymZero`` | round toward zero (*DEFAULT*) |
+---------------+----------------------------------------------------------+
| ``NonSymPos`` | round toward +inf |
+---------------+----------------------------------------------------------+
| ``NonSymNeg`` | round toward -inf |
+---------------+----------------------------------------------------------+
| ``ConvEven`` | round to closest even |
+---------------+----------------------------------------------------------+
| ``ConvOdd`` | round to closest odd |
+---------------+----------------------------------------------------------+
| ``Floor`` | round to largest previous |
+---------------+----------------------------------------------------------+
| ``Ceil`` | round to smallest following |
+---------------+----------------------------------------------------------+
+-------------------------------------------+
| **Overflow methods** |
+------------------+------------------------+
| ``Sat`` | saturate |
+------------------+------------------------+
| ``Wrap`` | wrap around -- DEFAULT |
+------------------+------------------------+
:param value: value to represent in fix point
:param fmt: fix point format
:param rnd: round method
:param over: overflow method
:type value: np.ndarray(ndim > 0) or float
:type fmt: FixFmt
:type rnd: str
:type over: str
"""
# pylint: disable=too-many-instance-attributes
def __init__(self, value, fmt, rnd="SymZero", over="Wrap"):
# init instance members
self.fmt = gu.check_args(fmt, FixFmt)
self.rnd = gu.check_enum(rnd, ERoundMethod)
self.over = gu.check_enum(over, EOverMethod)
self._index = 0 # for generator feature
# internal constants
self._to_int_coeff = 2**self.fmt.frac_bits # to integer representation coefficient
self._fix_size_mask = (1 << self.fmt.bit_length) - 1 # correct representation
# always cast to np.float64
try:
# turn into array
self.value, self.shape = self._to_array(value)
# round and overflow process in int format
self.value = self._over(self._round(self.value * self._to_int_coeff))
# back to float
self.value = self.value / self._to_int_coeff
except ValueError:
print('Wrong input value type, only numeric list/np.arrays are allowed')
raise
# support methods
@staticmethod
def _value2line(value):
"""Turn input value into vector form."""
return np.reshape(value, -1)
def _tmp_int(self):
"""Geneate integer representation of the fix object."""
tmp_value = self._value2line(self.value) * 2**self.fmt.frac_bits
return np.array([np.int(x) if x >= 0 else
(np.int(x) & self._fix_size_mask) for x in tmp_value])
@staticmethod
def _to_array(value):
"""Turn input value into an array even when a simple number.
:param value: single number or vector.
:type value: numpy.ndarray or float
:return: tuple in the form (value, shape), where value is always indexable.
:rtype: tuple[numpy.ndarray, tuple]"""
# turn into array
value = np.array(value, dtype=np.float64) # pylint: disable=no-member
shape = value.shape if value.shape else (1, )
# ensure also single values are indexable
return (np.reshape(value, shape), shape)
# private methods
def _round(self, value):
"""Round input using object rounding method.
:param value: an indexable object.
:type value: numpy.ndarray
:return: rounded value.
:rtype: numpy.ndarray
"""
if self.rnd is ERoundMethod.SYM_INF:
value[value > 0] += .5
value[value < 0] -= .5
elif self.rnd is ERoundMethod.SYM_ZERO:
value[value > 0] += .4
value[value < 0] -= .4
elif self.rnd is ERoundMethod.NON_SYM_POS:
value[value > 0] += .5
value[value < 0] -= .4
elif self.rnd is ERoundMethod.NON_SYM_NEG:
value[value > 0] += .4
value[value < 0] -= .5
elif self.rnd in [ERoundMethod.CONV_EVEN, ERoundMethod.CONV_ODD]:
even_sel, odd_sel = value.astype(int) % 2 == 0, value.astype(int) % 2 != 0
# even
value[np.logical_and(even_sel, value > 0)] += .4 if self.rnd is ERoundMethod.CONV_EVEN else .5
value[np.logical_and(even_sel, value < 0)] -= .4 if self.rnd is ERoundMethod.CONV_EVEN else .5
# odd
value[np.logical_and(odd_sel, value > 0)] += .5 if self.rnd is ERoundMethod.CONV_EVEN else .4
value[np.logical_and(odd_sel, value < 0)] -= .5 if self.rnd is ERoundMethod.CONV_EVEN else .4
elif self.rnd is ERoundMethod.FLOOR:
# round to the previous largest
value = np.floor(value)
elif self.rnd is ERoundMethod.CEIL:
# round to the next smallest
value = np.ceil(value)
else:
raise ValueError("_ERROR_: %r is not valid round value." % self.rnd)
# convert to integer
return value.astype(int)
def _over(self, value):
"""Apply current object overflow method on input value.
:param value: current object value.
:type value: numpy.ndarray or float
:return: overflowed value.
:rtype: numpy.ndarray or float"""
if self.over is EOverMethod.SAT:
value = np.maximum(
np.minimum(value, self.fmt.maxvalue(fmt=EFormat.INT)),
self.fmt.minvalue(fmt=EFormat.INT))
elif self.over is EOverMethod.WRAP:
# selection masks
high_bit_mask = (1 << (self.fmt.bit_length-1))
# pos / neg selector
value = np_and(value, self._fix_size_mask)
pos_sel = np_and(value, high_bit_mask) == 0
neg_sel = np.logical_not(pos_sel)
if self.fmt.signed:
# non negative
value[pos_sel] = value[pos_sel]
# negative
value[neg_sel] = -np_and((np_not(value[neg_sel]) + 1), self._fix_size_mask)
else:
raise ValueError("_ERROR_: %r is not valid overflow value." % self.over)
return value
# public methods
[docs] def change_fix(self, new_fmt, new_rnd=None, new_over=None):
"""Change fix parameters of current object.
**WARNING**: this action may lead to information loss due to new format and round/overflow methods.
:param new_fmt: new format (mandatory).
:param new_rnd: new round method, if not specified current is used.
:param new_over: new saturation method, if not specified current is used.
:type new_fmt: FixFmt
:type new_rnd: str or None
:type new_over: str or None
:return: new formatted fix-point object.
:rtype: FixFmt
"""
return FixNum(self.value, new_fmt,
self.rnd if new_rnd is None else new_rnd,
self.over if new_over is None else new_over)
@property
def binfmt(self):
"""Represent fix-point object in binary format."""
# correct string representation
tmp_bin = np.array([_bin2fixstring(x, self.fmt.bit_length) for x in self._tmp_int()])
return np.reshape(tmp_bin, self.shape)
@property
def hexfmt(self):
"""Represent fix-point object in hexadecimal format."""
tmp_hex = np.array([hex(x) for x in self._value2line(self.intfmt)])
# correct string representation
tmp_hex = np.array(['0x' + (int(np.ceil(self.fmt.bit_length / 4)) - len(x[2:])) * '0' + x[2:]
for x in tmp_hex])
return np.reshape(tmp_hex, self.shape)
@property
def intfmt(self):
"""Represent fix-point object in integer format."""
return np.reshape(self._tmp_int(), self.shape)
@property
def fimath(self):
"""Return fix math as tuple (round method, overflow mode)."""
return (self.rnd, self.over)
# data model
# # representation
def __str__(self):
return """
%s
fmt: %s
rnd: %s
over: %s""" % (self.value, self.fmt, self.rnd, self.over)
def __repr__(self):
return """%s
<%s at %s>""" % (self.__str__(), gu.get_class_name(self), hex(id(self)))
# # container methods
def __contains__(self, elem):
if isinstance(elem, FixNum):
return elem.value in self.value
return elem in self.value
def __getitem__(self, idx):
return FixNum(self.value[idx], self.fmt, self.rnd, self.over)
def __setitem__(self, idx, repleace_value):
if isinstance(repleace_value, FixNum):
self.value[idx] = self._over(self._round(
self._to_array(repleace_value.value)[0]*self._to_int_coeff))/self._to_int_coeff
else:
self.value[idx] = self._over(self._round(
self._to_array(repleace_value)[0]*self._to_int_coeff))/self._to_int_coeff
def __len__(self):
return self.shape
# # generator
def __iter__(self):
self._index = 0
return self
def __next__(self):
try:
ret = FixNum(self.value[self._index], self.fmt, self.rnd, self.over)
self._index += 1
except IndexError:
raise StopIteration
return ret
# # operators
@staticmethod
def _op_out_casting(op, other, out_fmt=None, out_rnd="SymZero", out_over="Wrap"):
"""Implement format and fimath casting on defualt operations.
:param op: operation function name (__add__, __sub__, etc...).
:param other: fix-point object.
:param out_fmt: optional format operation result can be casted to.
:param out_rnd: round method adopted on result (default ```SymZero```).
:param out_over: overflow method adopted on result (default ```Wrap```).
:type op: method
:type other: FixNum
:type out_fmt: FixFmt
:type out_rnd: str
:type out_over: str
:return: addition result.
:rtype: FixNum"""
tmp_fix = op(other)
tmp_fmt = tmp_fix.fmt if out_fmt is None else out_fmt
return tmp_fix.change_fix(tmp_fmt, out_rnd, out_over)
# ## Addition methods
def __add__(self, other):
"""x + y --> x.__add__(y)"""
tmp_val = self.value + other.value
tmp_fmt = FixFmt(self.fmt.signed or other.fmt.signed,
max(self.fmt.int_bits, other.fmt.int_bits)+1,
max(self.fmt.frac_bits, other.fmt.frac_bits))
if (self.rnd != other.rnd) or (self.over != other.over):
print('_WARNING_: operators have round and/or overflow methods ' +
'not equal, those of first operator will be considered')
return FixNum(tmp_val, tmp_fmt, self.rnd, self.over)
[docs] def add(self, *args, **kwargs):
"""Addition method.
*Usage: add(other, out_fmt=None, out_rnd="SymZero", out_over="Wrap")*
It allows to decide the output format.
If not indicated, full-precision format will be adopted.
:param other: fix-point object.
:param out_fmt: optional format operation result can be casted to.
:param out_rnd: round method adopted on result (default ```SymZero```).
:param out_over: overflow method adopted on result (default ```Wrap```).
:type other: FixNum
:type out_fmt: FixFmt
:type out_rnd: str
:type out_over: str
:return: addition result.
:rtype: FixNum"""
return self._op_out_casting(self.__add__, *args, **kwargs)
# ## Subtraction methods
def __sub__(self, other):
tmp_val = self.value - other.value
tmp_fmt = FixFmt(self.fmt.signed or other.fmt.signed,
max(self.fmt.int_bits, other.fmt.int_bits)+1,
max(self.fmt.frac_bits, other.fmt.frac_bits))
if (self.rnd != other.rnd) or (self.over != other.over):
print('_WARNING_: operators have round and / or overflow methods ' +
'not equal, those of first operator will be considered')
return FixNum(tmp_val, tmp_fmt, self.rnd, self.over)
[docs] def sub(self, *args, **kwargs):
"""Subtraction method.
*Usage: sub(other, out_fmt=None, out_rnd="SymZero", out_over="Wrap")*
It allows to decide output format.
If not indicated, full-precision format will be adopted.
:param other: fix-point object.
:param out_fmt: optional format operation result can be casted to.
:param out_rnd: round method adopted on result (default ```SymZero```).
:param out_over: overflow method adopted on result (default ```Wrap```).
:type other: FixNum
:type out_fmt: FixFmt
:type out_rnd: str
:type out_over: str
:return: operation result.
:rtype: FixNum"""
return self._op_out_casting(self.__sub__, *args, **kwargs)
# ## Multiplication methods
def __mul__(self, other):
tmp_val = self.value * other.value
tmp_fmt = FixFmt(self.fmt.signed or other.fmt.signed,
self.fmt.int_bits + other.fmt.int_bits,
self.fmt.frac_bits + other.fmt.frac_bits)
if (self.rnd != other.rnd) or (self.over != other.over):
print('_WARNING_: operators have round and / or overflow methods ' +
'not equal, those of first operator will be considered')
return FixNum(tmp_val, tmp_fmt, self.rnd, self.over)
[docs] def mult(self, *args, **kwargs):
"""Multiplication method.
*Usage: mult(other, out_fmt=None, out_rnd="SymZero", out_over="Wrap")*
It allows to decide output format.
If not indicated, full-precision format will be adopted.
:param other: fix-point object.
:param out_fmt: optional format operation result can be casted to.
:param out_rnd: round method adopted on result (default ```SymZero```).
:param out_over: overflow method adopted on result (default ```Wrap```).
:type other: FixNum
:type out_fmt: FixFmt
:type out_rnd: str
:type out_over: str
:return: operation result.
:rtype: FixNum"""
return self._op_out_casting(self.__mul__, *args, **kwargs)
# ## Negation method
def __neg__(self):
return FixNum(-self.value, self.fmt, self.rnd, self.over)
# ## Comparison methods
def __lt__(self, other):
return self.value < other
def __le__(self, other):
return self.value <= other
def __eq__(self, other):
return self.value == other
def __ne__(self, other):
return self.value != other
def __gt__(self, other):
return self.value > other
def __ge__(self, other):
return self.value >= other
# private methods
def _bin2fixstring(value, out_length):
"""Convert a number to bin format with leading zeros."""
value_bin_no_prefix = bin(value)[2:]
return '0b' + (out_length - len(value_bin_no_prefix)) * '0' + value_bin_no_prefix