diff --git a/app.py b/app.py index 6efa89113fafc6ebd085f2fca2d83dd94433ee7f..72323858509971fc985b782c8faa75c2c481ae21 100644 --- a/app.py +++ b/app.py @@ -6,8 +6,53 @@ import dearpygui.dearpygui as dpg import numpy as np from dotenv import load_dotenv -from static_ui import construct_ui -from constants import ICON_PATH +import config_ui +import remote_run_ui +import map_edit_ui +from style import setup_fonts, setup_themes +from handlers import setup_handlers +from paths import ICON_PATH +from remote_run_constants import LOMONOSOV + + +def exit_callback(): + connections = dpg.get_item_user_data('run_window').get('connections', []) + builds = dpg.get_item_user_data('run_window').get('builds', []) + + # with open('credentials.txt', 'a+', encoding='utf-8') as log: + # for host in connections: + # conn = connections[host] + # if conn.host == LOMONOSOV: + # if conn.id_rsa_path: + # desc = f'{conn.host};{conn.username};{conn.id_rsa_path}' + # else: + # desc = f'{conn.host};{conn.username}' + # else: + # desc = f'{conn.host};{conn.username};{conn.password}' + # log.write(desc + '\n') + + # with open('builds.txt', 'a+', encoding='utf-8') as log: + # for machine, model in builds: + # log.write(f'{machine};{model};{builds[(machine, model)]}\n') + + # with open('sessions.txt', 'a+', encoding='utf-8') as log: + # for row in dpg.get_item_children('models_table', 1): + # running_session_id = dpg.get_item_alias(row) + # model = dpg.get_value(f'{running_session_id}_model') + # machine = dpg.get_value(f'{running_session_id}_machine') + # log.write(f'{running_session_id};{machine};{model}\n') + + for conn in connections: + connections[conn].close() + +def construct_ui(): + remote_run_ui.construct_ui() + map_edit_ui.construct_ui() + config_ui.construct_ui() + + setup_handlers() + setup_fonts() + setup_themes() load_dotenv() @@ -25,6 +70,7 @@ if __name__ == '__main__': dpg.show_viewport() construct_ui() + dpg.set_exit_callback(exit_callback) dpg.set_primary_window('main', True) dpg.start_dearpygui() diff --git a/callbacks.py b/callbacks.py index ad52205f5859b18291ab078d2f2772f8132a0d42..4e8e0a4bed84d76efba5bcd2d8cb4903b4697d7a 100644 --- a/callbacks.py +++ b/callbacks.py @@ -1,9 +1,10 @@ import dearpygui.dearpygui as dpg import tifffile +from uuid import uuid4 from utils import * from constants import SPACE, TREE_COLOR, ROAD_COLOR, BUILDING_COLOR -from remote_run import start_remote_execution, start_model_build, get_server_output_structure +from remote_run import start_execution_lab_server, start_model_build, get_server_output_structure from map_edit import dump_height_map, load_height_map, update_texture from styles import get_accent_color, apply_theme from generator import LCZ @@ -166,7 +167,7 @@ def generate_map_clb(s, a, u): set_map_window_value('buildings_map', height_map) set_map_window_value('roads_map', np.zeros((h, w))) set_map_window_value('trees_map', np.zeros((h, w))) - set_map_window_value('height_map_file_path', f'{dpg.generate_uuid()}.txt') + set_map_window_value('height_map_file_path', f'{uuid4()}.txt') set_map_window_value('max_height', max_height) construct_map_layers(w, h) @@ -231,7 +232,7 @@ def start_remote_execution_clb(s, a, u): if u == 'build_model': start_model_build(dpg.get_value('server_username'), dpg.get_value('server_password'), dpg.get_value('gitlab_username'), dpg.get_value('gitlab_password')) elif u == 'run_on_lab_server': - start_remote_execution(dpg.get_value('server_username'), dpg.get_value('server_password')) + start_execution_lab_server(dpg.get_value('server_username'), dpg.get_value('server_password')) elif u == 'download_output': s = get_server_output_structure(dpg.get_value('server_username'), dpg.get_value('server_password')) folders, filepaths = parse_dirs(s) diff --git a/config-parser/config-parser.cpp b/config-parser/config-parser.cpp index d81338429958f7dd1a0ce14eeec4fea28ccfebc7..2c699a3b3683945e70c89b7dfb2cfdd9f151a0f7 100644 --- a/config-parser/config-parser.cpp +++ b/config-parser/config-parser.cpp @@ -709,7 +709,6 @@ void nse::ConfigParser::print() const { for (int i = 0; i < nvars; i++) { var[i].print(); - // getc(stdin); } } diff --git a/config.py b/config.py index 0096a2b6ac9dc8076fc716330cfa3f3922999a4c..a35a5417feb8dee77fd42fb9c372fbabdaaa6460 100644 --- a/config.py +++ b/config.py @@ -3,6 +3,7 @@ import subprocess import os from enum import Enum +from paths import CONFIG_PARSER_EXECUTABLE class CfgVar: def __init__(self, key, value=None, value_type='STRING', comment=""): @@ -45,11 +46,7 @@ class Namespace(Token): return f"NAMESPACE {self.name}" -def tokenize(file_path): - text = "" - if os.path.exists(file_path): - text = open(file_path, "r", encoding="utf-8").read() - +def tokenize(text): tokens = [] it = text.__iter__() @@ -114,12 +111,12 @@ def tokenize(file_path): return tokens -def parse(file_path): +def parse(text): stack = [] data = dict() - tokens = tokenize(file_path) + tokens = tokenize(text) it = tokens.__iter__() try: @@ -140,13 +137,32 @@ def parse(file_path): except StopIteration as e: return data +class ConfigParserError(Exception): + pass def load_config(file_path): - config = parse(file_path) + try: + text = open(file_path, "r", encoding="utf-8").read() + except OSError as e: + raise OSError + + lines = [] + + p = subprocess.Popen([CONFIG_PARSER_EXECUTABLE, file_path], stdout=subprocess.PIPE) + while True: + line = p.stdout.readline().decode() + + if not line: + break + + if 'unexpected' in line: + raise ConfigParserError + + lines.append(line) - output = subprocess.run(["config-parser/parser-cli", file_path], capture_output=True, text=True) + config = parse(text) - for line in output.stdout.split("\n"): + for line in lines: match = re.match(r" > (\w+) '(.*)' = *(.*)", line) if match: value_type, path, value = match.groups() @@ -235,4 +251,4 @@ def dump_config(config, file_path): if __name__ == '__main__': - print(load_config('config.txt')) \ No newline at end of file + print(load_config('../config-ex.txt')) \ No newline at end of file diff --git a/config.txt b/config.txt deleted file mode 100644 index ccd18ce4fd3ac017333c962d8008fde5c36075eb..0000000000000000000000000000000000000000 --- a/config.txt +++ /dev/null @@ -1,977 +0,0 @@ -MAIN_DIR = ""; - -# sometimes you have to set main directory, e.g. on Lengau working in Lustre: -# MAIN_DIR = "/mnt/lustre/users/my_account/build/"; - -domain -{ -# -# domain setup in [meters] -# x - streamwise, y - spanwise, z - wall-normal directions -# - x = 0.0; y = 0.0; z = 0.0; # point of origin - length = 400.0; width = 200.0; height = 80.0; # domain length(x), width(y) and height(z) -} -# ----------------------------------------------------------------------------- - -time -{ - begin = 0.0; - end = 50.0 * 3600.0; # start and end time of integration [s] - - dt = 0.2; # time step [s] - - # use_calendar = false; # optional, default = false - calendar - { - mode = "local"; - # mode = "local" - local time - # mode = "local-UTC" - local UTC time - # mode = "set" - prescribed time & date as - # [year, month, day, hour, min, sec, UTC_offset] values - - year = 2020; - month = 9; - day = 1; - hour = 12; - min = 0; - sec = 0.0; - UTC_offset = 3; - } -} -# ----------------------------------------------------------------------------- - -grid -{ - type = "uniform"; # type = "uniform" || "stretched" || "stretched-up" || "z-coord-ascii" - - filename = "z-coord.txt"; # argument for type = "z-coord-ascii" - - cx = 80; cy = 40; cz = 16; # number of cells in each direction - - ksi_z = 1.2; # near-wall grid stretching parameter - - adaptive { - # mode = false; # enable adaptive grid [optional, default = false] - - beta = 0.3; # relaxation time scale [<= 1] - - dz_min = 0.25 * (domain.height / grid.cz); # min grid step - dz_max = 4.0 * (domain.height / grid.cz); # max grid step - - # --- adaptation parameters - TKE_threshold_coeff = 0.1; # threshold coeff. to define hbl - hbl_max_coeff = 1.1; # boundary layer height multiplier - C_smooth_coeff = 5.0; # number of cells in smoothing region - - # --- begin & end time of adaptation [optional, default: all integration period] - # begin = time.begin; end = time.end; - - nskip = 1; # adaptation once in nskip iterations, each iteration := 1 - } -} -# ----------------------------------------------------------------------------- - -mpi_setup -{ -# -# MPI-process distribution -# in 'mpirun -np [N]' [N] overrides config specs if differs -# - dimx = 2; dimy = 2; dimz = 1; -} -# ----------------------------------------------------------------------------- - -phys -{ - f = 0.0; # coriolis frequency [1/s] - - nu = 1.25 * 0.00001; # kinematic viscosity [m^2/s] - xi = (1.0 / 0.7) * nu; # thermal diffusivity [m^2/s] - - rho_ref = 1.25; # reference density of air [kg/m^3] - - g = 9.81; # gravitational acceleration [m/s^2] - Theta_ref = 288.15; # reference temperature [K] - - # --- no buoyancy - beta = 0.0; # = g * thermal expansion coefficient = g / Theta_ref [m/(K*s^2)] -} -# ----------------------------------------------------------------------------- - -# optinally define state variables -# default defs. depend on model configuration -state_def -{ - # Theta_p = false; - # Tabs = false; - - # Rho = false; - # Rho_ideal = false; - - # Exner_pz = false; - # Pressure_ref_pz = false; - - # Qliquid = false; - # Qsolid = false; - # Qvapor = false; - - # Qvs_water = false; Pvs_water = false; - # Qvs_ice = false; Pvs_ice = false; -} -# ----------------------------------------------------------------------------- - -topography -{ - mode = "hand"; # topography def. mode - # mode = "hand" -- define only 'patches' in configuration file - # mode = "ascii-mask" -- read height map from file - - # --- using file height map parameters - # mode = "ascii-mask"; - # filename = "map-ex.txt"; # filename path - # xmin = domain.x; # map -x coordinates in computation domain - # xmax = domain.x + domain.length; - # ymin = domain.y; # map -y coordinates in computation domain - # ymax = domain.y + domain.width; - # height_scale = 1.0; # scale height factor - - npatch = 1; # number of patches - patch_1 { - type = "box"; # patch type: "box" || "hill" - - xmin = 15.0; xmax = 35.0; # patch dimensions - ymin = 10.0; ymax = 90.0; - height = 20.0; - - xperiod = 50.0; # optional periodicity in -x - yperiod = 100.0; # optional periodicity in -y - } -} -# ----------------------------------------------------------------------------- - -geo_wind -{ -# NOTE: skipped if mode not set -# forcing priority: [t,z], [t], [const] - - # --- geostrophic wind components - U = 0.0; V = 0.0; -} -# ----------------------------------------------------------------------------- - -external_pressure_grad -{ -# NOTE: skipped if mode not set -# forcing priority: [t,z], [t], [const] - - # --- pressure gradient components [optional] - dPdx = -0.0004; # [Pa/m] - # dPdy = 0.0; # [Pa/m] -} -# ----------------------------------------------------------------------------- - -subsidence -{ -} -# ----------------------------------------------------------------------------- - -external_tendency -{ -} -# ----------------------------------------------------------------------------- - -nudging -{ -} -# ----------------------------------------------------------------------------- - -rayleigh_friction -{ -} -# ----------------------------------------------------------------------------- - -surface -{ - Theta { - mode = "const"; - - # --- fixed surface temperature - value = 288.15; # initial surface temperature [K] - } - Qhum { - # --- predefined mode [land surface model] - mode = "lsm"; - } - - z0_m = 0.1; # aerodynamic roughness [m] - z0_h = 0.1; # heat roughness [m] - - kappa = 0.4; # von Karman constant - Pr_t_0 = 1.0; # turbulent Prandt number (neutral) - - # --- stability function coefficients - Cm = 4.8; - Ch = 7.8; - - alpha_m = 16.0; - alpha_h = 16.0; - - # --- latent heat flux alpha/beta model - lhflux_alpha = 1.0; - lhflux_beta = 0.025; -} -# ----------------------------------------------------------------------------- - -initial_conditions -{ - # optional [U], [V] - # if not set initial profiles are set to match geostrophic wind - - U { - # mode = "half-channel"; - # bulk = 6.0; - } - - Theta { - # --- predefined mode - mode = "const"; - - surface_value = 288.15; # initial boundary layer temperature [K] - height = 0.0; # boundary layer height [m] - grad_z = 0.0; # humidity gradient above boundary layer [kg/(kg*m)] - } - - Qhum { - # --- predefined mode - mode = "mixed-layer"; - - surface_value = 0.0025; # initial boundary layer humidity [kg/kg] - height = 0.0; # boundary layer height [m] - grad_z = 0.0; # humidity gradient above boundary layer [kg/(kg*m)] - } -} -# ----------------------------------------------------------------------------- - -damping -{ - is_enabled = false; - - # use_avgxy_ref = false; # damp to -xy average profile [optional] - - f = 0.2; # damping frequency [1/s], = 0.2 (WRF model) - - # --- damping layer [z1, z2] - z1 = domain.z + 0.75 * domain.height; # [m] - z2 = domain.z + domain.height; # [m] -} -# ----------------------------------------------------------------------------- - -les -{ - is_dynamic_momentum = true; - is_dynamic_scalar = true; - - is_ssm_mixed_momentum = false; - is_ssm_mixed_scalar = false; - - is_amd_model = false; # use AMD subgrid model - # dynamic, ssm keys not supported, have to be := false - - - # --- SSM model params - C_ssm_momentum = 1.0; - C_ssm_scalar = 1.0; - - - # --- static LES model params - C_smag = 0.08; # GABLS-1 fit = 0.08 - # Lilly = 0.17 - Prandtl_sgs = 0.7; # subgrid scale Prandtl [0.4, 1.0] - - - # --- dynamic LES model params - dynamic { - # --- dynamic coefficient = (C_{s} * delta_{g})^2 clipping - C_smag_max = 0.25; - Prandtl_sgs_min = 0.4; - - alpha = 1.73; # test-to-base filter width ratio - - avg_mode = "lagrange"; # "none", "plane", "filter", "lagrange" - - nskip = 3; - use_transport = false; - - C_T_lagrange_momentum = 1.5; - C_T_lagrange_scalar = 3.0; - } - - base_filter_reset = false; - test_filter_reset = false; - clip_filter_reset = false; -} -# ----------------------------------------------------------------------------- - -passive_tracers -{ - # num = 2; # number of tracers, skipped if not defined - - # --- each tracer field defines diffusivity & surface values - tracer_1 { - diffusivity = phys.xi; - - surface { - flux = -0.01; - - # --- setting localized source, all surface if not defined - xmin = 180.0; xmax = 220.0; - ymin = 180.0; ymax = 220.0; - - # --- active in [begin, end], [time.begin, time.end] if not defined - begin = 7.0 * 3600.0; - # end = time.end; - } - } - - tracer_2 { - # name = "tracer[2]"; # optional tracer name - - diffusivity = phys.xi; - - # --- setting 'life-time' using decay frequency [1/s] [optional] - # f_decay = 1.0 / 600.0; - - surface { - flux = -0.02; - - # --- active in [begin, end], [time.begin, time.end] if not defined - begin = 7.0 * 3600.0; - # end = time.end; - } - } -} -# ----------------------------------------------------------------------------- - -# particle simulation setup -# used only if INCLUDE_PARTICLES is defined, see [model-defines.h] -ptcl -{ - is_passive_transport = false; # passive particles flag - - # --- particle parameters for all sources - density = 1000.0; # particle density [kg/m^3] - diameter = 0.000001; # particle diameter [m] - - g = -9.81; # gravity acceleration [m/s^2] - - # f_decay = 1.0 / 100.0; # optional decay constant [1/s] - # half-life = ln(2) / f - - # --- number of sources - nsources = 1; - - source_1 { - n = 524288 * 8; # number of particles to release - begin = 5.0 * 3600.0; # release time - end = time.end; # instant release if not defined - - # --- setting flux [lower priority than 'n' -- number of particles] - # both begin & end must be specified -- not instant source - # flux = 10.0; - # flux_direction = "X"; - - # --- source volume - xmin = domain.x + 50.0 - 10.0; xmax = domain.x + 50.0 + 10.0; - ymin = domain.y; ymax = domain.y + domain.width; - zmin = domain.z; zmax = domain.z + 5.0; - } - - # --- number of sinks [optional] - nsinks = 1; - - sink_1 { - # --- optional, default mode = "inside" - mode = "outside"; # "inside" || "outside" - - # --- define volume - xmin = domain.x; xmax = domain.x + domain.length; - ymin = domain.y - 0.5 * domain.width; ymax = domain.y + 1.5 * domain.width; - zmin = domain.z + 0.01; zmax = domain.z + domain.height; - } -} -# ----------------------------------------------------------------------------- - -# particle tracking setup -# used only if INCLUDE_PARTICLES_TRACKING is defined, see [model-defines.h] -ptcl_track -{ - is_passive_transport = ptcl.is_passive_transport; - - # --- particle parameters for all sources - density = ptcl.density; # particle density [kg/m^3] - diameter = ptcl.diameter; # particle diameter [m] - - g = ptcl.g; # gravity acceleration [m/s^2] - - # f_decay = ptcl.f_decay; # optional decay constant [1/s] - # half-life = ln(2) / f - - group_max_size = 256; # max number of particles per group - max_memory = 10 * 1024 * 1024; # max memory in bytes for keeping trajectories in memory - - # --- number of sources - nsources = 1; - - source_1 - { - n = 128; # number of particles to release - begin = 8.0 * 3600.0; # release time - - # --- setting flux [lower priority than 'n' -- number of particles] - # both begin & end must be specified -- not instant source - # flux = 100.0; - # flux_direction = "Z"; - - # --- source volume - xmin = domain.x + 0.4 * domain.length; xmax = domain.x + 0.6 * domain.length; - ymin = domain.y + 0.4 * domain.width; ymax = domain.y + 0.6 * domain.width; - zmin = domain.z; zmax = domain.z + 0.1 * domain.height; - } - - # --- number of sinks [optional] - nsinks = 0; -} -# ----------------------------------------------------------------------------- - -# canopy setup -# used only if INCLUDE_CANOPY_DRAG is defined, see [model-defines.h] -canopy -{ - Cd = 0.15; - drag_type = "non-linear"; # || "linearized" || "mean" - - num = 0; - patch_1 { - type = "sharp"; # || "fancy" || "obs" - - # --- patch volume - xmin = domain.x; xmax = domain.x + domain.length; - ymin = domain.y; ymax = domain.y + domain.width; - zmin = domain.z; zmax = domain.z + 50.0; - - # --- make patch 'periodic' in -x and/or -y directions - # single patch if not defined - # xperiod = 1.0; - # yperiod = 1.0; - - LAI = 1.0; - # --- set zm - height of max(LAD) for type = "obs": - # zm = zmin + 0.8 * (zmax - zmin); - - # --- OR: set a file - # type = "sharp-map"; - # filename = "map-ex.txt"; - - # switch_ij_order = false; # [optional, default = false] - # normalize_height = 1.0; # [optional, default = 1.0] - } -} -# ----------------------------------------------------------------------------- - -poisson -{ -# -# Poisson equation solver setup -# - # use_cg_solver = false; # use CG as base solver [optional, default = BiCGstab] - - retol = 0.0001; abstol = 0.00001; # relative and absolute tolerance - miniters = 1; maxiters = 100; # minimum and maximum number of iterations - - piters = 1; # number of preconditioner (multigrid) iterations - - multigrid - { - ngrid = 5; # number of grids in multigrid sequence (= [0] - auto definition) - - down_iters = 2; # number of smoother iterations on fine->coarse traverse - up_iters = 3; # number of smoother iterations on coarse->fine traverse - direct_iters = 5; # number of smoother iterations on coarsest grid - - smooth_up_omega = 1.84; # relaxation value on coarse->fine traverse - smooth_up_omega_fine = 1.64; # relaxation value on reaching finest grid - } -} -# ----------------------------------------------------------------------------- - -output -{ -# NOTE: netcdf output is enabled only if INCLUDE_NETCDF is defined in [nse-sys.h] - - DIR = MAIN_DIR + "output/"; # output directory - make_unique_DIR = true; # make output directory unique for each run - - convert_dsq_to_tecplot = true; # convert series .dsq output to .plt [tecplot] format - # *: on model completion only - # convert_dsq_to_netcdf = false; # convert series .dsq output to .nc [netcdf] format - # *: on model completion only - - num = 1; # number of output units - - unit_1 - { - SUBDIR = ""; # unit sub-directory - - begin = 1.0 * 3600.0; # start time [s] for writing output - dt = 1.0 * 3600.0; # time step [s] for writing output - - # --- output subdomain [optional, applicable in 3D output only] - xmin = domain.x; xmax = domain.x + domain.length; # -x output domain setup - ymin = domain.y; ymax = domain.y + domain.width; # -y output domain setup - zmin = domain.z; zmax = domain.z + domain.height; # -z output domain setup - - # --- output controls [default, if not defined, value is 'false'] - cntrl_avgxy_plt = true; # 1D -xy averaged .plt fields - cntrl_avgxy_netcdf = false; # 1D -xy averaged netcdf fields - - cntrl_3d_plt = true; # 3D .plt fields - cntrl_3d_bin = false; # 3D .nsx fields - cntrl_3d_netcdf = false; # 3D netcdf fields - - cntrl_geometry_3d_plt = false; # geometry 3D .plt fields - cntrl_geometry_3d_netcdf = false; # geometry 3D netcdf fields - - cntrl_topography_plt = true; # topography .plt fields - cntrl_topography_netcdf = false; # topography netcdf fields - - cntrl_2d_plt = false; # 2D .plt fields - cntrl_2d_netcdf = false; # 2D netcdf fields - - cntrl_surface_plt = true; # surface .plt fields - cntrl_surface_netcdf = false; # surface netcdf fields - - cntrl_grid_plt = false; # grid .plt data - cntrl_grid_netcdf = false; # grid netcdf data - - cntrl_meteo_avgxy_plt = false; # 1D -xy averaged meteo forcing .plt fields - cntrl_meteo_all_plt = false; # all meteo forcing .plt fields - - # --- output keys for filtered fields - cntrl_filtered_3d_plt = false; - # filter { kxmin = 0; kxmax = 0; kymin = 0; kymax = 5; is_remove_mean = true; } - - # --- spectrum output - spectrum - { - # --- controls - x_cntrl = true; - y_cntrl = true; - xy_cntrl = true; - - # np = 2; # number of z planes, skipped if not defined - z_1 = 50.0; - z_2 = 100.0; - - # --- additional options for 1d & 2d spectrum, default all = true if not defined - # is_remove_mean_1d = true; - # is_centered_fft_1d = true; - - # is_remove_mean_2d = true; - # is_centered_fft_2d = true; - # is_log10_2d = true; - } - - # --- particles & trajectories output - # used only if INCLUDE_PARTICLES is defined - cntrl_ptcl_bin = false; # particles binary output - cntrl_ptcl_plt = true; # particles .plt output - cntrl_ptcl_coords_plt = false; # particles coordinates .plt output - # used only if INCLUDE_PARTICLES_TRACKING is defined - cntrl_ptcl_traj_bin = false; # particles trajectories binary output - cntrl_ptcl_traj_plt = false; # particles trajectories .plt output - - # --- profiles [= 0 if not defined] - x_profile_num = 0; - y_profile_num = 0; - z_profile_num = 0; - # --- e.g.: - x_profile_1 { y = domain.y + 0.5 * domain.width; z = domain.z + 0.5 * domain.height; } - - # --- slices [ = 0 if not defined] - xy_slice_num = 0; - xz_slice_num = 0; - yz_slice_num = 0; - # --- e.g.: - xy_slice_1 { z = domain.z + 0.5 * domain.height; } - } - - screen { - begin = time.begin; # start time [s] of onscreen output - - # nskip = 360; # output once in nskip iterations, each iteration := 1 - dt = 0.05 * 3600.0; # output time step [s], --higher-- priority than 'nskip' - - # --- screen controls [optional, default = true] - # cntrl_status = true; - # cntrl_progress_bar = true; - - # --- screen controls [optional, default = false] - # cntrl_terminal_mode = false; - } -} -# ----------------------------------------------------------------------------- - -checkup -{ - # --- additional checkups [InF, NaN values etc.] - begin = 0.0; # start time of checks - - nskip = 3600; # check once in nskip iterations, each iteration := 1 - # dt = 1.0 * 3600.0; # check time step, --higher-- priority than 'nskip' -} -# ----------------------------------------------------------------------------- - -dump -{ - DIR = MAIN_DIR + "dump/"; # dump directory - - begin = 10.0 * 3600.0; # start time [s] for writing model dump - dt = 10.0 * 3600.0; # time step [s] for writing model dump -} -# ----------------------------------------------------------------------------- - -startup -{ - DIR = MAIN_DIR + "init/"; # initial conditions directory -} -# ----------------------------------------------------------------------------- - -series -{ - begin = 60.0; # start time [s] - - # nskip = 2; # calculate once in nskip iterations, each iteration := 1 - dt = 1.0; # time step [s], --higher-- priority than 'nskip' - - # --- point flow measurements - point_set - { - # mode = "define"; # optional point set mode - # = "define" [default] || "grid-xy" - - # np = 3; # number of points, skipped if not defined - point_1 { x = domain.x + 0.5 * domain.length; y = domain.y + 0.5 * domain.width; z = 25.0; } - point_2 { x = domain.x + 0.5 * domain.length; y = domain.y + 0.5 * domain.width; z = 50.0; } - point_3 { x = domain.x + 0.5 * domain.length; y = domain.y + 0.5 * domain.width; z = 100.0; } - - # --- OR: set point set grid on z=const planes - # mode = "grid-xy"; - - # --- domain - # x = domain.x; y = domain.y; - # length = domain.length; width = domain.width; - - # --- grid dimensions, number of edges - # nx = 7; ny = 5; - # --- number of z=const planes - # nz = 3; - # z_1 = 25.0; - # z_2 = 50.0; - # z_3 = 100.0; - } - - # --- [xy] averaged energy at fixed 'z' - energy_avgxy_set - { - # np = 2; # number of z planes, skipped if not defined - z_1 = 50.0; - z_2 = 100.0; - } - - # --- [xy] averaged fluxes at fixed 'z' - flux_avgxy_set - { - # np = 2; # number of z planes, skipped if not defined - z_1 = 50.0; - z_2 = 100.0; - } - - # --- additional parameters - # --- TKE hbl def. [optional] - # TKE_hbl_threshold_stable = 0.3; - # TKE_hbl_threshold_unstable = 0.1; - - # --- max length to hold data in memory [optional] - # max_mem_length = 100 * 1024; -} -# ----------------------------------------------------------------------------- - -spectrum_series -{ - begin = time.begin; # start time - - nskip = 1; # calculate once in nskip iterations, each iteration := 1 - # dt = 1.0; # time step, --higher-- priority than 'nskip' - - # num = 3; # number of series, skipped if not defined - - # --- wavevenumbers in: - kxmax <= kx <= kmax, 0 <= ky <= kymax - unit_1 { zp = domain.z + 0.3 * domain.height; kxmax = 3; kymax = 3; } - unit_2 { zp = domain.z + 0.2 * domain.height; kxmax = 3; kymax = 3; } - unit_3 { zp = domain.z + 0.1 * domain.height; kxmax = 3; kymax = 3; } - - # --- max length to hold data in memory [optional] - # max_mem_length = 100 * 1024; -} -# ----------------------------------------------------------------------------- - -time_scan -{ -# NOTE: dump is not supported for time scans - - begin = 60.0; # scan start time [s] - - # nskip = 1000; # calculate once in nksip iterations, each iteration := 1 - dt = 60.0; # scan time step [s], --higher-- priority than 'nskip' - - # --- optionally set output variables - - cntrl_grid = false; # all keys, optional, default = all 'false' - cntrl_grid { - # dz = true; - - # --- cell center & edge coordinates - # pz = true; - # ez = true; - - # --- only in adaptive grid mode - # monitor_function = true; - } - - cntrl_avgxy = true; # all keys, optional, default = all 'false' - cntrl_avgxy { - # U = true; - # V = true; - # W = true; - - # Theta = true; - # Theta_p = true; - # Tabs = true; - - # Q = true; - # Qvapor = true; - - # UW_flux = true; - # VW_flux = true; - # TW_flux = true; - # QW_flux = true; - - # U_variance = true; - # V_variance = true; - # W_variance = true; - # Theta_variance = true; - # Q_variance = true; - } - - cntrl_z_profile = false; # all keys, optional, default = all 'false' - cntrl_z_profile { - # U = true; - # V = true; - # W = true; - - # Theta = true; - # Theta_p = true; - # Tabs = true; - - # Q = true; - # Qvapor = true; - } - - z_profile_num = 0; - z_profile_1 { x = domain.x + 0.5 * domain.length; y = domain.y + 0.5 * domain.width; } -} -# ----------------------------------------------------------------------------- - -stats -{ - num = 2; - - unit_1 { - begin = time.end - 4.0 * 3600.0; # start time for averaging - end = time.end; # end time for averaging, to \infty if not defined - - nskip = 10; - axis = "XYZ"; - type = "energy-eq"; - - output.SUBDIR = "stat_end/"; - dump.SUBDIR = "stat_end/"; - - # --- regular mode [optional] -- shifts accumulation window - # is_regular = false; - - # --- ensemble mode [optional] -- ensemble averaging - # is_ensemble = false; - } - unit_2 { - begin = time.end - 8.0 * 3600.0; # start time for averaging - end = time.end - 4.0 * 3600.0; # end time for averaging, to \infty if not defined - - nskip = 10; - axis = "XYZ"; - type = "energy-eq"; - - output.SUBDIR = "stat_end-1/"; - dump.SUBDIR = "stat_end-1"; - - # --- regular mode [optional] -- shifts accumulation window - # is_regular = false; - - # --- ensemble mode [optional] -- ensemble averaging - # is_ensemble = false; - } -} -# ----------------------------------------------------------------------------- - -pdf -{ - # num = 1; - - unit_1 { - begin = 7.5 * 3600.0; - end = 9.0 * 3600.0; - dt = 1.0; - - zp = domain.z + 0.25 * domain.height; - - output.SUBDIR = "pdf/"; - dump.SUBDIR = "pdf/"; - - cntrl_default = true; - nbins_default = 256; - - # --- reset options: [begin, reset_time] -- find histogram parameters - # if not defined set min-max range for each variable in controls - reset_time = 8.0 * 3600.0; - reset_safety = 0.25; # relative to min-max found - # e.g.: min' = min - reset_safety * (max - min) - - # --- regular mode [optional] -- shifts accumulation window - # is_regular = false; - - # --- change default control using keys - # cntrl { - # U = true; V = true; W = true; Pressure = true; - # U_grad_x = true; U_grad_y = true; U_grad_z = true; - # V_grad_x = true; V_grad_y = true; V_grad_z = true; - # W_grad_x = true; W_grad_y = true; W_grad_z = true; - # Suv = true; Suw = true; Svw = true; # strain-tensor - # Omega_uv = true; Omega_uw = true; Omega_vw = true; # vorticity-tensor - # Theta = true; - # Theta_grad_x = true; Theta_grad_y = true; Theta_grad_z = true; - # Q = true; - # Q_grad_x = true; Q_grad_y = true; Q_grad_z = true; - - # --- tracers 'C' correspond to passive_tracers {} def. [use index in: 1...passive_tracers.num] - # C1 = true; - # C1_grad_x = true; C1_grad_y = true; C1_grad_z = true; - # C2 = true; - # - # } - - # --- setting min-max for any variable - # this has to be defined if 'reset_time' is skipped, e.g.: - # U { min = -1.0; max = 1.0; } - - # --- setting number of bins for any variable, e.g.: - # U { nbins = 512; } - } -} -# ----------------------------------------------------------------------------- - -joint_pdf -{ - # num = 1; - - unit_1 { - begin = 7.5 * 3600.0; - end = 9.0 * 3600.0; - dt = 1.0; - - zp = domain.z + 0.25 * domain.height; - - output.SUBDIR = "joint-pdf/"; - dump.SUBDIR = "joint-pdf/"; - - cntrl_default = true; - nxbins_default = 256; - nybins_default = 256; - - # --- reset options: [begin, reset_time] -- find histogram parameters - # if not defined set min-max range for each variable in controls - reset_time = 8.0 * 3600.0; - reset_safety = 0.25; # relative to min-max found - # e.g.: min' = min - reset_safety * (max - min) - - # --- regular mode [optional] -- shifts accumulation window - # is_regular = false; - - # --- change default control using keys - # cntrl { - # UV = true; UW = true; VW = true; - # PSuu = true; PSvv = true; PSww = true; - # PSuv = true; PSuw = true; PSvw = true; - # TU = true; TV = true; TW = true; - # QU = true; QV = true; QW = true; - # QT = true; - - # --- tracers 'C' correspond to passive_tracers {} def. [use index in: 1...passive_tracers.num] - # C1U = true; C1V = true; C1W = true; - # C1T = true; C1Q = true; - # --- tracer/tracer joint pdf, using strict upper diagonal notation, e.g.: - # C1C2 = true; C1C3 = true; C2C3 = true; - # - # } - - # --- setting min-max for any variable - # this has to be defined if 'reset_time' is skipped, e.g.: - # UV { xmin = -1.0; xmax = 1.0; ymin = - 1.0; ymax = 1.0; } - - # --- setting number of bins for any variable, e.g.: - # UV { nxbins = 512; nybins = 512; } - } -} -# ----------------------------------------------------------------------------- - -runtime_filter -{ - # begin = 600.0; # set a regular filter - # dt = 600.0; - - mark = 600.0; # OR: apply filter at time = mark only - # mark has --higher-- priority than regular mode - - # num = 0; # number of filters applied, output = sum of all units - # u = u_f[1] + u_f[2] + ... - # --- skipped if not defined - - unit_1 { - # --- keep some rolls - mode = "include"; - is_remove_mean = false; - - kxmin = 0; kxmax = 0; - kymin = 0; kymax = 5; - } - - unit_2 { - # --- add small scale disturbances - mode = "exclude"; # adding (u - u_f) - is_remove_mean = false; - - kxmin = 0; kxmax = 10; - kymin = 0; kymax = 10; - } -} -# ----------------------------------------------------------------------------- diff --git a/config_callbacks.py b/config_callbacks.py new file mode 100644 index 0000000000000000000000000000000000000000..0cf9b6c5e9ca9b32cdd104694b4f67fb6b2ef6e5 --- /dev/null +++ b/config_callbacks.py @@ -0,0 +1,76 @@ +import dearpygui.dearpygui as dpg + +from utils import get_config_file_path, delete_config_items_and_clear_aliases, update_config_values, get_config, update_config_values, set_main_window_value, change_color_temporarily, path_iter + +from style_constants import SPACE, COLLAPSING_HEADERS_UI +from style import get_accent_color, apply_theme +from config import load_config, dump_config, ConfigParserError +from dynamic_ui import construct_config_ui, construct_model_output_structure_ui + +def set_file_path_clb(s, a, u): + file_path = a['file_path_name'] + dpg.set_value(u, file_path) + +def construct_config_ui_clb(s, a, u): + if not get_config_file_path(): + return + + delete_config_items_and_clear_aliases() + update_config_values() + construct_config_ui(get_config_file_path(), get_config()) + +def config_save_as_clb(s, a, u): + update_config_values() + dump_config(get_config(), a['file_path_name']) + show_message('file saved successfully:)') + +def config_open_clb(s, a, u): + delete_config_items_and_clear_aliases() + config_file_path = a['file_path_name'] + + try: + config = load_config(config_file_path) + except OSError: + show_message('no file found') + return + except ConfigParserError: + show_message('bad file format') + return + + set_main_window_value('config', config) + set_main_window_value('config_file_path', config_file_path) + construct_config_ui(config_file_path, config) + +def config_save_clb(s, a, u): + update_config_values() + dump_config(get_config(), get_config_file_path()) + show_message('file saved successfully:)') + +def open_config_open_dialog_clb(s, a, u): + dpg.show_item('config_open') + +def show_config_search_window_clb(s, a, u): + dpg.show_item('search_popup') + +def open_config_save_as_dialog_clb(s, a, u): + dpg.show_item('config_open') + +def search_clb(s, data, u): + if not get_config_file_path(): + return + + if not data: + return + + for path in get_config(): + if data.lower() in path.split('.')[-1]: + dpg.set_y_scroll('main', dpg.get_item_pos('.'.join([get_config_file_path(), path.split('.')[0]]))[1]) + for tag in path_iter(path, prefix=get_config_file_path()): + if dpg.get_value(COLLAPSING_HEADERS_UI) == True: + dpg.set_value(tag, True) + else: + dpg.set_value(dpg.get_item_parent(dpg.get_item_parent(tag)), dpg.get_item_parent(tag)) + change_color_temporarily('.'.join([get_config_file_path(), path, 'text']), color=get_accent_color()) + +def apply_theme_clb(s, a, u): + apply_theme(s) \ No newline at end of file diff --git a/config_ui.py b/config_ui.py new file mode 100644 index 0000000000000000000000000000000000000000..69c389611cc1b14aeda5a480c6f452d878e81d98 --- /dev/null +++ b/config_ui.py @@ -0,0 +1,61 @@ +import dearpygui.dearpygui as dpg + +from utils import combo_menu +from style_constants import * +from style import apply_theme, get_accent_color +from config_callbacks import * + +def construct_main_window_ui(): + with dpg.window(label='config file editor', tag='main', user_data={'config': None, 'config_file_path': None}): + with dpg.menu_bar(): + with dpg.menu(label='config'): + dpg.add_menu_item(label='open\t\t', shortcut='ctrl+o', callback=open_config_open_dialog_clb, tag='open_config_open_dialog') + dpg.add_menu_item(label='search\t\t', shortcut='ctrl+f', callback=show_config_search_window_clb, tag='show_config_search_window') + dpg.add_menu_item(label='save\t\t', shortcut='ctrl+s', callback=config_save_clb, tag='save_config') + dpg.add_menu_item(label='save as\t\t', shortcut='ctrl+shift+s', callback=open_config_save_as_dialog_clb, tag='open_config_save_as_dialog') + + dpg.add_menu_item(label='map', callback=lambda: dpg.show_item('map_window')) + dpg.add_menu_item(label='run', callback=lambda: dpg.show_item('run_window')) + + with dpg.menu(label='view'): + with dpg.menu(label='ui', tag='ui'): + for ui in UIS: + dpg.add_menu_item(label=ui, check=True, default_value=False, callback=combo_menu(construct_config_ui_clb), tag=ui) + dpg.set_value(UIS[1], True) + + with dpg.menu(label='theme', tag='theme'): + for theme in THEMES: + dpg.add_menu_item(label=theme, check=True, default_value=False, callback=combo_menu(apply_theme_clb), tag=theme) + dpg.set_value('orange', True) + +def construct_config_open_dialog(): + with dpg.file_dialog(directory_selector=False, modal=True, show=False, callback=config_open_clb, tag='config_open', width=600, height=600): + dpg.add_file_extension('.txt', color=(0, 255, 0, 255)) + dpg.add_file_extension('.*', color=(255, 255, 255, 255)) + +def construct_config_save_as_dialog(): + with dpg.file_dialog(directory_selector=False, modal=True, show=False, callback=config_save_as_clb, tag='config_save_as', width=600, height=400): + dpg.add_file_extension('.txt', color=(0, 255, 0, 255)) + dpg.add_file_extension('.*', color=(255, 255, 255, 255)) + +def construct_set_file_path_dialog(): + with dpg.file_dialog(directory_selector=True, modal=True, show=False, callback=set_file_path_clb, tag='set_folder_dialog', width=500, height=400): + dpg.add_file_extension('.*', color=(255, 255, 255, 255)) + +def construct_message_window(): + with dpg.window(label='', no_collapse=True, autosize=True, no_resize=True, modal=True, tag='message_window', show=False, pos=(dpg.get_viewport_width() / 3, dpg.get_viewport_height() / 3)): + dpg.add_text('file saved successfully:)', tag='message') + +def construct_search_window(): + with dpg.window(label='', no_collapse=True, autosize=True, no_resize=True, tag='search_popup', show=False, pos=(40, 40)): + dpg.add_input_text(hint='search for...', on_enter=True, callback=search_clb, tag='search') + +def construct_ui(): + construct_main_window_ui() + construct_message_window() + + construct_config_save_as_dialog() + construct_config_open_dialog() + construct_search_window() + + construct_set_file_path_dialog() \ No newline at end of file diff --git a/constants.py b/constants.py index ba44caf50b11c1534b5d27630dd7a8737c7d4411..28d924d673225bb3c0da17f43c524dfc691ac59d 100644 --- a/constants.py +++ b/constants.py @@ -42,7 +42,7 @@ TEXT = (0, 1) ACCENT = (5, 18, 19, 20, 15, 16, 35, 37, 34, 23, 22, 25, 26, 27, 28, 29, 10, 11, 12, 30, 31, 32, 49, 50) PRIMARY = (2, 3, 39) SECONDARY = (36, 33, 21, 24, 4, 7, 8, 9, 13, 14) -APPLY_ALPHA = (1, 22, 25, 31, 34, 16) +APPLY_ALPHA = (22, 25, 31, 34, 16) COLOR_TYPE_TO_DPG_ITEM = { 'TEXT': (0, 1), 'PRIMARY': (2, 3, 39), @@ -51,17 +51,33 @@ COLOR_TYPE_TO_DPG_ITEM = { COLOR_TO_COLOR_TYPE = {0: 'TEXT', 1: 'TEXT', 2: 'PRIMARY', 3: 'PRIMARY', 39: 'PRIMARY', 36: 'SECONDARY', 21: 'SECONDARY', 24: 'SECONDARY', 4: 'SECONDARY', 7: 'SECONDARY', 8: 'SECONDARY', 9: 'SECONDARY', 13: 'SECONDARY', 33: 'SECONDARY', 14: 'SECONDARY', 35: 'ACCENT', 20: 'ACCENT', 37: 'ACCENT', 22: 'ACCENT', 23: 'ACCENT', 5: 'ACCENT', 25: 'ACCENT', 26: 'ACCENT', 27: 'ACCENT', 28: 'ACCENT', 29: 'ACCENT', 10: 'ACCENT', 30: 'ACCENT', 11: 'ACCENT', 31: 'ACCENT', 12: 'ACCENT', 32: 'ACCENT', 49: 'ACCENT', 34: 'ACCENT', 15: 'ACCENT', 50: 'ACCENT', 16: 'ACCENT', 18: 'ACCENT', 19: 'ACCENT', 1: 'TEXT'} BLUE = { - 'TEXT': (255, 255, 255, 255), - 'PRIMARY': (37, 37, 38, 255), - 'SECONDARY': (51, 51, 55, 255), - 'ACCENT': (0, 119, 200, 153) + 'enabled': { + 'TEXT': (255, 255, 255, 255), + 'PRIMARY': (37, 37, 38, 255), + 'SECONDARY': (51, 51, 55, 255), + 'ACCENT': (0, 119, 200, 153) + }, + 'disabled': { + 'TEXT': (255, 255, 255, 150), + 'PRIMARY': (37, 37, 38, 255), + 'SECONDARY': (51, 51, 55, 255), + 'ACCENT': (0, 119, 200, 153) + }, } ORANGE = { - 'TEXT': (255, 255, 255, 255), - 'PRIMARY': (37, 37, 38, 255), - 'SECONDARY': (51, 51, 55, 255), - 'ACCENT': (251, 133, 0, 153) + 'enabled': { + 'TEXT': (255, 255, 255, 255), + 'PRIMARY': (37, 37, 38, 255), + 'SECONDARY': (51, 51, 55, 255), + 'ACCENT': (251, 133, 0, 153) + }, + 'disabled': { + 'TEXT': (255, 255, 255, 150), + 'PRIMARY': (37, 37, 38, 255), + 'SECONDARY': (51, 51, 55, 255), + 'ACCENT': (251, 133, 0, 153) + }, } THEMES = { diff --git a/dynamic_ui.py b/dynamic_ui.py index 1b1df188c38216c9e4daa8d711237778fb84a08a..7a8d52922b8c599acaaf534440b7828dba36f841 100644 --- a/dynamic_ui.py +++ b/dynamic_ui.py @@ -1,13 +1,13 @@ import dearpygui.dearpygui as dpg -from utils import * -from constants import * +from utils import change_height, get_current_ui_type, get_config, get_config_file_path, does_tab_bar_child_exist, get_max_n_emission_point, set_main_window_value +from style_constants import * +from config import CfgVar def check_every_child_checkbox(s, a): parent = dpg.get_item_parent(s) for child in dpg.get_item_children(parent, 1): - print(s, dpg.get_item_alias(child), child, dpg.get_item_type(child)) t = dpg.get_item_type(child) if t == 'mvAppItemType::mvGroup': dpg.set_value(dpg.get_item_children(child, 1)[1], a) @@ -19,6 +19,29 @@ def check_every_child_checkbox(s, a): def folder_checkbox_clb(s, a, u): check_every_child_checkbox(s, a) +def construct_model_output_structure_ui(filepaths, folders): + dpg.add_collapsing_header(label='output', parent='model_output_child_window', tag='output_header', indent=0, default_open=True) + dpg.add_checkbox(label='select all', default_value=False, parent='output_header', tag='output', callback=folder_checkbox_clb) + + for path in folders: + folders = path.split('/') + parent = 'output_header' + for folder in folders: + if not dpg.does_item_exist(f'{folder}_header'): + with dpg.collapsing_header(label=folder, parent=parent, tag=f'{folder}_header', indent=INDENT): + dpg.add_checkbox(label='select all', default_value=False, tag=folder, callback=folder_checkbox_clb) + + parent = f'{folder}_header' + + for filepath in filepaths: + if '/' not in filepath: + parent = 'output_header' + else: + parent = filepath.split('/')[-2] + '_header' + with dpg.group(horizontal=True, parent=parent, indent=INDENT, xoffset=XOFFSET): + dpg.add_text(filepath.split('/')[-1], tag=f'{filepath}.text') + dpg.add_checkbox(default_value=False, tag=filepath) + def open_folder_dialog_clb(s, a, u): dpg.show_item('set_folder_dialog') dpg.set_item_user_data('set_folder_dialog', u) @@ -79,6 +102,7 @@ def add_emission_point_and_update_ui(x, y): 'sy': CfgVar(key='sy', value=20.0, value_type='DOUBLE', comment='[m]'), 'sz': CfgVar(key='sz', value=10.0, value_type='DOUBLE', comment='[m]') } + prefix = f'passive_tracers.tracer_{get_max_n_emission_point() + 1}.point_emission' config = get_config() @@ -90,51 +114,6 @@ def add_emission_point_and_update_ui(x, y): set_main_window_value('config', config) -def delete_emission_point_and_update_ui(x, y): - emission_point = { - 'value': CfgVar(key='value', value=28481176.531511, value_type='DOUBLE', comment=''), - 'begin': CfgVar(key='begin', value=25200.0, value_type='DOUBLE', comment='[s]'), - 'xpos': CfgVar(key='xpos', value=x, value_type='DOUBLE', comment='[m]'), - 'ypos': CfgVar(key='ypos', value=y, value_type='DOUBLE', comment='[m]'), - 'zpos': CfgVar(key='zpos', value=60.0, value_type='DOUBLE', comment='[m]'), - 'sx': CfgVar(key='sx', value=20.0, value_type='DOUBLE', comment='[m]'), - 'sy': CfgVar(key='sy', value=20.0, value_type='DOUBLE', comment='[m]'), - 'sz': CfgVar(key='sz', value=10.0, value_type='DOUBLE', comment='[m]') - } - prefix = f'passive_tracers.tracer_{get_max_n_emission_point() + 1}.point_emission' - - config = get_config() - - for key in emission_point: - full_path = '.'.join([prefix, key]) - config[full_path] = emission_point[key] - add_item_to_config_ui(full_path) - - set_main_window_value('config', config) - -def construct_model_output_structure_ui(filepaths, folders): - dpg.add_collapsing_header(label='output', parent='model_output_window', tag='output_header', indent=0, default_open=True) - dpg.add_checkbox(label='select all', default_value=False, parent='output_header', tag='output', callback=folder_checkbox_clb) - - for path in folders: - folders = path.split('/') - parent = 'output_header' - for folder in folders: - if not dpg.does_item_exist(f'{folder}_header'): - with dpg.collapsing_header(label=folder, parent=parent, tag=f'{folder}_header', indent=INDENT): - dpg.add_checkbox(label='select all', default_value=False, tag=folder, callback=folder_checkbox_clb) - - parent = f'{folder}_header' - - for filepath in filepaths: - if '/' not in filepath: - parent = 'output_header' - else: - parent = filepath.split('/')[-2] + '_header' - with dpg.group(horizontal=True, parent=parent, indent=INDENT, xoffset=XOFFSET): - dpg.add_text(filepath.split('/')[-1], tag=f'{filepath}.text') - dpg.add_checkbox(default_value=False, tag=filepath) - def construct_config_ui_collapsing_headers(config_file_path, config): dpg.add_collapsing_header(label=config_file_path, parent='main', tag=config_file_path, indent=0, default_open=True) diff --git a/handlers.py b/handlers.py index 77be6bd95c2ee07d4bd05dd45824d783f5663ee2..4f9eedd62228222f891b6b8ee75b9d35d58dc1ff 100644 --- a/handlers.py +++ b/handlers.py @@ -1,26 +1,24 @@ import dearpygui.dearpygui as dpg -from utils import * -from callbacks import * +from utils import get_config_file_path, change_height from map_edit import mouse_down_callback, mouse_release_callback, mouse_click_callback +from config_callbacks import config_save_clb def on_ctrl_o(sender, app_data): - if dpg.is_key_down(dpg.mvKey_Control) and dpg.is_key_down(dpg.mvKey_O): + if dpg.is_key_down(dpg.mvKey_LControl) and dpg.is_key_down(dpg.mvKey_O): dpg.show_item('config_open') def on_ctrl_f(sender, app_data): - if dpg.is_key_down(dpg.mvKey_Control) and dpg.is_key_down(dpg.mvKey_F): + if dpg.is_key_down(dpg.mvKey_LControl) and dpg.is_key_down(dpg.mvKey_F): dpg.show_item('search_popup') def on_ctrl_s(sender, app_data): - if dpg.is_key_down(dpg.mvKey_Control) and dpg.is_key_down(dpg.mvKey_Shift) and dpg.is_key_down(dpg.mvKey_S): + if dpg.is_key_down(dpg.mvKey_LControl) and dpg.is_key_down(dpg.mvKey_LShift) and dpg.is_key_down(dpg.mvKey_S): dpg.show_item('config_save_as') - elif dpg.is_key_down(dpg.mvKey_Control) and dpg.is_key_down(dpg.mvKey_S): - config_save_clb() + elif dpg.is_key_down(dpg.mvKey_LControl) and dpg.is_key_down(dpg.mvKey_S): + config_save_clb('', '', '') def change_height_(): - dpg.set_item_pos('status_text', (dpg.get_item_width('main') - dpg.get_text_size(dpg.get_value('status_text'))[0] - 2 * SPACE, dpg.get_item_pos('status_text')[1])) - if get_config_file_path(): change_height(get_config_file_path()) diff --git a/map_edit.py b/map_edit.py index 4b2a8618f4652125610b2c0281b13c7c6e408750..5b96d1dbe38c9c4f9992eab4aa5edf98e153cac6 100644 --- a/map_edit.py +++ b/map_edit.py @@ -1,12 +1,13 @@ import dearpygui.dearpygui as dpg -from utils import * -from callbacks import * -from constants import MAX_MAP_WIDTH, MAX_MAP_HEIGHT, EMISSION_POINT_COLOR -from dynamic_ui import construct_config_ui, add_emission_point_and_update_ui +import numpy as np + +from utils import get_map_window_value, get_config_file_path, is_drawing, get_current_action, set_map_window_value, get_emission_points, get_prev_coords, get_height_map_file_path +from map_edit_constants import EMISSION_POINT_COLOR +from dynamic_ui import add_emission_point_and_update_ui def load_height_map(file_path): - with open(file_path, "r") as file: + with open(file_path, "r", encoding='utf-8') as file: lines = file.readlines()[1:] height_map = [list(map(int, line.strip().split())) for line in lines] return np.array(height_map, dtype=np.float64) @@ -26,12 +27,12 @@ def update_texture(): x = int(emission_points[n]['xpos']) y = int(emission_points[n]['ypos']) coords = get_triangle_coords(x, y) - dpg.draw_polygon(coords, color=EMISSION_POINT_COLOR, fill=(255, 0, 0, 255), parent=f'{get_height_map_file_path()}map_drawlist', thickness=0) + tag = f'emission_point_{n}_triangle' + if not dpg.does_item_exist(tag): + dpg.draw_polygon(coords, color=EMISSION_POINT_COLOR, fill=(255, 0, 0, 255), tag=tag, parent='map_drawlist', thickness=0) texture_map = np.repeat(get_map_window_value(f'{dpg.get_value('layer')}_map'), 4) - print(texture_map.shape, np.all(texture_map == 0), f'map_{dpg.get_value('layer')}_texture', np.all(get_map_window_value(f'{dpg.get_value('layer')}_map') == 0)) - texture_map[3::4] = (texture_map[::4] != 0) dpg.set_value(f'map_{dpg.get_value('layer')}_texture', texture_map) @@ -59,6 +60,7 @@ def mouse_down_callback(): return if not dpg.is_item_focused('map_child_window'): return + if not is_drawing(): set_map_window_value('prev_coords', dpg.get_drawing_mouse_pos()) set_map_window_value('drawing', True) @@ -80,8 +82,6 @@ def mouse_release_callback(): if not dpg.is_item_focused('map_child_window'): return - dpg.hide_item('emission_values') - coords = dpg.get_drawing_mouse_pos() if get_current_action() == 'erase': @@ -106,6 +106,10 @@ def mouse_click_callback(): if not get_config_file_path(): return + if dpg.is_item_visible('emission_settings_window'): + dpg.hide_item('emission_settings_window') + return + emission_points = get_emission_points() x, y = dpg.get_drawing_mouse_pos() @@ -113,11 +117,11 @@ def mouse_click_callback(): is_pos_emission_point, n = is_mouse_pos_emission_point(x, y, emission_points) if is_pos_emission_point: - dpg.set_item_width('emission_values', 300) - dpg.set_item_height('emission_values', 300) - dpg.set_item_pos('emission_values', dpg.get_drawing_mouse_pos()) - dpg.show_item('emission_values') - dpg.set_value('emission_settings', f'value: {emission_points[n]['value']}\nbegin: {emission_points[n]['begin']}') + for key in emission_points[n]: + dpg.set_value(f'emission_point_{key}', emission_points[n][key]) + dpg.set_item_user_data('emission_settings_window', n) + dpg.set_item_pos('emission_settings_window', dpg.get_drawing_mouse_pos()) + dpg.show_item('emission_settings_window') else: if 0 <= x < dpg.get_item_width('map_child_window') and 0 <= y < dpg.get_item_height('map_child_window'): add_emission_point_and_update_ui(x, y) @@ -150,21 +154,6 @@ def is_mouse_pos_emission_point(posx, posy, emission_points, side=30): return True, n return False, 0 -def draw_triangle(array, x, y, side=30, color=1.): - - h = (3 ** 0.5 / 2) * side - - x1, y1 = x, y - (2 / 3) * h - x2, y2 = x + side / 2, y + (1 / 3) * h - x3, y3 = x - side / 2, y + (1 / 3) * h - - for i in range(y - side, y + side): - for j in range(x - side, x + side): - if is_point_in_triangle(x1, y1, x2, y2, x3, y3, j, i): - array[i, j] = color - - return array - def get_triangle_coords(x, y, side=30): h = (3 ** 0.5 / 2) * side diff --git a/map_edit_callbacks.py b/map_edit_callbacks.py new file mode 100644 index 0000000000000000000000000000000000000000..f13050f26e26fe1e3f2e6c4347fbfdee780cdac0 --- /dev/null +++ b/map_edit_callbacks.py @@ -0,0 +1,217 @@ +import dearpygui.dearpygui as dpg +import tifffile +import numpy as np + +from map_edit_constants import TREE_COLOR, ROAD_COLOR, BUILDING_COLOR, LAYER_TO_COLOR +from map_edit import dump_height_map, load_height_map, update_texture +from generator import LCZ +from utils import show_message, set_main_window_value, get_config, get_config_file_path, delete_item_and_clear_alias, delete_item_children_and_clear_aliases, set_map_window_value, get_map_window_value, get_height_map_file_path + +def delete_emission_point_clb(s, a, u): + n = dpg.get_item_user_data('emission_settings_window') + prefix = f'{get_config_file_path()}.passive_tracers.tracer_{n}.point_emission' + + keys = ['value', 'begin', 'xpos', 'ypos', 'zpos', 'sx', 'sy', 'sz'] + + config = get_config() + + for key in keys: + path = f'passive_tracers.tracer_{n}.point_emission.{key}' + del config[path] + + set_main_window_value('config', config) + + delete_item_children_and_clear_aliases(prefix) + + dpg.hide_item('emission_settings_window') + + tag = f'emission_point_{n}_triangle' + + dpg.delete_item(tag) + +def set_emission_point_value_clb(s, value, u): + n = dpg.get_item_user_data('emission_settings_window') + + key = s.split('_')[-1] + + path = f'{get_config_file_path()}.passive_tracers.tracer_{n}.point_emission.{key}' + + if dpg.does_item_exist(path): + dpg.set_value(path, value) + + config = get_config() + + config[path].value = value + + set_main_window_value('config', config) + +def clear_map(): + delete_item_and_clear_alias('map_buildings_texture') + delete_item_and_clear_alias('map_trees_texture') + delete_item_and_clear_alias('map_roads_texture') + delete_item_children_and_clear_aliases('map_drawlist') + settings = { + 'buildings_map': None, + 'trees_map': None, + 'roads_map': None, + 'height_map_file_path': None, + 'drawing': False, + 'prev_coords': None, + 'current_action': None, + 'max_height': 0 + } + dpg.set_item_user_data('map_window', settings) + +def map_window_on_close_clb(s, a, u): + clear_map() + +def construct_map_layers(w, h): + dpg.set_item_width('map_child_window', w) + dpg.set_item_height('map_child_window', h) + + dpg.add_dynamic_texture(w, h, np.zeros(w * h * 4), tag='map_buildings_texture', parent='textures') + dpg.add_dynamic_texture(w, h, np.zeros(w * h * 4), tag='map_trees_texture', parent='textures') + dpg.add_dynamic_texture(w, h, np.zeros(w * h * 4), tag='map_roads_texture', parent='textures') + + dpg.bind_item_theme('map_child_window', 'map_image_theme') + dpg.add_drawlist(w, h, tag=f'map_drawlist', parent='map_child_window', pos=[0, 0]) + + dpg.draw_image(f'map_buildings_texture', color=BUILDING_COLOR, tag=f'map_buildings_image', pmin=(0, 0), pmax=(w, h), parent=f'map_drawlist') + dpg.draw_image(f'map_trees_texture', color=TREE_COLOR, tag=f'map_trees_image', pmin=(0, 0), pmax=(w, h), parent=f'map_drawlist') + dpg.draw_image(f'map_roads_texture', color=ROAD_COLOR, tag=f'map_roads_image', pmin=(0, 0), pmax=(w, h), parent=f'map_drawlist') + +def map_open_layer_clb(s, a, u): + height_map_file_path = a['file_path_name'] + + layer = dpg.get_value('layer_radio_button') + + if height_map_file_path.endswith('txt'): + try: + height_map = load_height_map(height_map_file_path) + except FileNotFoundError: + show_message('no file found') + return + except Exception: + show_message('bad file format') + return + max_height = np.max(height_map) + height_map /= max_height + else: + height_map = tifffile.imread(height_map_file_path) + max_height = 1 + + h, w = height_map.shape + + delete_item_and_clear_alias(f'map_{layer}_texture') + + if dpg.does_alias_exist(f'map_{layer}_texture'): + dpg.remove_alias(f'map_{layer}_texture') + + delete_item_and_clear_alias(f'map_{layer}_image') + + if dpg.does_alias_exist(f'map_{layer}_image'): + dpg.remove_alias(f'map_{layer}_image') + + set_map_window_value(f'{layer}_map', height_map) + + dpg.add_dynamic_texture(w, h, np.zeros(w * h * 4), tag=f'map_{layer}_texture', parent='textures') + + dpg.draw_image(f'map_{layer}_texture', color=LAYER_TO_COLOR[layer], tag=f'map_{layer}_image', pmin=(0, 0), pmax=(w, h), parent=f'map_drawlist') + + dpg.set_value('layer', layer) + + update_texture() + +def map_open_clb(s, a, u): + height_map_file_path = a['file_path_name'] + + if height_map_file_path.endswith('txt'): + try: + height_map = load_height_map(height_map_file_path) + except FileNotFoundError: + show_message('no file found') + return + except Exception: + show_message('bad file format') + return + max_height = np.max(height_map) + height_map /= max_height + else: + height_map = tifffile.imread(height_map_file_path) + max_height = 1 + + h, w = height_map.shape + + clear_map() + + set_map_window_value('buildings_map', height_map) + set_map_window_value('roads_map', np.zeros((h, w))) + set_map_window_value('trees_map', np.zeros((h, w))) + set_map_window_value('height_map_file_path', height_map_file_path) + set_map_window_value('max_height', max_height) + + construct_map_layers(w, h) + + dpg.set_value('layer', 'buildings') + + update_texture() + + dpg.show_item('map_window') + +def generate_map_clb(s, a, u): + height_map = LCZ(config_path=f'configs/{s}.json').to_height_map(dtype=np.float64) + + max_height = np.max(height_map) + height_map /= max_height + + h, w = height_map.shape + + clear_map() + + set_map_window_value('buildings_map', height_map) + set_map_window_value('roads_map', np.zeros((h, w))) + set_map_window_value('trees_map', np.zeros((h, w))) + set_map_window_value('height_map_file_path', 'map.txt') + set_map_window_value('max_height', max_height) + + construct_map_layers(w, h) + + dpg.set_value('layer', 'buildings') + + update_texture() + + dpg.show_item('map_window') + +def save_map(file_path): + if file_path.endswith('.tif') or file_path.endswith('.tiff'): + with TiffWriter(file_path) as tif: + tif.write(get_map_window_value('buildings_map')) + tif.write(get_map_window_value('roads_map')) + tif.write(get_map_window_value('trees_map')) + else: + dump_height_map(get_map_window_value('buildings_map'), file_path) + +def map_save_clb(s, a, u): + file_path = get_height_map_file_path() + save_map(file_path) + show_message('file saved successfully:)') + +def map_save_as_clb(s, a, u): + file_path = a['file_path_name'] + save_map(file_path) + show_message('file saved successfully:)') + +def open_map_save_as_dialog_clb(s, a, u): + dpg.show_item('map_save_as') + +def open_map_open_dialog_clb(s, a, u): + dpg.show_item('map_open') + +def open_map_open_layer_dialog_clb(s, a, u): + dpg.show_item('map_open_layer') + +def set_action_clb(s, a, u): + set_map_window_value('current_action', s) + +def set_action_none_clb(s, a, u): + set_map_window_value('current_action', None) \ No newline at end of file diff --git a/map_edit_constants.py b/map_edit_constants.py new file mode 100644 index 0000000000000000000000000000000000000000..0265eb5bf3092b815d711699fb64d8b742bc32c3 --- /dev/null +++ b/map_edit_constants.py @@ -0,0 +1,25 @@ +from config import CfgVar + +ROAD_COLOR = (221, 161, 94, 255) +TREE_COLOR = (96, 108, 56, 255) +BUILDING_COLOR = (188, 108, 37, 255) +EMISSION_POINT_COLOR = (255, 0, 0, 255) + +LAYER_TO_COLOR = { + 'buildings': BUILDING_COLOR, + 'trees': TREE_COLOR, + 'roads': ROAD_COLOR +} + +LCZS = ['compact_high_rise', 'compact_low_rise', 'compact_mid_rise', 'heavy_industry', 'large_low_rise', 'lightweight_low_rise', 'open_high_rise', 'open_low_rise', 'open_mid_rise', 'sparsley_build'] + +EMISSION_POINT_CONFIG_VARIABLES = { + 'value': CfgVar(key='value', value=28481176.531511, value_type='DOUBLE', comment=''), + 'begin': CfgVar(key='begin', value=25200.0, value_type='DOUBLE', comment='[s]'), + 'xpos': CfgVar(key='xpos', value=200.0, value_type='DOUBLE', comment='[m]'), + 'ypos': CfgVar(key='ypos', value=200.0, value_type='DOUBLE', comment='[m]'), + 'zpos': CfgVar(key='zpos', value=60.0, value_type='DOUBLE', comment='[m]'), + 'sx': CfgVar(key='sx', value=20.0, value_type='DOUBLE', comment='[m]'), + 'sy': CfgVar(key='sy', value=20.0, value_type='DOUBLE', comment='[m]'), + 'sz': CfgVar(key='sz', value=10.0, value_type='DOUBLE', comment='[m]') +} \ No newline at end of file diff --git a/map_edit_ui.py b/map_edit_ui.py new file mode 100644 index 0000000000000000000000000000000000000000..ff877d3bf233799749b746ac30b270cc1cb7307c --- /dev/null +++ b/map_edit_ui.py @@ -0,0 +1,123 @@ +import dearpygui.dearpygui as dpg + +from map_edit_constants import * +from utils import combo_menu, apply_tint_func +from map_edit_callbacks import * + +def construct_map_open_dialog(): + with dpg.file_dialog(directory_selector=False, modal=True, show=False, callback=map_open_clb, tag='map_open', width=600, height=400): + dpg.add_file_extension("Map files (*.txt *.tif *.tiff){.txt,.tif,.tiff}", color=(0, 255, 0, 255)) + +def construct_map_open_layer_dialog(): + with dpg.file_dialog(directory_selector=False, modal=True, show=False, callback=map_open_layer_clb, tag='map_open_layer', width=600, height=400): + dpg.add_file_extension("Map files (*.txt *.tif *.tiff){.txt,.tif,.tiff}", color=(0, 255, 0, 255)) + + dpg.add_radio_button(items=('buildings', 'trees', 'roads'), default_value='buildings', tag='layer_radio_button') + +def construct_map_save_as_dialog(): + with dpg.file_dialog(directory_selector=False, modal=True, show=False, callback=map_save_as_clb, tag='map_save_as', width=600, height=400): + dpg.add_file_extension("Map files (*.txt *.tif *.tiff){.txt,.tif,.tiff}", color=(0, 255, 0, 255)) + dpg.add_file_extension('.*', color=(255, 255, 255, 255)) + +def construct_map_edit_window(): + user_data = { + 'buildings_map': None, + 'trees_map': None, + 'roads_map': None, + 'height_map_file_path': None, + 'drawing': False, + 'prev_coords': None, + 'current_action': None, + 'max_height': 0 + } + + dpg.add_texture_registry(tag='textures') + + with dpg.theme() as item_theme: + with dpg.theme_component(dpg.mvAll): + dpg.add_theme_color(dpg.mvThemeCol_ChildBg, (0, 0, 0, 0), category=dpg.mvThemeCat_Core) + dpg.add_theme_style(dpg.mvStyleVar_ChildRounding, 0, category=dpg.mvThemeCat_Core) + + with dpg.theme(tag='map_image_theme'): + with dpg.theme_component(dpg.mvAll): + dpg.add_theme_style(dpg.mvStyleVar_WindowPadding, 0, 0, category=dpg.mvThemeCat_Core) + dpg.add_theme_style(dpg.mvStyleVar_ItemSpacing, 0, 0, category=dpg.mvThemeCat_Core) + dpg.add_theme_style(dpg.mvStyleVar_FramePadding, 0, 0, category=dpg.mvThemeCat_Core) + dpg.add_theme_style(dpg.mvStyleVar_ItemInnerSpacing, 0, 0, category=dpg.mvThemeCat_Core) + + dpg.add_colormap_registry(label='colormap registry', tag='colormap_registry') + dpg.add_colormap(list(map(apply_tint_func(BUILDING_COLOR), [[0, 0, 0], [100, 100, 100], [255, 255, 255]])), False, tag='buildings_colormap', parent='colormap_registry') + dpg.add_colormap(list(map(apply_tint_func(TREE_COLOR), [[0, 0, 0], [100, 100, 100], [255, 255, 255]])), False, tag='trees_colormap', parent='colormap_registry') + dpg.add_colormap(list(map(apply_tint_func(ROAD_COLOR), [[255, 255, 255], [255, 255, 255], [255, 255, 255]])), False, tag='roads_colormap', parent='colormap_registry') + + with dpg.window( + label='edit map', + tag='map_window', + user_data=user_data, + horizontal_scrollbar=True, + show=False, + width=600, + height=400, + pos=[20, 60], + on_close=map_window_on_close_clb): + with dpg.menu_bar(): + with dpg.menu(label='file'): + dpg.add_menu_item(label='open', callback=open_map_open_dialog_clb, tag='open_map_open_dialog') + dpg.add_menu_item(label='open layer', callback=open_map_open_layer_dialog_clb, tag='open_map_open_layer_dialog') + with dpg.menu(label='generate'): + for lcz_type in LCZS: + dpg.add_menu_item(label=lcz_type, check=False, callback=combo_menu(generate_map_clb), tag=lcz_type) + dpg.add_menu_item(label='save', callback=map_save_clb, tag='map_save') + dpg.add_menu_item(label='save as', callback=open_map_save_as_dialog_clb, tag='open_map_save_as_dialog') + dpg.add_menu_item(label='add map to config', tag='add_map_to_config') + + with dpg.group(horizontal=True): + with dpg.group(width=200, tag='tools'): + dpg.add_radio_button(items=('buildings', 'trees', 'roads'), tag='layer', default_value='buildings', callback=lambda s, a, u: dpg.bind_colormap('height_input', f'{a}_colormap')) + dpg.add_button(label='erase', callback=set_action_clb, tag='erase') + dpg.add_button(label='add rectangle', callback=set_action_clb, tag='draw_rect') + dpg.add_colormap_slider(tag='height_input') + dpg.bind_colormap(dpg.last_item(), 'buildings_colormap') + + with dpg.child_window(no_scrollbar=True, no_scroll_with_mouse=True, menubar=False, border=True, tag='map_child_window'): + dpg.add_child_window(tag='drawing_frame', show=False, width=0, height=0) + + dpg.bind_item_theme('drawing_frame', item_theme) + dpg.bind_item_theme('map_child_window', 'map_image_theme') + +def construct_emission_point_settings_ui(): + xoffset = 100 + width = 300 + with dpg.window(tag='emission_settings_window', show=False, autosize=True): + dpg.add_button(label='delete', tag='delete_emission_point', callback=delete_emission_point_clb) + with dpg.group(horizontal=True, xoffset=xoffset, width=width): + dpg.add_text('value') + dpg.add_input_double(default_value=0, tag='emission_point_value', format='%.10f', callback=set_emission_point_value_clb) + with dpg.group(horizontal=True, xoffset=xoffset, width=width): + dpg.add_text('begin') + dpg.add_input_double(default_value=0, tag='emission_point_begin', format='%.10f', callback=set_emission_point_value_clb) + with dpg.group(horizontal=True, xoffset=xoffset, width=width): + dpg.add_text('xpos') + dpg.add_text('', tag='emission_point_xpos') + with dpg.group(horizontal=True, xoffset=xoffset, width=width): + dpg.add_text('ypos') + dpg.add_text('', tag='emission_point_ypos') + with dpg.group(horizontal=True, xoffset=xoffset, width=width): + dpg.add_text('zpos') + dpg.add_input_double(default_value=0, tag='emission_point_zpos', format='%.10f', callback=set_emission_point_value_clb) + with dpg.group(horizontal=True, xoffset=xoffset, width=width): + dpg.add_text('sx') + dpg.add_input_double(default_value=0, tag='emission_point_sx', format='%.10f', callback=set_emission_point_value_clb) + with dpg.group(horizontal=True, xoffset=xoffset, width=width): + dpg.add_text('sy') + dpg.add_input_double(default_value=0, tag='emission_point_sy', format='%.10f', callback=set_emission_point_value_clb) + with dpg.group(horizontal=True, xoffset=xoffset, width=width): + dpg.add_text('sz') + dpg.add_input_double(default_value=0, tag='emission_point_sz', format='%.10f', callback=set_emission_point_value_clb) + +def construct_ui(): + construct_map_open_dialog() + construct_map_save_as_dialog() + construct_map_edit_window() + construct_emission_point_settings_ui() + construct_map_open_layer_dialog() \ No newline at end of file diff --git a/paths.py b/paths.py new file mode 100644 index 0000000000000000000000000000000000000000..ebb9cf724d6f49b379435f20faafb17b32634fdc --- /dev/null +++ b/paths.py @@ -0,0 +1,5 @@ +import os + +FONT_PATH = os.path.join('fonts', 'Montserrat-Medium.ttf') +ICON_PATH = os.path.join('icons', 'icon.ico') +CONFIG_PARSER_EXECUTABLE = "config-parser/parser-cli" \ No newline at end of file diff --git a/remote_run.py b/remote_run.py index 6e49ed3bc00a3f8d5d76b4294f4643b9338ed72e..76a91d022e822996bfa11d12133dfb1263220430 100644 --- a/remote_run.py +++ b/remote_run.py @@ -1,158 +1,171 @@ -import threading -import os -import re -import time +from uuid import uuid4 +from paramiko import SSHClient, AutoAddPolicy from scp import SCPClient -import dearpygui.dearpygui as dpg -import paramiko -from utils import show_status_text +class Connection: + def __init__(self, host, username, password=None, id_rsa_path=None): + self.host = host + self.client = SSHClient() + self.transport = None + self.username = username + self.password = password + self.id_rsa_path = id_rsa_path -HOST = 'geophyslab.srcc.msu.ru' -URBAN_LES_REPOS = ( - 'git clone http://{username}:{password}@tesla.parallel.ru/emortikov/nselibx-common.git', - 'git clone http://{username}:{password}@tesla.parallel.ru/emortikov/nselibx-wstgrid.git', - 'git clone http://{username}:{password}@tesla.parallel.ru/emortikov/nse-gabls1-urban-les.git' -) - -def build_model(server_username, server_password, gitlab_username, gitlab_password, repos=URBAN_LES_REPOS): - try: - ssh = paramiko.SSHClient() - ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - ssh.connect(HOST, username=server_username, password=server_password) - - commands = [ - 'mkdir run', - 'cd ./run' - ] - - for repo in repos: - commands.append(repo.format(username=gitlab_username, password=gitlab_password)) - - commands += [ - 'cd ./nse-gabls1-urban-les/nse-gabls1-urban-les/', - 'chmod +x cpall.sh', - './cpall.sh ../../code', - 'cd ../../code', - 'make -B MACHINE=local COMPILER=gnu -j 23', - ] - - commands = '; '.join(commands) - - show_status_text(f'building model') - dpg.show_item('loading') - x, y = dpg.get_item_pos('status_text') - dpg.set_item_pos('loading', (x - 40, y)) - - stdin, stdout, stderr = ssh.exec_command(commands) - - with open('log.txt', 'w') as file: - file.write(stdout.read().decode()) - - ssh.close() - - show_status_text('') - dpg.hide_item('loading') - - show_status_text('model builded:) log written to log.txt.') + self.connect() - except Exception as e: - show_status_text(f'error: {str(e)}') - -def make_run_command(): - command = f'mpirun -np {int(dpg.get_value('-np'))} ./nsenx' - if dpg.get_value('-arch') != 'cpu': - command += f' -arch {dpg.get_value('-arch')}' - if dpg.get_value('use-udump'): - command += f' -udump {dpg.get_value('-udump')}' - if dpg.get_value('use-model-output'): - command += f' -model-output {dpg.get_value('-model-output')}' - return command - -def run_on_lab_server(username, password): - try: - show_status_text('model running...') - dpg.show_item('progress_bar') - ssh = paramiko.SSHClient() - ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - ssh.connect(HOST, username=username, password=password) - - scp = SCPClient(ssh.get_transport()) - - scp.put('tmp.txt', 'run/code/config.txt') - - command = make_run_command() - - commands = [ - 'cd ./run', - 'cd ./code', - command - ] - + def connect(self): + try: + self.client.set_missing_host_key_policy(AutoAddPolicy()) + if self.password is not None: + self.client.connect(self.host, username=self.username, password=self.password) + elif self.id_rsa_path is not None: + self.client.connect(self.host, username=self.username, key_filename=self.id_rsa_path) + else: + self.client.connect(self.host, username=self.username) + except Exception as e: + raise e + + def exec_command(self, command): + try: + stdin, stdout, stderr = self.client.exec_command(command) + return stdout + except Exception: + try: + self.connect() + stdin, stdout, stderr = self.client.exec_command(command) + return stdout + except Exception as e: + raise e + + def upload(self, local_filename, server_filename): + if not self.transport: + self.transport = SCPClient(self.client.get_transport()) + + self.transport.put(local_filename, server_filename) + + def download(self, server_filename, local_filename): + if not self.transport: + self.transport = SCPClient(self.client.get_transport()) + + self.transport.get(server_filename, local_filename) + + def close(self): + self.client.close() + +class BuildingSession: + def __init__(self, connection, build_commands, executable_file_path, gitlab_username=None, gitlab_password=None, uuid=None): + self.conn = connection + if uuid is not None: + self.uuid = uuid + else: + self.uuid = str(uuid4()) + self.build_commands = build_commands + self.executable_file_path = f'{self.uuid}/{executable_file_path}' + + commands = [f'mkdir {self.uuid}', f'cd {self.uuid}'] + self.build_commands commands = '; '.join(commands) - - log = '' - - stdin, stdout, stderr = ssh.exec_command(commands) - - while not stdout.channel.exit_status_ready(): - if stdout.channel.recv_ready(): - output = stdout.channel.recv(512).decode() - update_progress(output) - log += '\n\n' + output - # time.sleep(1) - - output = stdout.read().decode() - dpg.set_value('progress_bar', 1.) - - log += '\n\n' + output - - with open('log.txt', 'w') as file: - file.write(log) - scp.close() - ssh.close() - - dpg.hide_item('progress_bar') - show_status_text('success:) log written to log.txt.') - - except Exception as e: - show_status_text(f'error: {str(e)}') - -def update_progress_building_model(): - show_status_text(f'building model') - dpg.show_item('loading') - x, y = dpg.get_item_pos('status_text') - dpg.set_item_pos('loading', (x - 40, y)) - -def update_progress(output): - if not re.findall(r'([0-9]+)%', output): - progress = 0.0 - else: - progress = int(re.findall(r'([0-9]+)%', output)[-1]) / 100 - dpg.set_value('progress_bar', progress) - -def start_remote_execution(server_username, server_password): - threading.Thread(target=run_on_lab_server, args=(server_username, server_password)).start() - -def start_model_build(server_username, server_password, gitlab_username, gitlab_password): - threading.Thread(target=build_model, args=(server_username, server_password, gitlab_username, gitlab_password)).start() - -def get_server_output_structure(username, password): - try: - ssh = paramiko.SSHClient() - ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - ssh.connect(HOST, username=username, password=password) - - stdin, stdout, stderr = ssh.exec_command('cd ./run/code/output; find . -type f') - - output = stdout.read().decode() - - ssh.close() - - return output - - except Exception as e: - show_status_text(f'error: {str(e)}') \ No newline at end of file + if gitlab_username and gitlab_password: + self.stdout = self.conn.exec_command(commands.format(username=gitlab_username, password=gitlab_password)) + else: + self.stdout = self.conn.exec_command(commands) + + def building(self): + return not self.stdout.channel.exit_status_ready() + + def output(self): + if self.stdout.channel.recv_ready(): + return self.stdout.channel.recv(512).decode() + return '' + +class RunningSession: + def __init__(self, connection, config_file_path, run_commands, executable_file_path, uuid=None): + self.conn = connection + if uuid is not None: + self.uuid = uuid + else: + self.uuid = str(uuid4()) + self.config_file_path = config_file_path + self.run_commands = run_commands + self.executable_file_path = executable_file_path + + self.conn.exec_command(f'mkdir {self.uuid}; cp {self.executable_file_path} {self.uuid}/nsenx') + self.conn.upload(self.config_file_path, f'{self.uuid}/config.txt') + commands = [f'cd {self.uuid}'] + self.run_commands + + self.stdout = self.conn.exec_command('; '.join(commands)) + + def running(self): + return not self.stdout.channel.exit_status_ready() + + def output(self): + if self.stdout.channel.recv_ready(): + return self.stdout.channel.recv(512).decode() + return '' + +if __name__ == '__main__': + from dotenv import load_dotenv + import time + import os + + load_dotenv() + + gitlab_login = os.getenv('GITLAB_LOGIN', '') + gitlab_password = os.getenv('GITLAB_PASS', '') + server_login = os.getenv('SERVER_LOGIN', '') + server_password = os.getenv('SERVER_PASS', '') + + BUILD_COMMANDS = [ + 'git clone http://{username}:{password}@tesla.parallel.ru/emortikov/nselibx-common.git', + 'git clone http://{username}:{password}@tesla.parallel.ru/emortikov/nselibx-wstgrid.git', + 'git clone http://{username}:{password}@tesla.parallel.ru/emortikov/nse-gabls1-urban-les.git', + 'cd ./nse-gabls1-urban-les/nse-gabls1-urban-les/', + 'chmod +x cpall.sh', + './cpall.sh ../../code', + 'cd ../../code', + 'make -B MACHINE=local COMPILER=gnu -j 23' + ] + + LOM_BUILD = [ + 'git clone http://{username}:{password}@tesla.parallel.ru/emortikov/nselibx-common.git', + 'git clone http://{username}:{password}@tesla.parallel.ru/emortikov/nselibx-wstgrid.git', + 'git clone http://{username}:{password}@tesla.parallel.ru/emortikov/nse-gabls1-urban-les.git', + 'mkdir build', + 'cd build', + 'module load intel/2019.5 openmpi/4.0.1-icc cmake/3.19.8', + 'cmake -DCMAKE_CONFIG_MODULE=lomonosov_pascal_ompi -DCOMPILER=intel -S ../nse-gabls1-urban-les/ -B ./', + 'make' + ] + + LAB_HOST = 'geophyslab.srcc.msu.ru' + LOMONOSOV_HOST = 'lomonosov2.parallel.ru' + + conn = Connection(LOMONOSOV_HOST, 'gashchuk2011_2043', id_rsa_path='D:/lab/id_rsa') + + building_session = BuildingSession(conn, LOM_BUILD, 'code/nsenx', gitlab_login, gitlab_password) + + while building_session.building(): + print(building_session.output()) + time.sleep(2) + + # conn = Connection(LAB_HOST, server_login, server_password) + + # building_session = BuildingSession(conn, BUILD_COMMANDS, 'code/nsenx', gitlab_login, gitlab_password) + + # print(building_session.uuid) + + # while building_session.building(): + # print('building') + # print(building_session.output()) + # time.sleep(2) + + # running_session = RunningSession(conn, '../config-ex.txt', ['mpirun -np 8 ./nsenx'], '4e26cb1f-187b-4462-9794-5e8cc3844a1f/code/nsenx')#building_session.executable_file_path) + + # print(running_session.uuid) + + # while running_session.running(): + # print('running') + # print(running_session.output()) + # time.sleep(2) diff --git a/remote_run_callbacks.py b/remote_run_callbacks.py new file mode 100644 index 0000000000000000000000000000000000000000..87cdd2cb97bf612d99efa5437d9f763e585b383e --- /dev/null +++ b/remote_run_callbacks.py @@ -0,0 +1,250 @@ +import os +import re +import threading + +from socket import error +from uuid import uuid4 + +import dearpygui.dearpygui as dpg +from paramiko.ssh_exception import BadHostKeyException, AuthenticationException, UnableToAuthenticate, NoValidConnectionsError, SSHException + +from utils import get_config_file_path, get_config, update_config_values, get_run_window_value, set_run_window_value +from remote_run import Connection, BuildingSession, RunningSession +from remote_run_constants import * +from style_constants import FONT_SIZE +from dynamic_ui import construct_model_output_structure_ui +from config import dump_config +from style import get_accent_color + +def update_run_model_window(): + builds = get_run_window_value('builds') + connections = get_run_window_value('connections') + + machine = LABEL_TO_MACHINE[dpg.get_value('machine')] + model = LABEL_TO_MODEL[dpg.get_value('model')] + + if (machine, model) in builds: + dpg.hide_item('credentials') + elif machine in connections: + dpg.show_item('gitlab_group') + dpg.hide_item('lab_server_group') + dpg.hide_item('lomonosov_server_group') + else: + if machine == LAB: + dpg.show_item('gitlab_group') + dpg.show_item('lab_server_group') + dpg.hide_item('lomonosov_server_group') + elif machine == LOMONOSOV: + dpg.show_item('gitlab_group') + dpg.hide_item('lab_server_group') + dpg.show_item('lomonosov_server_group') + +def update_run_model_window_clb(s, a, u): + update_run_model_window() + +def show_run_model_window_clb(s, a, u): + if get_config_file_path(): + update_config_values() + dump_config(get_config(), get_config_file_path()) + + dpg.set_value('config_file_path', get_config_file_path()) + + update_run_model_window() + + dpg.show_item('run_model_window') + +def add_running_session_ui(running_session_id, machine, model): + with dpg.table_row(parent='models_table', tag=running_session_id): + dpg.add_text(MODEL_TO_LABEL[model], tag=f'{running_session_id}_model') + dpg.add_text(MACHINE_TO_LABEL[machine], tag=f'{running_session_id}_machine') + dpg.add_text('building', color=list(get_accent_color()[:3]) + [255], tag=f'{running_session_id}_status') + with dpg.group(horizontal=False): + dpg.add_spacer(height=0) + dpg.add_progress_bar(label='progress', default_value=0.0, tag=f'{running_session_id}_progress_bar', height=FONT_SIZE, width=150) + dpg.add_button(label='download output', enabled=False, tag=f'{running_session_id}_download_button', callback=show_download_output_window_clb, user_data=running_session_id) + dpg.bind_item_theme(f'{running_session_id}_progress_bar', 'progress_bar_theme') + +def make_execute_command(): + np = int(dpg.get_value('-np')) + arch = dpg.get_value('-arch') + udump = dpg.get_value('-udump') + model_output = dpg.get_value('-model-output') + + command = f'mpirun -np {np} ./nsenx -arch {arch}' + if dpg.get_value('use-udump'): + command += f' -udump {dpg.get_value('-udump')}' + if dpg.get_value('use-model-output'): + command += f' -model-output {dpg.get_value('-model-output')}' + + return command + +def update_progress(running_session_id, output): + if not re.findall(r'([0-9]+)%', output): + progress = dpg.get_value(f'{running_session_id}_progress_bar') + else: + progress = int(re.findall(r'([0-9]+)%', output)[-1]) / 100 + dpg.set_value(f'{running_session_id}_progress_bar', progress) + +def run_model(): + dpg.hide_item('run_model_window') + + config_file_path = dpg.get_value('config_file_path') + + if not os.path.exists(config_file_path): + show_message(f'no {config_file_path} file :(') + return + + builds = get_run_window_value('builds') + connections = get_run_window_value('connections') + + machine = LABEL_TO_MACHINE[dpg.get_value('machine')] + model = LABEL_TO_MODEL[dpg.get_value('model')] + + if machine not in connections: + if machine == LAB: + username = dpg.get_value('server_username') + password = dpg.get_value('server_password') + elif machine == LOMONOSOV: + username = dpg.get_value('lomonosov_username') + if 'ID_RSA_PATH' in os.environ: + id_rsa_path = os.environ['ID_RSA_PATH'] + else: + id_rsa_path = None + try: + if machine == LAB: + conn = Connection(MACHINE_TO_HOST[machine], username, password) + elif machine == LOMONOSOV: + if id_rsa_path: + conn = Connection(MACHINE_TO_HOST[machine], username, id_rsa_path=id_rsa_path) + else: + conn = Connection(MACHINE_TO_HOST[machine], username) + except BadHostKeyException: + show_message('server’s host key could not be verified\ntry again') + return + except AuthenticationException: + show_message('wrong username or password') + return + except error: + show_message('check your internet connection :)') + return + except NoValidConnectionsError: + show_message('something wrong with the server ¯\\_(ツ)_/¯') + return + except SSHException: + show_message('i have no idea what happend\nplease contact @maryshca') + return + connections[machine] = conn + set_run_window_value('connections', connections) + else: + conn = connections[machine] + + running_session_id = str(uuid4()) + + if machine == LOMONOSOV: + running_session_id = '~/_scratch/' + running_session_id + + add_running_session_ui(running_session_id, machine, model) + + if (machine, model) not in builds: + dpg.hide_item(f'{running_session_id}_progress_bar') + + gitlab_username = dpg.get_value('gitlab_username') + gitlab_password = dpg.get_value('gitlab_password') + build_commands = BUILD_COMMANDS[(machine, model)] + exec_file_path = EXECUTABLE_FILE_PATH[(machine, model)] + building_session = BuildingSession(conn, build_commands, exec_file_path, gitlab_username, gitlab_password) + + while building_session.building(): + output = building_session.output() + update_progress(running_session_id, output) + + executable_file_path = building_session.executable_file_path + + builds[(machine, model)] = executable_file_path + set_run_window_value('builds', builds) + + dpg.show_item(f'{running_session_id}_progress_bar') + else: + executable_file_path = builds[(machine, model)] + + dpg.set_value(f'{running_session_id}_status', 'running') + + if machine == LOMONOSOV: + run_commands = RUN_COMMANDS[(machine, model)] # + make_run_sh_for_slurm() + ['sbatch ./run.sh'] + else: + run_commands = RUN_COMMANDS[(machine, model)] + [make_execute_command()] + + running_session = RunningSession(conn, config_file_path, run_commands, executable_file_path, running_session_id) + + if machine == LOMONOSOV: + output = running_session.output() + sbatch_id = re.findall(r'([0-1]+)', output)[0] + return + + while running_session.running(): + output = running_session.output() + update_progress(running_session_id, output) + + dpg.set_value(f'{running_session_id}_status', 'successful run') + dpg.set_value(f'{running_session_id}_progress_bar', 1.0) + dpg.enable_item(f'{running_session_id}_download_button') + +def run_model_clb(): + threading.Thread(target=run_model).start() + +def show_download_output_window_clb(s, a, running_session_id): + delete_item_children_and_clear_aliases('output_header') + + machine = LABEL_TO_MACHINE[dpg.get_value(f'{running_session_id}_machine')] + + conn = get_run_window_value('connections')[machine] + stdout = conn.exec_command(f'cd ./{running_session_id}/output; find . -type f') + output = stdout.read().decode() + dirs, filepaths = parse_dirs(output) + + construct_model_output_structure_ui(filepaths, dirs) + + dpg.set_item_user_data('model_output_window', (dirs, filepaths, running_session_id)) + + dpg.show_item('model_output_window') + +def open_download_to_folder_dialog_clb(): + dpg.show_item('download_to_folder_dialog') + +def open_config_file_path_dialog_clb(): + dpg.show_item('config_file_path_dialog') + +def set_download_output_to_folder_clb(s, a, u): + dpg.set_value('download_output_to_folder', a['file_path_name']) + +def set_config_file_path_clb(s, a, u): + dpg.set_value('config_file_path', a['file_path_name']) + +def download_output(): + dpg.hide_item('model_output_window') + dirs, filepaths, running_session_id = dpg.get_item_user_data('model_output_window') + + download = [] + for filepath in filepaths: + if dpg.get_value(filepath): + download.append(filepath) + + machine = LABEL_TO_MACHINE[dpg.get_value(f'{running_session_id}_machine')] + + download_to_folder = dpg.get_value('download_output_to_folder') + + dpg.set_value(f'{running_session_id}_status', 'downloading') + + prefix = os.path.join(download_to_folder) + + construct_folder_structure(dirs, prefix=prefix) + + conn = get_run_window_value('connections')[machine] + + for filepath in download: + conn.download(f'{running_session_id}/output/{filepath}', os.path.join(prefix, filepath)) + + dpg.set_value(f'{running_session_id}_status', 'downloaded') + +def download_output_clb(s, a, u): + threading.Thread(target=download_output).start() \ No newline at end of file diff --git a/remote_run_constants.py b/remote_run_constants.py new file mode 100644 index 0000000000000000000000000000000000000000..a52caec7cc4e9864a9d1e27bfbd9456592e8720e --- /dev/null +++ b/remote_run_constants.py @@ -0,0 +1,69 @@ +LAB = 'geophyslab.srcc.msu.ru' +LOMONOSOV = 'lomonosov2.parallel.ru' + +LAB_HOST = 'geophyslab.srcc.msu.ru' +LOMONOSOV_HOST = 'lomonosov2.parallel.ru' + +MACHINES = [LAB, LOMONOSOV] + +MACHINE_TO_HOST = { + LAB: LAB_HOST, + LOMONOSOV: LOMONOSOV_HOST +} + +URBAN_LES = 'urban-les' + +MODELS = [URBAN_LES] + +BUILD_COMMANDS = { + (LAB, URBAN_LES): [ + 'git clone http://{username}:{password}@tesla.parallel.ru/emortikov/nselibx-common.git', + 'git clone http://{username}:{password}@tesla.parallel.ru/emortikov/nselibx-wstgrid.git', + 'git clone http://{username}:{password}@tesla.parallel.ru/emortikov/nse-gabls1-urban-les.git', + 'cd ./nse-gabls1-urban-les/nse-gabls1-urban-les/', + 'chmod +x cpall.sh', + './cpall.sh ../../code', + 'cd ../../code', + 'make -B MACHINE=local COMPILER=gnu -j 23' + ], + (LOMONOSOV, URBAN_LES): [ + 'git clone http://{username}:{password}@tesla.parallel.ru/emortikov/nselibx-common.git', + 'git clone http://{username}:{password}@tesla.parallel.ru/emortikov/nselibx-wstgrid.git', + 'git clone http://{username}:{password}@tesla.parallel.ru/emortikov/nse-gabls1-urban-les.git', + 'mkdir build', + 'cd build', + 'module load intel/2019.5 openmpi/4.0.1-icc cmake/3.19.8', + 'cmake -DCMAKE_CONFIG_MODULE=lomonosov_pascal_ompi -DCOMPILER=intel -S ../nse-gabls1-urban-les/ -B ./', + 'make' + ], +} + +EXECUTABLE_FILE_PATH = { + (LAB, URBAN_LES): 'code/nsenx', + (LOMONOSOV, URBAN_LES): 'build/nse-gabls1-urban-les' +} + +RUN_COMMANDS = { + (LAB, URBAN_LES): [], + (LOMONOSOV, URBAN_LES): [ + 'module load intel/2019.5 openmpi/4.0.1-icc slurm', + 'sbatch -p compute -n 1 ompi ./nsenx' + ] +} + +RUN_SH_LINES = ['#!/bin/sh', + '#SBATCH --job-name=nsenx', + '#SBATCH -p pascal', + '#SBATCH --ntasks=8', + '## -- setting CPUs per task [skipped]', + '##SBATCH --cpus-per-task=1', + '#SBATCH --time=01:00:00', + '## --- setting number of OMP threads [skipped]', + '#export OMP_NUM_THREADS=\\$SLURM_CPUS_PER_TASK', +] + +LABEL_TO_MACHINE = {'lab server': LAB, 'lomonosov 2': LOMONOSOV} +LABEL_TO_MODEL = {'urban les': URBAN_LES} + +MACHINE_TO_LABEL = {LAB: 'lab server', LOMONOSOV: 'lomonosov 2'} +MODEL_TO_LABEL = {URBAN_LES: 'urban les'} \ No newline at end of file diff --git a/remote_run_ui.py b/remote_run_ui.py new file mode 100644 index 0000000000000000000000000000000000000000..cb4123a8015cfad724fef67716999a8a4b0ac7ca --- /dev/null +++ b/remote_run_ui.py @@ -0,0 +1,109 @@ +import dearpygui.dearpygui as dpg + +import os + +from remote_run_constants import * +from remote_run_callbacks import * + +def construct_remote_run_window(): + user_data = { + 'connections': dict(), + 'builds': dict() + } + + with dpg.window( + label='remote run', + tag='run_window', + horizontal_scrollbar=True, + show=False, + width=1000, + height=600, + pos=[20, 60], + user_data=user_data): + with dpg.menu_bar(): + dpg.add_menu_item(label='run', callback=show_run_model_window_clb, tag='run') + + with dpg.table(header_row=True, row_background=False, borders_outerH=True, borders_innerH=True, borders_innerV=False, borders_outerV=False, delay_search=True, tag='models_table'): + dpg.add_table_column(label='Type') + dpg.add_table_column(label='Running on') + dpg.add_table_column(label='Status') + dpg.add_table_column(label='Progress') + dpg.add_table_column(label='') + + with dpg.theme(tag='progress_bar_theme'): + with dpg.theme_component(dpg.mvAll): + dpg.add_theme_color(dpg.mvThemeCol_FrameBg, (255, 255, 255, 100), category=dpg.mvThemeCat_Core) + +def construct_run_model_window(): + xoffset = 400 + width = 300 + with dpg.window(label='remote run settings', show=False, tag='run_model_window', autosize=True, pos=[50, 50]): + with dpg.group(horizontal=True, xoffset=xoffset, tag='machines_group'): + dpg.add_text('choose machine') + dpg.add_radio_button(items=('lab server', 'lomonosov 2'), default_value='lab server', callback=update_run_model_window_clb, tag='machine', horizontal=True) + with dpg.group(horizontal=True, xoffset=xoffset, tag='models_group'): + dpg.add_text('choose model') + dpg.add_radio_button(items=('urban les',), callback=update_run_model_window_clb, default_value='urban les', tag='model', horizontal=True) + + with dpg.child_window(label='credentials', tag='credentials', auto_resize_y=True): + with dpg.group(tag='gitlab_group'): + dpg.add_input_text(label='gitlab username', tag='gitlab_username', password=False, default_value=os.getenv('GITLAB_LOGIN', ''), width=width) + dpg.add_input_text(label='gitlab password', tag='gitlab_password', password=True, default_value=os.getenv('GITLAB_PASS', ''), width=width) + + with dpg.group(tag='lab_server_group'): + dpg.add_input_text(label='server username', tag='server_username', password=False, default_value=os.getenv('SERVER_LOGIN', ''), width=width) + dpg.add_input_text(label='server password', tag='server_password', password=True, default_value=os.getenv('SERVER_PASS', ''), width=width) + + with dpg.group(tag='lomonosov_server_group'): + dpg.add_input_text(label='lomonosov username', tag='lomonosov_username', default_value=os.getenv('LOMONOSOV_LOGIN', ''), width=width) + + with dpg.group(horizontal=True, xoffset=xoffset, tag='config_file_path_group'): + dpg.add_text('config file path') + with dpg.group(horizontal=True): + dpg.add_input_text(default_value='', tag='config_file_path', width=width) + dpg.add_button(label='...', callback=open_config_file_path_dialog_clb) + with dpg.group(horizontal=True, xoffset=xoffset): + dpg.add_text('number of processes for mpi run') + dpg.add_input_int(default_value=8, width=width, min_value=1, max_value=200, min_clamped=True, max_clamped=True, tag='-np') + with dpg.group(horizontal=True, xoffset=xoffset): + dpg.add_text('arch') + dpg.add_radio_button(items=('gpu', 'cpu', 'mix'), label='arch', default_value='cpu', tag='-arch', horizontal=True) + with dpg.group(horizontal=True, xoffset=xoffset): + dpg.add_text('use dump') + dpg.add_checkbox(default_value=False, tag='use-udump', callback=lambda s, a, u: dpg.show_item('-udump-group') if a else dpg.hide_item('-udump-group')) + with dpg.group(horizontal=True, xoffset=xoffset, tag='-udump-group', show=False): + dpg.add_text('dump from this control points') + dpg.add_input_int(default_value=1, width=width, min_value=1, max_value=200, min_clamped=True, max_clamped=True, tag='-udump', enabled=False) + with dpg.group(horizontal=True, xoffset=xoffset): + dpg.add_text('write model stdout to file') + dpg.add_checkbox(default_value=False, tag='use-model-stdout', callback=lambda s, a, u: dpg.show_item('-model-stdout-group') if a else dpg.hide_item('-model-stdout-group')) + with dpg.group(horizontal=True, xoffset=xoffset, tag='-model-stdout-group', show=False): + dpg.add_text('filename') + dpg.add_input_text(default_value='log.txt', tag='-model-stdout', width=width, enabled=False) + + dpg.add_button(label='run', callback=run_model_clb) + +def construct_select_download_folder_dialog(): + with dpg.file_dialog(directory_selector=True, modal=True, show=False, callback=set_download_output_to_folder_clb, tag='download_to_folder_dialog', width=500, height=400): + dpg.add_file_extension('.*', color=(255, 255, 255, 255)) + +def construct_select_config_file_path_dialog(): + with dpg.file_dialog(directory_selector=False, modal=True, show=False, callback=set_config_file_path_clb, tag='config_file_path_dialog', width=500, height=400): + dpg.add_file_extension('.txt', color=(0, 255, 0, 255)) + dpg.add_file_extension('.*', color=(255, 255, 255, 255)) + +def construct_model_output_window(): + with dpg.window(label='select model output to download', show=False, tag='model_output_window', width=600, height=600, pos=[50, 50]): + dpg.add_child_window(height=500, tag='model_output_child_window') + with dpg.group(horizontal=True): + dpg.add_text('download to') + dpg.add_input_text(default_value='output', tag='download_output_to_folder') + dpg.add_button(label='...', callback=open_download_to_folder_dialog_clb) + dpg.add_button(label='download selected files', callback=download_output_clb) + +def construct_ui(): + construct_remote_run_window() + construct_run_model_window() + construct_model_output_window() + construct_select_download_folder_dialog() + construct_select_config_file_path_dialog() \ No newline at end of file diff --git a/static_ui.py b/static_ui.py index 8704c2fc9006977b5973e33f5c6a8b1d530c57bc..e06c8470983a22ea5d0a9c5976627294ce5e3f6b 100644 --- a/static_ui.py +++ b/static_ui.py @@ -28,9 +28,14 @@ def construct_main_window_ui(): with dpg.menu(label='run'): dpg.add_menu_item(label='edit remote run default values', callback=lambda: dpg.show_item('remote_run_settings_window')) - dpg.add_menu_item(label='build model on lab server', callback=show_get_credentials_modal_clb, user_data='build_model') - dpg.add_menu_item(label='run on lab server', callback=show_get_credentials_modal_clb, user_data='run_on_lab_server') - dpg.add_menu_item(label='download output', callback=show_get_credentials_modal_clb, user_data='download_output') + with dpg.menu(label='lab server'): + dpg.add_menu_item(label='build model', callback=show_get_credentials_modal_clb, user_data='build_model_lab_server', tag='build_model_lab_server') + dpg.add_menu_item(label='execute model', callback=show_get_credentials_modal_clb, enabled=False, user_data='execute_model_lab_server', tag='execute_model_lab_server') + dpg.add_menu_item(label='download output', callback=show_get_credentials_modal_clb, enabled=False, user_data='download_output_lab_server', tag='download_output_lab_server') + with dpg.menu(label='lomonosov cluster'): + dpg.add_menu_item(label='build model', callback=show_get_credentials_modal_clb, user_data='build_model_lomonosov_cluster', tag='build_model_lomonosov_cluster') + dpg.add_menu_item(label='execute model', callback=show_get_credentials_modal_clb, enabled=False, user_data='execute_model_lomonosov_cluster', tag='execute_model_lomonosov_cluster') + dpg.add_menu_item(label='download output', callback=show_get_credentials_modal_clb, enabled=False, user_data='download_output_lomonosov_cluster', tag='download_output_lomonosov_cluster') with dpg.menu(label='view'): with dpg.menu(label='ui', tag='ui'): diff --git a/style.py b/style.py new file mode 100644 index 0000000000000000000000000000000000000000..d5862bbaa0785bab6e9664540a52c6ef3ee1cddc --- /dev/null +++ b/style.py @@ -0,0 +1,154 @@ +import os + +import dearpygui.dearpygui as dpg + +from style_constants import * +from paths import FONT_PATH + +def apply_alpha(color, divisor=2): + r, g, b, a = color + return (r, g, b, a / divisor) + +def add_colors_to_theme(colors, theme): + APPLY_ALPHA = ( + dpg.mvThemeCol_ButtonHovered, + dpg.mvThemeCol_HeaderHovered, + dpg.mvThemeCol_ResizeGripHovered, + dpg.mvThemeCol_Tab, + dpg.mvThemeCol_ScrollbarGrabHovered + ) + + COLOR_TYPE_TO_DPG_ITEM = { + 'PRIMARY' : ( + dpg.mvThemeCol_WindowBg, + dpg.mvThemeCol_ChildBg, + dpg.mvThemeCol_TableHeaderBg, + dpg.mvThemeCol_TableBorderLight, + dpg.mvThemeCol_TableRowBg, + dpg.mvThemeCol_PopupBg + ), + 'SECONDARY' : ( + dpg.mvThemeCol_TabHovered, + dpg.mvThemeCol_TabUnfocusedActive, + dpg.mvThemeCol_Button, + dpg.mvThemeCol_Header, + dpg.mvThemeCol_FrameBg, + dpg.mvThemeCol_FrameBgHovered, + dpg.mvThemeCol_FrameBgActive, + dpg.mvThemeCol_MenuBarBg, + dpg.mvThemeCol_ScrollbarBg, + dpg.mvThemeCol_TableRowBgAlt + ), + 'ACCENT' : ( + dpg.mvThemeCol_TableBorderStrong, + dpg.mvThemeCol_Border, + dpg.mvThemeCol_CheckMark, + dpg.mvThemeCol_SliderGrab, + dpg.mvThemeCol_SliderGrabActive, + dpg.mvThemeCol_ScrollbarGrab, + dpg.mvThemeCol_ScrollbarGrabHovered, + dpg.mvThemeCol_TabActive, + dpg.mvThemeCol_TabUnfocused, + dpg.mvThemeCol_Tab, + dpg.mvThemeCol_ButtonActive, + dpg.mvThemeCol_ButtonHovered, + dpg.mvThemeCol_HeaderHovered, + dpg.mvThemeCol_HeaderActive, + dpg.mvThemeCol_Separator, + dpg.mvThemeCol_SeparatorHovered, + dpg.mvThemeCol_SeparatorActive, + dpg.mvThemeCol_TitleBg, + dpg.mvThemeCol_TitleBgActive, + dpg.mvThemeCol_TitleBgCollapsed, + dpg.mvThemeCol_ResizeGrip, + dpg.mvThemeCol_ResizeGripHovered, + dpg.mvThemeCol_ResizeGripActive, + dpg.mvThemeCol_TextSelectedBg + ) + } + + with dpg.theme_component(dpg.mvAll, parent=theme): + for color_type in COLOR_TYPE_TO_DPG_ITEM: + for item in COLOR_TYPE_TO_DPG_ITEM[color_type]: + color = colors['enabled'][color_type] + if item in APPLY_ALPHA: + color = apply_alpha(color) + dpg.add_theme_color(item, color) + with dpg.theme_component(dpg.mvMenuItem, parent=theme, enabled_state=False): + for color_type in COLOR_TYPE_TO_DPG_ITEM: + for item in COLOR_TYPE_TO_DPG_ITEM[color_type]: + color = colors['disabled'][color_type] + if item in APPLY_ALPHA: + color = apply_alpha(color) + dpg.add_theme_color(item, color) + with dpg.theme_component(dpg.mvButton, parent=theme, enabled_state=False): + for color_type in COLOR_TYPE_TO_DPG_ITEM: + for item in COLOR_TYPE_TO_DPG_ITEM[color_type]: + color = colors['disabled'][color_type] + if item in APPLY_ALPHA: + color = apply_alpha(color) + dpg.add_theme_color(item, color) + dpg.add_theme_color(dpg.mvThemeCol_ButtonHovered, colors['disabled']['SECONDARY']) + +def add_styles_to_theme(theme): + rounding = ( + dpg.mvStyleVar_ChildRounding, + dpg.mvStyleVar_WindowRounding, + dpg.mvStyleVar_FrameRounding, + dpg.mvStyleVar_TabRounding, + dpg.mvStyleVar_GrabRounding, + dpg.mvStyleVar_ScrollbarRounding, + dpg.mvStyleVar_PopupRounding + ) + + spacing = ( + dpg.mvStyleVar_ItemSpacing, + dpg.mvStyleVar_ItemInnerSpacing, + dpg.mvStyleVar_WindowPadding, + dpg.mvStyleVar_FramePadding, + dpg.mvStyleVar_CellPadding + ) + + with dpg.theme_component(dpg.mvAll, parent=theme): + for item in rounding: + dpg.add_theme_style(item, ROUNDING, category=dpg.mvThemeCat_Core) + + for item in spacing: + dpg.add_theme_style(item, SPACE, SPACE, category=dpg.mvThemeCat_Core) + + dpg.add_theme_style(dpg.mvStyleVar_ChildBorderSize, BORDER, category=dpg.mvThemeCat_Core) + dpg.add_theme_style(dpg.mvStyleVar_WindowBorderSize, BORDER, category=dpg.mvThemeCat_Core) + dpg.add_theme_style(dpg.mvStyleVar_FrameBorderSize, 0, category=dpg.mvThemeCat_Core) + + +def setup_fonts(): + cyrillic_capitals_start = 0x00C0 + cyrillic_capitals_end = 0x00DF + cyrillic_smalls_end = 0x00FF + unicode_cyrillic_capital_A = 0x0410 + alphabet_length = cyrillic_capitals_start - cyrillic_capitals_end + 1 + with dpg.font_registry(): + with dpg.font(FONT_PATH, FONT_SIZE, default_font=True) as montserrat_font: + dpg.add_font_range_hint(dpg.mvFontRangeHint_Default) + dpg.add_font_range_hint(dpg.mvFontRangeHint_Cyrillic) + for i in range(alphabet_length): + dpg.add_char_remap(cyrillic_capitals_start + i, unicode_cyrillic_capital_A + i) + dpg.add_char_remap(cyrillic_capitals_start + i + alphabet_length, unicode_cyrillic_capital_A + i + alphabet_length) + dpg.bind_font(montserrat_font) + +def apply_theme(theme_name): + theme = dpg.add_theme() + add_styles_to_theme(theme) + add_colors_to_theme(THEMES[theme_name], theme) + dpg.bind_theme(theme) + +def get_current_theme(): + for child in dpg.get_item_children('theme', 1): + if dpg.get_value(child): + return dpg.get_item_alias(child) + +def setup_themes(): + apply_theme(get_current_theme()) + +def get_accent_color(): + return THEMES[get_current_theme()]['enabled']['ACCENT'] \ No newline at end of file diff --git a/style_constants.py b/style_constants.py new file mode 100644 index 0000000000000000000000000000000000000000..f587445319f3b5cf4efbb41ba02ea90cde6650c6 --- /dev/null +++ b/style_constants.py @@ -0,0 +1,48 @@ +BLUE = { + 'enabled': { + 'TEXT': (255, 255, 255, 255), + 'PRIMARY': (37, 37, 38, 255), + 'SECONDARY': (51, 51, 55, 255), + 'ACCENT': (0, 119, 200, 153) + }, + 'disabled': { + 'TEXT': (255, 255, 255, 150), + 'PRIMARY': (37, 37, 38, 255), + 'SECONDARY': (51, 51, 55, 255), + 'ACCENT': (0, 119, 200, 153) + }, +} + +ORANGE = { + 'enabled': { + 'TEXT': (255, 255, 255, 255), + 'PRIMARY': (37, 37, 38, 255), + 'SECONDARY': (51, 51, 55, 255), + 'ACCENT': (251, 133, 0, 153) + }, + 'disabled': { + 'TEXT': (255, 255, 255, 150), + 'PRIMARY': (37, 37, 38, 255), + 'SECONDARY': (51, 51, 55, 255), + 'ACCENT': (251, 133, 0, 153) + }, +} + +THEMES = { + 'blue': BLUE, + 'orange': ORANGE +} + +FONT_SIZE = 24 +ACCENT_COLOR = (0, 119, 200, 100) +ROUNDING = 10 +SPACE = 10 +INDENT = 40 +BORDER = 3 + +TABS_UI = 'tabs_ui' +COLLAPSING_HEADERS_UI = 'collapsing_headers_ui' +WIDGET_WIDTH = 300 +XOFFSET = 300 +LCZS = ['compact_high_rise', 'compact_low_rise', 'compact_mid_rise', 'heavy_industry', 'large_low_rise', 'lightweight_low_rise', 'open_high_rise', 'open_low_rise', 'open_mid_rise', 'sparsley_build'] +UIS = [TABS_UI, COLLAPSING_HEADERS_UI] \ No newline at end of file diff --git a/styles.py b/styles.py index d8a8bbd6f4b72a30da77a2218254c5c2a2bab05b..dd72209443adbf3bb5aef9483124548a930d2137 100644 --- a/styles.py +++ b/styles.py @@ -15,10 +15,17 @@ def add_colors_to_theme(colors, theme): with dpg.theme_component(dpg.mvAll, parent=theme): for color_type in COLOR_TYPE_TO_DPG_ITEM: for item in COLOR_TYPE_TO_DPG_ITEM[color_type]: - color = colors[color_type] + color = colors['enabled'][color_type] if item in APPLY_ALPHA: color = apply_alpha(color) - dpg.add_theme_color(item, color, category=dpg.mvThemeCat_Core) + dpg.add_theme_color(item, color) + with dpg.theme_component(dpg.mvMenuItem, parent=theme, enabled_state=False): + for color_type in COLOR_TYPE_TO_DPG_ITEM: + for item in COLOR_TYPE_TO_DPG_ITEM[color_type]: + color = colors['disabled'][color_type] + if item in APPLY_ALPHA: + color = apply_alpha(color) + dpg.add_theme_color(item, color) def add_styles_to_theme(theme): rounding = ( diff --git a/utils.py b/utils.py index baf43fab13fb50b3590883d79356e95daa513e93..8b8b286b718c75f372f4cc303a193984e9699cf4 100644 --- a/utils.py +++ b/utils.py @@ -1,13 +1,26 @@ import re import threading +import os import numpy as np import dearpygui.dearpygui as dpg -from constants import * +from style_constants import SPACE + +def construct_folder_structure(folder_paths, prefix): + for folder_path in folder_paths: + full_path = os.path.join(prefix, folder_path) + os.makedirs(full_path, exist_ok=True) + +def update_config_values(): + config = get_config() + for path in config: + if dpg.does_item_exist(f'{get_config_file_path()}.{path}'): + config[path].value = dpg.get_value(f'{get_config_file_path()}.{path}') + set_main_window_value('config', config) def parse_dirs(s): - filepaths = set(map(lambda x: x[2:], s.strip('\n').split('\n')[1:])) + filepaths = set(map(lambda x: x[2:], s.strip('\n').split('\n'))) dirs = set() for path in filepaths: lst = path.split('/') @@ -41,6 +54,14 @@ def set_main_window_value(key, value): def get_main_window_value(key): return dpg.get_item_user_data('main').get(key, None) +def set_run_window_value(key, value): + data = dpg.get_item_user_data('run_window') + data[key] = value + dpg.set_item_user_data('run_window', data) + +def get_run_window_value(key): + return dpg.get_item_user_data('run_window').get(key, None) + def get_height_map_file_path(): return dpg.get_item_user_data('map_window')['height_map_file_path'] @@ -61,9 +82,14 @@ def set_map_window_value(key, value): def get_map_window_value(key): return dpg.get_item_user_data('map_window').get(key, None) -def show_status_text(message): - dpg.set_item_pos('status_text', (dpg.get_item_width('main') - dpg.get_text_size(message)[0] - 2 * SPACE, dpg.get_item_pos('status_text')[1])) - dpg.set_value('status_text', message) +def show_message(message): + dpg.set_value('message', message) + dpg.show_item('message_window') + +def show_loading_indicator(): + dpg.show_item('loading') + x, y = dpg.get_item_pos('status_text') + dpg.set_item_pos('loading', (x - 40, y)) def get_emission_points(): points = dict()