diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..efa407c --- /dev/null +++ b/.gitignore @@ -0,0 +1,162 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ \ No newline at end of file diff --git a/const.py b/const.py index 60a899f..7c63d3c 100644 --- a/const.py +++ b/const.py @@ -1,42 +1,131 @@ from functools import cache +from typing import Tuple import numpy as np import const_utils +import matplotlib.pyplot as plt + +class ConstellationPoints(): + + def __init__(self, length:int=None, constellation_dict:dict=None, radius:Tuple[int, float]=1): + self._radius = radius + if constellation_dict: + self._constellation = constellation_dict + elif length: + self._length = length + + + def _generate_from_length(self, length): + const_utils.validate_intpow2(length, 'length') + object.__setattr__(self, '_length', length) + object.__setattr__(self, '_n', int(self._length/2+0.5)) + object.__setattr__(self, '_m', self._length // 2) + object.__setattr__(self, '_constellation', const_utils.bidict(generate_rectangular_constellation(self._length))) + + + def _generate_from_dict(self, constellation_dict): + const_utils.validate_coords(constellation_dict) + const_utils.validate_int(next(iter(constellation_dict.keys())), 'labels') + + # check if constellation is a one-to-one mapping + if len(set(constellation_dict.values())) != len(constellation_dict): + raise ValueError('constellation must be a one-to-one mapping of labels and coordinates') + + object.__setattr__(self, '_constellation', const_utils.bidict(constellation_dict)) + + object.__setattr__(self, '_length', len(constellation_dict)) + object.__setattr__(self, '_n', int(self._length/2+0.5)) + object.__setattr__(self, '_m', self._length // 2) + + + def plot(self): + # STUB ConstellationPots.plot() + raise NotImplementedError() + + + def __len__(self): + return self._length + + + @property + def symbols(self): + return dict(self._constellation).values() + + + @property + def labels(self): + return dict(self._constellation.inverse).values() + + + @property + def constellation(self): + return dict(self._constellation) + + + @property + def lookup(self): + return dict(self._constellation.inverse) + + + def get_symbol(self, label): + return self._constellation[label] + + + def get_label(self, symbol): + return self._constellation.inverse[symbol] + + + def __setattr__(self, name: str, value) -> None: + if name == '_constellation': + self._generate_from_dict(value) + elif name == '_length': + self._generate_from_length(value) + elif name == '_radius': + self._set_radius(value) + else: + object.__setattr__(self, name, value) + + + def _set_radius(self, value): + if isinstance(value, (int, float)): + object.__setattr__(self, '_radius',value) + else: + raise TypeError('radius must be an integer or a float') -# https://www.ieee802.org/3/bn/public/nov13/prodan_3bn_02_1113.pdf @cache -def gray_1d(k, label): +def gray_1d(k: int, label: int) -> int: const_utils.gray_1d_input_validation(k, label) - # special case + if k == 1: return 1 if label==0 else -1 - # all other cases -> recurse b0, new_symbol = const_utils.next_symbol(k, label) return (1-2*b0)*(2**(k-1)+gray_1d(k-1, new_symbol)) -def gray_2d(n, m, label): +def gray_2d(n: int, m: int, label: int) -> tuple: const_utils.gray_2d_input_validation(n, m, label) - # n or m is 0 - if (coord:=const_utils.gray_2d_handle_1d(n, m, label)) is not None: - return coord + + if m == 0: + return (gray_1d(n, label), 0) # it's a 1d case in disguise! - # all other cases symbol_i, symbol_q = const_utils.split_symbol(n, m, label) return (gray_1d(n, symbol_i), gray_1d(m, symbol_q)) + def hamming_dist(a, b): if not isinstance(a, int): raise ValueError('a must be an integer') if not isinstance(b, int): raise ValueError('b must be an integer') + def euclidean_distance(coord1, coord2): if isinstance(coord1, int): return abs(coord1 - coord2) return np.sqrt((coord1[0] - coord2[0])**2 + (coord1[1] - coord2[1])**2) + def find_nearest(coord, coords): min_distance = float('inf') nearest_symbols = [] @@ -50,12 +139,12 @@ def find_nearest(coord, coords): nearest_symbols = [c] elif dist == min_distance: nearest_symbols.append(c) - # else: - # pass return nearest_symbols + def gray_penalty(constellation): + raise NotImplementedError # constellation: {label_0:coordinate_0, label_1:coordinate_1, .., label_2^n-1:coordinate_2^n-1} # 2^n-QAM -> 2^n symbols S_i, where i=0,1,..2^n-1, ex. S_0 = (-3,-3) or S_0 = -2 @@ -64,12 +153,11 @@ def gray_penalty(constellation): # l(S): label given by mapping -> inverse of gray_Qd -> generate all symbols/labels for given constellation # wt(l_1, l_2), hamming distance btw. two labels - t = len(constellation) + t = constellation['meta']['len'] inverted_constellation = {tuple(symbol):label for label,symbol in constellation.items() if label != 'meta'} # -> invert constellation dict syms = [symbol for _, symbol in constellation.items()] - if (n:=np.log2(t)) != int(n): - raise ValueError('only constellations with 2^n points supported') + const_utils.validate_intpow2() G = 0 for li, si in constellation.items(): @@ -81,51 +169,61 @@ def gray_penalty(constellation): return G -def find_rows_columns(coordinates): - if not coordinates: - return 0, 0 - min_row = min(coord[0] for coord in coordinates.values()) - max_row = max(coord[0] for coord in coordinates.values()) - row_spacing = abs(coordinates[next(iter(coordinates))][0] - coordinates[next(iter(coordinates))][0]) +def generate_rectangular_constellation(length: int): + # const_utils.validate_int(length, 'length') + const_utils.validate_intpow2(length, 'length') + lengthexp = int(np.log2(length)) + n = int(lengthexp/2+0.5) # ceil + m = lengthexp // 2 # floor + return {label:gray_2d(n, m, label) for label in range(length)} - min_col = min(coord[1] for coord in coordinates.values()) - max_col = max(coord[1] for coord in coordinates.values()) - col_spacing = abs(coordinates[next(iter(coordinates))][1] - coordinates[next(iter(coordinates))][1]) - num_rows = (max_row - min_row) // row_spacing + 1 - num_cols = (max_col - min_col) // col_spacing + 1 +def transform_rectangular_mapping(constellation: ConstellationPoints): + n, m = constellation['meta'] # TODO def generate_rectangular_constellation(n) + # r, c = find_rows_columns(constellation) - return num_rows, num_cols + # # example: 32-qam -> 2^(2n+1) -> n = 2 -def transform_rectangular_mapping(constellation): - n, m = find_rows_columns(constellation) + # two_n1 = np.log2(len(constellation)) + # if int(two_n1) != two_n1: + # raise ValueError('only constellations with 2^m points allowed') - # example: 32-qam -> 2^(2n+1) -> n = 2 + # if r == 1: # 1D-constellation + # return constellation - two_n1 = np.log2(len(constellation)) - if int(two_n1) != two_n1: - raise ValueError('only constellations with 2^m points allowed') + # # get n and m for one quadrant + # n = c/2 + # m = r/2 - if n == 1 or m == 1: # 1D-constellation - return constellation + # const_utils._validate_integer(n, 'n') + # const_utils._validate_integer(m, 'm') - n = c/2 - m = r/2 - - const_utils._validate_integer(n, 'n') - const_utils._validate_integer(m, 'm') + # [ ] set transformed flag in constellation? if n == m: # square 2^(2n)-QAM return constellation if n == 2 and m == 1: # rectangular 8-QAM (4*2) return transform_8QAM(constellation) - elif n == m+2: + + elif n == m+2: # REVIEW m+2 correct? p. 7 new_const = {} s = 2**(n-1) for label, symbol in constellation.items(): + # STUB transfrom_rectangular_mapping(constellation) -> generalized non-square-QAM + raise NotImplementedError() + + else: + # TODO define what should happen here + return constellation + # 2^(2n+1) + # for 32-QAM: 2^5 -> n = 2 + # rectangular grid of 4*8 -> 2*4 per quadrant + + # for 128-QAM: 2^7 -> n = 3 + # rectangular grid of def transform_8QAM(constellation): @@ -141,8 +239,13 @@ def transform_8QAM(constellation): return new_const# rectangular 2^(m+n)-QAM - if __name__ == '__main__': - # print(gray_1d(2, 0)) - print(gray_2d(2, 3, 4)) - print(gray_2d(0, 2, 4)) + const0 = ConstellationPoints() + const128 = ConstellationPoints(length=128) + const_ext = ConstellationPoints(constellation_dict=generate_rectangular_constellation(64)) + + print(vars(const0)) + print(vars(const128)) + print(vars(const_ext)) + + # constellation_128 = {label:gray_2d(3, 4, label) for label in range(128)} diff --git a/const_utils.py b/const_utils.py index 76ad1dc..84a9b4f 100644 --- a/const_utils.py +++ b/const_utils.py @@ -1,12 +1,11 @@ -from const import gray_1d +import numpy as np - -def _validate_integer(value, name): +def validate_int(value, name): if not isinstance(value, int): raise ValueError(f'{name} must be an integer') -def _validate_range(value, name, min_val=None, max_val=None): +def validate_range(value, name, min_val=None, max_val=None): if max_val is not None and value > max_val: raise ValueError(f'{name} must be \u2265 {max_val}') if min_val is not None and value < min_val: @@ -14,10 +13,10 @@ def _validate_range(value, name, min_val=None, max_val=None): def gray_1d_input_validation(k, symbol): - _validate_integer(symbol, 'symbol') - _validate_integer(k, 'k') - _validate_range(k, 'k', min_val=1) - _validate_range(symbol, 'symbol', min_val=0, max_val=2**k-1) + validate_int(symbol, 'symbol') + validate_int(k, 'k') + validate_range(k, 'k', min_val=1) + validate_range(symbol, 'symbol', min_val=0, max_val=2**k-1) def next_symbol(k, symbol): @@ -28,25 +27,83 @@ def next_symbol(k, symbol): def gray_2d_input_validation(n, m, symbol): - _validate_integer(n, 'n') - _validate_integer(m, 'm') - _validate_integer(symbol, 'symbol') - _validate_range(n, 'n', min_val=0) - min_m = 0 if n > 0 else 1 - _validate_range(m, 'm', min_val=min_m) - _validate_range(symbol, 'symbol', min_val=0, max_val=2**(m+n)-1) - - -def gray_2d_handle_1d(n, m, symbol): - n,m,swapped = m,n,True if n == 0 else n,m,False # swap n and m if n is zero -> if only one > 0, it's n - if m == 0: - return (gray_1d(n, symbol),None) if swapped else (None,gray_1d(n, symbol)) - else: - return None + validate_int(n, 'n') + validate_int(m, 'm') + validate_int(symbol, 'symbol') + validate_range(m, 'm', min_val=0) + validate_range(n, 'n', min_val=m) + validate_range(symbol, 'symbol', min_val=0, max_val=2**(m+n)-1) def split_symbol(n, m, symbol): bits = format(symbol, 'b').zfill(n+m) symbol_i = int(bits[:n], 2) symbol_q = int(bits[n:], 2) - return symbol_i,symbol_q \ No newline at end of file + return symbol_i,symbol_q + + +def validate_intpow2(value, name): + exponent = np.log2(value) + if exponent != int(exponent): + raise ValueError(f'{name} must be an integer power of 2') + + +def validate_coords(constellation_dict): + # TODO validate all coords, not only first + # bit of a hack, only looking at first element + if not isinstance(temp:=next(iter(constellation_dict.values())), tuple) or not isinstance(temp[0], int): + raise ValueError('coords must be tuples of integers') + + +# https://www.ieee802.org/3/bn/public/nov13/prodan_3bn_02_1113.pdf + +class bidict(dict): + ''' + ### Summary: + A bidirectional dictionary (bidict) that allows bidirectional mapping between keys and values. + + ### Explanation: + This class extends the functionality of a standard dictionary to maintain a bidirectional mapping between keys and values. It provides methods to set, delete items, and retrieve the inverse mapping efficiently. + + ### Methods: + - `__init__(*args, **kwargs)`: Initializes the bidict with optional initial key-value pairs. + - `__setitem__(key, value)`: Sets a key-value pair in the bidict, updating the inverse mapping. + - `__delitem__(key)`: Deletes a key-value pair from the bidict and updates the inverse mapping. + - `__repr__()`: Returns a string representation of the bidict instance. + + ### Attributes: + - `inverse`: A dictionary that stores the inverse mapping of values to keys. + + ### Returns: + - No explicit return value for methods. The bidict instance is modified in place. + + ### Raises: + - No specific exceptions are raised by the methods in this class. + + ### Source: + bidict by user 'Basj' at https://stackoverflow.com/a/21894086 (CC BY-SA 4.0) + ''' + + def __init__(self, *args, **kwargs): + super(bidict, self).__init__(*args, **kwargs) + self.inverse = {} + for key, value in self.items(): + self.inverse.setdefault(value, []).append(key) + + + def __setitem__(self, key, value): + if key in self: + self.inverse[self[key]].remove(key) + super(bidict, self).__setitem__(key, value) + self.inverse.setdefault(value, []).append(key) + + + def __delitem__(self, key): + self.inverse.setdefault(self[key], []).remove(key) + if self[key] in self.inverse and not self.inverse[self[key]]: + del self.inverse[self[key]] + super(bidict, self).__delitem__(key) + + + def __repr__(self): + return f'