Skip to content

Commit

Permalink
added learning project
Browse files Browse the repository at this point in the history
  • Loading branch information
nickdelgrosso committed Dec 12, 2023
1 parent 243f244 commit 60fd936
Show file tree
Hide file tree
Showing 17 changed files with 421 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.pytest_cache
__pycache__
.mypy_cache
30 changes: 30 additions & 0 deletions learning-projects/DearPyGuiWithMultiprocessingAndAsyncIO/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Learning Project: Asynchronous Coding and Dear PyGui

This is a small project to explore some of the built-in python tools around parallel processing, using an example of displaying a real-time signal that is being collected by some sensor.

Some things I wanted to learn:
- What's it like to use DearPyGui, and does it seem like a useful tool for data collection applications? I've already come to like Streamlit
for web apps, and I'm happy with FastAPI, but somehow Tkinter and PyQt have only been marginal
successes in my book, and Vispy and Kivy never matured enough for me to be totally happy with real-time data applications. DearPyGui has potential.

- Can I find some simple, reusable patterns for organizing and composing together multiple data streams?


After playing around a bit with `threading`, `multiprocessing`, and `asynicio`, I settled on using a combination of multiprocessing and asyncio:
- Multiprocessing's `Manager` to separate each script into its own environment and define a clear interface between the two. (which should be even cleaner and higher-perfoamnce from Python 3.12 onward, now that seperate GILs are being put in place for each process).

