-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
309 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
# Build-specific ignores | ||
__pycache__/ | ||
build/ | ||
dist/ | ||
*.spec |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
import customtkinter as ctk | ||
|
||
from plot import PlotFrame | ||
from input import InputsFrame | ||
|
||
|
||
class App(ctk.CTk): | ||
def __init__(self): | ||
super().__init__() | ||
def _on_click_callback(event): | ||
event.widget.focus_set() | ||
self.bind_all('<Button-1>', lambda event: _on_click_callback(event)) | ||
|
||
def _on_key_press_callback(event): | ||
if (event.char == '\r'): | ||
self.focus_set() | ||
self.bind('<KeyPress>', lambda event: _on_key_press_callback(event)) | ||
|
||
self.title('Second Order Dynamics Tool') | ||
self.geometry('1366x768') | ||
self.grid_rowconfigure((0, 2), weight=0) | ||
self.grid_rowconfigure((1), weight=1) | ||
self.grid_columnconfigure((0), weight=1) | ||
self.grid_columnconfigure((1), weight=20) | ||
|
||
self.title_bar = ctk.CTkLabel(master=self, text='Second Order\nDynamics', font=('Cascadia Code', 26)) | ||
self.title_bar.grid(row=0, column=0, padx=10, pady=10) | ||
|
||
self.input_frame = InputsFrame(master=self) | ||
self.plot_frame = PlotFrame(master=self, input_frame=self.input_frame) | ||
self.input_frame.set_plot_callback(self.plot_frame.update_plot) | ||
self.exit_button = ctk.CTkButton(master=self, text='Exit', font=('Cascadia Code', 16), command=self._exit_callback) | ||
|
||
self.input_frame.grid(row=1, column=0, padx=10, pady=(10, 5), sticky='nsew') | ||
self.plot_frame.grid(row=0, rowspan=2, column=1, padx=(5, 10), pady=(10, 5), sticky='nsew') | ||
self.exit_button.grid(row=2, column=0, padx=10, pady=(5, 10), sticky='sew') | ||
|
||
self.wm_protocol('WM_DELETE_WINDOW', self._quit) | ||
|
||
self.input_frame.register_validators() | ||
self.plot_frame.update_plot() | ||
|
||
|
||
def _exit_callback(self): | ||
self._quit() | ||
|
||
|
||
def _quit(self): | ||
self.withdraw() | ||
self.quit() | ||
|
||
|
||
if __name__ == '__main__': | ||
ctk.set_appearance_mode('dark') | ||
app = App() | ||
app.mainloop() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
from math import modf | ||
|
||
|
||
def map_range(value, from_, to): | ||
return to[0] + (to[1] - to[0]) * ((value - from_[0]) / (from_[1] - from_[0])) | ||
|
||
|
||
def default_x_function(t): | ||
if t < 0.9: | ||
return 0.6 | ||
elif t < 1.1: | ||
return map_range(t, from_=(0.9, 1.1), to=(0.6, 1.0)) | ||
elif t < 2.4: | ||
return 1.0 | ||
elif t < 2.5: | ||
return map_range(t, from_=(2.4, 2.5), to=(1.0, 0.25)) | ||
elif t < 3.5: | ||
return 0.25 | ||
elif t < 4.5: | ||
return map_range(t, from_=(3.5, 4.5), to=(0.25, 0.8)) | ||
elif t < 5.0: | ||
return map_range(t, from_=(4.5, 5.0), to=(0.8, 0.6)) | ||
else: | ||
return 0.6 | ||
|
||
|
||
def spiky_x_function(t): | ||
t_prime = modf(t)[0] | ||
if t_prime < 0.5: | ||
return map_range(t_prime, from_=(0.0, 0.5), to=(0.65, 0.75)) | ||
else: | ||
return map_range(t_prime, from_=(0.5, 1.0), to=(0.75, 0.65)) | ||
|
||
|
||
def jitter_x_function(t): | ||
t_prime = modf(15.0 * t)[0] | ||
if t_prime < 0.5: | ||
return map_range(t_prime, from_=(0.0, 0.5), to=(0.68, 0.72)) | ||
else: | ||
return map_range(t_prime, from_=(0.5, 1.0), to=(0.72, 0.68)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
import numpy as np | ||
import customtkinter as ctk | ||
|
||
from re import match | ||
|
||
|
||
class InputVarFrame(ctk.CTkFrame): | ||
def __init__(self, master, name, default_val=0.0, slider_range=(0, 1), assignment_char=' =', font_size=26): | ||
super().__init__(master) | ||
|
||
self.grid_rowconfigure((0), weight=1) | ||
self.grid_columnconfigure((0), weight=1) | ||
self.grid_columnconfigure((1), weight=1) | ||
self.grid_columnconfigure((2), weight=5) | ||
|
||
self.slider_from, self.slider_to = slider_range | ||
number_of_steps = (self.slider_to - self.slider_from) * 100 | ||
self.label = ctk.CTkLabel (master=self, text=f'{name}{assignment_char}', font=('Cascadia Code', font_size)) | ||
self.entry = ctk.CTkEntry (master=self, placeholder_text=f'{default_val}', font=('Cascadia Code', 16), justify='center', width=50) | ||
self.slider = ctk.CTkSlider(master=self, from_=self.slider_from, to=self.slider_to, number_of_steps=number_of_steps, command=self._slider_modify_callback) | ||
|
||
self.entry.insert(0, str(default_val)) | ||
self.slider.set(np.clip(default_val, self.slider_from, self.slider_to)) | ||
|
||
self.label.grid (row=0, column=0, padx=(20, 0), sticky='ew') | ||
self.entry.grid (row=0, column=1, sticky='ew') | ||
self.slider.grid(row=0, column=2, padx=(10, 20), sticky='ew') | ||
|
||
self.cached_entry_content = '0.0' | ||
self.entry.bind('<FocusIn>', lambda event: self._on_entry_focus_in_callback(event)) | ||
self.entry.bind('<FocusOut>', lambda event: self._on_entry_focus_out_callback(event)) | ||
|
||
|
||
def get_value(self): | ||
return self.slider.get() | ||
|
||
|
||
def set_plot_callback(self, plot_callback): | ||
self.plot_callback = plot_callback | ||
|
||
|
||
def register_validator(self): | ||
entry_assert_command = self.register(self._entry_modify_callback) | ||
self.entry.configure(validate='all', validatecommand=(entry_assert_command, '%d', '%V', '%P')) | ||
|
||
|
||
def _on_entry_focus_in_callback(self, event): | ||
self.cached_entry_content = self.entry.get() | ||
|
||
|
||
def _on_entry_focus_out_callback(self, event): | ||
entry_content = self.entry.get() | ||
if (entry_content == '' or entry_content == '.'): | ||
formatted_entry_val = float(self.cached_entry_content) | ||
else: | ||
formatted_entry_val = round(float(entry_content), 2) | ||
formatted_entry_val = np.clip(formatted_entry_val, self.slider_from, self.slider_to) | ||
formatted_entry = str(formatted_entry_val) | ||
self.entry.delete(0, len(entry_content)) | ||
self.entry.insert(0, formatted_entry) | ||
|
||
|
||
def _entry_modify_callback(self, action, reason, pending_mod): | ||
if pending_mod == '' or pending_mod == '.': | ||
return True | ||
|
||
is_valid_match = match(r'^[+-]?([0-9]+([.][0-9]*)?|[.][0-9]+)$', pending_mod) | ||
if is_valid_match is None: | ||
return False | ||
|
||
parsed_value = float(pending_mod) | ||
parsed_value = np.clip(parsed_value, self.slider_from, self.slider_to) | ||
self.slider.set(parsed_value) | ||
self.plot_callback() | ||
return True | ||
|
||
|
||
def _slider_modify_callback(self, value): | ||
self.entry.delete(0, len(self.entry.get())) | ||
self.entry.insert(0, f'{round(value, 2)}') | ||
self.plot_callback() | ||
|
||
|
||
class InputsFrame(ctk.CTkFrame): | ||
def __init__(self, master): | ||
super().__init__(master) | ||
|
||
self.grid_rowconfigure((0, 1, 2, 4, 5), weight=1) | ||
self.grid_rowconfigure((3), weight=5) | ||
self.grid_columnconfigure((0), weight=1) | ||
|
||
self.f_var = InputVarFrame(master=self, name='f', default_val=1.0, slider_range=(0.01, 6.0)) | ||
self.z_var = InputVarFrame(master=self, name='ζ', default_val=0.5, slider_range=(0.0, 6.0)) | ||
self.r_var = InputVarFrame(master=self, name='r', default_val=2.0, slider_range=(-3.0, 3.0)) | ||
self.ts_var = InputVarFrame(master=self, name='Time Span', default_val=6.0, slider_range=(0.01, 10.0), assignment_char=':', font_size=20) | ||
|
||
self.f_var.grid (row=0, column=0, padx=10, pady=(10, 5), sticky='nsew') | ||
self.z_var.grid (row=1, column=0, padx=10, pady=( 5, 5), sticky='nsew') | ||
self.r_var.grid (row=2, column=0, padx=10, pady=( 5, 5), sticky='nsew') | ||
self.ts_var.grid(row=5, column=0, padx=10, pady=( 5, 10), sticky='nsew') | ||
|
||
|
||
def set_plot_callback(self, plot_callback): | ||
self.f_var.set_plot_callback(plot_callback) | ||
self.z_var.set_plot_callback(plot_callback) | ||
self.r_var.set_plot_callback(plot_callback) | ||
self.ts_var.set_plot_callback(plot_callback) | ||
|
||
|
||
def register_validators(self): | ||
self.f_var.register_validator() | ||
self.z_var.register_validator() | ||
self.r_var.register_validator() | ||
self.ts_var.register_validator() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
import numpy as np | ||
import customtkinter as ctk | ||
import matplotlib.pyplot as plt | ||
|
||
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg | ||
|
||
from functions import * | ||
|
||
|
||
class PlotFrame(ctk.CTkFrame): | ||
def __init__(self, master, input_frame): | ||
super().__init__(master) | ||
|
||
self.input_frame = input_frame | ||
|
||
self.grid_rowconfigure((0), weight=0) | ||
self.grid_rowconfigure((1), weight=1) | ||
self.grid_columnconfigure((0), weight=1) | ||
|
||
master_color_dark = self.cget('fg_color')[1] | ||
master_color_dark_name = master_color_dark.rstrip('0123456789') | ||
master_color_dark_percent = int(master_color_dark[len(master_color_dark_name):]) | ||
master_color_dark_value = round((master_color_dark_percent / 100) * 255) | ||
master_color_dark_value_hex = hex(master_color_dark_value)[2:] | ||
master_color_dark_hex = f'#{master_color_dark_value_hex}{master_color_dark_value_hex}{master_color_dark_value_hex}' | ||
|
||
self.fig, self.ax = plt.subplots(facecolor=master_color_dark_hex, dpi=100) | ||
self.ax.set_facecolor(master_color_dark_hex) | ||
|
||
self.ax.axhline(linewidth=2, color='white') | ||
self.ax.axvline(linewidth=2, color='white') | ||
self.ax.set_xlabel('Time', color='white', fontsize=14) | ||
self.ax.set_ylabel('Position', color='white', fontsize=14) | ||
self.ax.tick_params(direction='out', length=6, width=2, colors='white', grid_color='#444444', grid_alpha=0.5) | ||
|
||
self.x_functions = { | ||
'Default': default_x_function, | ||
'Spiky': spiky_x_function, | ||
'Jitter': jitter_x_function | ||
} | ||
self.last_plots = None | ||
|
||
self.plot_selection = ctk.CTkComboBox(master=self, values=list(self.x_functions.keys()), state='readonly', font=('Cascadia Code', 16), command=self._combobox_callback) | ||
self.plot_selection.grid(row=0, column=0, padx=40, pady=(20, 0), sticky='nw') | ||
self.plot_selection.set('Default') | ||
|
||
self.canvas = FigureCanvasTkAgg(self.fig, master=self) | ||
self.canvas.get_tk_widget().grid(row=1, column=0, padx=10, pady=(0, 10), sticky='nsew') | ||
|
||
self._combobox_callback('Default') | ||
|
||
|
||
def set_x_function(self, x_function): | ||
self.x_function = x_function | ||
|
||
|
||
def update_plot(self): | ||
if not self.last_plots is None: | ||
for plot in self.last_plots: plot.remove() | ||
|
||
f = self.input_frame.f_var.get_value() | ||
z = self.input_frame.z_var.get_value() | ||
r = self.input_frame.r_var.get_value() | ||
ts = self.input_frame.ts_var.get_value() | ||
|
||
dt = 0.01 | ||
T = np.arange(0.0, ts, dt) | ||
X_vec = np.vectorize(self.x_function) | ||
X = X_vec(T) | ||
|
||
k1 = z / (np.pi * f) | ||
k2 = 1.0 / ((2.0 * np.pi * f) * (2.0 * np.pi * f)) | ||
k3 = (r * z) / (2.0 * np.pi * f) | ||
|
||
Y = np.zeros(shape=T.shape) | ||
Y[0] = X[0] | ||
yd = 0 | ||
for i in range(1, Y.size): | ||
xd = (X[i] - X[i - 1]) / dt | ||
stable_k2 = max(k2, 0.5*dt*dt + 0.5*dt*k1, dt*k1) | ||
Y[i] = Y[i - 1] + dt * yd | ||
yd = yd + dt * (X[i] + k3*xd - Y[i] - k1*yd) / stable_k2 | ||
|
||
plt.xlim(0.0, ts) | ||
plt.ylim(0, 1.5) | ||
plt.grid(visible=True, which='both') | ||
self.last_plots = self.ax.plot(T, X, color='#8EB173', linewidth=2.5) | ||
self.last_plots = self.last_plots + self.ax.plot(T, Y, color='#1F6AA5', linewidth=2.5) | ||
self.canvas.draw() | ||
|
||
|
||
def _combobox_callback(self, choice): | ||
self.set_x_function(self.x_functions[choice]) | ||
self.update_plot() |