| Philipp Le | b0e45c8 | 2022-04-19 22:03:44 +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, LEFT, X, BOTH |
| 10 | from tkinter.constants import DISABLED, NORMAL |
| 11 | from pydantic import conint, confloat |
| 12 | from dcs.config import default_store, ConfigObject, ui_create, ConfigControlFrame |
| 13 | import numpy as np |
| 14 | import threading |
| 15 | import time |
| 16 | from typing import Optional |
| 17 | from dcs.frames.base import BaseFrame, Window |
| 18 | from dcs.frames.groups import Ch02Group |
| 19 | |
| 20 | import matplotlib |
| 21 | matplotlib.use('TkAgg') |
| 22 | from matplotlib.pyplot import Circle, Arrow |
| 23 | from matplotlib.figure import Figure |
| 24 | from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg |
| 25 | |
| 26 | |
| 27 | class ConfigCh02PhasorFrame(ConfigControlFrame): |
| 28 | frm1: ttk.Frame |
| 29 | wdg_real_input: ttk.Spinbox |
| 30 | wdg_imag_input: ttk.Spinbox |
| 31 | frm2: ttk.Frame |
| 32 | wdg_freq_input: ttk.Spinbox |
| 33 | |
| 34 | |
| 35 | @ui_create |
| 36 | class ConfigCh02Phasor(ConfigObject): |
| 37 | _KEY = 'ch02_phasor' |
| 38 | |
| 39 | real: confloat(ge=-5.0, lt=5.0, multiple_of=0.1) = 2.0 |
| 40 | imag: confloat(ge=-5.0, lt=5.0, multiple_of=0.1) = 0.0 |
| 41 | |
| 42 | freq: conint(ge=0, lt=5) = 1 |
| 43 | |
| 44 | def make_config_widget(self, parent: ttk.Widget) -> ConfigControlFrame: |
| 45 | frm = ConfigCh02PhasorFrame(parent) |
| 46 | frm.frm1 = ttk.Frame(frm, borderwidth=1, relief='raised') |
| 47 | frm.frm1.pack() |
| 48 | frm.frm2 = ttk.Frame(frm, borderwidth=1, relief='raised') |
| 49 | frm.frm2.pack() |
| 50 | |
| 51 | ttk.Label(frm.frm1, text='Real:').grid(row=0, column=0) |
| 52 | frm.wdg_real_input = self.ui_create_real(frm.frm1) |
| 53 | frm.wdg_real_input.grid(row=0, column=1) |
| 54 | |
| 55 | ttk.Label(frm.frm1, text='Imag:').grid(row=1, column=0) |
| 56 | frm.wdg_imag_input = self.ui_create_imag(frm.frm1) |
| 57 | frm.wdg_imag_input.grid(row=1, column=1) |
| 58 | |
| 59 | ttk.Label(frm.frm2, text='Frequency:').grid(row=0, column=0) |
| 60 | frm.wdg_freq_input = self.ui_create_freq(frm.frm2) |
| 61 | frm.wdg_freq_input.grid(row=0, column=1) |
| 62 | |
| 63 | return frm |
| 64 | |
| 65 | |
| 66 | class SimThread(threading.Thread): |
| 67 | def __init__(self, ch01_frm: ConfigCh02PhasorFrame): |
| 68 | super().__init__() |
| 69 | self.ch01_frm = ch01_frm |
| 70 | |
| 71 | def run(self): |
| 72 | t_max = 5 |
| 73 | n = 100 |
| 74 | for ctr in range(n): |
| 75 | self.ch01_frm.draw_phasor(ctr, n) |
| 76 | self.ch01_frm.draw_signal(ctr, n) |
| 77 | time.sleep(t_max / n) |
| 78 | self.ch01_frm.wdg_start_btn['state'] = NORMAL |
| 79 | |
| 80 | |
| 81 | class Ch02PhasorFrame(BaseFrame): |
| 82 | def __init__(self, *args, **kwargs): |
| 83 | super().__init__(*args, **kwargs) |
| 84 | |
| 85 | self._config = default_store().get_config(ConfigCh02Phasor) |
| 86 | |
| 87 | self.ctrl_frm = self._create_control() |
| 88 | self.ctrl_frm.pack(side=LEFT) |
| 89 | |
| 90 | phasor_frm = self._create_phasor_canvas() |
| 91 | phasor_frm.pack(fill=X) |
| 92 | |
| 93 | signal_frm = self._create_signal_canvas() |
| 94 | signal_frm.pack(expand=True, fill=BOTH) |
| 95 | |
| 96 | def _create_control(self) -> ConfigCh02PhasorFrame: |
| 97 | frm = self._config.make_config_widget(self) |
| 98 | |
| 99 | frm.wdg_real_input.listen_change(self._on_change) |
| 100 | frm.wdg_imag_input.listen_change(self._on_change) |
| 101 | |
| 102 | ttk.Label(frm.frm1, text='Absolute:').grid(row=2, column=0) |
| 103 | self.wdg_abs_output = ttk.Label(frm.frm1) |
| 104 | self.wdg_abs_output.grid(row=2, column=1) |
| 105 | |
| 106 | ttk.Label(frm.frm1, text='Angle:').grid(row=3, column=0) |
| 107 | self.wdg_angle_output = ttk.Label(frm.frm1) |
| 108 | self.wdg_angle_output.grid(row=3, column=1) |
| 109 | |
| 110 | self.wdg_start_btn = ttk.Button(frm.frm2, text='Start', command=self._on_start) |
| 111 | self.wdg_start_btn.grid(row=1, column=1) |
| 112 | |
| 113 | return frm |
| 114 | |
| 115 | def _on_change(self, _, __, ___): |
| 116 | default_store().save() |
| 117 | self.draw_phasor() |
| 118 | |
| 119 | def _on_start(self): |
| 120 | default_store().save() |
| 121 | self.wdg_start_btn['state'] = DISABLED |
| 122 | self._thread = SimThread(self) |
| 123 | self._thread.start() |
| 124 | |
| 125 | def _calc_sim_val(self, idx: int | np.ndarray, n: int) -> np.ndarray: |
| 126 | phas = self._config.real + 1j * self._config.imag |
| 127 | t = np.exp(1j * 2 * np.pi * self._config.freq * idx / n) |
| 128 | return phas * t |
| 129 | |
| 130 | def _create_phasor_canvas(self) -> ttk.Widget: |
| 131 | frm = ttk.Frame(self) |
| 132 | |
| 133 | self._phasor_fig = Figure(figsize=(5, 4), dpi=100) |
| 134 | |
| 135 | self._phasor_canvas = FigureCanvasTkAgg(self._phasor_fig, frm) |
| 136 | self._phasor_canvas.get_tk_widget().pack(expand=True, fill=BOTH) |
| 137 | self._phasor_canvas.mpl_connect('button_press_event', self._on_click_phasor) |
| 138 | |
| 139 | self.draw_phasor() |
| 140 | |
| 141 | return frm |
| 142 | |
| 143 | def draw_phasor(self, idx: Optional[int] = None, n: Optional[int] = None): |
| 144 | num = self._calc_sim_val(0, 1) |
| 145 | |
| 146 | self.wdg_abs_output.configure(text=str(np.abs(num))) |
| 147 | self.wdg_angle_output.configure(text=str(np.angle(num) * 180 / np.pi) + ' °') |
| 148 | |
| 149 | self._phasor_fig.clear() |
| 150 | ax = self._phasor_fig.add_subplot(1, 1, 1) |
| 151 | ax.set_xlim(-5.0, 5.0) |
| 152 | ax.set_ylim(-5.0, 5.0) |
| 153 | ax.set_xlabel('real') |
| 154 | ax.set_ylabel('imag') |
| 155 | |
| 156 | ax.add_artist(Circle((0, 0), np.abs(num), fill=None)) |
| 157 | ax.add_artist(Arrow(0, 0, self._config.real, self._config.imag, label='Phasor')) |
| 158 | ax.plot(self._config.real, self._config.imag, color='blue', marker='*') |
| 159 | |
| 160 | if idx is not None and n is not None: |
| 161 | x = self._calc_sim_val(idx, n) |
| 162 | ax.add_artist(Arrow(0, 0, x.real, x.imag, color='green', label='Current')) |
| 163 | ax.add_artist(Arrow(0, 0, x.real, 0, color='red', label='Current real')) |
| 164 | |
| 165 | ax.legend() |
| 166 | |
| 167 | self._phasor_canvas.draw() |
| 168 | |
| 169 | def _on_click_phasor(self, ev): |
| 170 | if ev.xdata is not None: |
| 171 | self._config.real = np.round(10 * ev.xdata) / 10. |
| 172 | self.ctrl_frm.wdg_real_input.sync_in() |
| 173 | if ev.ydata is not None: |
| 174 | self._config.imag = np.round(10 * ev.ydata) / 10. |
| 175 | self.ctrl_frm.wdg_imag_input.sync_in() |
| 176 | |
| 177 | default_store().save() |
| 178 | self.draw_phasor() |
| 179 | |
| 180 | def _create_signal_canvas(self) -> ttk.Widget: |
| 181 | frm = ttk.Frame(self) |
| 182 | |
| 183 | self._signal_fig = Figure(figsize=(5, 4), dpi=100) |
| 184 | |
| 185 | self._signal_canvas = FigureCanvasTkAgg(self._signal_fig, frm) |
| 186 | self._signal_canvas.get_tk_widget().pack(expand=True, fill=BOTH) |
| 187 | |
| 188 | return frm |
| 189 | |
| 190 | def draw_signal(self, idx: int, n: int): |
| 191 | self._signal_fig.clear() |
| 192 | ax = self._signal_fig.add_subplot(1, 1, 1) |
| 193 | ax.set_xlim(0.0, 1.0) |
| 194 | ax.set_ylim(-5.0, 5.0) |
| 195 | ax.set_xlabel('time') |
| 196 | ax.set_ylabel('value') |
| 197 | ax.set_title('Signal') |
| 198 | |
| 199 | t = np.linspace(0.0, 1.0, n) |
| 200 | t = t[:idx] |
| 201 | |
| 202 | x = self._calc_sim_val(np.arange(idx), n) |
| 203 | |
| 204 | ax.plot(t, x.real, color='red') |
| 205 | |
| 206 | self._signal_canvas.draw() |
| 207 | |
| 208 | |
| 209 | class Ch02PhasorWindow(Window): |
| 210 | GROUP = Ch02Group |
| 211 | TITLE = 'Phasor' |
| 212 | FRAME = Ch02PhasorFrame |
| 213 | |
| 214 | |
| 215 | if __name__ == '__main__': |
| 216 | Ch02PhasorWindow.main() |
| 217 | |