blob: 0a89f6e700f48f19ec157438206eff32bfcf7934 [file] [log] [blame]
Philipp Le4ea7d442022-05-23 22:05:25 +02001# 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
7from __future__ import annotations
8
9from typing import List, Dict
10from enum import Enum
11import numpy as np
12
13from .signal import Signal
14from .filter import Filter
15from .iq_mixer import IqTxBasebandGenerator
16from .symbols import Symbols
17
18
19class 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
92class 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
121class 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]