| Philipp Le | f7d1d45 | 2022-05-01 21:46:58 +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, Listbox, LEFT, BOTH |
| 10 | from pydantic import confloat |
| 11 | from dcs.config import default_store, ConfigObject, ui_create, ConfigControlFrame |
| 12 | import numpy as np |
| 13 | import scipy.signal.windows |
| 14 | from dcs.frames.base import BaseFrame, Window |
| 15 | from dcs.frames.groups import Ch04Group |
| 16 | from dcs.utils import swap_freq, abs_log_fft |
| 17 | from typing import List |
| 18 | from enum import Enum |
| 19 | |
| 20 | import matplotlib |
| 21 | matplotlib.use('TkAgg') |
| 22 | from matplotlib.figure import Figure |
| 23 | from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg |
| 24 | |
| 25 | |
| 26 | LENGTH = 256 |
| 27 | WINDOW_OVERSAMPLING = 64 |
| 28 | |
| 29 | |
| 30 | @ui_create |
| 31 | class Function(ConfigObject): |
| 32 | freq: confloat(ge=0.0, lt=128.0, multiple_of=0.01) = 1.0 |
| 33 | amplitude: confloat(ge=-100.0, lt=10.0, multiple_of=0.01) = 0.0 |
| 34 | phase: confloat(ge=-180.0, le=180.0, multiple_of=0.1) = 0.0 |
| 35 | |
| 36 | def make_config_widget(self, parent: ttk.Widget) -> ConfigControlFrame: |
| 37 | frm = ConfigControlFrame(parent, borderwidth=1, relief='raised') |
| 38 | |
| 39 | ttk.Label(frm, text='Frequency:').grid(row=0, column=0) |
| 40 | self.ui_create_freq(frm).grid(row=0, column=1) |
| 41 | ttk.Label(frm, text=f'x sampling frequency / {LENGTH}').grid(row=0, column=2) |
| 42 | |
| 43 | ttk.Label(frm, text='Amplitude:').grid(row=1, column=0) |
| 44 | self.ui_create_amplitude(frm).grid(row=1, column=1) |
| 45 | ttk.Label(frm, text='dB').grid(row=1, column=2) |
| 46 | |
| 47 | ttk.Label(frm, text='Phase:').grid(row=2, column=0) |
| 48 | self.ui_create_phase(frm).grid(row=2, column=1) |
| 49 | ttk.Label(frm, text='°').grid(row=2, column=2) |
| 50 | |
| 51 | return frm |
| 52 | |
| 53 | def get_lin_amplitude(self) -> np.ndarray: |
| 54 | return np.power(10, (self.amplitude / 20)) |
| 55 | |
| 56 | def calc_signal(self, t: np.ndarray = 0) -> np.ndarray: |
| 57 | phasor = self.get_lin_amplitude() * np.exp(1j * self.phase * np.pi / 180) |
| 58 | phi = np.exp(1j * 2 * np.pi * self.freq * t) |
| 59 | return np.real(phasor * phi) |
| 60 | |
| 61 | def make_title(self): |
| 62 | return f'f={self.freq}, A={self.get_lin_amplitude()}={self.amplitude}dB' |
| 63 | |
| 64 | |
| 65 | class WindowFunc(str, Enum): |
| 66 | Rectangular = 'Rectangular' |
| 67 | Hamming = 'Hamming' |
| 68 | Blackman = 'Blackman' |
| 69 | Bartlett = 'Bartlett' |
| 70 | Hann = 'Hann' |
| 71 | Gaussian_025 = 'Gaussian (sigma = 0.25)' |
| 72 | Gaussian_0125 = 'Gaussian (sigma = 0.125)' |
| 73 | Gaussian_00625 = 'Gaussian (sigma = 0.0625)' |
| 74 | |
| 75 | |
| 76 | class ConfigCh04WindowFrame(ConfigControlFrame): |
| 77 | wdg_funcs: Listbox |
| 78 | wdg_window: ttk.OptionMenu |
| 79 | wgd_win_sampling_delay: ttk.Spinbox |
| 80 | |
| 81 | |
| 82 | @ui_create |
| 83 | class ConfigCh04Window(ConfigObject): |
| 84 | _KEY = 'ch04_window' |
| 85 | |
| 86 | functions: List[Function] = [ |
| 87 | Function(), |
| 88 | ] |
| 89 | window: WindowFunc = WindowFunc.Rectangular |
| 90 | win_sampling_delay: confloat(ge=0.0, lt=1.0, multiple_of=0.01) = 0.25 |
| 91 | |
| 92 | def make_config_widget(self, parent: ttk.Widget) -> ConfigControlFrame: |
| 93 | frm = ConfigCh04WindowFrame(parent, borderwidth=1, relief='raised') |
| 94 | |
| 95 | frm1 = ttk.Frame(frm) |
| 96 | frm1.pack() |
| 97 | |
| 98 | ttk.Label(frm1, text='Window:').grid(row=0, column=0) |
| 99 | frm.wdg_window = self.ui_create_window_dropdown(frm1) |
| 100 | frm.wdg_window.grid(row=0, column=1) |
| 101 | |
| 102 | ttk.Label(frm1, text='Window Sampling Delay:').grid(row=1, column=0) |
| 103 | frm.wgd_win_sampling_delay = self.ui_create_win_sampling_delay(frm1) |
| 104 | frm.wgd_win_sampling_delay.grid(row=1, column=1) |
| 105 | ttk.Label(frm1, text='x 360°').grid(row=1, column=2) |
| 106 | |
| 107 | ttk.Label(frm, text='Functions:').pack() |
| 108 | frm.wdg_funcs = self.ui_create_functions_list(frm, lambda e: e.make_title()) |
| 109 | frm.wdg_funcs.pack() |
| 110 | |
| 111 | return frm |
| 112 | |
| 113 | |
| 114 | class Ch04WindowFrame(BaseFrame): |
| 115 | def __init__(self, *args, **kwargs): |
| 116 | super().__init__(*args, **kwargs) |
| 117 | |
| 118 | self._config: ConfigCh04Window = default_store().get_config(ConfigCh04Window) |
| 119 | |
| 120 | ctrl_frm = self._create_control() |
| 121 | ctrl_frm.pack(side=LEFT) |
| 122 | |
| 123 | signal_frm = self._create_signal_canvas() |
| 124 | signal_frm.pack(expand=True, fill=BOTH) |
| 125 | |
| 126 | def _create_control(self) -> ConfigCh04Window: |
| 127 | frm: ConfigCh04Window = self._config.make_config_widget(self) |
| 128 | frm.wdg_funcs.listen_change(self._on_change) |
| 129 | frm.wdg_window.listen_change(self._on_change) |
| 130 | frm.wgd_win_sampling_delay.listen_change(self._on_change) |
| 131 | return frm |
| 132 | |
| 133 | def _on_change(self, _, __, ___): |
| 134 | default_store().save() |
| 135 | self.draw_signal() |
| 136 | |
| 137 | def _create_signal_canvas(self) -> ttk.Widget: |
| 138 | frm = ttk.Frame(self) |
| 139 | |
| 140 | self._signal_fig = Figure(figsize=(12, 6), dpi=100) |
| 141 | |
| 142 | self._signal_canvas = FigureCanvasTkAgg(self._signal_fig, frm) |
| 143 | self._signal_canvas.get_tk_widget().pack(expand=True, fill=BOTH) |
| 144 | |
| 145 | self.draw_signal() |
| 146 | |
| 147 | return frm |
| 148 | |
| 149 | def draw_signal(self): |
| 150 | self._signal_fig.clear() |
| 151 | ax_sig_td = self._signal_fig.add_subplot(2, 2, 1) |
| 152 | ax_sig_td.set_xlim(-0.2, 1.2) |
| 153 | ax_sig_td.set_xlabel('time') |
| 154 | ax_sig_td.set_ylabel('value') |
| 155 | ax_sig_td.set_title('Sampled Time-Domain Signal') |
| 156 | ax_win_td = self._signal_fig.add_subplot(2, 2, 2) |
| 157 | ax_win_td.set_xlim(-0.2, 1.2) |
| 158 | ax_win_td.set_ylim(-0.2, 1.2) |
| 159 | ax_win_td.set_xlabel('time') |
| 160 | ax_win_td.set_ylabel('value') |
| 161 | ax_win_td.set_title('Window Time-Domain') |
| 162 | ax_sig_fd = self._signal_fig.add_subplot(2, 2, 3) |
| 163 | ax_sig_fd.set_xlim(-int(LENGTH/4) - 1, int(LENGTH/4) + 1) |
| 164 | ax_sig_fd.set_ylim(-120.0, 23.0) |
| 165 | ax_sig_fd.set_xlabel('frequency') |
| 166 | ax_sig_fd.set_ylabel('value (logarithmic)') |
| 167 | ax_sig_fd.set_title('Frequency-Domain of Windowed Signal') |
| 168 | ax_win_fd = self._signal_fig.add_subplot(2, 2, 4) |
| 169 | ax_win_fd.set_xlim(-int(LENGTH/4) - 1, int(LENGTH/4) + 1) |
| 170 | ax_win_fd.set_ylim(-180.0, 0.0) |
| 171 | ax_win_fd.set_xlabel('frequency') |
| 172 | ax_win_fd.set_ylabel('value (logarithmic)') |
| 173 | ax_win_fd.set_title('Window Frequency-Domain') |
| 174 | |
| 175 | t = np.arange(0, LENGTH, 1) / LENGTH |
| 176 | f = swap_freq(np.fft.fftfreq(t.shape[-1], 1.0/LENGTH)) |
| 177 | t_ovs = np.arange(0, LENGTH, (1/WINDOW_OVERSAMPLING)) / LENGTH |
| 178 | f_win_ovs = swap_freq(np.fft.fftfreq(t_ovs.shape[-1], 1.0/LENGTH)) |
| 179 | f_sig_ovs = swap_freq(np.fft.fftfreq(t_ovs.shape[-1], 1.0/(LENGTH * WINDOW_OVERSAMPLING))) |
| 180 | |
| 181 | x = np.zeros((len(self._config.functions), len(t))) |
| 182 | x_ovs = np.zeros((len(self._config.functions), len(t_ovs))) |
| 183 | |
| 184 | for index, func in enumerate(self._config.functions): |
| 185 | x[index, :] = func.calc_signal(t) |
| 186 | x_ovs[index, :] = func.calc_signal(t_ovs) |
| 187 | func = np.sum(x, axis=0) |
| 188 | func_ovs = np.sum(x_ovs, axis=0) |
| 189 | |
| 190 | func_ovs_fft = abs_log_fft(func_ovs) |
| 191 | |
| 192 | if self._config.window == WindowFunc.Rectangular: |
| 193 | win = np.ones((len(t),)) |
| 194 | elif self._config.window == WindowFunc.Hamming: |
| 195 | win = scipy.signal.windows.hamming(len(t)) |
| 196 | elif self._config.window == WindowFunc.Blackman: |
| 197 | win = scipy.signal.windows.blackman(len(t)) |
| 198 | elif self._config.window == WindowFunc.Bartlett: |
| 199 | win = scipy.signal.windows.bartlett(len(t)) |
| 200 | elif self._config.window == WindowFunc.Hann: |
| 201 | win = scipy.signal.windows.hann(len(t)) |
| 202 | elif self._config.window == WindowFunc.Gaussian_025: |
| 203 | win = scipy.signal.windows.gaussian(len(t), len(t)/4) |
| 204 | elif self._config.window == WindowFunc.Gaussian_0125: |
| 205 | win = scipy.signal.windows.gaussian(len(t), len(t)/8) |
| 206 | elif self._config.window == WindowFunc.Gaussian_00625: |
| 207 | win = scipy.signal.windows.gaussian(len(t), len(t)/16) |
| 208 | else: |
| 209 | raise Exception(f'Unsupported window') |
| 210 | |
| 211 | func_windowed = func * win |
| 212 | |
| 213 | win_ovs = np.zeros((LENGTH * WINDOW_OVERSAMPLING, )) |
| 214 | start_idx = int(LENGTH * WINDOW_OVERSAMPLING / 2) - int(LENGTH / 2) |
| 215 | for idx in range(len(t)): |
| 216 | win_ovs[start_idx + idx] = win[idx] * WINDOW_OVERSAMPLING |
| 217 | # win_ovs[idx * WINDOW_OVERSAMPLING] = win[idx] |
| 218 | |
| 219 | # win_fft = 20 * np.log10(np.abs(self._swap(np.fft.fft(win))) / len(win)) |
| 220 | win_ovs_fft = abs_log_fft(win_ovs) |
| 221 | win_fft = np.zeros((len(win), )) |
| 222 | win_180_fft = np.zeros((len(win), )) |
| 223 | win_tau_fft = np.zeros((len(win), )) |
| 224 | win_idx_delay = int(self._config.win_sampling_delay * WINDOW_OVERSAMPLING) |
| 225 | win_phase_delay = np.round(360.0 * win_idx_delay / WINDOW_OVERSAMPLING) |
| 226 | win_frac_delay = np.round(10000.0 * win_idx_delay / WINDOW_OVERSAMPLING) / 10000.0 |
| 227 | for idx in range(len(win)): |
| 228 | win_fft[idx] = win_ovs_fft[idx * WINDOW_OVERSAMPLING] |
| 229 | win_180_fft[idx] = win_ovs_fft[idx * WINDOW_OVERSAMPLING + int(WINDOW_OVERSAMPLING/2)] |
| 230 | win_tau_fft[idx] = win_ovs_fft[idx * WINDOW_OVERSAMPLING + win_idx_delay] |
| 231 | |
| 232 | func_windowed_fft = abs_log_fft(func_windowed) |
| 233 | |
| 234 | ax_sig_td.plot(t, func, label='Time-Domain', linestyle='solid', linewidth=1) |
| 235 | ax_sig_fd.plot(f, func_windowed_fft, label='Frequency-Domain Windows', linestyle='solid', linewidth=1) |
| 236 | ax_win_td.plot(t, win, label='Time-Domain', linestyle='solid', linewidth=1) |
| 237 | ax_win_fd.plot(f_win_ovs, win_ovs_fft, label='Window', linestyle='solid', linewidth=1) |
| 238 | ax_win_fd.plot(f, win_fft, label='Window Sampled', linestyle='dashed', linewidth=1) |
| 239 | ax_win_fd.plot(f, win_180_fft, label='Window Sampled (180° shift)', linestyle='dashed', linewidth=1) |
| 240 | ax_win_fd.plot(f, win_tau_fft, label=f'Window Sampled ({win_frac_delay} x 360° = {win_phase_delay}° shift)', linestyle='dashed', linewidth=1) |
| 241 | ax_win_fd.legend() |
| 242 | |
| 243 | self._signal_canvas.draw() |
| 244 | |
| 245 | |
| 246 | class Ch04WindowWindow(Window): |
| 247 | GROUP = Ch04Group |
| 248 | TITLE = 'Windowing' |
| 249 | FRAME = Ch04WindowFrame |
| 250 | |
| 251 | |
| 252 | if __name__ == '__main__': |
| 253 | Ch04WindowWindow.main() |
| 254 | |