- Separate AsynicIO event loops for each process, with async functions to help increase the performance of IO-bound signals (here, that's mainly graphic rendering and console writing and reading)


Some Learnings:
- New Functions/classes:
- `multiprocessing.Manager`
- `asyncio.ensure_future()`
- `sys.stdout.buffer.write()` and `flush()`
- Much higher-performance version of `print()`, useful for simulating the high-speed sensor (I think I got about 10kHz)
- DearPyGui is pretty great! I particularly liked how simple it was to implement an asynicio-based event loop. Also, keeping the Gui responsive was really straightforward, as was debugging problems. Very nice!
- There were some interesting bugs while handling the data stream; in the future, it might be good to automate performance testings to verify these tradeoffs (what "Evolutionary Architecture" calls "fitness functions"), as they were really subtle and time-consuming to track down.


Overall, really neat!

Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import dearpygui.dearpygui as dpg
from multiprocessing import Process, Manager
import time



def setup(data):
# Setup
dpg.create_context()
dpg.create_viewport()
dpg.setup_dearpygui()
dpg.show_viewport()


with dpg.window(label='win'):
dpg.add_button(label='Hi!', tag='button')

# Update and Render Loop
while dpg.is_dearpygui_running():
dpg.set_item_label('button', data['time'])
dpg.render_dearpygui_frame()

# Cleanup
dpg.destroy_context()


def log_time(data):
while True:
time.sleep(.001)
current = time.time()
data['time'] = current
print(current)


def manage_app(timer_model, render_model):
while True:
time.sleep(0)
render_model['time'] = str(round(timer_model['time'], 2))


manager = Manager()
timer_model = manager.dict(time=3.14)
render_model = manager.dict(time='3.123424242324324')


gui_proc = Process(target=setup, kwargs=dict(data=render_model))
timer_proc = Process(target=log_time, kwargs=dict(data=timer_model), daemon=True)
main_proc = Process(target=manage_app, kwargs=dict(timer_model=timer_model, render_model=render_model), daemon=True)

gui_proc.start()
timer_proc.start()
main_proc.start()
gui_proc.join()
gui_proc.close()
timer_proc.terminate()
main_proc.terminate()
while timer_proc.is_alive() or main_proc.is_alive(): # There's a bit of delay before the terminate() call actually takes effect.
pass
timer_proc.close()
main_proc.close()
print('done!')
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import dearpygui.dearpygui as dpg

dpg.create_context()

def change_text(sender, app_data):
dpg.set_value("text item", f"Mouse Button ID: {app_data}")

def visible_call(sender, app_data):
print("I'm visible")

with dpg.item_handler_registry(tag="widget handler") as handler:
dpg.add_item_clicked_handler(callback=change_text)
dpg.add_item_visible_handler(callback=visible_call)

with dpg.window(width=500, height=300):
dpg.add_text("Click me with any mouse button", tag="text item")
dpg.add_text("Close window with arrow to change visible state printing to console", tag="text item 2")

# bind item handler registry to item
dpg.bind_item_handler_registry("text item", "widget handler")
dpg.bind_item_handler_registry("text item 2", "widget handler")

dpg.create_viewport(title='Custom Title', width=800, height=600)
dpg.setup_dearpygui()
dpg.show_viewport()
dpg.start_dearpygui()
dpg.destroy_context()
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import tkinter as tk
import time
import multiprocessing as mp
from queue import Empty


def generate_layout_model():
return {"text": "Hello, World!"}


def gui_process(layout_queue):
def update_label():
try:
layout = layout_queue.get_nowait()
label.config(text=layout["text"], fg=layout["color"])
except Empty:
pass

root.after(1000, update_label)

root = tk.Tk()
root.title("GUI Framework")
label = tk.Label(root, text="", font=("Arial", 14))
label.pack(padx=20, pady=20)
update_label()
root.mainloop()


def layout_generator_process(layout_queue):
while True:
layout = generate_layout_model()
layout_queue.put(layout)
time.sleep(1)


if __name__ == "__main__":
layout_queue = mp.Queue()

gui = mp.Process(target=gui_process, args=(layout_queue,))
layout_generator = mp.Process(target=layout_generator_process, args=(layout_queue,))

gui.start()
layout_generator.start()

gui.join()
layout_generator.join()
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import dearpygui.dearpygui as dpg
import time
import matplotlib.pyplot as plt

dpg.create_context()

dpg.create_viewport()
dpg.set_viewport_vsync(False) # if vsync is off, it's nonblocking
dpg.setup_dearpygui()
dpg.show_viewport()

dts = []

last_time = time.perf_counter()
for _ in range(200):
dpg.render_dearpygui_frame()
current_time = time.perf_counter()
# time.sleep(.01)
dt = current_time - last_time
last_time = current_time
dts.append(dt)

dpg.destroy_context()


plt.hist(dts, bins=15)
plt.show()
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import dearpygui.dearpygui as dpg
import dearpygui.demo as demo

dpg.create_context()
dpg.create_viewport(title='Custom Title', width=600, height=600)

demo.show_demo()

dpg.setup_dearpygui()
dpg.show_viewport()
dpg.start_dearpygui()
dpg.destroy_context()
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .main import run
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from __future__ import annotations

import asyncio
from contextlib import contextmanager
from typing import NamedTuple

import dearpygui.dearpygui as dpg


@contextmanager
def create_context(vsync: bool = True):
dpg.create_context()
dpg.create_viewport()
dpg.set_viewport_vsync(vsync) # if vsync is off, it's nonblocking
dpg.setup_dearpygui()
dpg.show_viewport()
# dpg.render_dearpygui_frame()
yield
dpg.destroy_context()



class FrameData(NamedTuple):
num: int
dt: float = 0

def __repr__(self) -> str:
return f"FrameData(num={self.num}, dt={round(self.dt, 4)})"




async def handle_shutdown(loop):
while dpg.is_dearpygui_running():
await asyncio.sleep(0.01)
loop.stop()



Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
from typing import List, TypedDict
import dearpygui.dearpygui as dpg


class Model(TypedDict):
x_data: List[int]
y_data: List[int]


def build_model(app_data) -> Model:
model: Model = {'x_data': app_data['x'], 'y_data': app_data['y']}
return model


def update_gui(app_data):
model: Model = build_model(app_data=app_data)
dpg.set_value('line_series', [model['x_data'], model['y_data']])
dpg.set_axis_limits('x_axis', ymin=min(model['x_data']), ymax=max(model['x_data']))
# dpg.set_axis_limits('y_axis', ymin=min(model['y_data']), ymax=max(model['y_data']))


def setup_gui():
with dpg.window(label='Plot'):
with dpg.plot(label='Sine Wave', width=1200, height=600):
x_axis = dpg.add_plot_axis(dpg.mvXAxis, label='X Axis', tag='x_axis')
y_axis = dpg.add_plot_axis(dpg.mvYAxis, label='Y Axis', tag='y_axis')

dpg.add_line_series(
[2, 3, 4, 5],
[-1, 0, 1, 0],
label='The Data',
parent=y_axis,
tag='line_series',
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from __future__ import annotations

import asyncio
from typing import TYPE_CHECKING

import dearpygui.dearpygui as dpg

from gui import dpg_utils
from gui.gui import setup_gui, update_gui
from utils import repeat

if TYPE_CHECKING:
from main import AppData

def run(proc_data: AppData = None):

if proc_data is None:
proc_data = {'x': [2, 3, 4], 'y': [4, 4.5, 6]}
with dpg_utils.create_context(vsync=False):
setup_gui()
asyncio.ensure_future(repeat(dpg.render_dearpygui_frame, dt=.015, run_cond=dpg.is_dearpygui_running))
asyncio.ensure_future(repeat(update_gui, data=(proc_data,), dt=.002, run_cond=dpg.is_dearpygui_running))
asyncio.get_event_loop().run_forever()
24 changes: 24 additions & 0 deletions learning-projects/DearPyGuiWithMultiprocessingAndAsyncIO/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@

import asyncio
from typing import List, TypedDict
import multiprocessing as mp

import gui
import sensor


class AppData(TypedDict):
x: List[int|float]
y: List[int|float]


if __name__ == '__main__':
manager = mp.Manager()
data: AppData = manager.dict({'x': [1, 2, 5], 'y': [6, 8, 8.2]})

gui_proc = mp.Process(target=gui.run, args=(data,))
daq_proc = mp.Process(target=lambda: asyncio.run(sensor.simulate_data_update(data)), daemon=True)
gui_proc.start()
daq_proc.start()
gui_proc.join()
daq_proc.join()
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
pytest
pytest-asyncio
dearpygui
asyncstdlib
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .sensor import simulate_data_update
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import random
import struct
import sys
import time
from math import sin

SAMPLING_RATE_HZ = 100_000

while True:
# time.sleep(1 / SAMPLING_RATE_HZ)
time.sleep(0.0000000000000000000000000000000001)
t = time.perf_counter()
y = sin(8 * t + .2 * random.random())
packet = struct.pack('dd', t, y)
try:
sys.stdout.buffer.write(packet)
sys.stdout.buffer.flush()
except BrokenPipeError:
break

Loading

0 comments on commit 60fd936

Please sign in to comment.