blob: 6e1ae23aa5c5f87d5e28c2581cbac319816bb312 [file] [log] [blame]
Philipp Lecd7baec2022-05-25 00:58:54 +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 tkinter import ttk
10from pydantic import confloat
11from dcs.config import ConfigObject, ui_create, ConfigControlFrame
12from typing import Dict
13import numpy as np
14from abc import abstractmethod, ABC
15
16from .signal import Signal
17from .chain import TxBasebandGenerator, RfBandGenerator, RxBasebandGenerator
18from .filter import Filter
19
20
21@ui_create
22class 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
43def 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
81class 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
106class 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
125class 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
134class 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
150class 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]