| Philipp Le | cd7baec | 2022-05-25 00:58:54 +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 tkinter import ttk |
| 10 | from pydantic import confloat |
| 11 | from dcs.config import ConfigObject, ui_create, ConfigControlFrame |
| 12 | from typing import Dict |
| 13 | import numpy as np |
| 14 | from abc import abstractmethod, ABC |
| 15 | |
| 16 | from .signal import Signal |
| 17 | from .chain import TxBasebandGenerator, RfBandGenerator, RxBasebandGenerator |
| 18 | from .filter import Filter |
| 19 | |
| 20 | |
| 21 | @ui_create |
| 22 | class IqOscillatorBase(ConfigObject, ABC): |
| 23 | freq: confloat(ge=0) = 100.0 |
| 24 | phase: confloat(ge=-180.0, le=180.0, multiple_of=0.1) = 0.0 |
| 25 | phase_imbalance: confloat(ge=-90.0, le=90.0, multiple_of=0.01) = 0.0 |
| 26 | amplitude_imbalance: confloat(ge=-1.0, le=1.0, multiple_of=0.001) = 0.0 |
| 27 | |
| 28 | def calc_signal(self, t: np.ndarray) -> np.ndarray: |
| 29 | phase_rad = self.phase * np.pi / 180 |
| 30 | phi = 2 * np.pi * self.freq * t |
| 31 | out_i = np.cos(phi + phase_rad) |
| 32 | out_q = (1 + self.amplitude_imbalance) * np.sin(phi + phase_rad + self.phase_imbalance) |
| 33 | return out_i + (1j * out_q) |
| 34 | |
| 35 | @abstractmethod |
| 36 | def make_config_widget(self, parent: ttk.Widget) -> ConfigControlFrame: |
| 37 | ... |
| 38 | |
| 39 | def make_title(self): |
| 40 | return f'f={self.freq}, phi={self.phase}°' |
| 41 | |
| 42 | |
| 43 | def create_iq_oscillator_class(sample_rate: int, imbalance_wdg: bool = True): |
| 44 | @ui_create |
| 45 | class _IqOscillator(IqOscillatorBase): |
| 46 | freq: confloat(ge=0, lt=sample_rate/4, multiple_of=(4.0/sample_rate)) = 100.0 |
| 47 | |
| 48 | def make_config_widget(self, parent: ttk.Widget) -> ConfigControlFrame: |
| 49 | frm = ConfigControlFrame(parent) |
| 50 | |
| 51 | ttk.Label(frm, text='Frequency:').grid(row=0, column=0) |
| 52 | w = self.ui_create_freq(frm) |
| 53 | frm.add_widget(w) |
| 54 | w.grid(row=0, column=1) |
| 55 | |
| 56 | ttk.Label(frm, text='Phase:').grid(row=1, column=0) |
| 57 | w = self.ui_create_phase(frm) |
| 58 | frm.add_widget(w) |
| 59 | w.grid(row=1, column=1) |
| 60 | ttk.Label(frm, text='°').grid(row=1, column=2) |
| 61 | |
| 62 | if imbalance_wdg: |
| 63 | ttk.Label(frm, text='Phase Imbalance:').grid(row=2, column=0) |
| 64 | w = self.ui_create_phase_imbalance(frm) |
| 65 | frm.add_widget(w) |
| 66 | w.grid(row=2, column=1) |
| 67 | ttk.Label(frm, text='°').grid(row=2, column=2) |
| 68 | |
| 69 | ttk.Label(frm, text='Amplitude Imbalance:').grid(row=3, column=0) |
| 70 | w = self.ui_create_amplitude_imbalance(frm) |
| 71 | frm.add_widget(w) |
| 72 | w.grid(row=3, column=1) |
| 73 | ttk.Label(frm, text='Rel. Error').grid(row=3, column=2) |
| 74 | |
| 75 | return frm |
| 76 | |
| 77 | _IqOscillator.update_forward_refs(sample_rate=sample_rate) |
| 78 | return _IqOscillator |
| 79 | |
| 80 | |
| 81 | class IqUpMixer: |
| 82 | def __init__(self, tx_osc: IqOscillatorBase, noise_dBc: float, random_seed: int): |
| 83 | self.tx_osc = tx_osc |
| 84 | self.noise_dBc = noise_dBc |
| 85 | self.random_seed = random_seed |
| 86 | |
| 87 | def _make_noise(self, t: np.ndarray) -> np.ndarray: |
| 88 | np.random.seed(self.random_seed) |
| 89 | noise_rms = np.power(10, (self.noise_dBc / 20)) |
| 90 | noise_sigma = np.sqrt(2) * noise_rms |
| 91 | return np.random.normal(0, noise_sigma, len(t)) |
| 92 | |
| 93 | def calc_rf_signal(self, tx_baseband_signal: Signal) -> Signal: |
| 94 | t = tx_baseband_signal.t |
| 95 | baseband = tx_baseband_signal.signal |
| 96 | osc = self.tx_osc.calc_signal(t) |
| 97 | i_mixed = np.real(baseband) * np.real(osc) |
| 98 | q_mixed = np.imag(baseband) * np.imag(osc) |
| 99 | return Signal( |
| 100 | t=t, |
| 101 | signal=i_mixed - q_mixed + self._make_noise(t), |
| 102 | sample_rate=tx_baseband_signal.sample_rate, |
| 103 | ) |
| 104 | |
| 105 | |
| 106 | class IqDownMixer: |
| 107 | def __init__(self, rx_osc: IqOscillatorBase, baseband_filter: Filter): |
| 108 | self.rx_osc = rx_osc |
| 109 | self.baseband_filter = baseband_filter |
| 110 | |
| 111 | def calc_rx_baseband_signal(self, rf_signal: Signal) -> Signal: |
| 112 | osc = self.rx_osc.calc_signal(rf_signal.t) |
| 113 | i_mixed = 2 * rf_signal.signal * np.real(osc) |
| 114 | q_mixed = 2 * rf_signal.signal * np.imag(osc) |
| 115 | base = i_mixed - (1j * q_mixed) |
| 116 | return self.baseband_filter.filter( |
| 117 | Signal( |
| 118 | t=rf_signal.t, |
| 119 | signal=base, |
| 120 | sample_rate=rf_signal.sample_rate, |
| 121 | ) |
| 122 | ) |
| 123 | |
| 124 | |
| 125 | class IqTxBasebandGenerator(TxBasebandGenerator, ABC): |
| 126 | @abstractmethod |
| 127 | def generate_tx_baseband_signal(self, sample_rate: float) -> Signal: |
| 128 | ... |
| 129 | |
| 130 | def make_rf_band_generator(self, up_mix: IqUpMixer) -> IqRfBandGenerator: |
| 131 | return IqRfBandGenerator(self, up_mix) |
| 132 | |
| 133 | |
| 134 | class IqRfBandGenerator(RfBandGenerator): |
| 135 | def __init__(self, tx_baseband_generator: IqTxBasebandGenerator, up_mix: IqUpMixer): |
| 136 | self.tx_baseband_generator = tx_baseband_generator |
| 137 | self.up_mix = up_mix |
| 138 | self._lazy_eval_store: Dict[float, Signal] = {} |
| 139 | |
| 140 | def generate_rf_signal(self, sample_rate: float) -> Signal: |
| 141 | if sample_rate not in self._lazy_eval_store: |
| 142 | tx_baseband_signal = self.tx_baseband_generator.generate_tx_baseband_signal(sample_rate) |
| 143 | self._lazy_eval_store[sample_rate] = self.up_mix.calc_rf_signal(tx_baseband_signal) |
| 144 | return self._lazy_eval_store[sample_rate] |
| 145 | |
| 146 | def make_iq_down_mixer(self, down_mix: IqDownMixer) -> IqRxBasebandGenerator: |
| 147 | return IqRxBasebandGenerator(self, down_mix) |
| 148 | |
| 149 | |
| 150 | class IqRxBasebandGenerator(RxBasebandGenerator): |
| 151 | def __init__(self, rf_generator: IqRfBandGenerator, down_mix: IqDownMixer): |
| 152 | self.rf_generator = rf_generator |
| 153 | self.down_mix = down_mix |
| 154 | self._lazy_eval_store: Dict[float, Signal] = {} |
| 155 | |
| 156 | def generate_rx_baseband_signal(self, sample_rate: float) -> Signal: |
| 157 | if sample_rate not in self._lazy_eval_store: |
| 158 | rf_signal = self.rf_generator.generate_rf_signal(sample_rate) |
| 159 | self._lazy_eval_store[sample_rate] = self.down_mix.calc_rx_baseband_signal(rf_signal) |
| 160 | return self._lazy_eval_store[sample_rate] |