| Philipp Le | 4ea7d44 | 2022-05-23 22:05:25 +0200 | [diff] [blame^] | 1 | # SPDX-License-Identifier: MPL-2.0 |
| 2 | # Copyright (c) 2022 Philipp Le <philipp@philipple.de>. |
| 3 | # This Source Code Form is subject to the terms of the Mozilla Public |
| 4 | # License, v. 2.0. If a copy of the MPL was not distributed with this |
| 5 | # file, You can obtain one at https://mozilla.org/MPL/2.0/. |
| 6 | |
| 7 | from __future__ import annotations |
| 8 | |
| 9 | from typing import List, Dict |
| 10 | from enum import Enum |
| 11 | import numpy as np |
| 12 | |
| 13 | from .signal import Signal |
| 14 | from .filter import Filter |
| 15 | from .iq_mixer import IqTxBasebandGenerator |
| 16 | from .symbols import Symbols |
| 17 | |
| 18 | |
| 19 | class ModulationMethod(str, Enum): |
| 20 | ASK = 'ASK' |
| 21 | BPSK = 'BPSK' |
| 22 | QPSK = 'QPSK' |
| 23 | PSK8 = '8-PSK' |
| 24 | QAM16 = '16-QAM' |
| 25 | QAM64 = '64-QAM' |
| 26 | QAM256 = '256-QAM' |
| 27 | |
| 28 | @classmethod |
| 29 | def _make_constellation_psk(cls, points: int) -> List[complex]: |
| 30 | prim_root = np.exp(1j * 2 * np.pi / points) |
| 31 | return np.power(prim_root, np.arange(0, points)) |
| 32 | |
| 33 | @classmethod |
| 34 | def _make_constellation_qam(cls, points: int) -> List[complex]: |
| 35 | dim = int(np.sqrt(points)) |
| 36 | const = [] |
| 37 | for x in np.linspace(-1.0, 1.0, dim): |
| 38 | for y in np.linspace(-1.0, 1.0, dim): |
| 39 | sym = x + (1j * y) |
| 40 | const.append(sym) |
| 41 | return const |
| 42 | |
| 43 | def make_constellation(self) -> List[complex]: |
| 44 | if self == ModulationMethod.ASK: |
| 45 | return [0.2, 1.0] |
| 46 | elif self == ModulationMethod.BPSK: |
| 47 | return self._make_constellation_psk(2) |
| 48 | elif self == ModulationMethod.QPSK: |
| 49 | return self._make_constellation_psk(4) |
| 50 | elif self == ModulationMethod.PSK8: |
| 51 | return self._make_constellation_psk(8) |
| 52 | elif self == ModulationMethod.QAM16: |
| 53 | return self._make_constellation_qam(16) |
| 54 | elif self == ModulationMethod.QAM64: |
| 55 | return self._make_constellation_qam(64) |
| 56 | elif self == ModulationMethod.QAM256: |
| 57 | return self._make_constellation_qam(256) |
| 58 | else: |
| 59 | raise Exception('Unknown modulation') |
| 60 | |
| 61 | def bits_per_symbol(self) -> int: |
| 62 | if self == ModulationMethod.ASK: |
| 63 | return 1 |
| 64 | elif self == ModulationMethod.BPSK: |
| 65 | return 1 |
| 66 | elif self == ModulationMethod.QPSK: |
| 67 | return 2 |
| 68 | elif self == ModulationMethod.PSK8: |
| 69 | return 3 |
| 70 | elif self == ModulationMethod.QAM16: |
| 71 | return 4 |
| 72 | elif self == ModulationMethod.QAM64: |
| 73 | return 6 |
| 74 | elif self == ModulationMethod.QAM256: |
| 75 | return 8 |
| 76 | else: |
| 77 | raise Exception('Unknown modulation') |
| 78 | |
| 79 | def make_constellation_str(self) -> List[str]: |
| 80 | return [f'{idx:0{self.bits_per_symbol()}b}' for idx in range(2**self.bits_per_symbol())] |
| 81 | |
| 82 | def make_constellation_map(self) -> Dict[int, complex]: |
| 83 | constell = self.make_constellation() |
| 84 | return {idx: constell[idx] for idx in range(2 ** self.bits_per_symbol())} |
| 85 | |
| 86 | def make_constellation_map_str(self) -> Dict[str, complex]: |
| 87 | constell = self.make_constellation() |
| 88 | str_list = self.make_constellation_str() |
| 89 | return {str_list[idx]: constell[idx] for idx in range(2 ** self.bits_per_symbol())} |
| 90 | |
| 91 | |
| 92 | class QamBasebandModulator: |
| 93 | def __init__(self, symbol_rate: float, method: ModulationMethod, baseband_filter: Filter): |
| 94 | self.symbol_rate = symbol_rate |
| 95 | self.method = method |
| 96 | self.baseband_filter = baseband_filter |
| 97 | |
| 98 | def _to_iq_symbols(self, symbols: Symbols) -> List[complex]: |
| 99 | lut = self.method.make_constellation() |
| 100 | return [lut[x] for x in symbols.symbols] |
| 101 | |
| 102 | def _make_t_vec(self, symbols: Symbols, sample_rate: float) -> np.ndarray: |
| 103 | t_end = len(symbols) * (1 / self.symbol_rate) |
| 104 | n_smpls = int(t_end * sample_rate) |
| 105 | return np.arange(0, n_smpls, 1) / sample_rate |
| 106 | |
| 107 | def calc_tx_baseband_signal(self, symbols: Symbols, sample_rate: float) -> Signal: |
| 108 | reenc_syms = symbols.reencode(self.method.bits_per_symbol()) |
| 109 | syms = self._to_iq_symbols(reenc_syms) |
| 110 | t = self._make_t_vec(reenc_syms, sample_rate) |
| 111 | idx_vec = [int(x) for x in np.floor(t * self.symbol_rate)] |
| 112 | return self.baseband_filter.filter( |
| 113 | Signal( |
| 114 | t=t, |
| 115 | signal=np.array([syms[idx] if 0 <= idx < len(syms) else 0 for idx in idx_vec]), |
| 116 | sample_rate=sample_rate, |
| 117 | ) |
| 118 | ) |
| 119 | |
| 120 | |
| 121 | class QamBasebandGenerator(IqTxBasebandGenerator): |
| 122 | def __init__(self, symbols: Symbols, qam_mod: QamBasebandModulator): |
| 123 | self.symbols = symbols |
| 124 | self.qam_mod = qam_mod |
| 125 | self._lazy_eval_store: Dict[float, Signal] = {} |
| 126 | |
| 127 | def generate_tx_baseband_signal(self, sample_rate: float) -> Signal: |
| 128 | if sample_rate not in self._lazy_eval_store: |
| 129 | self._lazy_eval_store[sample_rate] = self.qam_mod.calc_tx_baseband_signal(self.symbols, sample_rate) |
| 130 | return self._lazy_eval_store[sample_rate] |