From 0b0c8f54d79cf969effab23a8d81adaf143cb7c5 Mon Sep 17 00:00:00 2001 From: anhhcao Date: Fri, 4 Aug 2023 05:54:02 -0400 Subject: [PATCH 001/116] added frame slider to plot1d --- plot1d.py | 84 ++++++++++++++++++++++++++++++++++++++++++++--------- pysg_run.py | 21 +++++++------- 2 files changed, 81 insertions(+), 24 deletions(-) diff --git a/plot1d.py b/plot1d.py index b6d965e..2606a61 100755 --- a/plot1d.py +++ b/plot1d.py @@ -6,7 +6,6 @@ import glob # IMPORTING FROM ANIMATE2 - # function that draws each frame of the animation def animate(i): global xcol, ycol, current_frame @@ -23,13 +22,21 @@ def animate(i): ax.plot(x, y) #ax.set_xlim([0,20]) #ax.set_ylim([0,10]) - ax.set_title(f'Frame: {current_frame + 1} / {length}\nTime: {time}') + ax.set_title(f'Time: {float(time)}', loc='left') ax.set_xlabel(xcol) ax.set_ylabel(ycol) # END IMPORT +# hard-pauses the animation +# that is, the only way to unpause is by using the play or restart button +def hpause(self=None): + global hard_paused + hard_paused = True + bpause.color = '0.5' + pause() + # pauses the animation -def pause(self=None): +def pause(): global is_playing, fig if is_playing: fig.canvas.stop_event_loop() @@ -37,14 +44,18 @@ def pause(self=None): # plays the animation (either starts or resumes) def play(self=None): - global is_playing, current_frame, ax, length + global is_playing, current_frame, ax, length, frame_sliding, hard_paused if not is_playing: is_playing = True + hard_paused = False + bpause.color = '0.85' while current_frame < length and is_playing: animate(current_frame) current_frame += 1 fig.canvas.draw_idle() fig.canvas.start_event_loop(delay) + frame_sliding = False + fslider.set_val(current_frame) is_playing = False if loop and current_frame == length: restart() @@ -55,7 +66,7 @@ def loopf(self=None): if loop: bloop.color = '0.85' # this is the default color else: - bloop.color = 'cyan' # highlighted color + bloop.color = '0.5' # highlighted color loop = not loop # restarts the animation from the beginning @@ -69,13 +80,22 @@ def restart(self=None): def select_h(label): global current_frame, ycol update_cols(label, ycol) - restart() + if not hard_paused: + restart() + else: + animate(current_frame) + fig.canvas.draw_idle() + # select the verticle variable def select_v(label): global current_frame, xcol update_cols(xcol, label) - restart() + if not hard_paused: + restart() + else: + animate(current_frame) + fig.canvas.draw_idle() def update_cols(x, y): global xcol, ycol, ixcol, iycol @@ -84,10 +104,26 @@ def update_cols(x, y): ixcol = variables.index(x) iycol = variables.index(y) +# TODO logarithmic delay slider def update_delay(x): global delay delay = x / 1000 +def update_fslider(n): + global frame_sliding, current_frame + current_frame = n + animate(current_frame - 1) + fig.canvas.draw_idle() + +def entered_fslider(e): + if e.inaxes == fax: + pause() + +# BUG: if mouse moves off slider and directly onto something else too fast it might not play +def left_fslider(e): + if e.inaxes == fax and not hard_paused: + play() + argparser = ArgumentParser(description='plots the athena tab files specified') argparser.add_argument('-d', '--dir', help='the directory containing the tab files') args = argparser.parse_args() @@ -101,7 +137,9 @@ def update_delay(x): # global vars current_frame = 0 is_playing = False +hard_paused = True loop = False +frame_sliding = False # the time in seconds between frames delay= 100 / 1000 @@ -120,10 +158,11 @@ def update_delay(x): # plotting configuration fig, ax = plt.subplots() -fig.subplots_adjust(left=0.34, bottom=0.34) +fig.subplots_adjust(left=0.34, bottom=0.32, top=0.85) # old bottom was 0.34 # pause on close otherwise we might freeze # i wonder if this actually works -fig.canvas.mpl_connect('close_event', pause) +# i think it does +fig.canvas.mpl_connect('close_event', hpause) rax = fig.add_axes([0.05, 0.7, 0.15, 0.15]) radio = RadioButtons(rax, tuple(variables)) @@ -148,17 +187,34 @@ def update_delay(x): bres = Button(fig.add_axes([0.81 - lshift, 0.125, 0.1, 0.075]), 'Play') bres.on_clicked(play) -bpause = Button(fig.add_axes([0.7 - lshift, 0.125, 0.1, 0.075]), 'Pause') -bpause.on_clicked(pause) +bpause = Button(fig.add_axes([0.7 - lshift, 0.125, 0.1, 0.075]), 'Pause', color='0.5') # by default it is paused +bpause.on_clicked(hpause) -amp_slider = Slider( +# make slider nonlinear +delay_slider = Slider( ax=fig.add_axes([0.18, 0.05, 0.65, 0.03]), label='Delay (ms)', valmin=1, valmax=1000, - valinit=100, + valinit=2, ) -amp_slider.on_changed(update_delay) +delay_slider.on_changed(update_delay) + +fax = fig.add_axes([0.18, 0.95, 0.65, 0.03]) +fslider = Slider( + ax=fax, + label='Frame', + valmin=1, + valmax=length, + valinit=1, + valstep=1 +) + +fslider.on_changed(update_fslider) + +# in order to pause the animation when using the frame slider +fig.canvas.mpl_connect('axes_enter_event', entered_fslider) +fig.canvas.mpl_connect('axes_leave_event', left_fslider) plt.show() \ No newline at end of file diff --git a/pysg_run.py b/pysg_run.py index 4439762..f5541ad 100755 --- a/pysg_run.py +++ b/pysg_run.py @@ -38,7 +38,9 @@ primary = 'PySimpleGUIQt' backup = 'PySimpleGUI' -if args.tk: +using_tk = args.tk + +if using_tk: primary = 'PySimpleGUI' backup = 'PySimpleGUIQt' @@ -46,14 +48,13 @@ sg = import_module(primary) except: print(f'Falied to import {primary}. Falling back to {backup}') - if (args.tk): - args.tk = False + using_tk = not using_tk sg = import_module(backup) # removes the trailing zeroes then the dot from a string float x, then returns an int # utility function used by buidd_layout def rm_dot(x): - if args.tk: + if using_tk: return float(x) # being too precise causes problems, but hopefully this is enough s = '%.8g' % float(x) @@ -97,7 +98,7 @@ def build_layout(data, info): # otherwise use row = [sg.Text(k, tooltip=e['help'][1:].strip(), background_color=bgstd), sg.Stretch(background_color=bgstd)] - if e['gtype'] == 'SCALE': + if e['gtype'] == 'SCALE': # TODO add textbox for custom values # getting scale params # min:max:increment? # (\d*\.?\d*) also accepts just dots, so beware @@ -105,7 +106,7 @@ def build_layout(data, info): # scale = slider # build sliders differently depending on whether tk or qt is used scaled_default = rm_dot(e['value']) - if not args.tk: + if not using_tk: # if using qt, we need to prepare our own number display since one is not available by default sliders[k] = { 'key':e['value']+'_display', @@ -168,7 +169,7 @@ def run(input_file, output_dir, data, values): if values[k+o]: cmd += f'{k}={o} ' break - elif not args.tk and e['gtype'] == 'SCALE': + elif not using_tk and e['gtype'] == 'SCALE': cmd += '%s=%s ' % (k, values[k] / sliders[k]['factor']) else: cmd += f'{k}={values[k]} ' @@ -245,13 +246,13 @@ def display_conf_dir(dir_path): inner_layout = build_layout(data, info) # pysgqt elements seem to be smaller than their tkinter counterparts, so it might be better to reduce the width scaling scale_factor = 30 -if args.tk: +if using_tk: scale_factor = 40 win_size = (500, len(inner_layout) * scale_factor) layout = [[sg.Column(inner_layout, size=win_size, scrollable=False, background_color=bgstd)]] # only allow verticle scroll for the tk version, otherwise a horizontal scroll bar will show up -#if args.tk: +#if using_tk: # layout[0][0].VerticalScrollOnly = True # create the main window @@ -281,7 +282,7 @@ def display_conf_dir(dir_path): elif event == 'help': display_help(data) # update slider displays if using qt - elif not args.tk and event in sliders: + elif not using_tk and event in sliders: info = sliders[event] window[info['key']].update(values[event] / info['factor']) From fed64e1ce599b82e38be6c0e88103c7575af1387 Mon Sep 17 00:00:00 2001 From: anhhcao Date: Sat, 5 Aug 2023 06:33:37 -0400 Subject: [PATCH 002/116] additional features in plot1d, reskin for pysg_ru, bug fixes --- plot1d.py | 183 ++++++++++++++++++++++++++++++++++------------------ pysg_run.py | 146 +++++++++++++++++++++-------------------- 2 files changed, 196 insertions(+), 133 deletions(-) diff --git a/plot1d.py b/plot1d.py index 2606a61..d7b228d 100755 --- a/plot1d.py +++ b/plot1d.py @@ -1,28 +1,39 @@ #! /usr/bin/env python import numpy as np import matplotlib.pyplot as plt -from matplotlib.widgets import RadioButtons, Button, Slider +from matplotlib.widgets import RadioButtons, Button, Slider, CheckButtons from argparse import ArgumentParser import glob +# TODO list +# round buttons +# round check boxes +# nonlinear (logarithmic?) slider + # IMPORTING FROM ANIMATE2 # function that draws each frame of the animation def animate(i): - global xcol, ycol, current_frame + global xcol, ycol, current_frame, xlim, ylim # print(f[i]) d = np.loadtxt(f[i]).T x = d[ixcol] y = d[iycol] - file = open(f[i]) # open file to retreive time information - # first line is target - # terribly lazy but it works, maybe just use regex - time = file.readline().split('=')[1].split(' ')[0] - file.close() + if not args.hst: + file = open(f[i]) # open file to retreive time information + # first line is target + # terribly lazy but it works, maybe just use regex + time = file.readline().split('=')[1].split(' ')[0] + file.close() ax.clear() + if xlim: + ax.set_xlim(xlim) + if ylim: + ax.set_ylim(ylim) ax.plot(x, y) - #ax.set_xlim([0,20]) - #ax.set_ylim([0,10]) - ax.set_title(f'Time: {float(time)}', loc='left') + if not args.hst: + ax.set_title(f'Time: {float(time)}', loc='left') + else: + ax.set_title(f'History', loc='left') ax.set_xlabel(xcol) ax.set_ylabel(ycol) # END IMPORT @@ -36,7 +47,7 @@ def hpause(self=None): pause() # pauses the animation -def pause(): +def pause(self=None): global is_playing, fig if is_playing: fig.canvas.stop_event_loop() @@ -104,7 +115,6 @@ def update_cols(x, y): ixcol = variables.index(x) iycol = variables.index(y) -# TODO logarithmic delay slider def update_delay(x): global delay delay = x / 1000 @@ -115,21 +125,28 @@ def update_fslider(n): animate(current_frame - 1) fig.canvas.draw_idle() -def entered_fslider(e): +def fix_axes(v): + global xlim, ylim + if v == 'Fix X': + xlim = ax.get_xlim() if not xlim else None + else: + ylim = ax.get_ylim() if not ylim else None + +def mouse_moved(e): if e.inaxes == fax: pause() - -# BUG: if mouse moves off slider and directly onto something else too fast it might not play -def left_fslider(e): - if e.inaxes == fax and not hard_paused: + elif not hard_paused: play() argparser = ArgumentParser(description='plots the athena tab files specified') -argparser.add_argument('-d', '--dir', help='the directory containing the tab files') +argparser.add_argument('-d', '--dir', help='the directory containing the tab files', required=True) +argparser.add_argument('--hst', action='store_true', help='plots the hst file rather animating the tab files') +argparser.add_argument('-n', '--name', help='name of the problem being plotted') args = argparser.parse_args() # fnames='run1/tab/LinWave*tab' -f = glob.glob(args.dir + '/*tab') +t = '/*hst' if args.hst else '/*tab' +f = glob.glob(args.dir + t) f.sort() length = len(f) #print('DEBUG: %s has %d files' % (fnames,len(f))) @@ -140,6 +157,18 @@ def left_fslider(e): hard_paused = True loop = False frame_sliding = False +xlim = None +ylim = None + +# plot settings +left = 0.34 +bottom = 0.32 +top = 0.85 + +# change plot settings if plotting a hst file +if args.hst: + top = 0.9 + bottom = 0.1 # the time in seconds between frames delay= 100 / 1000 @@ -148,8 +177,12 @@ def left_fslider(e): file = open(f[0]) # just use the first file file.readline() variables = file.readline().split()[2:] +if args.hst: + variables = [v.split('=')[1] for v in variables] file.close() +var_len = len(variables) + # 0-based, change from animate2 xcol=variables[0] ycol=variables[0] @@ -158,63 +191,87 @@ def left_fslider(e): # plotting configuration fig, ax = plt.subplots() -fig.subplots_adjust(left=0.34, bottom=0.32, top=0.85) # old bottom was 0.34 +fig.subplots_adjust(left=left, bottom=bottom, top=top) # old bottom was 0.34 +# plt.rcParams['font.family'] = 'Arial' # pause on close otherwise we might freeze -# i wonder if this actually works -# i think it does -fig.canvas.mpl_connect('close_event', hpause) - -rax = fig.add_axes([0.05, 0.7, 0.15, 0.15]) -radio = RadioButtons(rax, tuple(variables)) -rax.text(-0.055, 0.07, 'Horizontal Axis') - +fig.canvas.mpl_connect('close_event', pause) +# fig.set_size_inches(10, 10) + +plt.get_current_fig_manager().set_window_title(args.name if args.name else f[0].split('.')[0]) + +rheight = var_len / 25 +rwidth = 0.045 +rdleft = 0.03 + +rax = fig.add_axes([rdleft, top - rheight, rwidth, rheight]) + +radio = RadioButtons(rax, + tuple(variables), + label_props={'color': ['white' for _ in variables]}, + radio_props={'color': ['#1f77b4' for _ in variables], 'edgecolor': ['black' for _ in variables]}) +rax.axis('off') # removes the border around the radio buttons radio.on_clicked(select_h) -rax = fig.add_axes([0.05, 0.4, 0.15, 0.15]) -radio2 = RadioButtons(rax, tuple(variables)) -rax.text(-0.055, 0.07, 'Vertical Axis') +# use same axes to add text in order to make it easier to adjust +rax.text(-0.055, 0.05, 'X') +rax.text(0.055, 0.05, 'Y') + +rax = fig.add_axes([rdleft + 0.015, top - rheight, 0.25, rheight]) +radio2 = RadioButtons(rax, + tuple(variables), + radio_props={'color': ['#1f77b4' for _ in variables], 'edgecolor': ['black' for _ in variables]}) +rax.axis('off') radio2.on_clicked(select_v) -# button shift -lshift = 0.65 +if not args.hst: + + # button shift + lshift = 0.65 + + bloop = Button(fig.add_axes([1.028 - lshift, 0.125, 0.1, 0.075]), 'Loop') + bloop.on_clicked(loopf) -bloop = Button(fig.add_axes([1.028 - lshift, 0.125, 0.1, 0.075]), 'Loop') -bloop.on_clicked(loopf) + brestart = Button(fig.add_axes([0.919 - lshift, 0.125, 0.1, 0.075]), 'Restart') + brestart.on_clicked(restart) -brestart = Button(fig.add_axes([0.919 - lshift, 0.125, 0.1, 0.075]), 'Restart') -brestart.on_clicked(restart) + bres = Button(fig.add_axes([0.81 - lshift, 0.125, 0.1, 0.075]), 'Play') + bres.on_clicked(play) -bres = Button(fig.add_axes([0.81 - lshift, 0.125, 0.1, 0.075]), 'Play') -bres.on_clicked(play) + bpause = Button(fig.add_axes([0.7 - lshift, 0.125, 0.1, 0.075]), 'Pause', color='0.5') # by default it is paused + bpause.on_clicked(hpause) -bpause = Button(fig.add_axes([0.7 - lshift, 0.125, 0.1, 0.075]), 'Pause', color='0.5') # by default it is paused -bpause.on_clicked(hpause) + # make slider nonlinear + delay_slider = Slider( + ax=fig.add_axes([0.18, 0.05, 0.65, 0.03]), + label='Delay (ms)', + valmin=1, + valmax=1000, + valinit=100, + ) -# make slider nonlinear -delay_slider = Slider( - ax=fig.add_axes([0.18, 0.05, 0.65, 0.03]), - label='Delay (ms)', - valmin=1, - valmax=1000, - valinit=2, -) + delay_slider.on_changed(update_delay) -delay_slider.on_changed(update_delay) + fax = fig.add_axes([0.18, 0.95, 0.65, 0.03]) + fslider = Slider( + ax=fax, + label='Frame', + valmin=1, + valmax=length, + valinit=1, + valstep=1 + ) -fax = fig.add_axes([0.18, 0.95, 0.65, 0.03]) -fslider = Slider( - ax=fax, - label='Frame', - valmin=1, - valmax=length, - valinit=1, - valstep=1 -) + fslider.on_changed(update_fslider) -fslider.on_changed(update_fslider) + cbax = fig.add_axes([1.15 - lshift, 0.1, 0.1, 0.125]) + fix_cbox = CheckButtons( + ax=cbax, + labels=[' Fix X', ' Fix Y'] + ) + cbax.axis('off') + # in order to pause the animation when using the frame slider + fig.canvas.mpl_connect('motion_notify_event', mouse_moved) -# in order to pause the animation when using the frame slider -fig.canvas.mpl_connect('axes_enter_event', entered_fslider) -fig.canvas.mpl_connect('axes_leave_event', left_fslider) + fix_cbox.on_clicked(fix_axes) plt.show() \ No newline at end of file diff --git a/pysg_run.py b/pysg_run.py index f5541ad..8274511 100755 --- a/pysg_run.py +++ b/pysg_run.py @@ -2,9 +2,10 @@ from re import match from aparser import parse_generic as parse from argparse import ArgumentParser -from os import getcwd, mkdir, environ, path +from os import getcwd, remove, mkdir, environ, path from subprocess import Popen, PIPE from importlib import import_module +from glob import glob cwd = getcwd() @@ -13,8 +14,8 @@ fstd_bold = ('Helvetica', 10, 'bold') # background colors used in the GUI -bgstd = '#2a2e32' -bgstd2 = '#23272b' +bgstd = 'white' +bgstd2 = 'gray' sliders = {} @@ -64,40 +65,40 @@ def rm_dot(x): s = s.replace('.', '') return float(s) +# TODO implement the rest of the tkrun gui markers def build_layout(data, info): global cwd - layout = [[sg.Text('Problem:', font=fstd_bold, background_color=bgstd), - sg.Stretch(background_color=bgstd), - sg.Text(info['problem'], background_color=bgstd)]] + layout = [[sg.Text('Problem:', font=fstd_bold), + sg.Stretch(), + sg.Text(info['problem'])]] reference = info['reference'] if reference: # empty strings are falsy # in the future, go to the link and get the abstract if possible - layout.append([sg.Text('Reference:', font=fstd_bold, background_color=bgstd), - sg.Stretch(background_color=bgstd), - sg.Text(info['reference'], background_color=bgstd)]) + layout.append([sg.Text('Reference:', font=fstd_bold), + sg.Stretch(), + sg.Text(info['reference'])]) else: - layout.append([sg.Text('Reference:', font=fstd_bold, background_color=bgstd), - sg.Stretch(background_color=bgstd), - sg.Text('N/A', background_color=bgstd)]) + layout.append([sg.Text('Reference:', font=fstd_bold), + sg.Stretch(), + sg.Text('N/A')]) layout.extend([[sg.Text('Output directory:', font=fstd_bold, - tooltip='The directory where the output files will be dumped', - background_color=bgstd), + tooltip='The directory where the output files will be dumped'), + sg.Stretch(), sg.In(size=(25, 0.75), enable_events=True, default_text=cwd, key='output-dir', - background_color=bgstd2, - text_color='white'), - sg.FolderBrowse(initial_folder=cwd, button_color=('white', bgstd2))], - [sg.Text('Parameters:', font=fstd_bold, background_color=bgstd)]]) + background_color='#eeeeee'), + sg.FolderBrowse(initial_folder=cwd)], + [sg.Text('Parameters:', font=fstd_bold)]]) for k in data: e = data[k] # use this if removing the prefix and underscore is desired # row = [sg.Text(match('.*_(.+)', k).group(1))] # otherwise use - row = [sg.Text(k, tooltip=e['help'][1:].strip(), background_color=bgstd), - sg.Stretch(background_color=bgstd)] + row = [sg.Text(f' {k}', tooltip=e['help'][1:].strip()), + sg.Stretch()] if e['gtype'] == 'SCALE': # TODO add textbox for custom values # getting scale params # min:max:increment? @@ -106,23 +107,25 @@ def build_layout(data, info): # scale = slider # build sliders differently depending on whether tk or qt is used scaled_default = rm_dot(e['value']) - if not using_tk: - # if using qt, we need to prepare our own number display since one is not available by default - sliders[k] = { - 'key':e['value']+'_display', - 'factor':round(scaled_default / float(e['value'])) - } - row.append(sg.Text(float(e['value']), key=sliders[k]['key'], background_color=bgstd)) + # if using qt, we need to prepare our own number display since one is not available by default + sliders[k] = { + 'key':e['value']+'_display', + 'factor':round(scaled_default / float(e['value'])) + } + #row.append(sg.Text(float(e['value']), key=sliders[k]['key'], background_color=bgstd)) + row.append(sg.InputText(default_text=float(e['value']), key=sliders[k]['key'], justification='right', size=(7, 0.75), background_color='#eeeeee')) # rm_dot only does anything significant if we are using qt - row.append(sg.Slider( + slider = sg.Slider( range=(rm_dot(m.group(1)), rm_dot(m.group(2))), resolution=rm_dot(m.group(3)), default_value=scaled_default, enable_events=True, key=k, - orientation='horizontal', - background_color=bgstd - )) + orientation='horizontal' + ) + if using_tk: + slider.DisableNumericDisplay = True + row.append(slider) elif e['gtype'] == 'ENTRY': # entry = text box # size of textboxes seem ok by default when right justified @@ -131,24 +134,22 @@ def build_layout(data, info): e['value'], enable_events=True, key=k, - size=(20, 0.75), - background_color=bgstd2, - text_color='white' + size=(20, 0.75) )) elif e['gtype'] == 'RADIO': # number of options is not predetermined, so can't use regex for o in e['gparams'].split(','): - row.append(sg.Radio(o, k, key=k+o, default= o == e['value'], background_color=bgstd)) + row.append(sg.Radio(o, k, key=k+o, default= o == e['value'])) else: print('GUI type %s not implemented' % e['gtype']) exit() layout.append(row) # add buttons to run/quit/help - layout.extend([[sg.Text(background_color=bgstd)], + layout.extend([[sg.Text()], [ - sg.Button('Run', key='run', button_color=('white', bgstd2)), - sg.Button('Quit', key='quit', button_color=('white', bgstd2)), - sg.Button('Help', key='help', button_color=('white', bgstd2)) + sg.Button('Run', key='run'), + sg.Button('Quit', key='quit'), + sg.Button('Help', key='help') ]]) return layout @@ -170,7 +171,7 @@ def run(input_file, output_dir, data, values): cmd += f'{k}={o} ' break elif not using_tk and e['gtype'] == 'SCALE': - cmd += '%s=%s ' % (k, values[k] / sliders[k]['factor']) + cmd += '%s=%s ' % (k, values[sliders[k]['key']]) else: cmd += f'{k}={values[k]} ' # also print it since its easier to copy the text that way @@ -180,9 +181,8 @@ def run(input_file, output_dir, data, values): # builds and displays a new window containing only the athena command def display_cmd(s): window = sg.Window('Athena Output', - [[sg.Text(s, background_color=bgstd)]], - font=fstd, - background_color=bgstd) + [[sg.Text(s)]], + font=fstd) while True: event, _ = window.read() if event == sg.WIN_CLOSED: @@ -191,10 +191,9 @@ def display_cmd(s): # builds and displays a new window with a progress bar tracking the process of athena's output def display_pbar(s, time): - window = sg.Window('Athena Output', + window = sg.Window('Loading Plot', [[sg.ProgressBar(100, orientation='h', size=(20, 20), key='pbar')]], - font=fstd, - background_color=bgstd) + font=fstd) p = Popen(s.split(), stdout=PIPE) while True: event, _ = window.Read(timeout=0) @@ -209,16 +208,14 @@ def display_pbar(s, time): # builds and displays a new window containing the help information def display_help(data): layout = [[ sg.Text('Output directory:', - font=fstd_bold, - background_color=bgstd), - sg.Text('The directory where the output files will be dumped', - background_color=bgstd), - sg.Stretch(background_color=bgstd)]] + font=fstd_bold), + sg.Stretch(), + sg.Text('The directory where the output files will be dumped')]] for k in data: - layout.append([sg.Text(k + ':', font=fstd_bold, background_color=bgstd), - sg.Text(data[k]['help'][1:].strip(), background_color=bgstd), - sg.Stretch(background_color=bgstd)]) - window = sg.Window('Help', layout, font=fstd, background_color=bgstd) + layout.append([sg.Text(k + ':', font=fstd_bold), + sg.Stretch(), + sg.Text(data[k]['help'][1:].strip())]) + window = sg.Window('Help', layout, font=fstd) while True: event, _ = window.read() if event == sg.WIN_CLOSED: @@ -226,9 +223,9 @@ def display_help(data): window.close() def display_conf_dir(dir_path): - layout = [[sg.Text(f'Directory {dir_path} does not exist. Create it?', background_color=bgstd)], - [sg.Button('Yes', key='yes', button_color=('white', bgstd2)), sg.Button('No', key='no', button_color=('white', bgstd2))]] - window = sg.Window('Directory Not Found', layout, font=fstd, background_color=bgstd) + layout = [[sg.Text(f'Directory {dir_path} does not exist. Create it?')], + [sg.Button('Yes', key='yes'), sg.Button('No', key='no')]] + window = sg.Window('Directory Not Found', layout, font=fstd) while True: event, _ = window.read() if event == 'no': @@ -242,22 +239,27 @@ def display_conf_dir(dir_path): # parse the input files data, info, type = parse(args.file) +sg.theme('Default1') + +sg.SetOptions(background_color='white', + text_element_background_color='white', + element_background_color='white', + slider_border_width=0) + # start building gui inner_layout = build_layout(data, info) # pysgqt elements seem to be smaller than their tkinter counterparts, so it might be better to reduce the width scaling -scale_factor = 30 +scale_factor = 27 if using_tk: - scale_factor = 40 + scale_factor = 30 win_size = (500, len(inner_layout) * scale_factor) -layout = [[sg.Column(inner_layout, size=win_size, scrollable=False, background_color=bgstd)]] - +#layout = [[sg.Column(inner_layout, size=win_size, scrollable=False, background_color=bgstd)]] # only allow verticle scroll for the tk version, otherwise a horizontal scroll bar will show up #if using_tk: # layout[0][0].VerticalScrollOnly = True - -# create the main window -window = sg.Window('pysg_run', layout, size=win_size, font=fstd, background_color='#777777') - +# create the main window '#777777' +# already resizable by default? +window = sg.Window('pysg_run', inner_layout, size=win_size, font=fstd, resizable=True) # primary event loop while True: event, values = window.read() @@ -269,20 +271,24 @@ def display_conf_dir(dir_path): if not path.exists(athena): print('Athena not found\nExiting') exit() - # will the tlim variable always be like this? - display_pbar(cmd, values['time/tlim']) # open the plot in a subprocess # remove the forward slash at the end if there is one odir = values['output-dir'] if odir[-1] == '/': odir = odir[:-1] - Popen(['python', 'plot1d.py', '-d', values['output-dir']]) + # remove the hst file since it always gets appended to + # intentional? + remove(glob(odir + '/*.hst')[0]) + # will the tlim variable always be like this? + display_pbar(cmd, values['time/tlim']) + Popen(['python', 'plot1d.py', '-d', values['output-dir'], '-n', info['problem']]) + Popen(['python', 'plot1d.py', '-d', values['output-dir'], '--hst', '-n', info['problem'] + ' history']) elif cmd: display_cmd(cmd) elif event == 'help': display_help(data) # update slider displays if using qt - elif not using_tk and event in sliders: + elif event in sliders: info = sliders[event] window[info['key']].update(values[event] / info['factor']) From bc819538227ff01c73f7a5f0ba654f517220e6e8 Mon Sep 17 00:00:00 2001 From: anhhcao Date: Sat, 5 Aug 2023 06:48:48 -0400 Subject: [PATCH 003/116] additions to makefile for quality of life improvements --- Makefile | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Makefile b/Makefile index 415dbd4..c90b6e1 100644 --- a/Makefile +++ b/Makefile @@ -150,6 +150,14 @@ test3: pyuic5 -x test3.ui -o test3.py python test3.py +dump1: + athena/bin/athena -i athinput.linear_wave1d -d dump output2/file_type=tab + +test_pysg1: + python pysg_run.py athinput.linear_wave1d + +test_plot1: + python plot.py dump # collaborations agui_t: From eb24bbbbb1e6537bacb9d189d96be04354ba129f Mon Sep 17 00:00:00 2001 From: Anh <114714284+anhhcao@users.noreply.github.com> Date: Sat, 5 Aug 2023 07:17:36 -0400 Subject: [PATCH 004/116] Update Makefile --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index c90b6e1..83906c2 100644 --- a/Makefile +++ b/Makefile @@ -154,10 +154,10 @@ dump1: athena/bin/athena -i athinput.linear_wave1d -d dump output2/file_type=tab test_pysg1: - python pysg_run.py athinput.linear_wave1d + python pysg_run.py athinput.linear_wave1d -r test_plot1: - python plot.py dump + python plot.py -d dump # collaborations agui_t: From b3a3c68cb15707244bbef21d9c7fdbaa82703b79 Mon Sep 17 00:00:00 2001 From: Peter Teuben Date: Sun, 6 Aug 2023 00:15:20 -0400 Subject: [PATCH 005/116] running AGUI --- README.md | 77 ++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 59 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index e150435..af0d70a 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ -# Testing an Athena* GUI +# Developing an Athena GUI (AGUI) -We show some examples of executing *athena* using a dynamic GUI. In -theory could apply to any of the +We show some examples of executing **athena** using a dynamic GUI. In +theory this could apply to any of the [Athena](https://www.athena-astro.app/) family (athena [AC], athena++ [AX], or athenak [AK]). It is easiest to work with **athenak**, since all the problems are compiled into one executable. We cover some @@ -10,15 +10,17 @@ yet public. Related and inspired by is NEMO's **tkrun** and **run** frontends, -but we aim to use python based software here. +but we aim to use python based software here. The GUI directive we +are proposing are an updated version of the one that was used in +[tkrun](https://teuben.github.io/nemo/man_html/tkrun.l.html) ## Example (athenak) Using **athenak** is preferred, as it has *all* problems compiled -into the executable. Some older comments on athena++ can be found below. +into one executable. Examples using **athena++** can be found below. -Again, an example how to compile and run the code +First, an example how to compile and run the code ```bash git clone --recursive https://gitlab.com/theias/hpc/jmstone/athena-parthenon/athenak @@ -28,14 +30,14 @@ Again, an example how to compile and run the code make -j ``` -or if you're lazy, use the Makefile in this directory: +or if you're lazy, use the Makefile in this agui directory: ```bash make build ``` -this takes a bit longer than athena++, -mostly because the kokkos library has to be compiled with). The binary is +this compilation takes a bit longer than athena++, +mostly because the kokkos library has to be compiled first. The binary is now in **athenak/build/src/athena**. ```text @@ -90,13 +92,15 @@ After this we can run it bin/athena -i inputs/hydro/athinput.linear_wave1d -d run1 ``` -but the default output from that **athinput** file is the **vtk** data format, which for this demo will +but the default output from that **athinput** file is the **vtk** data format, which for this GUI demo would be too complex to parse. TBD. For now we switch to the ascii table format, viz. ```bash bin/athena -i inputs/hydro/athinput.linear_wave1d -d run2 output2/file_type=tab ``` +which also shows how the GUI will have to deal with command line parameters beyond the -i and -d options. + ### Animations First example is NEMO biased, effectively quickly plotting all **tab** files using tabplot. Noting that the second row @@ -154,30 +158,33 @@ median: 2.50312 0.003125 1 -3.21566e-23 0 0 2.49757e-13 # TKRUN format -Old style V1 (the parameter setting and GUI specifications were separated): +The old style V1 tkrun format separated the parameter setting from the GUI specification: #> RADIO mode=gauss gauss,newton,leibniz -new style V2 will allow us to mix the GUI specifications with +but in the new style V2 will allow us to mix the GUI specifications with the (language dependent) key=val construct that gives it a default value, e.g. -bash/csh: +bash: + + mode=gauss # specify the integration method #> RADIO gauss,newton,leibniz + +csh: - mode=gauss # specify the integration method #> RADIO gauss,newton,leibniz - set mode = gauss # specify the integration method #> RADIO gauss,newton,leibniz + set mode = gauss # specify the integration method #> RADIO gauss,newton,leibniz python: - mode="gauss" # specify the integration method #> RADIO gauss,newton,leibniz + mode="gauss" # specify the integration method #> RADIO gauss,newton,leibniz athinput: - mode = gauss # specify the integration method #> RADIO gauss,newton,leibniz + mode = gauss # specify the integration method #> RADIO gauss,newton,leibniz -but will still leave open the option to build an executionar. +but will still leave open the option to build an executionar. # Which GUI builder? @@ -190,6 +197,40 @@ We explored gooey and psg (pySimpleGui): python pysg_special_run.py linear_wave_hydro.athinput +# How to run AGUI + +This is how we envision running **agui**: + +```text + agui [-i athinput] [-x athena] + + -i optional athinput file. If not provided, a filebrowser will let you select one + Default: athinput + -x the name (and or location) of the athena executable to use. + Default: athena +``` + +This will bring up a succession of 3 GUI's: + +1. The (optional) athinput file selector. Here the defaults of all parameters are given + +2. Setting parameters for the run + 1. The "-d" run directory + 2. parameters parsed from the athinput file (with or without the "#> GUI" specifications + + The user can then run the simulation. When the run is finished two separate GUI's will show up: + +3. History (.hst) file browser. This is a file that as function of simulation time has stored a number + of variables. This GUI will allow you to plot any column vs. any other column using a standard + matplotlib windows embedded in the GUI. This will otherwise be a static plot, as time is one of + the columns in the history table. + +3. Results (1D: .tab) browser. This browser is similar to the history file browser, except results + are available for each selected dump time. An animation button will allow you to move through + time, as well as select two variables from the results table. + +This will of course fine for 1D problems, for 2D problems the last GUI ("plot1D") will be a "plot2D" +widget that shows an image with a color-bar instead of a 1D plot. This has not been implemented. # References From 06eabf60558cb5a5d90289fbfd1f4ee7a73b2cbe Mon Sep 17 00:00:00 2001 From: Peter Teuben Date: Sun, 6 Aug 2023 00:15:37 -0400 Subject: [PATCH 006/116] add more tests --- Makefile | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 7f5a8f7..51ab7fd 100644 --- a/Makefile +++ b/Makefile @@ -63,6 +63,7 @@ branch: athena: git clone $(URL1) + $(MAKE) build_athena athenak: git clone --recursive $(URL2) @@ -129,13 +130,15 @@ run8: # base=run8/tab/ViscTest xcol=3 ycol=6 + +# ran1 and ran2 are made by ATHENA++ ran1: athena - (cd athena; bin/athena -i inputs/hydro/athinput.linear_wave1d -d ran1) - @echo Results in athena/ran1 + (cd athena; bin/athena -i inputs/hydro/athinput.linear_wave1d -d ../ran1) + @echo Results in ran1 ran2: athena - (cd athena; bin/athena -i inputs/hydro/athinput.linear_wave1d -d ran2 output2/file_type=tab) - @echo Results in athena/ran2 + (cd athena; bin/athena -i inputs/hydro/athinput.linear_wave1d -d ../ran2 output2/file_type=tab) + @echo Results in ran2 test1: @@ -159,6 +162,16 @@ test5: test6: python pyqt.py testfile2 +test7: + ./gooey_run2.py linear_wave_hydro.athinput + +# will try Qt first, else fall back to tkinter +test8: + ./pysg_run.py linear_wave_hydro.athinput + +test9: ran2 + ./plot1d.py -d ran2 + # collaborations agui_t: git clone $(URL5a) agui_t From f2558018f1301322d740c0595bd06994c5eeeb39 Mon Sep 17 00:00:00 2001 From: Peter Teuben Date: Sun, 6 Aug 2023 00:26:53 -0400 Subject: [PATCH 007/116] add qt --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 6be76d8..c2c967c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ # for python PySimpleGUI +PySimpleGUIQt gooey From 8649fd8493586696efd9f66b94c02b6489a9f271 Mon Sep 17 00:00:00 2001 From: anhhcao Date: Sun, 6 Aug 2023 04:27:16 -0400 Subject: [PATCH 008/116] added test10 to makefile, implemented all tkrun markers in pysg_run --- Makefile | 9 ++---- pysg_run.py | 64 ++++++++++++++++++++++++++++++--------- test.athinput | 84 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 136 insertions(+), 21 deletions(-) create mode 100644 test.athinput diff --git a/Makefile b/Makefile index 6fc5333..26e0fef 100644 --- a/Makefile +++ b/Makefile @@ -171,14 +171,9 @@ test8: test9: ran2 ./plot1d.py -d ran2 -dump1: - athena/bin/athena -i athinput.linear_wave1d -d dump output2/file_type=tab -test_pysg1: - python pysg_run.py athinput.linear_wave1d -r - -test_plot1: - python plot.py -d dump +test10: + ./pysg_run.py test.athinput # collaborations agui_t: diff --git a/pysg_run.py b/pysg_run.py index 8274511..5596abe 100755 --- a/pysg_run.py +++ b/pysg_run.py @@ -18,6 +18,7 @@ bgstd2 = 'gray' sliders = {} +checks = {} athena = (environ['AGUI'] if 'AGUI' in environ else cwd) + '/athena/bin/athena' @@ -66,6 +67,14 @@ def rm_dot(x): return float(s) # TODO implement the rest of the tkrun gui markers +#> IFILE in= +#> OFILE out= +#> IDIR indir= +#> ODIR odir= +#> ENTRY eps=0.01 +#> RADIO mode=gauss gauss,newton,leibniz +#> CHECK options=mean,sigma sum,mean,sigma,skewness,kurtosis +#> SCALE n=3.141592 0:10:0.01 def build_layout(data, info): global cwd layout = [[sg.Text('Problem:', font=fstd_bold), @@ -88,18 +97,18 @@ def build_layout(data, info): sg.In(size=(25, 0.75), enable_events=True, default_text=cwd, - key='output-dir', - background_color='#eeeeee'), + key='output-dir'), sg.FolderBrowse(initial_folder=cwd)], [sg.Text('Parameters:', font=fstd_bold)]]) for k in data: e = data[k] + t = e['gtype'] # use this if removing the prefix and underscore is desired # row = [sg.Text(match('.*_(.+)', k).group(1))] # otherwise use row = [sg.Text(f' {k}', tooltip=e['help'][1:].strip()), sg.Stretch()] - if e['gtype'] == 'SCALE': # TODO add textbox for custom values + if t == 'SCALE': # TODO add textbox for custom values # getting scale params # min:max:increment? # (\d*\.?\d*) also accepts just dots, so beware @@ -113,7 +122,7 @@ def build_layout(data, info): 'factor':round(scaled_default / float(e['value'])) } #row.append(sg.Text(float(e['value']), key=sliders[k]['key'], background_color=bgstd)) - row.append(sg.InputText(default_text=float(e['value']), key=sliders[k]['key'], justification='right', size=(7, 0.75), background_color='#eeeeee')) + row.append(sg.InputText(default_text=float(e['value']), key=sliders[k]['key'], justification='right', size=(7, 0.75))) # rm_dot only does anything significant if we are using qt slider = sg.Slider( range=(rm_dot(m.group(1)), rm_dot(m.group(2))), @@ -126,7 +135,7 @@ def build_layout(data, info): if using_tk: slider.DisableNumericDisplay = True row.append(slider) - elif e['gtype'] == 'ENTRY': + elif t == 'ENTRY': # entry = text box # size of textboxes seem ok by default when right justified # however, if changing the size is desired later, then remember that it is a pair not a single value like in the tkinter version @@ -136,10 +145,30 @@ def build_layout(data, info): key=k, size=(20, 0.75) )) - elif e['gtype'] == 'RADIO': + elif t == 'RADIO': # number of options is not predetermined, so can't use regex for o in e['gparams'].split(','): row.append(sg.Radio(o, k, key=k+o, default= o == e['value'])) + elif t == 'CHECK': + values = e['value'].split(',') + checks[k] = {} + for o in e['gparams'].split(','): + # default value? + key = k+o + checks[k][key] = o + row.append(sg.Checkbox(o, key=key, default= o in values)) + elif t == 'IFILE' or t == 'OFILE': + row.extend([sg.In(size=(25, 0.75), + enable_events=True, + default_text=e['value'], + key=k), + sg.FileBrowse(initial_folder=e['value'])]) + elif t == 'IDIR' or t == 'ODIR': + row.extend([sg.In(size=(25, 0.75), + enable_events=True, + default_text=e['value'], + key=k), + sg.FileBrowse(initial_folder=e['value'])]) else: print('GUI type %s not implemented' % e['gtype']) exit() @@ -163,15 +192,23 @@ def run(input_file, output_dir, data, values): cmd = f'{athena} -i {input_file} -d {output_dir} output2/file_type=tab ' for k in data: e = data[k] + t = e['gtype'] # radio buttons are a special case # we have to loop through each button to see which is selected - if e['gtype'] == 'RADIO': + if t == 'RADIO': for o in e['gparams'].split(','): if values[k+o]: cmd += f'{k}={o} ' break elif not using_tk and e['gtype'] == 'SCALE': cmd += '%s=%s ' % (k, values[sliders[k]['key']]) + elif t == 'CHECK' and checks[k]: + cmd += f'{k}=' + cs = checks[k] + for ck in cs: + if values[ck]: + cmd += f'{cs[ck]},' + cmd = cmd[:-1] + ' ' else: cmd += f'{k}={values[k]} ' # also print it since its easier to copy the text that way @@ -181,7 +218,7 @@ def run(input_file, output_dir, data, values): # builds and displays a new window containing only the athena command def display_cmd(s): window = sg.Window('Athena Output', - [[sg.Text(s)]], + [[sg.Multiline(s)]], font=fstd) while True: event, _ = window.read() @@ -241,17 +278,14 @@ def display_conf_dir(dir_path): sg.theme('Default1') -sg.SetOptions(background_color='white', - text_element_background_color='white', - element_background_color='white', - slider_border_width=0) +sg.SetOptions(slider_border_width=0) # start building gui inner_layout = build_layout(data, info) # pysgqt elements seem to be smaller than their tkinter counterparts, so it might be better to reduce the width scaling scale_factor = 27 if using_tk: - scale_factor = 30 + scale_factor = 33 win_size = (500, len(inner_layout) * scale_factor) #layout = [[sg.Column(inner_layout, size=win_size, scrollable=False, background_color=bgstd)]] # only allow verticle scroll for the tk version, otherwise a horizontal scroll bar will show up @@ -278,7 +312,9 @@ def display_conf_dir(dir_path): odir = odir[:-1] # remove the hst file since it always gets appended to # intentional? - remove(glob(odir + '/*.hst')[0]) + h = glob(odir + '/*.hst') + if len(h) > 0: + remove(h[0]) # will the tlim variable always be like this? display_pbar(cmd, values['time/tlim']) Popen(['python', 'plot1d.py', '-d', values['output-dir'], '-n', info['problem']]) diff --git a/test.athinput b/test.athinput new file mode 100644 index 0000000..e8e3490 --- /dev/null +++ b/test.athinput @@ -0,0 +1,84 @@ +# AthenaXXX input file for HYDRO linear wave tests + + +problem = hydro linear waves +reference = Stone et al, ApJS 178, 137 (2008), sect 8.1 + + +basename = LinWave # problem ID: basename of output filenames + + +nghost = 2 # Number of ghost cells +nx1 = 64 # Number of zones in X1-direction #> SCALE 16:512:16 +x1min = 0.0 # minimum value of X1 +x1max = 3.0 # maximum value of X1 +ix1_bc = periodic # inner-X1 boundary flag +ox1_bc = periodic # outer-X1 boundary flag + +nx2 = 32 # Number of zones in X2-direction #> RADIO 1,16,32,64,128,256 +x2min = 0.0 # minimum value of X2 +x2max = 1.5 # maximum value of X2 +ix2_bc = periodic # inner-X2 boundary flag +ox2_bc = periodic # outer-X2 boundary flag + +nx3 = 32 # Number of zones in X3-direction #> RADIO 1,16,32,64,128,256 +x3min = 0.0 # minimum value of X3 +x3max = 1.5 # maximum value of X3 +ix3_bc = periodic # inner-X3 boundary flag +ox3_bc = periodic # outer-X3 boundary flag + + +nx1 = 64 # Number of cells in each MeshBlock, X1-dir +nx2 = 32 # Number of cells in each MeshBlock, X2-dir +nx3 = 32 # Number of cells in each MeshBlock, X3-dir + +