blob: 47ac15357b99fb9e97211c0167ea0035fa10a035 [file] [log] [blame]
# 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()