diff --git a/changelog.txt b/changelog.txt index df2e22c..78bc6d6 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,3 +1,10 @@ +version 0.4.6 +-------------------------------------------------- +* Fix complex `truediv` and `floordiv` methods (issue #53). +* Fix complex binary, hexadecimal and base representation for arrays (issue #56). +* Tests updates for complex representations. +* Solve init by dtype when it is complex (issue #58). + version 0.4.5 -------------------------------------------------- * Fix FutureWarning in subdtype 'str' comparison (issue #45). diff --git a/fxpmath/__init__.py b/fxpmath/__init__.py index 55c88f2..bfcdb42 100644 --- a/fxpmath/__init__.py +++ b/fxpmath/__init__.py @@ -1,4 +1,4 @@ -__version__ = '0.4.5' +__version__ = '0.4.6' import sys import os diff --git a/fxpmath/functions.py b/fxpmath/functions.py index 4778e90..c95c1f7 100644 --- a/fxpmath/functions.py +++ b/fxpmath/functions.py @@ -369,15 +369,35 @@ def floordiv(x, y, out=None, out_like=None, sizing='optimal', method='raw', **kw """ def _floordiv_repr(x, y): return x // y + + def _floordiv_repr_complex(x, y): + y_norm = y.real ** 2 + y.imag ** 2 + real_part = (x.real * y.real + x.imag * y.imag) // y_norm + imag_part = (x.imag * y.real - x.real * y.imag) // y_norm + return real_part + 1j*imag_part + def _floordiv_raw(x, y, n_frac): precision_cast = (lambda m: np.array(m, dtype=object)) if n_frac >= _n_word_max else (lambda m: m) return ((x.val * precision_cast(2**(n_frac - x.n_frac))) // (y.val * precision_cast(2**(n_frac - y.n_frac)))) * precision_cast(2**n_frac) + def _floordiv_raw_complex(x, y, n_frac): + precision_cast = (lambda m: np.array(m, dtype=object)) if n_frac >= _n_word_max else (lambda m: m) + y_norm = (y.val.real ** 2 + y.val.imag ** 2) * precision_cast(2**(n_frac - 2*y.n_frac)) + real_part = (x.val.real * y.val.real + x.val.imag * y.val.imag) * precision_cast(2**(n_frac - x.n_frac - y.n_frac)) // y_norm + imag_part = (x.val.imag * y.val.real - x.val.real * y.val.imag) * precision_cast(2**(n_frac - x.n_frac - y.n_frac)) // y_norm + + return (real_part + 1j*imag_part) * precision_cast(2**n_frac) + + if not isinstance(x, Fxp): x = Fxp(x) if not isinstance(y, Fxp): y = Fxp(y) + if x.vdtype == complex or y.vdtype == complex: + _floordiv_repr = _floordiv_repr_complex + _floordiv_raw = _floordiv_raw_complex + signed = x.signed or y.signed n_int = x.n_int + y.n_frac + signed n_frac = 0 @@ -392,16 +412,30 @@ def truediv(x, y, out=None, out_like=None, sizing='optimal', method='raw', **kwa """ def _truediv_repr(x, y): return x / y + def _truediv_raw(x, y, n_frac): precision_cast = (lambda m: np.array(m, dtype=object)) if n_frac >= _n_word_max else (lambda m: m) return (x.val * precision_cast(2**(n_frac - x.n_frac + y.n_frac))) // y.val # return np.floor_divide(np.multiply(x.val, precision_cast(2**(n_frac - x.n_frac + y.n_frac))), y.val) + def _truediv_raw_complex(x, y, n_frac): + precision_cast = (lambda m: np.array(m, dtype=object)) if n_frac >= _n_word_max else (lambda m: m) + + y_norm = y.val.real ** 2 + y.val.imag ** 2 + real_part = (x.val.real * y.val.real + x.val.imag * y.val.imag) * precision_cast(2**(n_frac - x.n_frac + y.n_frac)) // y_norm + imag_part = (x.val.imag * y.val.real - x.val.real * y.val.imag) * precision_cast(2**(n_frac - x.n_frac + y.n_frac)) // y_norm + + return real_part + 1j*imag_part + + if not isinstance(x, Fxp): x = Fxp(x) if not isinstance(y, Fxp): y = Fxp(y) + if x.vdtype == complex or y.vdtype == complex: + _truediv_raw = _truediv_raw_complex + signed = x.signed or y.signed n_int = x.n_int + y.n_frac + signed n_frac = x.n_frac + y.n_int diff --git a/fxpmath/objects.py b/fxpmath/objects.py index 23548de..c9d250e 100644 --- a/fxpmath/objects.py +++ b/fxpmath/objects.py @@ -219,7 +219,9 @@ def __init__(self, val=None, signed=None, n_word=None, n_frac=None, n_int=None, # check if a string-based format has been provided if dtype is not None: - signed, n_word, n_frac = self._parseformatstr(dtype) + signed, n_word, n_frac, complex_flag = self._parseformatstr(dtype) + + self.vdtype = complex if complex_flag else self.vdtype # size if not _initialized: @@ -329,15 +331,22 @@ def _parseformatstr(self, fmt): else: n_frac = int(mo.group(3)[1:]) n_word = n_frac + n_int + complex_dtype = False else: mo = self._fxpfmt.match(fmt) if mo: signed = mo.group(1) == 's' n_word = int(mo.group(2)) n_frac = int(mo.group(3)) + complex_dtype = False + if mo.lastindex > 3: + _complex_str = str(mo.group(4)) + if _complex_str == '-complex': + complex_dtype = True + else: raise ValueError('unrecognized format string') - return signed, n_word, n_frac + return signed, n_word, n_frac, complex_dtype def _init_size(self, val=None, signed=None, n_word=None, n_frac=None, n_int=None, max_error=_max_error, n_word_max=_n_word_max, raw=False): # sign by default @@ -394,7 +403,9 @@ def resize(self, signed=None, n_word=None, n_frac=None, n_int=None, restore_val= if dtype is not None: if signed is not None or n_word is not None or n_frac is not None or n_int is not None: raise ValueError('If dtype is specified, other sizing parameters must be `None`!') - signed, n_word, n_frac = self._parseformatstr(dtype) + signed, n_word, n_frac, complex_flag = self._parseformatstr(dtype) + + self.vdtype = complex if complex_flag else self.vdtype # n_int defined: if n_word is None and n_frac is not None and n_int is not None: @@ -611,7 +622,11 @@ def _format_inupt_val(self, val, return_sizes=False, raw=False): if val is None: val = 0 - vdtype = int + + if self.vdtype is None: + vdtype = int if n_frac < 1 else float + else: + vdtype = self.vdtype elif isinstance(val, Fxp): # if val is an Fxp object @@ -729,7 +744,7 @@ def _update_dtype(self, notation=None): self._dtype = 'fxp-{sign}{nword}/{nfrac}{comp}'.format(sign='s' if self.signed else 'u', nword=self.n_word, nfrac=self.n_frac, - comp='-complex' if self.val.dtype == complex else '') + comp='-complex' if (self.val.dtype == complex or self.vdtype == complex) else '') else: self._dtype = 'fxp-{sign}{nword}/{nfrac}'.format(sign='s' if self.signed else 'u', nword=self.n_word, @@ -1479,12 +1494,16 @@ def bin(self, frac_dot=False): if isinstance(self.val, (list, np.ndarray)) and self.val.ndim > 0: if self.vdtype == complex: - rval = [ utils.binary_repr(int(val.real), n_word=self.n_word, n_frac=n_frac_dot) + '+' + utils.binary_repr(int(val.imag), n_word=self.n_word, n_frac=n_frac_dot) + 'j' for val in self.val] + real_val = [utils.binary_repr(utils.int_array(val.real), n_word=self.n_word, n_frac=n_frac_dot) for val in self.val] + imag_val = [utils.binary_repr(utils.int_array(val.imag), n_word=self.n_word, n_frac=n_frac_dot) for val in self.val] + rval = utils.complex_repr(real_val, imag_val) else: - rval = [utils.binary_repr(int(val), n_word=self.n_word, n_frac=n_frac_dot) for val in self.val] + rval = [utils.binary_repr(utils.int_array(val), n_word=self.n_word, n_frac=n_frac_dot) for val in self.val] else: if self.vdtype == complex: - rval = utils.binary_repr(int(self.val.real), n_word=self.n_word, n_frac=n_frac_dot) + '+' + utils.binary_repr(int(self.val.imag), n_word=self.n_word, n_frac=n_frac_dot) + 'j' + real_val = utils.binary_repr(utils.int_array(self.val.real), n_word=self.n_word, n_frac=n_frac_dot) + imag_val = utils.binary_repr(utils.int_array(self.val.imag), n_word=self.n_word, n_frac=n_frac_dot) + rval = utils.complex_repr(real_val, imag_val) else: rval = utils.binary_repr(int(self.val), n_word=self.n_word, n_frac=n_frac_dot) return rval @@ -1497,14 +1516,18 @@ def hex(self, padding=True): if isinstance(self.val, (list, np.ndarray)) and self.val.ndim > 0: if self.vdtype == complex: - rval = [ utils.hex_repr(int(val.split('+')[0], 2), n_word=hex_n_word) + '+' + utils.hex_repr(int(val.split('+')[1][:-1], 2), n_word=hex_n_word) + 'j' for val in self.bin()] + real_val = [utils.hex_repr(utils.binary_repr(utils.int_array(val.real), n_word=self.n_word, n_frac=None), n_word=hex_n_word, base=2) for val in self.val] + imag_val = [utils.hex_repr(utils.binary_repr(utils.int_array(val.imag), n_word=self.n_word, n_frac=None), n_word=hex_n_word, base=2) for val in self.val] + rval = utils.complex_repr(real_val, imag_val) else: - rval = [utils.hex_repr(int(val, 2), n_word=hex_n_word) for val in self.bin()] + rval = [utils.hex_repr(val, n_word=hex_n_word, base=2) for val in self.bin()] else: if self.vdtype == complex: - rval = utils.hex_repr(int(self.bin().split('+')[0], 2), n_word=hex_n_word) + '+' + utils.hex_repr(int(self.bin().split('+')[1][:-1], 2), n_word=hex_n_word) + 'j' + real_val = utils.hex_repr(utils.binary_repr(utils.int_array(self.val.real), n_word=self.n_word, n_frac=None), n_word=hex_n_word, base=2) + imag_val = utils.hex_repr(utils.binary_repr(utils.int_array(self.val.imag), n_word=self.n_word, n_frac=None), n_word=hex_n_word, base=2) + rval = utils.complex_repr(real_val, imag_val) else: - rval = utils.hex_repr(int(self.bin(), 2), n_word=hex_n_word) + rval = utils.hex_repr(self.bin(), n_word=hex_n_word, base=2) return rval def base_repr(self, base, frac_dot=False): @@ -1515,12 +1538,14 @@ def base_repr(self, base, frac_dot=False): if isinstance(self.val, (list, np.ndarray)) and self.val.ndim > 0: if self.vdtype == complex: - rval = [utils.base_repr(int(val.real), base=base, n_frac=n_frac_dot) + ('+' if val.imag >= 0 else '') + utils.base_repr(int(val.imag), base=base, n_frac=n_frac_dot) + 'j' for val in self.val] + real_val = [utils.base_repr(utils.int_array(val.real), base=base, n_frac=n_frac_dot) for val in self.val] + imag_val = [utils.base_repr(utils.int_array(val.imag), base=base, n_frac=n_frac_dot) for val in self.val] + rval = utils.complex_repr(real_val, imag_val) else: - rval = [utils.base_repr(int(val), base=base, n_frac=n_frac_dot) for val in self.val] + rval = [utils.base_repr(utils.int_array(val), base=base, n_frac=n_frac_dot) for val in self.val] else: if self.vdtype == complex: - rval = utils.base_repr(int(self.val.real), base=base, n_frac=n_frac_dot) + ('+' if self.val.imag >= 0 else '') + utils.base_repr(int(self.val.imag), base=base, n_frac=n_frac_dot) + 'j' + rval = utils.complex_repr(utils.base_repr(int(self.val.real), base=base, n_frac=n_frac_dot), utils.base_repr(int(self.val.imag), base=base, n_frac=n_frac_dot)) else: rval = utils.base_repr(int(self.val), base=base, n_frac=n_frac_dot) return rval diff --git a/fxpmath/utils.py b/fxpmath/utils.py index 06de9b7..f751d31 100644 --- a/fxpmath/utils.py +++ b/fxpmath/utils.py @@ -227,14 +227,23 @@ def insert_frac_point(x_bin, n_frac): return x_bin +@array_support def binary_repr(x, n_word=None, n_frac=None): if n_frac is None: - val = np.binary_repr(x, width=n_word) + val = np.binary_repr(int(x), width=n_word) else: val = insert_frac_point(np.binary_repr(x, width=n_word), n_frac=n_frac) return val -def hex_repr(x, n_word=None, padding=None): +@array_support +def hex_repr(x, n_word=None, padding=None, base=10): + if base == 2: + x = int(x, 2) + elif base == 10: + pass + else: + raise ValueError('base {base} for input value is not supported!') + if n_word is not None: val = '0x{0:0{1}X}'.format(x, int(np.ceil(n_word/4))) elif padding is not None: @@ -244,13 +253,32 @@ def hex_repr(x, n_word=None, padding=None): val = '0x'+val[2:].upper() return val +@array_support def base_repr(x, n_word=None, base=2, n_frac=None): if n_frac is None: val = np.base_repr(x, base=base) elif base == 2: val = insert_frac_point(np.base_repr(x, base=base), n_frac=n_frac) + else: + val = np.base_repr(x, base=base) return val +def complex_repr(r, i): + r = np.asarray(r) + i = np.asarray(i) + + assert r.shape == i.shape + + c = np.empty(r.shape, dtype=object) + + if r.dtype.type is np.str_ and i.dtype.type is np.str_: + for idx in np.ndindex(r.shape): + imag_sign_symbol = '' if ('-' in str(i[idx]) or '+' in str(i[idx])) else '+' + c[idx] = str(r[idx]) + imag_sign_symbol + str(i[idx]) + 'j' + else: + raise ValueError('parameters must be a list of array of strings!') + return c + def bits_len(x, signed=None): if signed is None and x < 0: signed = True diff --git a/tests/test_basic.py b/tests/test_basic.py index 6b66d34..dc3cddc 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -258,6 +258,21 @@ def test_base_representations(): assert x.hex() == '0x000000064' assert y.hex() == '0x1FFFFFF9C' + # arrays + arr_fxp = Fxp(np.array([[1, 2]])) + assert np.all(arr_fxp.bin() == np.array(['001', '010'])) + assert np.all(arr_fxp.hex() == np.array(['0x1', '0x2'])) + assert np.all(arr_fxp.base_repr(base=2) == np.array(['1', '10'])) + assert np.all(arr_fxp.base_repr(base=10) == np.array(['1', '2'])) + assert np.all(arr_fxp.base_repr(base=10) == np.array(['1', '2'])) + + arr_fxp = Fxp(np.array([[15, -16], [-1, 0]])) + assert np.all(arr_fxp.bin() == np.array([['01111', '10000'], ['11111', '00000']])) + assert np.all(arr_fxp.hex() == np.array([['0x0F', '0x10'], ['0x1F', '0x00']])) + assert np.all(arr_fxp.base_repr(base=2) == np.array([['1111', '-10000'], ['-1', '0']])) + assert np.all(arr_fxp.base_repr(base=10) == np.array([['15', '-16'], ['-1', '0']])) + assert np.all(arr_fxp.base_repr(base=16) == np.array([['F', '-10'], ['-1', '0']])) + def test_like(): ref = Fxp(0.0, True, 16, 4) diff --git a/tests/test_complex_numbers.py b/tests/test_complex_numbers.py new file mode 100644 index 0000000..3d7935d --- /dev/null +++ b/tests/test_complex_numbers.py @@ -0,0 +1,122 @@ +import os +import sys +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) + +import fxpmath as fxp +from fxpmath.objects import Fxp + +import numpy as np + +def test_complex_creation(): + x = Fxp(0.25 - 1j*14.5) + assert x() == 0.25 - 1j*14.5 + assert x.real == 0.25 + assert x.imag == -14.5 + assert x.dtype == 'fxp-s7/2-complex' + assert x.vdtype == complex + + x = Fxp(3.0, dtype='fxp-s8/4-complex') + assert x() == 3.0 + assert x.imag == 0.0 + + x = Fxp(1j*3.0, dtype='fxp-s8/4-complex') + assert x() == 1j*3.0 + assert x.real == 0.0 + assert x.imag == 3.0 + + x = Fxp([0.0, 1.0 + 1j*1.0, -1j*2.5], signed=True, n_word=8) + assert x.dtype == 'fxp-s8/1-complex' + assert x[0]() == 0.0 + assert x[1]() == 1.0 + 1j*1.0 + assert x[2]() == -1j*2.5 + + x = Fxp(0.25 - 1j*14.5, dtype='Q6.4') + assert x.dtype == 'fxp-s10/4-complex' + +def test_math_operations(): + c = 2.0 + x = 0.25 - 1j*14.5 + y = -1.0 + 1j*0.5 + + x_fxp = Fxp(x, dtype='Q14.3') + y_fxp = Fxp(y, dtype='Q14.3') + + # add + z = x + y + z_fxp = x_fxp + y_fxp + assert z_fxp() == z + + z = x + c + z_fxp = x_fxp + c + assert z_fxp() == z + + # sub + z = x - y + z_fxp = x_fxp - y_fxp + assert z_fxp() == z + + z = x - c + z_fxp = x_fxp - c + assert z_fxp() == z + + # mul + z = x * y + z_fxp = x_fxp * y_fxp + assert z_fxp() == z + + z = x * c + z_fxp = x_fxp * c + assert z_fxp() == z + + # div + z = x / y + z_fxp = x_fxp / y_fxp + assert z_fxp() == z + + z = x / c + z_fxp = x_fxp / c + assert z_fxp() == z + + # floor div + x = np.asarray(x) + y = np.asarray(y) + z = (x * y.conj()).real // (y * y.conj()).real + 1j* ((x * y.conj()).imag // (y * y.conj()).real) + z_fxp = x_fxp // y_fxp + assert z_fxp() == z + + c = np.asarray(c) + z = (x * c.conj()).real // (c * c.conj()).real + 1j* ((x * c.conj()).imag // (c * c.conj()).real) + z_fxp = x_fxp // c + assert z_fxp() == z + +def test_complex_repr(): + c_fxp = Fxp(1 + 1j*15) + assert c_fxp.bin() == '00001+01111j' + assert c_fxp.hex() == '0x01+0x0Fj' + assert c_fxp.base_repr(base=2) == '1+1111j' + assert c_fxp.base_repr(base=10) == '1+15j' + assert c_fxp.base_repr(base=16) == '1+Fj' + + c_fxp = Fxp(3.5 - 1j*0.25) + assert c_fxp.bin() == '01110+11111j' + assert c_fxp.bin(frac_dot=True) == '011.10+111.11j' + assert c_fxp.hex() == '0x0E+0x1Fj' + assert c_fxp.base_repr(base=2) == '1110-1j' + assert c_fxp.base_repr(base=2, frac_dot=True) == '11.10-.01j' + assert c_fxp.base_repr(base=10) == '14-1j' + assert c_fxp.base_repr(base=16) == 'E-1j' + + c_fxp = Fxp(12 - 1j*1) + assert c_fxp.bin() == '01100+11111j' + assert c_fxp.hex() == '0x0C+0x1Fj' + assert c_fxp.base_repr(base=2) == '1100-1j' + assert c_fxp.base_repr(base=10) == '12-1j' + assert c_fxp.base_repr(base=16) == 'C-1j' + + arr_fxp = Fxp(np.array([[1 + 1j*2, 2 - 1j*3]])) + assert np.all(arr_fxp.bin() == np.array(['001+010j', '010+101j'])) + assert np.all(arr_fxp.hex() == np.array(['0x1+0x2j', '0x2+0x5j'])) + assert np.all(arr_fxp.base_repr(base=2) == np.array(['1+10j', '10-11j'])) + assert np.all(arr_fxp.base_repr(base=10) == np.array(['1+2j', '2-3j'])) + assert np.all(arr_fxp.base_repr(base=16) == np.array(['1+2j', '2-3j'])) + diff --git a/tests/test_issues.py b/tests/test_issues.py index 32a7c05..4e8c6d2 100644 --- a/tests/test_issues.py +++ b/tests/test_issues.py @@ -187,4 +187,42 @@ def test_issue_44_v0_4_3(): assert b() == 8 b = Fxp(2**64+6, False, 64, 0, overflow='wrap', scaling=2, bias=8) - assert b() == 2**64+6 \ No newline at end of file + assert b() == 2**64+6 + +def test_issue_53_v0_4_5(): + x = Fxp(2j, dtype = 'fxp-u4/0-complex') + z = x/2 + + assert z() == 1j + +def test_issue_55_v0_4_5(): + x = Fxp(0b11+0b11*1j, dtype = 'fxp-u2/0-complex') + z = x & 0b01 + +def test_issue_56_v0_4_5(): + arr_fxp = Fxp(np.array([[1, 2]])) + assert np.all(arr_fxp.bin() == np.array(['001', '010'])) + +def test_issue_58_v0_4_5(): + # datatype definition + TAP = Fxp(None, dtype='fxp-s32/24-complex') + SIGNAL = Fxp(None, dtype='fxp-s32/24-complex') + + # signal + signal = np.array([(1 + 1j), (1 - 1j), (-1 + 1j), (-1 - 1j)], dtype=complex) + signal_fxp = Fxp(signal).like(SIGNAL) + signal_fxp1 = Fxp(signal, dtype='fxp-s32/24-complex') + + # generate filter + filt = np.arange(0.1, 0.5, 0.1) + filt_fxp = Fxp(filt).like(TAP) + filt_fxp1 = Fxp(filt, dtype='fxp-s32/24') + + # convolve signal and filter + out = np.convolve(signal_fxp, filt_fxp, 'same') + out1 = np.convolve(signal_fxp1, filt_fxp1, 'same') + + assert np.all(signal_fxp() == signal_fxp1()) + assert np.all(filt_fxp() == filt_fxp1()) + assert np.all(out() == out1()) +