blob: 47ac15357b99fb9e97211c0167ea0035fa10a035 [file] [log] [blame]
Philipp Leb0e45c82022-04-19 22:03:44 +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, LEFT, X, BOTH
10from tkinter.constants import DISABLED, NORMAL
11from pydantic import conint, confloat
12from dcs.config import default_store, ConfigObject, ui_create, ConfigControlFrame
13import numpy as np
14import threading
15import time
16from typing import Optional
17from dcs.frames.base import BaseFrame, Window
18from dcs.frames.groups import Ch02Group
19
20import matplotlib
21matplotlib.use('TkAgg')
22from matplotlib.pyplot import Circle, Arrow
23from matplotlib.figure import Figure
24from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
25
26
27class 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
36class 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
66class 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
81class 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
209class Ch02PhasorWindow(Window):
210 GROUP = Ch02Group
211 TITLE = 'Phasor'
212 FRAME = Ch02PhasorFrame
213
214
215if __name__ == '__main__':
216 Ch02PhasorWindow.main()
217