feat: Ch02 Phasor
Change-Id: Ifb6a8a0c48073467bf35111d0f5ec3600705c32c
diff --git a/dcs/frames/__init__.py b/dcs/frames/__init__.py
index 4e5dd51..533546c 100644
--- a/dcs/frames/__init__.py
+++ b/dcs/frames/__init__.py
@@ -7,9 +7,12 @@
from typing import List, Type
from . import base
+from .ch02_phasor import Ch02PhasorWindow
+
def get_windows() -> List[Type[base.Window]]:
return [
+ Ch02PhasorWindow,
]
diff --git a/dcs/frames/ch02_phasor.py b/dcs/frames/ch02_phasor.py
new file mode 100644
index 0000000..47ac153
--- /dev/null
+++ b/dcs/frames/ch02_phasor.py
@@ -0,0 +1,217 @@
+# SPDX-License-Identifier: MPL-2.0
+# Copyright (c) 2022 Philipp Le <philipp@philipple.de>.
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
+from __future__ import annotations
+
+from tkinter import ttk, LEFT, X, BOTH
+from tkinter.constants import DISABLED, NORMAL
+from pydantic import conint, confloat
+from dcs.config import default_store, ConfigObject, ui_create, ConfigControlFrame
+import numpy as np
+import threading
+import time
+from typing import Optional
+from dcs.frames.base import BaseFrame, Window
+from dcs.frames.groups import Ch02Group
+
+import matplotlib
+matplotlib.use('TkAgg')
+from matplotlib.pyplot import Circle, Arrow
+from matplotlib.figure import Figure
+from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
+
+
+class ConfigCh02PhasorFrame(ConfigControlFrame):
+ frm1: ttk.Frame
+ wdg_real_input: ttk.Spinbox
+ wdg_imag_input: ttk.Spinbox
+ frm2: ttk.Frame
+ wdg_freq_input: ttk.Spinbox
+
+
+@ui_create
+class ConfigCh02Phasor(ConfigObject):
+ _KEY = 'ch02_phasor'
+
+ real: confloat(ge=-5.0, lt=5.0, multiple_of=0.1) = 2.0
+ imag: confloat(ge=-5.0, lt=5.0, multiple_of=0.1) = 0.0
+
+ freq: conint(ge=0, lt=5) = 1
+
+ def make_config_widget(self, parent: ttk.Widget) -> ConfigControlFrame:
+ frm = ConfigCh02PhasorFrame(parent)
+ frm.frm1 = ttk.Frame(frm, borderwidth=1, relief='raised')
+ frm.frm1.pack()
+ frm.frm2 = ttk.Frame(frm, borderwidth=1, relief='raised')
+ frm.frm2.pack()
+
+ ttk.Label(frm.frm1, text='Real:').grid(row=0, column=0)
+ frm.wdg_real_input = self.ui_create_real(frm.frm1)
+ frm.wdg_real_input.grid(row=0, column=1)
+
+ ttk.Label(frm.frm1, text='Imag:').grid(row=1, column=0)
+ frm.wdg_imag_input = self.ui_create_imag(frm.frm1)
+ frm.wdg_imag_input.grid(row=1, column=1)
+
+ ttk.Label(frm.frm2, text='Frequency:').grid(row=0, column=0)
+ frm.wdg_freq_input = self.ui_create_freq(frm.frm2)
+ frm.wdg_freq_input.grid(row=0, column=1)
+
+ return frm
+
+
+class SimThread(threading.Thread):
+ def __init__(self, ch01_frm: ConfigCh02PhasorFrame):
+ super().__init__()
+ self.ch01_frm = ch01_frm
+
+ def run(self):
+ t_max = 5
+ n = 100
+ for ctr in range(n):
+ self.ch01_frm.draw_phasor(ctr, n)
+ self.ch01_frm.draw_signal(ctr, n)
+ time.sleep(t_max / n)
+ self.ch01_frm.wdg_start_btn['state'] = NORMAL
+
+
+class Ch02PhasorFrame(BaseFrame):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ self._config = default_store().get_config(ConfigCh02Phasor)
+
+ self.ctrl_frm = self._create_control()
+ self.ctrl_frm.pack(side=LEFT)
+
+ phasor_frm = self._create_phasor_canvas()
+ phasor_frm.pack(fill=X)
+
+ signal_frm = self._create_signal_canvas()
+ signal_frm.pack(expand=True, fill=BOTH)
+
+ def _create_control(self) -> ConfigCh02PhasorFrame:
+ frm = self._config.make_config_widget(self)
+
+ frm.wdg_real_input.listen_change(self._on_change)
+ frm.wdg_imag_input.listen_change(self._on_change)
+
+ ttk.Label(frm.frm1, text='Absolute:').grid(row=2, column=0)
+ self.wdg_abs_output = ttk.Label(frm.frm1)
+ self.wdg_abs_output.grid(row=2, column=1)
+
+ ttk.Label(frm.frm1, text='Angle:').grid(row=3, column=0)
+ self.wdg_angle_output = ttk.Label(frm.frm1)
+ self.wdg_angle_output.grid(row=3, column=1)
+
+ self.wdg_start_btn = ttk.Button(frm.frm2, text='Start', command=self._on_start)
+ self.wdg_start_btn.grid(row=1, column=1)
+
+ return frm
+
+ def _on_change(self, _, __, ___):
+ default_store().save()
+ self.draw_phasor()
+
+ def _on_start(self):
+ default_store().save()
+ self.wdg_start_btn['state'] = DISABLED
+ self._thread = SimThread(self)
+ self._thread.start()
+
+ def _calc_sim_val(self, idx: int | np.ndarray, n: int) -> np.ndarray:
+ phas = self._config.real + 1j * self._config.imag
+ t = np.exp(1j * 2 * np.pi * self._config.freq * idx / n)
+ return phas * t
+
+ def _create_phasor_canvas(self) -> ttk.Widget:
+ frm = ttk.Frame(self)
+
+ self._phasor_fig = Figure(figsize=(5, 4), dpi=100)
+
+ self._phasor_canvas = FigureCanvasTkAgg(self._phasor_fig, frm)
+ self._phasor_canvas.get_tk_widget().pack(expand=True, fill=BOTH)
+ self._phasor_canvas.mpl_connect('button_press_event', self._on_click_phasor)
+
+ self.draw_phasor()
+
+ return frm
+
+ def draw_phasor(self, idx: Optional[int] = None, n: Optional[int] = None):
+ num = self._calc_sim_val(0, 1)
+
+ self.wdg_abs_output.configure(text=str(np.abs(num)))
+ self.wdg_angle_output.configure(text=str(np.angle(num) * 180 / np.pi) + ' °')
+
+ self._phasor_fig.clear()
+ ax = self._phasor_fig.add_subplot(1, 1, 1)
+ ax.set_xlim(-5.0, 5.0)
+ ax.set_ylim(-5.0, 5.0)
+ ax.set_xlabel('real')
+ ax.set_ylabel('imag')
+
+ ax.add_artist(Circle((0, 0), np.abs(num), fill=None))
+ ax.add_artist(Arrow(0, 0, self._config.real, self._config.imag, label='Phasor'))
+ ax.plot(self._config.real, self._config.imag, color='blue', marker='*')
+
+ if idx is not None and n is not None:
+ x = self._calc_sim_val(idx, n)
+ ax.add_artist(Arrow(0, 0, x.real, x.imag, color='green', label='Current'))
+ ax.add_artist(Arrow(0, 0, x.real, 0, color='red', label='Current real'))
+
+ ax.legend()
+
+ self._phasor_canvas.draw()
+
+ def _on_click_phasor(self, ev):
+ if ev.xdata is not None:
+ self._config.real = np.round(10 * ev.xdata) / 10.
+ self.ctrl_frm.wdg_real_input.sync_in()
+ if ev.ydata is not None:
+ self._config.imag = np.round(10 * ev.ydata) / 10.
+ self.ctrl_frm.wdg_imag_input.sync_in()
+
+ default_store().save()
+ self.draw_phasor()
+
+ def _create_signal_canvas(self) -> ttk.Widget:
+ frm = ttk.Frame(self)
+
+ self._signal_fig = Figure(figsize=(5, 4), dpi=100)
+
+ self._signal_canvas = FigureCanvasTkAgg(self._signal_fig, frm)
+ self._signal_canvas.get_tk_widget().pack(expand=True, fill=BOTH)
+
+ return frm
+
+ def draw_signal(self, idx: int, n: int):
+ self._signal_fig.clear()
+ ax = self._signal_fig.add_subplot(1, 1, 1)
+ ax.set_xlim(0.0, 1.0)
+ ax.set_ylim(-5.0, 5.0)
+ ax.set_xlabel('time')
+ ax.set_ylabel('value')
+ ax.set_title('Signal')
+
+ t = np.linspace(0.0, 1.0, n)
+ t = t[:idx]
+
+ x = self._calc_sim_val(np.arange(idx), n)
+
+ ax.plot(t, x.real, color='red')
+
+ self._signal_canvas.draw()
+
+
+class Ch02PhasorWindow(Window):
+ GROUP = Ch02Group
+ TITLE = 'Phasor'
+ FRAME = Ch02PhasorFrame
+
+
+if __name__ == '__main__':
+ Ch02PhasorWindow.main()
+