diff --git a/tugui/gui_configuration.py b/tugui/gui_configuration.py index 9b41aa6..6cbdb6a 100644 --- a/tugui/gui_configuration.py +++ b/tugui/gui_configuration.py @@ -1,114 +1,130 @@ +from ast import List from enum import Enum import os import platform import re -from dataclasses import dataclass +from dataclasses import dataclass, field +from typing import Dict, Tuple, List -class IDGA(Enum): - """ - Enumeration storing the different types of plots (field "Type"). - """ - IDGA_1 = "1 - Different Curve Numbers" - IDGA_2 = "2 - Different Times" - IDGA_3 = "3 - Different Slices" +from plot_settings import GroupType +from support import IDGA -class IANT(Enum): - """ - Enumeration storing the different types of IANT field. - """ - IANT_1 = "IANT 1" - IANT_2 = "IANT 2" - IANT_3 = "IANT 3" @dataclass -class Diagram(): +class DiagramCharacteristics(): """ - Dataclass providing a record storing the basic features of a diagram. + Dataclass providing a record storing the characteristics of either a + TuPlot or a TuStat diagram. """ - number: str = '000' + group: GroupType = None + number: str = '' + idga: str = '' -@dataclass -class TuPlotDiagram(Diagram): - """ - Dataclass providing a record storing the features of a TuPlot diagram. - """ - group: str = 'Radius' - idga: str = '1' - - def define_group_from_num(self): - """ - Method that evaluates the group value based on the diagram number and - set the corresponding class attribute. + @staticmethod + def init_tuplot_DiagramCharacteristics(number: str, idga: str): """ - if int(self.number) >= 101 and int(self.number) <= 140: - self.group = 'Radius' - elif int(self.number) >= 201 and int(self.number) <= 251: - self.group = 'Time' - elif int(self.number) >= 252 and int(self.number) <= 270: - self.group = 'Time Integral' - elif int(self.number) >= 301 and int(self.number) <= 340: - self.group = 'Axial' + Method that, given the plot number and idga values, builds an instance + of the 'DiagramCharacteristics' dataclass and evaluates the group value + based on the diagram number. It then sets the corresponding attribute + of the built dataclass, which is returned. -@dataclass -class TuStatDiagram(Diagram): - """ - Dataclass providing a record storing the features of a TuPlot diagram. - """ - group: str = '' + N.B. This method should be called for a 'TuPlot' case diagram only. + """ + # Instantiate an object of the 'DiagramCharacteristics' class by passing + # the values for the 'number' and the 'idga' attributes + diagr_char = DiagramCharacteristics(number=number, idga=idga) + # Set the 'group' value according to the given 'number' attribute + if int(diagr_char.number) >= 101 and int(diagr_char.number) <= 140: + diagr_char.group = GroupType.group1 + elif int(diagr_char.number) >= 201 and int(diagr_char.number) <= 251: + diagr_char.group = GroupType.group2 + elif int(diagr_char.number) >= 252 and int(diagr_char.number) <= 270: + diagr_char.group = GroupType.group2A + elif int(diagr_char.number) >= 301 and int(diagr_char.number) <= 340: + diagr_char.group = GroupType.group3 + # Return the built 'DiagramCharacteristics' instance + return diagr_char +@dataclass class GuiPlotFieldsConfigurator(): """ - Class that reads the configuration files and extract the values in order to build - the option fields into the GUI. These configuration files are: - . Diagrams - file storing the plot numbers for each plot "Group" - . Group 1/2/2a/3 - file storing the available Kns for each plot "Number" - . Statdiag - file storing the plot numbers related to a statistical simulation. - A dictionary is built storing the plot "Group" VS a dictionary of the corresponding - "Number" VS their Kns. - As for the statistical diagrams, a dictionary of number VS their descriptive string is - built as well. - These configuration files need to be present in the "config" folder of the application. - Their presence is checked before extracting the plots information. + Dataclass providing a record storing all the needed information for filling up + the plot configuration fields into the GUI, as well as for enabling the plotting + functionalities. + To do so, some support dictionaries are stored as well. """ - def __init__(self): + diagr_path: str = '' + g1_path: str = '' + g2_path: str = '' + g2a_path: str = '' + g3_path: str = '' + stat_path: str = '' + groupVSnumVsKn: Dict[str, Dict[str, List[str]]] = field(default_factory=dict) + groupVStype: Dict[List, List[str]] = field(default_factory=dict) + iant1: Tuple = tuple() + iant2: Tuple = tuple() + idgaVSi: Dict[str, int] = field(default_factory=dict) + sta_numVSdescription: Dict[int, str] = field(default_factory=dict) + tuplot_path: str = '' + tustat_path: str = '' + + @staticmethod + def init_GuiPlotFieldsConfigurator_attrs(): + """ + Method that builds and configure the attributes of an instance of the + 'GuiPlotFieldsConfigurator' class. + Configuration files and plotting executables are checked for existence, + and an exception is thrown if any of these files cannot be found. + This method returns the built dataclass. + """ + # Declare an instance of the 'GuiPlotFieldsConfigurator' dataclass + gui_config = GuiPlotFieldsConfigurator() + #################################### + # Configure the dataclass attributes + #################################### + # --------------------------------------- + # Build and check the configuration files + # --------------------------------------- + # Build the configuration folder path + config = os.path.join(os.getcwd(), "../resources/config") # Build the paths to the configuration files - self.diagr_path = os.path.join(os.getcwd(), "../resources/config" + os.sep + "Diagrams") - self.g1_path = os.path.join(os.getcwd(), "../resources/config" + os.sep + "Group1") - self.g2_path = os.path.join(os.getcwd(), "../resources/config" + os.sep + "Group2") - self.g2a_path = os.path.join(os.getcwd(), "../resources/config" + os.sep + "Group2a") - self.g3_path = os.path.join(os.getcwd(), "../resources/config" + os.sep + "Group3") - self.stat_path = os.path.join(os.getcwd(), "../resources/config" + os.sep + "Statdiag") + gui_config.diagr_path = os.path.join(config, "Diagrams") + gui_config.g1_path = os.path.join(config, "Group1") + gui_config.g2_path = os.path.join(config, "Group2") + gui_config.g2a_path = os.path.join(config, "Group2a") + gui_config.g3_path = os.path.join(config, "Group3") + gui_config.stat_path = os.path.join(config, "Statdiag") # Check the configuration files existence into the application "config" folder - self.check_config_file_existence(self.diagr_path, "Diagrams") - self.check_config_file_existence(self.g1_path, "Group1") - self.check_config_file_existence(self.g2_path, "Group2") - self.check_config_file_existence(self.g2a_path, "Group2a") - self.check_config_file_existence(self.g3_path, "Group3") - self.check_config_file_existence(self.g3_path, "Statdiag") + gui_config.__check_config_file_existence(gui_config.diagr_path, "Diagrams") + gui_config.__check_config_file_existence(gui_config.g1_path, "Group1") + gui_config.__check_config_file_existence(gui_config.g2_path, "Group2") + gui_config.__check_config_file_existence(gui_config.g2a_path, "Group2a") + gui_config.__check_config_file_existence(gui_config.g3_path, "Group3") + gui_config.__check_config_file_existence(gui_config.g3_path, "Statdiag") print("###\nConfiguration files are present in the \"resources/config\" folder\n###") - ###################################################### + # ---------------------------------------------------- # Extract the information from the configuration files - ###################################################### - # Open the different "Group" files and fill the dictionary "Number"-"Kn" + # ---------------------------------------------------- + # Open the different 'Group' files and fill the dictionary 'Number'-'Kn' numberVsKn = dict() - self.build_nVsKn(self.g1_path, numberVsKn, "^1\d\d") - self.build_nVsKn(self.g2_path, numberVsKn, "^2\d\d") - self.build_nVsKn(self.g2a_path, numberVsKn, "^2\d\d") - self.build_nVsKn(self.g3_path, numberVsKn, "^3\d\d") + gui_config.__build_nVsKn(gui_config.g1_path, numberVsKn, "^1\d\d") + gui_config.__build_nVsKn(gui_config.g2_path, numberVsKn, "^2\d\d") + gui_config.__build_nVsKn(gui_config.g2a_path, numberVsKn, "^2\d\d") + gui_config.__build_nVsKn(gui_config.g3_path, numberVsKn, "^3\d\d") # Instantiate the dictionary holding the plots "Group" Vs the dictionary of corresponding "Number"-s VS "Kn"-s - self.groupVSnumVsKn = dict() + gui_config.groupVSnumVsKn = dict() # Initialize a string holding the "Group" name group_name = "" # Open the "Diagram" file and extract each of the plot "Number"-s for each "Group" # Open the given "Group" file by specifying the ANSI encoding they are built with - with open(self.diagr_path, 'r', encoding="cp1252") as dg: + with open(gui_config.diagr_path, 'r', encoding="cp1252") as dg: # Process the file line by line for line in dg: # Get the line specifying the plot "Group" @@ -116,7 +132,7 @@ def __init__(self): # Check if the saved "Group" name differs from the current line if(group_name != "" and group_name != line.split(" : ")[1]): # Assemble the entry of the dictionary of "Group" VS dictionary of "Number" VS "Kn"-s - self.groupVSnumVsKn[group_name] = numVskn + gui_config.groupVSnumVsKn[group_name] = numVskn # Declare a dictionary holding the plot "Number" VS the corresponding "Kn"-s of the current "Group" numVskn = dict() @@ -132,125 +148,122 @@ def __init__(self): numVskn[line.split('\n')[0]] = numberVsKn[num] # Add the last entry of the dictionary of "Group" VS dictionary of "Number" VS "Kn"-s - self.groupVSnumVsKn[group_name] = numVskn + gui_config.groupVSnumVsKn[group_name] = numVskn # Build a dictionary of plot "Group" VS the available "Type" values - self.groupVStype = { - list(self.groupVSnumVsKn.keys())[0]: ["1 - Different Curve Numbers", "2 - Different Times", "3 - Different Slices"], - list(self.groupVSnumVsKn.keys())[1]: ["1 - Different Curve Numbers", "3 - Different Slices"], - list(self.groupVSnumVsKn.keys())[2]: ["1 - Different Curve Numbers"], - list(self.groupVSnumVsKn.keys())[3]: ["1 - Different Curve Numbers", "2 - Different Times"] + gui_config.groupVStype = { + list(gui_config.groupVSnumVsKn.keys())[0]: ["1 - Different Curve Numbers", "2 - Different Times", "3 - Different Slices"], + list(gui_config.groupVSnumVsKn.keys())[1]: ["1 - Different Curve Numbers", "3 - Different Slices"], + list(gui_config.groupVSnumVsKn.keys())[2]: ["1 - Different Curve Numbers"], + list(gui_config.groupVSnumVsKn.keys())[3]: ["1 - Different Curve Numbers", "2 - Different Times"] } - # Build a tuple storing the available options for specific plot "Number"-s - self.iant1 = ([113], ["Temperature-distribution will be drawn over the fuel, the cladding and the structure"]) - self.iant2 = ([102, 103, 104, 105, 106, 107, 108], - ["The radial stresses and strains are only drawn for the cladding", - "The radial stresses and strains are only drawn for the fuel"]) + # Build a tuple storing the available options for specific plot "Number"-s + gui_config.iant1 = ([113], ["Temperature-distribution will be drawn over the fuel, the cladding and the structure"]) + gui_config.iant2 = ([102, 103, 104, 105, 106, 107, 108], + ["The radial stresses and strains are only drawn for the cladding", + "The radial stresses and strains are only drawn for the fuel"]) - # Build a map between the IDGA enumeration and their index - self.idgaVSi = { - IDGA(IDGA['IDGA_1']).value: 1, - IDGA(IDGA['IDGA_2']).value: 2, - IDGA(IDGA['IDGA_3']).value: 3, - } + # Build a map between the IDGA enumeration and their index + gui_config.idgaVSi = { + IDGA.IDGA_1.description: IDGA.IDGA_1.index, + IDGA.IDGA_2.description: IDGA.IDGA_2.index, + IDGA.IDGA_3.description: IDGA.IDGA_3.index + } - print(self.idgaVSi) + print(gui_config.idgaVSi) + # Build the dictionary of plot number VS their descriptive string for the statistical case + gui_config.sta_numVSdescription = dict() + with open(gui_config.stat_path, 'r', encoding="cp1252") as st: + # Process the file line by line + for line in st: + # Get the line specifying the plot number avoiding those lines indicating as + # "Dummy-Diagram" + if re.search("^\d+\s+(?!.*Dummy-Diagram).*", line.split('\n')[0]): + # Extract the plot number as an integer + num = int(line.split(' ')[0]) + # Add the number VS descriptive line into the statistical plot dictionary + gui_config.sta_numVSdescription[num] = line.strip() - # Build an enumeration holding the available values of the IANT1 option, only available - # if the selected "Number" is 113 - # self.iant1 = Enum('IANT1', ['Y', 'N']) - # p = self.iant1.N.name - # print("##\n", p) + # --------------------------------------------------------------------------- + # Check the presence of the TuPlot and TuStat executables in the "bin" folder + # --------------------------------------------------------------------------- + # Check the executables existence in the "bin" folder on the basis of the + # current OS + if platform.system() == "Linux": + print("LINUX HERE!") + gui_config.tuplot_path = os.path.join(os.getcwd(), "../resources/exec" + os.sep + "tuplotgui") + gui_config.tustat_path = os.path.join(os.getcwd(), "../resources/exec" + os.sep + "tustatgui") + gui_config.__check_exe_file_existence(gui_config.tuplot_path, "tuplotgui") + gui_config.__check_exe_file_existence(gui_config.tustat_path, "tustatgui") + elif platform.system() == "Windows": + print("WINDOWS HERE!") + gui_config.tuplot_path = os.path.join(os.getcwd(), "../resources/exec" + os.sep + "TuPlotGUI.exe") + gui_config.tustat_path = os.path.join(os.getcwd(), "../resources/exec" + os.sep + "TuStatGUI.exe") + gui_config.__check_exe_file_existence(gui_config.tuplot_path, "TuPlotGUI.exe") + gui_config.__check_exe_file_existence(gui_config.tustat_path, "TuStatGUI.exe") - # Build the dictionary of plot number VS their descriptive string for the statistical case - self.sta_numVSdescription = dict() - with open(self.stat_path, 'r', encoding="cp1252") as st: - # Process the file line by line - for line in st: - # Get the line specifying the plot number avoiding those lines indicating as - # "Dummy-Diagram" - if re.search("^\d+\s+(?!.*Dummy-Diagram).*", line.split('\n')[0]): - # Extract the plot number as an integer - num = int(line.split(' ')[0]) - # Add the number VS descriptive line into the statistical plot dictionary - self.sta_numVSdescription[num] = line.strip() + print("###\nExecutables files are present in the \"../resources/exec\" folder\n###") - ############################################################################# - # Check the presence of the TuPlot and TuStat executables in the "bin" folder - ############################################################################# - # Check the executables existence in the "bin" folder on the basis of the - # current OS - if platform.system() == "Linux": - print("LINUX HERE!") - self.tuplot_path = os.path.join(os.getcwd(), "../resources/exec" + os.sep + "tuplotgui") - self.tustat_path = os.path.join(os.getcwd(), "../resources/exec" + os.sep + "tustatgui") - self.check_exe_file_existence(self.tuplot_path, "tuplotgui") - self.check_exe_file_existence(self.tustat_path, "tustatgui") - elif platform.system() == "Windows": - print("WINDOWS HERE!") - self.tuplot_path = os.path.join(os.getcwd(), "../resources/exec" + os.sep + "TuPlotGUI.exe") - self.tustat_path = os.path.join(os.getcwd(), "../resources/exec" + os.sep + "TuStatGUI.exe") - self.check_exe_file_existence(self.tuplot_path, "TuPlotGUI.exe") - self.check_exe_file_existence(self.tustat_path, "TuStatGUI.exe") + # Return the configured 'GuiPlotFieldsConfigurator' dataclass + return gui_config - print("###\nExecutables files are present in the \"../resources/exec\" folder\n###") + def __check_config_file_existence(self, path2check: str, filename: str): + """ + Function that raises an exception if the given path does not correspond to an + existing file. + """ + if (not os.path.isfile(path2check)): + # Raise an exception + raise FileNotFoundError("Error: missing \"" + filename + "\" configuration file") - def build_nVsKn(self, group_file: str, group_dict: dict, search_num: str): + def __check_exe_file_existence(self, path2check: str, filename: str): """ - Method that, given the "Group" file to read (its path), it fills the input dictionary with: - . keys, being the line indicating the plot "Number" - . values, being a list of lines indicating the available Kn-s for the corresponding plot "Number" + Function that raises an exception if the given path does not correspond to an + existing executable file with right permission. """ - # Open the given "Group" file by specifying the ANSI encoding they are built with + # Check the executable file existence + self.__check_config_file_existence(path2check, filename) + # Check that the file has execution permission + if (not os.access(path2check, os.X_OK)): + # Raise an exception + raise PermissionError("Error: the \"" + filename + "\" does not have execution permission") + + def __build_nVsKn(self, group_file: str, group_dict: dict, search_num: str): + """ + Function that, given the 'Group' file to read (its path), it fills up the input dictionary with: + . keys, being the line indicating the plot 'Number' + . values, being a list of lines indicating the available Kn-s for the corresponding plot 'Number' + """ + # Open the given 'Group' file by specifying the ANSI encoding they are built with with open(group_file, 'r', encoding="cp1252") as g1: # Process the file line by line for line in g1: - # Get the line specifying the plot "Number" + # Get the line specifying the plot 'Number' num = re.search(search_num, line.split('\n')[0]) if num: - # Declare a list holding the Kn-s of the current found "Number" + # Declare a list holding the Kn-s of the current found 'Number' kns = list() - # Get the lines specifying the Kn-s corresponding to the current found "Number" + # Get the lines specifying the Kn-s corresponding to the current found 'Number' while True: # Read the following line line = g1.readline() # Check for the presence of an empy line indicating the end of the Kn-s for the - # current "Number" + # current 'Number' if (line.isspace() or line == "end\n"): - # Add the "Number"-Kn entry of the dictionary + # Add the 'Number'-Kn entry of the dictionary group_dict[num.group(0)] = kns break else: - # Add the Kn line to a list belonging to the current found "Number" + # Add the Kn line to a list belonging to the current found 'Number' kns.append(line.strip()) - def check_config_file_existence(self, path2check: str, filename: str): - """ - Method that raises an exception if the given path does not correspond to an - existing file. - """ - if (not os.path.isfile(path2check)): - # Raise an exception - raise FileNotFoundError("Error: missing \"" + filename + "\" configuration file") - - def check_exe_file_existence(self, path2check: str, filename: str): - """ - Method that raises an exception if the given path does not correspond to an - existing executable file with right permission. - """ - self.check_config_file_existence(path2check, filename) - # Check that the file has execution permission - if (not os.access(path2check, os.X_OK)): - # Raise an exception - raise PermissionError("Error: the \"" + filename + "\" does not have execution permission") - - if __name__ == "__main__": - # Instantiate the class - gui_config = GuiPlotFieldsConfigurator() + # Instantiate and configure the dataclass storing the GUI configuration + gui_config = GuiPlotFieldsConfigurator.init_GuiPlotFieldsConfigurator_attrs() + # Print the built dictionaries print(gui_config.groupVSnumVsKn) print(gui_config.sta_numVSdescription) diff --git a/tugui/gui_widgets.py b/tugui/gui_widgets.py index 30607b4..2a48744 100644 --- a/tugui/gui_widgets.py +++ b/tugui/gui_widgets.py @@ -1,9 +1,103 @@ import os import tkinter as tk from tkinter import ttk +from tkinter import messagebox from PIL import Image, ImageTk +class EntryVariable: + """ + Class defining a variable having a corresponding Entry object. Its value + is validated when set. + """ + def __init__(self, frame: tk.Frame, width: int, col: int, row: int, end: str) -> None: + """ + Constructor requiring the Frame object onto which putting the Entry. + The Entry width, as well as the column and row indices are passed to + configure the Entry object within the frame. + """ + # Instantiate the string variable + self.var = tk.StringVar() + # Instantiate the entry field + self.entry = ttk.Entry(frame, width = width, textvariable=self.var) + # Place the entry in the frame grid + self.entry.grid(column = col, row = row, sticky = 'ew') + + # Register the validation funtion of the Entry widget + valid_entry = (self.entry.register(self.validate), '%P') + # Configure the entry for checking the validity of its content when the + # widget looses focus + self.entry.configure(validate='focusout', validatecommand=valid_entry) + + # Entry file extension + self.entry_extension = end + + def validate(self, event=None, newval: str = ""): + """ + Method that checks if the entry is valid. The "end" parameter indicates the + extension to check against. + """ + # Check the entry value against the allowed extension + if re.match(r"^.*\." + self.entry_extension + "$", newval) is not None: + # The entry is valid if a match is found + print("The entry is valid!") + self.entry.configure(foreground="#343638") + return True + else: + # If no match is found, handle the invalid case only if the entry value is not empty + if newval != "": + self.on_invalid() + return False + + def on_invalid(self): + """ + Show the error message if the data is not valid. + """ + error_message = "The entry is not valid: please provide a path to a file with the valid \"" + self.entry_extension + "\" extension!" + print(error_message) + # Highlight the entry color in red + self.entry.configure(foreground="red") + # Show the error message as a pop-up window + messagebox.showerror("Error", error_message) + + +class StatusBar(ttk.Frame): + """ + Class describing a Frame where a label is shown. This represents a status bar + that provides useful log to the user. + """ + def __init__(self, container, color: str = 'light gray'): + # Initialize the Style object + s = ttk.Style() + # Configure the style for the status bar frame + s.configure('self.TFrame', background=color, border=1, borderwidth=1, relief=tk.GROOVE) + # Configure the style for the status bar label + s.configure('self.TLabel', background=color) + + # Call the superclass initializer passing the built style + super().__init__(container, style='self.TFrame') + + # Declare an empty label providing the status messages + self.label = ttk.Label(self, text="", style='self.TLabel') + # Align the label on the left + self.label.pack(side=tk.LEFT, padx=3, pady=3) + + # Configure the status bar in order to fill all the space in the horizontal direction + self.grid(sticky='ew') + + def set_text(self, new_text): + """ + Method that allow the modification of the status bar text. + """ + self.label.configure(text=new_text) + + def clear_label(self): + """ + Method that clears any text already present in the label status bar. + """ + self.set_text("") + + class CustomNotebook(ttk.Notebook): """ Class that provides a customization of the ttk.Notebook class that enables @@ -165,24 +259,29 @@ def set_text(self, new_text: str): self._btn.configure(text=new_text) -class LabelImage(ttk.Label): +def provide_label_image(container, img_path: str) -> ttk.Label: """ - Class that provides a label filled with an image. + Function that provides a label filled with an image. It builds an instance + of the 'ttk.Label' class by receiving the contaniner to put the label + into, as well as the path to the image file to load and assign to the + label. + The loaded image is added as an attribute to the 'ttk.Label' instance so + to keep a reference to it and prevent it to be garbage collected, thus + allowing to be correctly shown. """ - def __init__(self, container, img_path: str): - """ - Construct an instance of the 'LabelImage' class by receiving the - contaniner to put the label into, as well as the path to the - image file to load and assign to the label. - """ - # Call the superclass constructor by passing the container - super().__init__(container) - # Load the image - self.label_image = Image.open(img_path) - self.label_image = ImageTk.PhotoImage(self.label_image) + # Instantiate the 'ttk.Label' class + label = ttk.Label(container) + # Load the image + label_image = Image.open(img_path) + label_image = ImageTk.PhotoImage(label_image) + # Configure the label by assigning the image to it + label.configure(image=label_image) + # Add an attribute to the label to keep a reference to the image and prevent it + # from being garbage collected + label.image = label_image - # Configure the label by assigning the image to it - self.configure(image=self.label_image) + # Return the label + return label class OnOffClickableLabel(ttk.Frame): @@ -275,7 +374,7 @@ def deactivate_label(self): self._lbl.unbind('', self.click_event) -class WidgetTooltip(object): +class WidgetTooltip(): """ Class that provides a tooltip for a given widget passed as argument to this class constructor. diff --git a/tugui/main.py b/tugui/main.py index 5259a9d..5327aac 100644 --- a/tugui/main.py +++ b/tugui/main.py @@ -1,7 +1,6 @@ import tkinter as tk import os import re -import platform from tkinter import PhotoImage, ttk from tkinter import filedialog @@ -11,127 +10,15 @@ from plot_builder import PlotManager, PlotFigure from plot_settings import GroupType from tab_builder import TuPlotTabContentBuilder, TuStatTabContentBuilder -from tu_interface import InpHandler, MicReader, StaReader, DatGenerator, TuInp, PliReader, MacReader -from gui_configuration import IANT, GuiPlotFieldsConfigurator -from gui_widgets import CustomNotebook, LabelImage +from tu_interface import DatGenerator, InpHandler, MicReader, PliReader, StaReader, TuInp, MacReader +from gui_configuration import GuiPlotFieldsConfigurator +from gui_widgets import CustomNotebook, EntryVariable, StatusBar, provide_label_image +from support import IANT from shutil import copyfile ERROR_LEVEL: bool = 0 -class BaseWindow(ThemedTk): - """ - Base window without anything. It allows to set the window title, as well as - its dimensions. Given the screen size, the window is placed in the center. - """ - def __init__(self, window_title, width, height): - # Call the superclass constructor - super().__init__() - - # Set the window title - self.title(window_title) - # Set the theme to use - self.configure(theme='radiance') - # Set the top-left coordinate of the window so that the app is placed in the screen center - left = int(self.winfo_screenwidth() / 2 - width / 2) - top = int(self.winfo_screenheight() / 2 - height / 2) - # Set the window geometry - self.geometry(f"{width}x{height}+{left}+{top}") - - -class EntryVariable: - """ - Class defining a variable having a corresponding Entry object. Its value - is validated when set. - """ - def __init__(self, frame: tk.Frame, width: int, col: int, row: int, end: str) -> None: - """ - Constructor requiring the Frame object onto which putting the Entry. - The Entry width, as well as the column and row indices are passed to - configure the Entry object within the frame. - """ - # Instantiate the string variable - self.var = tk.StringVar() - # Instantiate the entry field - self.entry = ttk.Entry(frame, width = width, textvariable=self.var) - # Place the entry in the frame grid - self.entry.grid(column = col, row = row, sticky = 'ew') - - # Register the validation funtion of the Entry widget - valid_entry = (self.entry.register(self.validate), '%P') - # Configure the entry for checking the validity of its content when the - # widget looses focus - self.entry.configure(validate='focusout', validatecommand=valid_entry) - - # Entry file extension - self.entry_extension = end - - def validate(self, event=None, newval: str = ""): - """ - Method that checks if the entry is valid. The "end" parameter indicates the - extension to check against. - """ - # Check the entry value against the allowed extension - if re.match(r"^.*\." + self.entry_extension + "$", newval) is not None: - # The entry is valid if a match is found - print("The entry is valid!") - self.entry.configure(foreground="#343638") - return True - else: - # If no match is found, handle the invalid case only if the entry value is not empty - if newval != "": - self.on_invalid() - return False - - def on_invalid(self): - """ - Show the error message if the data is not valid. - """ - error_message = "The entry is not valid: please provide a path to a file with the valid \"" + self.entry_extension + "\" extension!" - print(error_message) - # Highlight the entry color in red - self.entry.configure(foreground="red") - # Show the error message as a pop-up window - messagebox.showerror("Error", error_message) - - -class StatusBar(ttk.Frame): - """ - Class describing a Frame where a label is shown. This represents a status bar - that provides useful log to the user. - """ - def __init__(self, container, color: str = 'light gray'): - # Initialize the Style object - s = ttk.Style() - # Configure the style for the status bar frame - s.configure('self.TFrame', background=color, border=1, borderwidth=1, relief=tk.GROOVE) - # Configure the style for the status bar label - s.configure('self.TLabel', background=color) - - # Call the superclass initializer passing the built style - super().__init__(container, style='self.TFrame') - - # Declare an empty label providing the status messages - self.label = ttk.Label(self, text="", style='self.TLabel') - # Align the label on the left - self.label.pack(side=tk.LEFT, padx=3, pady=3) - - # Configure the status bar in order to fill all the space in the horizontal direction - self.grid(sticky='ew') - - def set_text(self, new_text): - """ - Method that allow the modification of the status bar text. - """ - self.label.configure(text=new_text) - - def clear_label(self): - """ - Method that clears any text already present in the label status bar. - """ - self.set_text("") - - -class TuPostProcessingGui(BaseWindow): +class TuPostProcessingGui(ThemedTk): """ Class that builds a GUI for enabling the user to plot the quantities produced by a TU simulation. Two plot types are available: @@ -146,11 +33,15 @@ class TuPostProcessingGui(BaseWindow): . the plot area (right side) where the selected curves are shown . a status bar (bottom window) showing log messages. """ - def __init__(self, window_title, height, width): + def __init__(self, window_title, width, height): """ App windows's constructor """ - super().__init__(window_title, height, width) + # Call the superclass constructor + super().__init__() + + # Initialize the main GUI window + self.__initialize_gui_window(window_title, width, height) # Get the absolute path of the current file abspath = os.path.abspath(__file__) @@ -165,9 +56,9 @@ def __init__(self, window_title, height, width): # self.tk.call('wm', 'iconphoto', self._w, icon) # self.iconphoto(True, PhotoImage(file=os.path.join(os.getcwd(), "resources/tuoutgui.ico"))) - # Instantiate the GuiPlotFieldsConfigurator class in a try-except + # Instantiate and configure the 'GuiPlotFieldsConfigurator' class in a try-except try: - self.guiconfig = GuiPlotFieldsConfigurator() + self.guiconfig = GuiPlotFieldsConfigurator.init_GuiPlotFieldsConfigurator_attrs() except Exception as e: # Intercept any exception produced by running the configuration logic according to the selected error level if ERROR_LEVEL: messagebox.showerror("Error", type(e).__name__ + "–" + str(e)) @@ -228,7 +119,7 @@ def __init__(self, window_title, height, width): logo_frame = ttk.Frame(self) logo_frame.grid(column=1, row=0, sticky='nse') # Add newcleo logo - newcleo_logo = LabelImage(logo_frame, os.path.join(os.path.abspath(os.path.dirname(__file__)), "../resources/icons/newcleologo.png")) + newcleo_logo = provide_label_image(logo_frame, os.path.join(os.path.abspath(os.path.dirname(__file__)), "../resources/icons/newcleologo.png")) newcleo_logo.grid(column=0, row=0, sticky='nsew') ############################################################################### @@ -273,6 +164,22 @@ def __init__(self, window_title, height, width): # Bind the <> virtual event to the plot creation self.bind('<>', func=lambda event: self.display_plot()) + def __initialize_gui_window(self, title, width, height): + """ + Method that sets the GUI window title, as well as its dimensions, in terms + of width and height. Given the screen size, the window is placed in the center + of the screen, independently of its dimensions. + """ + # Set the window title + self.title(title) + # Set the theme to use + self.configure(theme='radiance') + # Set the top-left coordinate of the window so that the app is placed in the screen center + left = int(self.winfo_screenwidth() / 2 - width / 2) + top = int(self.winfo_screenheight() / 2 - height / 2) + # Set the window geometry + self.geometry(f"{width}x{height}+{left}+{top}") + def display_plot(self): """ Method that enables the display-only mode for plots provided by reading @@ -371,14 +278,16 @@ def display_inp_plots(self): # Set the default name of the files the plotting executable will create output_files_name = "TuStat" - # Instantiate the class interfacing the .inp file with plot executable - inp_to_dat = DatGenerator(plotexec_path=executable_path, - inp_path=self.loaded_inp_file, - plots_num=len(inpreader.diagrams_list), - cwd=self.output_dir, - output_files_name=output_files_name) - # Run the tuplotgui executable for creating the .dat and .plt files - inp_to_dat.run() + # Run the method that deals with instantiating the dataclass storing the needed + # information for the plotting executable to be run. The corresponding executable + # is run afterwards and the paths to the output .dat and .plt files, stored in the + # returned object, are updated. + inp_to_dat = DatGenerator.init_DatGenerator_and_run_exec( + plotexec_path=executable_path, + inp_path=self.loaded_inp_file, + plots_num=len(inpreader.diagrams_list), + cwd=self.output_dir, + output_files_name=output_files_name) # For each diagram configuration create a new PlotFigure object and plot the curves for i in range(0, len(inpreader.diagrams_list)): @@ -442,10 +351,9 @@ def retrieve_simulation_info(self): # Instantiate the PliReader class for retrieving info from the .pli file try: - self.plireader = PliReader(self.pli_entry.var.get()) + # Extract the information from the .pli file and instantiate the 'PliReader' class + self.plireader = PliReader.init_PliReader(self.pli_entry.var.get()) print("Path to the .pli file: " + self.plireader.pli_path) - # Extract the information from the .pli file - self.plireader.extract_sim_info() # Instantiate the MacReader class self.macreader = MacReader( @@ -526,7 +434,7 @@ def activate_tustat_area(self): # Set the list of slice names for the TuStat tab self.tustat_tab.set_slice_list(self.slice_settings) # Set the list providing the times of the statistical simulation - self.tustat_tab.set_times(self.sta_times) + self.tustat_tab.set_times(sta_times=self.sta_times) # Pass the herein-defined method to call whenever the "Run" button of the TuStat tab is pressed self.tustat_tab.run_plot(self.run_tuStat) @@ -550,7 +458,7 @@ def activate_tuplot_area(self): # Set the list of slice names for the TuPlot tab self.tuplot_tab.set_slice_list(self.slice_settings) # Set the lists providing the macro and micro time - self.tuplot_tab.set_times(self.macro_time, self.micro_time) + self.tuplot_tab.set_times(macro_time=self.macro_time, micro_time=self.micro_time) # Pass the herein-defined method to call whenever the "Run" button of the TuPlot object is pressed self.tuplot_tab.run_plot(self.run_tuPlot) @@ -566,6 +474,9 @@ def run_tuPlot(self): self.focus_set() try: + # Get the index corresponding to the IDGA option selected by users + idga_indx = self.guiconfig.idgaVSi[self.tuplot_tab.type_var.get()] + # Instantiate the class dealing with the .inp file generation based on the made choices # inp_generator = TuPlotInpGenerator(self.plireader.pli_path) # Build a dictionary of the needed information for building the .inp file by providing @@ -573,7 +484,7 @@ def run_tuPlot(self): inp_info = { "PLI": os.path.basename(self.plireader.pli_path).split(os.sep)[-1], "IDNF": self.tuplot_tab.number_var.get().split(' ')[0], - "IDGA": str(self.guiconfig.idgaVSi[self.tuplot_tab.type_var.get()]), + "IDGA": str(idga_indx), "NKN": str(len(self.tuplot_tab.plt_sett_cfg.field3.lb_selected_values)), "IANT1": "N", "IANT2": "F", @@ -587,17 +498,17 @@ def run_tuPlot(self): # Overwrite the default entry for the temperature distribution if plot 113 if hasattr(self.tuplot_tab, 'iant'): - if self.tuplot_tab.iant == IANT.IANT_1: + if self.tuplot_tab.iant == IANT.IANT_1.description: # TODO check why the manual says this field shoud be 'Y', but actually the executable fails inp_info["IANT1"] = "N" - elif self.tuplot_tab.iant == IANT.IANT_2: + elif self.tuplot_tab.iant == IANT.IANT_2.description: # Overwrite the default entry for the radial stresses, if plot 102-108 if hasattr(self.tuplot_tab, 'iant_entry'): if self.tuplot_tab.iant_entry.cbx.current() == 0: inp_info["IANT2"] = "C" # If the plot type (IDGA) is 1, put the list of selected Kn-s in the dictionary - if self.guiconfig.idgaVSi[self.tuplot_tab.type_var.get()] == 1: + if idga_indx == 1: # Get the list of strings identifying the chosen Kn-s kn_list = self.tuplot_tab.plt_sett_cfg.field3.lb_selected_values # Extract the Kn numbers only if the list is not empty @@ -613,7 +524,7 @@ def run_tuPlot(self): inp_info["KN"] = re.findall(r'\d+', self.tuplot_tab.plt_sett_cfg.field2.cbx_selected_value)[0] # Overwrite the default entry for the NLSUCH item - if self.guiconfig.idgaVSi[self.tuplot_tab.type_var.get()] == 3: + if idga_indx == 3: # Get the number of selected slices slice_list = self.tuplot_tab.plt_sett_cfg.field3.lb_selected_values # Extract the slice numbers only if the list is not empty @@ -628,7 +539,7 @@ def run_tuPlot(self): inp_info["NLSUCH"] = re.findall(r'\d+', self.tuplot_tab.plt_sett_cfg.field2.cbx_selected_value)[0] # Overwrite the default TIME (IASTUN/IASEC/FAMILY) entries on the basis of the selected time(s) - if self.guiconfig.idgaVSi[self.tuplot_tab.type_var.get()] == 1 or self.guiconfig.idgaVSi[self.tuplot_tab.type_var.get()] == 3: + if idga_indx == 1 or idga_indx == 3: # Curves at a specific time instant, i.e. one time for plot type 1 (different Kn-s) and 3 (different slices) if self.tuplot_tab.plt_sett_cfg.group == GroupType.group1 or self.tuplot_tab.plt_sett_cfg.group == GroupType.group3: # Only one time for group 1 (Radius) and 3 (Axial) @@ -638,16 +549,15 @@ def run_tuPlot(self): # Start and end times for group 2 (Time) and 2A (TimeIntegral) # Get the start/end times from field2 inp_info["TIME"] = "\n".join([self.tuplot_tab.plt_sett_cfg.field2.time1, self.tuplot_tab.plt_sett_cfg.field2.time2]) - elif self.guiconfig.idgaVSi[self.tuplot_tab.type_var.get()] == 2: + elif idga_indx == 2: # Curves for different time instants, list of times for plot type 1 (different Kn-s) and 3 (different slices) if self.tuplot_tab.plt_sett_cfg.group == GroupType.group1 or self.tuplot_tab.plt_sett_cfg.group == GroupType.group3: # Get the list of selected time instants from field3 times = self.tuplot_tab.plt_sett_cfg.field3.lb_selected_values inp_info["TIME"] = "\n".join(i for i in times) - # Instantiate the 'TuInp' dataclass for storing the plot configuration - tuplot_inp = TuInp() - tuplot_inp.configure_tuplot_inp_fields(inp_info) + # Build and configure the 'TuInp' dataclass for storing the plot configuration + tuplot_inp = TuInp.configure_tuplot_inp_fields(inp_info) # Get the 'PlotFigure' instance from the currently active tab of the plots notebook active_plotFigure = self.tuplot_tab.get_active_plotFigure() @@ -699,9 +609,8 @@ def run_tuStat(self): # Index 1 corresponds to "Probabilistic density" inp_info["DISTR"] = "d" - # Instantiate the 'TuInp' dataclass for storing the plot configuration - tustat_inp = TuInp() - tustat_inp.configure_tustat_inp_fields(inp_info) + # Build and configure the 'TuInp' dataclass for storing the plot configuration + tustat_inp = TuInp.configure_tustat_inp_fields(inp_info) # Get the 'PlotFigure' instance from the currently active tab of the plots notebook active_plotFigure = self.tustat_tab.get_active_plotFigure() @@ -733,14 +642,16 @@ def handle_plot_production(self, tuinp: TuInp, output_files_name: str, executabl # Store the .inp filename self.inp_filename = inp_path - # Instantiate the class interfacing input with the given plotting executable - inp_to_dat = DatGenerator(plotexec_path=executable_path, - inp_path=inp_path, - plots_num=1, - cwd=self.output_dir, - output_files_name=output_files_name) - # Run the plotting executable for creating the .dat and .plt files - inp_to_dat.run() + # Run the method that deals with instantiating the dataclass storing the needed + # information for the plotting executable to be run. The corresponding executable + # is run afterwards and the paths to the output .dat and .plt files, stored in the + # returned object, are updated. + inp_to_dat = DatGenerator.init_DatGenerator_and_run_exec( + plotexec_path=executable_path, + inp_path=inp_path, + plots_num=1, + cwd=self.output_dir, + output_files_name=output_files_name) # Store the currently .dat and .plt output files (first element in the # corresponding lists as only one plot is handled here) diff --git a/tugui/plot_settings.py b/tugui/plot_settings.py index 4cfe8da..f9b31d0 100644 --- a/tugui/plot_settings.py +++ b/tugui/plot_settings.py @@ -31,7 +31,7 @@ class GroupType(Enum): class PlotSettingsConfigurator(): """ Class that build the structure of the additional plot settings area as several rows - containing a label and a combobox each, provided as instances of the 'PlotSettingsField' + containing a label and a combobox each, provided as instances of the 'LabelledCombobox' class, with a listbox at the end, as instance of the 'PlotSettingsListBox' class. The plot group choice influences the structure of the built widgets. """ @@ -39,7 +39,7 @@ def __init__(self, container: ttk.Frame, group: GroupType, row_index: int): """ Build an instance of the 'PlotSettingsConfigurator' class that provides the structure of the additional plot settings area in terms of three field with the first two being - instances of the 'PlotSettingsField' class, whereas the last one of the + instances of the 'LabelledCombobox' class, whereas the last one of the 'PlotSettingsListBox' class. Depending on the plot group, the first field can be absent, that is for groups '2A' and '3'. It receives as parameters: . container: a frame object to which this instance is added @@ -56,8 +56,8 @@ def __init__(self, container: ttk.Frame, group: GroupType, row_index: int): match group: case GroupType.group1: # Instantiate a label followed by a combobox for both field 1 and 2 - self.field1 = PlotSettingsField(container, row_index, "", []) - self.field2 = PlotSettingsField(container, self.field1.row_next, "", []) + self.field1 = LabelledCombobox(container, row_index, "", []) + self.field2 = LabelledCombobox(container, self.field1.row_next, "", []) # Bind each field selection to the execution of a method that checks if all fields have been set self.field1.cbx.bind('<>', @@ -66,7 +66,7 @@ def __init__(self, container: ttk.Frame, group: GroupType, row_index: int): lambda event: self.handle_selection(container, self.field2.store_selected)) case GroupType.group2: # Instantiate a label followed by a combobox for field 1 - self.field1 = PlotSettingsField(container, row_index, "", []) + self.field1 = LabelledCombobox(container, row_index, "", []) # Instantiate a label followed by two label + combobox groups self.field2 = PlotSettingsField_2(container, self.field1.row_next, []) @@ -87,7 +87,7 @@ def __init__(self, container: ttk.Frame, group: GroupType, row_index: int): lambda event: self.handle_selection(container, self.field2.check_time_consistency)) case GroupType.group3: # Instantiate a label followed by a label + combobox group - self.field2 = PlotSettingsField(container, row_index, "Time (h s ms)", []) + self.field2 = LabelledCombobox(container, row_index, "Time (h s ms)", []) # Bind each field selection to the execution of a method that checks if all fields have been set self.field2.cbx.bind('<>', lambda event: self.handle_selection(container, self.field2.store_selected)) @@ -189,7 +189,7 @@ def set_fields_type(self, field1_type: FieldType, field2_type: FieldType, field3 self.field3_type = field3_type.value -class PlotSettingsField(): +class LabelledCombobox(): """ Class that provides a field for setting one of the configuration options in the GUI in terms of: . a label: providing a description of the option @@ -197,7 +197,7 @@ class PlotSettingsField(): """ def __init__(self, container: ttk.Frame, row_index: int, label_text: str, cbx_list: list, state: str='readonly'): """ - Build an instance of the 'PlotSettingsField' class that provides the content of a + Build an instance of the 'LabelledCombobox' class that provides the content of a configuration option. It receives as parameters: . container: a frame object to which this instance is added . row_index: an integer indicating the row index of the container grid where the @@ -271,9 +271,9 @@ def __init__(self, container, row_index: int, cbx_list: list): self.label.grid(column=0, row=row_index, sticky='w') # Add the configuration field for the start time - self.start_time = PlotSettingsField(container, row_index+1, "Start: ", cbx_list) + self.start_time = LabelledCombobox(container, row_index+1, "Start: ", cbx_list) # Add the configuration field for the end time (same values except for the first) - self.end_time = PlotSettingsField(container, row_index+2, "End: ", cbx_list[1:]) + self.end_time = LabelledCombobox(container, row_index+2, "End: ", cbx_list[1:]) # Update the index indicating the row where to add additional widgets self.row_next = row_index + 3 # Bind the selection of the time values to the check for their consistency diff --git a/tugui/support.py b/tugui/support.py new file mode 100644 index 0000000..c932965 --- /dev/null +++ b/tugui/support.py @@ -0,0 +1,47 @@ +from enum import Enum + + +class IDGA(Enum): + """ + Enumeration storing the different types of plots (field "Type"). + """ + IDGA_1 = (1, "1 - Different Curve Numbers") + IDGA_2 = (2, "2 - Different Times") + IDGA_3 = (3, "3 - Different Slices") + + @property + def index(self): + """ + Index of the element of the enumeration + """ + return self.value[0] + + @property + def description(self): + """ + Descriptive string of the element of the enumeration + """ + return self.value[1] + + +class IANT(Enum): + """ + Enumeration storing the different types of IANT field. + """ + IANT_1 = (1, "IANT 1") + IANT_2 = (2, "IANT 2") + IANT_3 = (3, "IANT 3") + + @property + def index(self): + """ + Index of the element of the enumeration + """ + return self.value[0] + + @property + def description(self): + """ + Descriptive string of the element of the enumeration + """ + return self.value[1] \ No newline at end of file diff --git a/tugui/tab_builder.py b/tugui/tab_builder.py index 00d5182..8ecfc72 100644 --- a/tugui/tab_builder.py +++ b/tugui/tab_builder.py @@ -4,12 +4,14 @@ from ttkthemes import ThemedTk -from gui_configuration import IANT, IDGA, GuiPlotFieldsConfigurator -from plot_settings import FieldType, GroupType, PlotSettingsConfigurator, PlotSettingsField +from abc import ABC, abstractmethod +from gui_configuration import GuiPlotFieldsConfigurator +from plot_settings import FieldType, GroupType, PlotSettingsConfigurator, LabelledCombobox from plot_builder import PlotFigure from gui_widgets import OnOffClickableLabel, WidgetTooltip, SquareButton, CustomNotebook +from support import IDGA, IANT -class TabContentBuilder(ttk.Frame): +class TabContentBuilder(ttk.Frame, ABC): """ Class that builds the content of a tab of the given notebook instance, in terms of its widgets. It inherits from a frame object and presents two areas: @@ -80,6 +82,15 @@ def set_slice_list(self, slices: list): """ self.slice_settings = slices + @abstractmethod + def set_times(self, **kwargs): + """ + Abstract method that allows to set the instance attributes that corresponds + to the simulation step times. + This method, being an interface, will be overridden by subclasses providing + their specific implementation. + """ + def _add_new_plot_figure(self, plot_name: str): """ Method that adds a new 'PlotFigure' object to this instance notebook. @@ -121,6 +132,7 @@ def _add_new_plot_figure(self, plot_name: str): # Change focus to the just created tab self.plotTabControl.select(self.plotTabControl.index('end')-1) + @abstractmethod def _build_configuration_fields(self, config_area: ttk.LabelFrame): """ Abstract method for building the plot configuration fields area, provided as @@ -409,13 +421,21 @@ def __init__(self, container: ttk.Notebook, guiConfig: GuiPlotFieldsConfigurator # Specify the text of the button for running the plot executable self.run_button.configure(text='Run TuPlot') - def set_times(self, macro_time: list, micro_time: list): + def set_times(self, **kwargs): """ Method that allows to set the instance attributes for the simulation macro and micro step times. """ - self.macro_time = macro_time - self.micro_time = micro_time + # Check if the correct arguments have been passed to the method + if not 'macro_time' in kwargs: + raise Exception("Error in passing arguments to this function. The macro step times\ + 'macro_time' argument is missing.") + if not 'micro_time' in kwargs: + raise Exception("Error in passing arguments to this function. The micro step times\ + 'mairo_time' argument is missing.") + # Store the times in the corresponding instance attributes + self.macro_time = kwargs['macro_time'] + self.micro_time = kwargs['micro_time'] def _activate_additional_settings(self, box_to_check: ttk.Combobox, container: ttk.Frame, row: int): """ @@ -435,14 +455,14 @@ def _activate_additional_settings(self, box_to_check: ttk.Combobox, container: t # Rebuild the frame for the additional configuration fields and add one for # allowing the setting of the temperature distribution choice row_index = self._build_iant_field( - container, row, "Temp. distr.: ", self.gui_config.iant1[1], row_index, IANT.IANT_1) + container, row, "Temp. distr.: ", self.gui_config.iant1[1], row_index, IANT.IANT_1.description) elif any(str(i) in self.number_var.get() for i in self.gui_config.iant2[0]): # Plot numbers 102-108 print(self.number_var.get()) # Rebuild the frame for the additional configuration fields and add one for # allowing the setting of the radiation stress choice row_index = self._build_iant_field( - container, row, "Rad. Struct.: ", self.gui_config.iant2[1], row_index, IANT.IANT_2) + container, row, "Rad. Struct.: ", self.gui_config.iant2[1], row_index, IANT.IANT_2.description) else: # Destroy the additional fields print("DESTROY") @@ -538,7 +558,7 @@ def _build_iant_field(self, container, row, label: str, values: list, row_index: # Build the frame self.additional_frame = self._build_frame(container, row) # Add a field for allowing the setting of the IANT1/2 choice - self.iant_entry = PlotSettingsField(self.additional_frame, 0, label, values) + self.iant_entry = LabelledCombobox(self.additional_frame, 0, label, values) # Store the value of the IANT enumeration self.iant = iant # Update the row index @@ -552,17 +572,17 @@ def _build_configuration_fields(self, config_area: ttk.LabelFrame): the specific implementation of the widgets for the frame provided as input. """ # Build the "Group" setup made of a label and a combobox, disabled by default - self.group = PlotSettingsField(config_area, 0, "Group: ", tuple(self.gui_config.groupVSnumVsKn.keys()), tk.DISABLED) + self.group = LabelledCombobox(config_area, 0, "Group: ", tuple(self.gui_config.groupVSnumVsKn.keys()), tk.DISABLED) # Increase the combobox width a little bit to better show items self.group.cbx.configure(width=25) # Build the "Number" setup made of a label and a combobox, disabled until the "Group" field is undefined - number = PlotSettingsField(config_area, 1, "Number: ", list(), tk.DISABLED) + number = LabelledCombobox(config_area, 1, "Number: ", list(), tk.DISABLED) # Declare a variable holding the "Number" field choosen value self.number_var = number.var # Build the "Type" setup made of a label and a combobox, disabled until the "Group" field is undefined - type = PlotSettingsField(config_area, 2, "Type: ", list(), tk.DISABLED) + type = LabelledCombobox(config_area, 2, "Type: ", list(), tk.DISABLED) # Declare a variable holding the "Type" field choosen value self.type_var = type.var @@ -609,9 +629,9 @@ def _configure_additional_fields(self, group: GroupType): time_to_show = self.micro_time # Handle the different curve types (IDGA value) - if (self.type_var.get() == IDGA['IDGA_1'].value): + if (self.type_var.get() == IDGA.IDGA_1.description): # Case of curves for different Kn-s --> IDGA 1 - print("IDGA is: " + IDGA['IDGA_1'].value) + print("IDGA is: " + IDGA.IDGA_1.description) # Configure the fields self.plt_sett_cfg.configure_fields( "Slice: ", self.slice_settings, @@ -620,9 +640,9 @@ def _configure_additional_fields(self, group: GroupType): # Set the fields type (1. Slice, 2. Time, 3. Kn) self.plt_sett_cfg.set_fields_type(FieldType['type3'], FieldType['type2'], FieldType['type1']) - elif (self.type_var.get() == IDGA['IDGA_2'].value): + elif (self.type_var.get() == IDGA.IDGA_2.description): # Case of curves for different times --> IDGA 2 - print("IDGA is: " + IDGA['IDGA_2'].value) + print("IDGA is: " + IDGA.IDGA_2.description) # Configure the fields self.plt_sett_cfg.configure_fields( @@ -632,9 +652,9 @@ def _configure_additional_fields(self, group: GroupType): # Set the fields type (1. Slice, 2. Kn, 3. Time) self.plt_sett_cfg.set_fields_type(FieldType['type3'], FieldType['type1'], FieldType['type2']) - elif (self.type_var.get() == IDGA['IDGA_3'].value): + elif (self.type_var.get() == IDGA.IDGA_3.description): # Case of curves for different slices --> IDGA 3 - print("IDGA is: " + IDGA['IDGA_3'].value) + print("IDGA is: " + IDGA.IDGA_3.description) # Configure the fields self.plt_sett_cfg.configure_fields( @@ -677,12 +697,17 @@ def __init__(self, container: ttk.Notebook, guiConfig: GuiPlotFieldsConfigurator # Specify the text of the button for running the plot executable self.run_button.configure(text='Run TuStat') - def set_times(self, sta_time: list): + def set_times(self, **kwargs): """ Method that allows to set the instance attribute for the step times of the statistical simulation. """ - self.sta_time = sta_time + # Check if the correct argument has been passed to the method + if not 'sta_times' in kwargs: + raise Exception("Error in passing arguments to this function. The statistical step times\ + 'sta_time' argument is missing.") + # Store the times in the corresponding instance attributes + self.sta_time = kwargs['sta_times'] def _activate_fields(self, event=None): """ @@ -734,17 +759,17 @@ def _build_configuration_fields(self, config_area: ttk.LabelFrame): the specific implementation of the widgets for the frame provided as input. """ # Build the "Diagram Nr." setup made of a label and a combobox - self.diagram = PlotSettingsField(config_area, 0, "Diagram Nr.: ", self.gui_config.sta_numVSdescription.values()) + self.diagram = LabelledCombobox(config_area, 0, "Diagram Nr.: ", self.gui_config.sta_numVSdescription.values()) # Build the "Slice" setup made of a label and a combobox, disabled until the "Diagram Nr." field is undefined - self.slice = PlotSettingsField(config_area, 1, "Slice: ", cbx_list=list(), state=tk.DISABLED) + self.slice = LabelledCombobox(config_area, 1, "Slice: ", cbx_list=list(), state=tk.DISABLED) # Build the "Time" setup made of a label and a combobox, disabled until the "Diagram Nr." field is undefined - self.time = PlotSettingsField(config_area, 2, "Time: ", cbx_list=list(), state=tk.DISABLED) + self.time = LabelledCombobox(config_area, 2, "Time: ", cbx_list=list(), state=tk.DISABLED) # Build the "Number of intervals" setup made of a label and a combobox, disabled until # the "Diagram Nr." field is undefined - self.n_intervals = PlotSettingsField(config_area, 3, "Number of intervals: ", cbx_list=list(), state=tk.DISABLED) + self.n_intervals = LabelledCombobox(config_area, 3, "Number of intervals: ", cbx_list=list(), state=tk.DISABLED) # Build the "Type of distribution" setup made of a label and a combobox, disabled until # the "Diagram Nr." field is undefined - self.distribution = PlotSettingsField(config_area, 4, "Type of distribution: ", cbx_list=list(), state=tk.DISABLED) + self.distribution = LabelledCombobox(config_area, 4, "Type of distribution: ", cbx_list=list(), state=tk.DISABLED) # Bind the activation of all the configuration fields to the "Diagram Nr." field item selection self.diagram.cbx.bind('<>', func=lambda event: self._activate_fields()) @@ -765,7 +790,7 @@ def _build_configuration_fields(self, config_area: ttk.LabelFrame): tabControl = ttk.Notebook(root) tabControl.pack(fill='both', expand=True) # Instantiate the GuiPlotFieldsConfigurator class providing the values for filling the fields - guiConfig = GuiPlotFieldsConfigurator() + guiConfig = GuiPlotFieldsConfigurator.init_GuiPlotFieldsConfigurator_attrs() # Instatiate a TabBuilder object holding the tabs tab1 = TuPlotTabContentBuilder( diff --git a/tugui/tu_interface.py b/tugui/tu_interface.py index 21b1c03..926e48c 100644 --- a/tugui/tu_interface.py +++ b/tugui/tu_interface.py @@ -1,13 +1,14 @@ -from io import TextIOWrapper import os import platform import shutil +from typing import Dict, List import numpy as np import re -from dataclasses import dataclass - -from gui_configuration import Diagram, TuPlotDiagram, TuStatDiagram +from abc import ABC, abstractmethod +from dataclasses import dataclass, field +from gui_configuration import DiagramCharacteristics +from io import TextIOWrapper @dataclass @@ -25,81 +26,96 @@ class TuInp: diagram_config: str = "" ikon: str = "" - diagr_type: Diagram = None + diagr_type: DiagramCharacteristics = None - def configure_tuplot_inp_fields(self, info: dict): + @staticmethod + def configure_tuplot_inp_fields(info: dict): """ - Method that configure the fields of this dataclass instance for the - 'TuPlot' case by getting the needed information from the input - dictionary that holds the plot configuration of a single diagram. + Method that builds and configures the fields of the 'TuInp' dataclass + for the 'TuPlot' case. This is done by getting the needed information + from the input dictionary that holds the plot configuration of a single + diagram. """ + # Instantiate the 'TuInp' dataclass + tuinp = TuInp() # Set the default file name for the 'TuPlot' case - self.file_name = "TuPlot.inp" + tuinp.file_name = "TuPlot.inp" # Set the .pli file name - self.pli_name = info['PLI'] + tuinp.pli_name = info['PLI'] # Set the plot number (IDNF) - self.idnf = info['IDNF'] + tuinp.idnf = info['IDNF'] # Set the type of diagram - self.diagr_type = TuPlotDiagram(number=self.idnf, idga=info['IDGA']) + tuinp.diagr_type = DiagramCharacteristics.init_tuplot_DiagramCharacteristics( + number=tuinp.idnf, idga=info['IDGA']) # Set the keyword stating the end of the plot/diagram/file - self.ikon = info['IKON'] + tuinp.ikon = info['IKON'] # -------------------------------------------------- # Store the diagram configuration as a single string # -------------------------------------------------- # Store the IDNF value (diagram number), followed by the IDGA value (diagram type), # followed by NKN (nuber of curves to plot) - self.diagram_config += info['IDNF'] + " " + info['IDGA'] + " " + info['NKN'] + "\n" + tuinp.diagram_config += info['IDNF'] + " " + info['IDGA'] + " " + info['NKN'] + "\n" # Store on the same line: # . IANT1 value (Y, N), valid for plot 113, indicates the temperature distribution is considered # . IANT2 value (C, F), valid for plots 102-108, indicates the stresses are considered for cladding (C) or fuel (F) # . IANT3 value (Y, N), driver for printing the input data and X-Y table (Y) or nothing (N) - self.diagram_config += info['IANT1'] + " " + info['IANT2'] + " " + info['IANT3'] + "\n" + tuinp.diagram_config += info['IANT1'] + " " + info['IANT2'] + " " + info['IANT3'] + "\n" # Store the curves number in increasing order: # . IDGA = 1, it is a list of the selected curves Kn-s # . IDGA = 2/3, it is equal to 1 - self.diagram_config += info['KN'] + "\n" + tuinp.diagram_config += info['KN'] + "\n" # Store the curves slices in increasing order: # . IDGA = 1/2, it is equal to 1 # . IDGA = 3, it is a list of the selected curves slices - self.diagram_config += info['NLSUCH'] + "\n" + tuinp.diagram_config += info['NLSUCH'] + "\n" # Store the time instants at which the curves are plotted or the start/end values (depending on plot group) - self.diagram_config += info["TIME"] + "\n" + tuinp.diagram_config += info["TIME"] + "\n" # Store the NMAS value, stating if a custom scaling is present (value fixed at 0, i.e. no custom extrema) - self.diagram_config += info["NMAS"] + "\n" + tuinp.diagram_config += info["NMAS"] + "\n" - def configure_tustat_inp_fields(self, info: dict): + # Return the built dataclass instance + return tuinp + + @staticmethod + def configure_tustat_inp_fields(info: dict): """ - Method that configure the fields of this dataclass instance for the - 'TuStat' case by getting the needed information from the input - dictionary that holds the plot configuration of a single diagram. + Method that builds and configures the fields of the 'TuInp' dataclass + for the 'TuStat' case. This is done by getting the needed information + from the input dictionary that holds the plot configuration of a single + diagram. """ + # Instantiate the 'TuInp' dataclass + tuinp = TuInp() # Set the default file name for the 'TuStat' case - self.file_name = "TuStat.inp" + tuinp.file_name = "TuStat.inp" # Set the .pli file name - self.pli_name = info['PLI'] + tuinp.pli_name = info['PLI'] # Set the plot number (DIAGNR) - self.idnf = info['DIAGNR'] + tuinp.idnf = info['DIAGNR'] # Set the type of diagram - self.diagr_type = TuStatDiagram(number=self.idnf) + tuinp.diagr_type = DiagramCharacteristics(number=tuinp.idnf) # Set the flag stating the diagram is not a 'TuPlot' case - self.is_tuplot = False + tuinp.is_tuplot = False # Set the keyword stating the end of the plot/diagram/file - self.ikon = info['CONTIN'] + tuinp.ikon = info['CONTIN'] # -------------------------------------------------- # Store the diagram configuration as a single string # -------------------------------------------------- # Store the DIAGNR value (diagram number) - self.diagram_config += info['DIAGNR'] + "\n" + tuinp.diagram_config += info['DIAGNR'] + "\n" # Store the number of required section/slice - self.diagram_config += info['NAXIAL'] + "\n" + tuinp.diagram_config += info['NAXIAL'] + "\n" # Store the time instants at which the curves are plotted - self.diagram_config += info["TIME"] + "\n" + tuinp.diagram_config += info["TIME"] + "\n" # Store the number of intervals of the statistical distribution (INTERV) - self.diagram_config += info["INTERV"] + "\n" + tuinp.diagram_config += info["INTERV"] + "\n" # Store the type of distribution (DISTR) - self.diagram_config += info["DISTR"] + "\n" + tuinp.diagram_config += info["DISTR"] + "\n" + + # Return the built dataclass instance + return tuinp class InpHandler(): @@ -176,11 +192,9 @@ def save_loaded_inp(self): # the file existence and retrieve the DAT file names whose presence needs # to be checked as well. if os.path.dirname(diagr.pli_name): - plireader = PliReader(diagr.pli_name) + plireader = PliReader.init_PliReader(diagr.pli_name) else: - plireader = PliReader(os.path.join(self.inp_dir, diagr.pli_name)) - # Extract the information from the .pli file - plireader.extract_sim_info() + plireader = PliReader.init_PliReader(os.path.join(self.inp_dir, diagr.pli_name)) # Check if any of the DAT files is missing check_file_existence(os.path.join(self.inp_dir, plireader.mac_path), 'mac') check_file_existence(os.path.join(self.inp_dir, plireader.mic_path), 'mic') @@ -228,20 +242,20 @@ def _extract_diagram_info(self, plot_index: int, inp_file_handle: TextIOWrapper) inp_config.pli_name = inp_file_handle.readline().strip() # Read the line containing the 'IDNF' value in order to interpret the plot: - # . if this line contains 3 values, the .inp file corresponds to a TuPlot case + # . if this line contains 3 values, the .inp file corresponds to a 'TuPlot' case # . if this line contains 1 value only, the .inp file corresponds to a TuStat case inp_config.diagram_config += inp_file_handle.readline() if len(inp_config.diagram_config.split()) == 1: - # Only one value --> TuStat case + # Only one value --> 'TuStat' case inp_config.is_tuplot = False - # Declare the dataclass for the TuStat case - inp_config.diagr_type = TuStatDiagram(inp_config.diagram_config.split()[0]) + # Declare the dataclass for the 'TuStat' case + inp_config.diagr_type = DiagramCharacteristics(number=inp_config.diagram_config.split()[0]) else: - # Declare the dataclass for the TuPlot case - inp_config.diagr_type = TuPlotDiagram(number=inp_config.diagram_config.split()[0], - idga=inp_config.diagram_config.split()[1]) - # Call the dataclass method for evaluating the group corresponding to the plot number - inp_config.diagr_type.define_group_from_num() + # Declare the dataclass for the 'TuPlot' case and evaluate its group according to + # the given plot number + inp_config.diagr_type = DiagramCharacteristics.init_tuplot_DiagramCharacteristics( + number=inp_config.diagram_config.split()[0], + idga=inp_config.diagram_config.split()[1]) # Get the plot number inp_config.idnf = inp_config.diagram_config.split()[0] @@ -273,133 +287,183 @@ def check_file_existence(file_path: str, file_extension: str): raise Exception(f"Error: the .{file_extension} file does not exist at the specified path.") +@dataclass class DatGenerator(): """ - Class that interfaces the plot configuration set in the GUI, and provided - as the .inp file, with the tuplotgui executable. - The result is the creation of the output .dat file containing the X-Y data - to be plotted. + Class that stores information about the paths of the input and output + files and that are needed for running the plotting executable. """ - def __init__(self, plotexec_path: str, inp_path: str, plots_num: int, cwd: str, output_files_name: str): - # Set the instance attributes - self.plotexec_path = plotexec_path - - # Set the input file path after checking the file existence - if os.path.isfile(inp_path): - self.inp_path = inp_path - # Extract the .inp file directory - self.inp_dir = os.path.dirname(inp_path) - else: - # If the file does not exists, throw an exception + plotexec_path: str = '' + inp_path: str = '' + inp_dir: str = '' + output_path: str = '' + dat_paths: List[str] = field(default_factory=list) + plt_paths: List[str] = field(default_factory=list) + out_paths: List[str] = field(default_factory=list) + + @staticmethod + def init_DatGenerator_and_run_exec(plotexec_path: str, inp_path: str, plots_num: int, cwd: str, output_files_name: str): + """ + Static method that initialize the 'DatGenerator' dataclass by providing all the needed + information received as input to this function. + Some checks are performed beforehands, that is on the existence of the .inp file + at the specified path and of the output directory. + Given the specific OS, the paths of the output .dat, .plt and .out files are built + accordingly. + Afterwards, a function is called to run the plotting executable which produces the + output files in the specified working directory, while updating the paths to the output + .dat and .plt files. + + Hence, this method returns an object of the 'DatGenerator' dataclass. + """ + # Check the input .inp file existence; raise an Exception if not found + if not os.path.isfile(inp_path): + # The file does not exist, hence raise an exception raise Exception("Error: the .inp file does not exist at the specified path.") + # Get the path to the .inp file directory + inp_dir = os.path.dirname(inp_path) - # Set the output directory after checking its existence - if os.path.isdir(cwd): - self.output_path = cwd - else: + # Check the output directory existence; raise an Exception if not found + if not os.path.isdir(cwd): + # The output directory does not exist, hence raise an exception raise Exception(f"Error: the output '{cwd}' folder does not exist at the specified path.") # Given the number of diagrams to produce, build a list of output file names - self.dat_paths = list() - self.plt_paths = list() - self.out_paths = list() + dat_paths = list() + plt_paths = list() + out_paths = list() # Build the paths to the output files that will be written by running the executable for i in range(plots_num): if platform.system() == "Linux": - self.dat_paths.append(os.path.join(self.inp_dir, output_files_name + str(i + 1).zfill(2) + ".dat")) - self.plt_paths.append(os.path.join(self.inp_dir, output_files_name + str(i + 1).zfill(2) + ".plt")) - self.out_paths.append(os.path.join(self.inp_dir, output_files_name + ".out")) + dat_paths.append(os.path.join(inp_dir, output_files_name + str(i + 1).zfill(2) + ".dat")) + plt_paths.append(os.path.join(inp_dir, output_files_name + str(i + 1).zfill(2) + ".plt")) + out_paths.append(os.path.join(inp_dir, output_files_name + ".out")) elif platform.system() == "Windows": - self.dat_paths.append(os.path.join(self.inp_dir, output_files_name + ".dat")) - self.plt_paths.append(os.path.join(self.inp_dir, output_files_name + ".plt")) - self.out_paths.append(os.path.join(self.inp_dir, output_files_name + ".out")) - - print("DIR --> " + self.inp_dir) - print("DAT --> " + self.dat_paths[i]) - print("PLT --> " + self.plt_paths[i]) - print("OUT --> " + self.out_paths[i]) - - def run(self): - """ - Method that runs the tuplotgui executable by feeding it with the .inp file. - Since the executable needs the input file to be in the same folder, if the - input path differs from the executable one, the .inp file is temporarily copied - into the run folder. - After running the executable, if no errors arose, the output files (.dat, .plt - and .out files) are moved into the working directory. - """ - # The current working directory is changed to the one of the .inp file - os.chdir(os.path.dirname(self.inp_path)) - print("CURRENT WD: " + os.getcwd()) - - # if os.path.dirname(self.tuplotgui_path) != os.path.dirname(self.inp_path): - # # Copy the input file into the tuplotgui executable folder, i.e. the current working directory - # shutil.copy2(src=self.inp_path, dst=os.getcwd()) - - # Assemble the command for running the executable - command = self.plotexec_path + " " + os.path.basename(self.inp_path).split(os.sep)[-1] - # Run the tuplotgui executable by passing the input file - print("RUN: " + command) - os.system(command) - - # Check for the presence of all of the output files - for i in range(len(self.dat_paths)): - if (os.path.isfile(self.dat_paths[i]) and os.path.isfile(self.plt_paths[i])): - # Move the output files into the user-specified working directory - shutil.move(self.dat_paths[i], os.path.join(self.output_path, os.path.basename(self.dat_paths[i]).split(os.sep)[-1])) - shutil.move(self.plt_paths[i], os.path.join(self.output_path, os.path.basename(self.plt_paths[i]).split(os.sep)[-1])) - - # Change the paths of the output files to the ones where they have just been moved - self.dat_paths[i] = os.path.join(self.output_path, os.path.basename(self.dat_paths[i]).split(os.sep)[-1]) - self.plt_paths[i] = os.path.join(self.output_path, os.path.basename(self.plt_paths[i]).split(os.sep)[-1]) - else: - # If any of the output files does not exist, raise an exception - raise Exception("Error: something wrong with the output extraction.\ - One of the output files has not been produced.") - # Handle the .out file case: given how the executables have been compiled, this file could not be present - if os.path.isfile(self.out_paths[i]): - # Move the output file into the user-specified working directory - shutil.move(self.out_paths[i], os.path.join(self.output_path, os.path.basename(self.out_paths[i]).split(os.sep)[-1])) - # Change the path of the output file to the one where it has just been moved - self.out_paths[i] = os.path.join(self.output_path, os.path.basename(self.out_paths[i]).split(os.sep)[-1]) - else: - # Set an empty string as the .out file path in case it has not been produced - self.out_paths[i] = "" - - print("OUTPUT FILES: " + self.dat_paths[i] + ", " + self.plt_paths[i] + ", " + self.out_paths[i]) + dat_paths.append(os.path.join(inp_dir, output_files_name + ".dat")) + plt_paths.append(os.path.join(inp_dir, output_files_name + ".plt")) + out_paths.append(os.path.join(inp_dir, output_files_name + ".out")) + + print("DIR --> " + inp_dir) + print("DAT --> " + dat_paths[i]) + print("PLT --> " + plt_paths[i]) + print("OUT --> " + out_paths[i]) + + # Buil an object of the 'DatGenerator' class with the given data + dat_gen = DatGenerator( + plotexec_path=plotexec_path, + inp_path=inp_path, + inp_dir=inp_dir, + output_path=cwd, + dat_paths=dat_paths, + plt_paths=plt_paths, + out_paths=out_paths + ) + + # Call the function that runs the plotting executables, given the information + # stored within the 'DatGenerator' dataclass + run_plot_files_generation(dat_gen) + + # Return an object of the 'DatGenerator' class, built with the given data + return dat_gen + + +def run_plot_files_generation(datGen: DatGenerator): + """ + Function that runs the plotting executable by feeding it with the .inp file. + Since the run needs to be in the folder of the .inp input file, the current + working directory is moved to the one of this file. + Afterwards, the plotting executable is run and the output .dat, .plt and .out + files are moved into the specified output directory, stored in the given + object of the 'DatGenerator' dataclass. If any of the .dat and .plt files has + not been created (a specific check is run), an exception is risen. + If the creation succedes, the corresponding paths stored in the given + 'DatGenerator' dataclass object are updated. + + The function hence returns the updated dataclass. + """ + # The current working directory is changed to the one of the .inp file + os.chdir(os.path.dirname(datGen.inp_path)) + print("CURRENT WD: " + os.getcwd()) + + # Assemble the command for running the executable + command = datGen.plotexec_path + " " + os.path.basename(datGen.inp_path).split(os.sep)[-1] + # Run the tuplotgui executable by passing the input file + print("RUN: " + command) + os.system(command) + + # Check for the presence of all of the output files + for i in range(len(datGen.dat_paths)): + if (os.path.isfile(datGen.dat_paths[i]) and os.path.isfile(datGen.plt_paths[i])): + # Move the output files into the user-specified working directory + shutil.move(datGen.dat_paths[i], os.path.join(datGen.output_path, os.path.basename(datGen.dat_paths[i]).split(os.sep)[-1])) + shutil.move(datGen.plt_paths[i], os.path.join(datGen.output_path, os.path.basename(datGen.plt_paths[i]).split(os.sep)[-1])) + + # Change the paths of the output files to the ones where they have just been moved + datGen.dat_paths[i] = os.path.join(datGen.output_path, os.path.basename(datGen.dat_paths[i]).split(os.sep)[-1]) + datGen.plt_paths[i] = os.path.join(datGen.output_path, os.path.basename(datGen.plt_paths[i]).split(os.sep)[-1]) + else: + # If any of the output files does not exist, raise an exception + raise Exception("Error: something wrong with the output extraction.\ + One of the output files has not been produced.") + # Handle the .out file case: given how the executables have been compiled, this file could not be present + if os.path.isfile(datGen.out_paths[i]): + # Move the output file into the user-specified working directory + shutil.move(datGen.out_paths[i], os.path.join(datGen.output_path, os.path.basename(datGen.out_paths[i]).split(os.sep)[-1])) + # Change the path of the output file to the one where it has just been moved + datGen.out_paths[i] = os.path.join(datGen.output_path, os.path.basename(datGen.out_paths[i]).split(os.sep)[-1]) + else: + # Set an empty string as the .out file path in case it has not been produced + datGen.out_paths[i] = "" - # Remove the .inp file copied in the tuplotgui executable folder - # os.remove(os.path.join(os.getcwd(), os.path.basename(self.inp_path).split('/')[-1])) + print("OUTPUT FILES: " + datGen.dat_paths[i] + ", " + datGen.plt_paths[i] + ", " + datGen.out_paths[i]) - # Restore the previous working directory - os.chdir(self.output_path) + # Restore the previous working directory + os.chdir(datGen.output_path) + # Return the updated dataclass + return datGen +@dataclass class PliReader(): """ - Class that interprets the content of the .pli file produced by the TU simulation. It contains + Datalass that interprets the content of the .pli file produced by the TU simulation. It contains information useful for extracting data from the .mac e .mic files. """ - def __init__(self, pli_path: str): - """ - Build an instance of the 'PliReader' class that interprets the content of the .pli - file produced by the TU simulation. + pli_path: str = '' + pli_folder: str = '' + opt_dict: Dict[str, str] = field(default_factory=dict) + mic_path: str = '' + mac_path: str = '' + sta_path: str = '' + mic_recordLength: str = '' + mac_recordLength: str = '' + sta_recordLength: str = '' + sta_micStep: str = '' + sta_macStep: str = '' + sta_dataset: str = '' + axial_steps: str = '' + + @staticmethod + def init_PliReader(pli_path: str): + """ + Method that builds and configures the 'PliReader' dataclass by providing all + the needed information that come by interpreting the content of the .pli file + produced by the TU simulation. It receives as parameter the path to the .pli file to read and checks the actual - existence of the file. + existence of the file. If no exception are risen, the fields of the 'PliReader' + dataclass are set. + This method returns the built instance of the 'PliReader' class. """ # Check the .pli file existence check_file_existence(pli_path, 'pli') - self.pli_path = pli_path - self.pli_folder = os.path.dirname(pli_path) + # Get the path to the .pli file directory + pli_dir = os.path.dirname(pli_path) + # Instantiate the 'PliReader' class + pli_reader = PliReader(pli_path=pli_path, pli_folder=pli_dir) - def extract_sim_info(self): - """ - Method that reads the content of the .pli file and extracts the values of the - relevant items. - """ # Open the .pli file in reading mode - with open(self.pli_path, 'r') as f: + with open(pli_path, 'r') as f: # Read line-by-line for line in f: # Get the line where the options are printed @@ -414,39 +478,42 @@ def extract_sim_info(self): raise Exception("Error: no match between the options and their values") # Build a dictionary holding the name of the options VS their values - self.opt_dict = {options[i]: options_values[i] for i in range(len(options))} + pli_reader.opt_dict = {options[i]: options_values[i] for i in range(len(options))} else: # Search for the lines where the paths of the .mic and .mac files and their record length are present if (re.search("^\w+.mic\s+", line)): # Save the path to the .mic file - self.mic_path = line.split()[0] + pli_reader.mic_path = line.split()[0] # Advance to the next line to get the .mic record length - self.mic_recordLength = f.readline().split()[0] + pli_reader.mic_recordLength = f.readline().split()[0] elif(re.search("^\w+.mac\s+", line)): # Save the path to the .mac file - self.mac_path = line.split()[0] + pli_reader.mac_path = line.split()[0] # Advance to the next line to get the .mac record length - self.mac_recordLength = f.readline().split()[0] + pli_reader.mac_recordLength = f.readline().split()[0] elif(re.search("^\w+.sta\s+", line)): # Save the path to the .sta file - self.sta_path = line.split()[0] + pli_reader.sta_path = line.split()[0] # Advance to the next line to get the .sta record length - self.sta_recordLength = f.readline().split()[0] + pli_reader.sta_recordLength = f.readline().split()[0] # Advance to the next line to get the .sta micro-step dataset record length - self.sta_micStep = f.readline().split()[0] + pli_reader.sta_micStep = f.readline().split()[0] # Advance to the next line to get the .sta macro-step dataset record length - self.sta_macStep = f.readline().split()[0] + pli_reader.sta_macStep = f.readline().split()[0] # Advance to the next line to get the .sta statistic dataset record length - self.sta_dataset = f.readline().split()[0] + pli_reader.sta_dataset = f.readline().split()[0] # Extract the number of axial sections depending on the ISLICE field - if self.opt_dict['ISLICE'] == 1: - self.axial_steps = int(self.opt_dict['M3']) + if pli_reader.opt_dict['ISLICE'] == 1: + pli_reader.axial_steps = int(pli_reader.opt_dict['M3']) else: - self.axial_steps = int(self.opt_dict['M3']) + 1 + pli_reader.axial_steps = int(pli_reader.opt_dict['M3']) + 1 + # Return the built instance + return pli_reader -class DaReader(): + +class DaReader(ABC): """ Base class for all the ones that interpret the content of a direct-access file produced by the TU simulation. @@ -461,6 +528,50 @@ def __init__(self, da_path: str, extension: str): check_file_existence(da_path, extension) # Store the direct-access file path self.da_path = da_path + # Initialize the time values read from the direct-access file + self.time_h = list() + self.time_s = list() + self.time_ms : list[int] = list() + # Call the ABC constructor + super().__init__() + + @abstractmethod + def extract_time_hsms(self, record_length: str) -> tuple[list[int], list[int], list[int]]: + """ + Method that extracts from the direct-access file the simulation time instants + as arrays for hours, seconds and milliseconds respectively. These arrays are + saved as instance attributes and returned as a tuple. + """ + + @abstractmethod + def read_tu_data(self, record_length): + """ + Method that opens the direct-access file and re-elaborate its content, originally stored + as a one-line array. Given the record length, the array is reshaped as a 2D array having: + . as rows, the values at each time for each igrob-slice + . as columns, the values of the quantities calculated for each m2(igrob)-slice element + Subclasses can override this method providing their own implementation in case the one + proposed is not valid (case of a .sta file). + """ + + +class MicReader(DaReader): + """ + Class that interprets the content of the .mic file produced by the TU simulation. + For every micro-step (TU internal step) several quantities are stored, that are + section/slice dependent variables and special quantities. + """ + def __init__(self, mic_path: str): + """ + Build an instance of the 'MicReader' class that interprets the content of the .mic + file produced by the TU simulation. + It receives as parameter the path to the .mic file to read and checks the actual + existence of the file. + """ + # Store the file extension + self.extension = 'mic' + # Call the superclass constructor + super().__init__(mic_path, self.extension) def extract_time_hsms(self, record_length): """ @@ -499,7 +610,7 @@ def read_tu_data(self, record_length): raise Exception(error_msg) -class MacReader(DaReader): +class MacReader(MicReader): """ Class that interprets the content of the .mac file produced by the TU simulation. It contains the radially dependent quantities at every simulation time for every (i, j)-th element of the @@ -512,8 +623,10 @@ def __init__(self, mac_path: str, n_slices: int): It receives as parameter the path to the .mac file to read and checks the actual existence of the file. """ + # Store the file extension + self.extension = 'mac' # Call the superclass constructor - super().__init__(mac_path, 'mac') + super().__init__(mac_path) # Store the number of slices self.n_slices = n_slices @@ -536,23 +649,6 @@ def extract_xtime_hsms(self, record_length): return (self.time_h, self.time_s, self.time_ms) -class MicReader(DaReader): - """ - Class that interprets the content of the .mic file produced by the TU simulation. - For every micro-step (TU internal step) several quantities are stored, that are - section/slice dependent variables and special quantities. - """ - def __init__(self, mic_path: str): - """ - Build an instance of the 'MicReader' class that interprets the content of the .mic - file produced by the TU simulation. - It receives as parameter the path to the .mic file to read and checks the actual - existence of the file. - """ - # Call the superclass constructor - super().__init__(mic_path, 'mic') - - class StaReader(DaReader): """ Class that interprets the content of the .sta file produced by the TU simulation. @@ -647,21 +743,20 @@ def read_tu_data(self, record_length: int, axial_steps: int, sta_dataset_length: # Get the file directory path dname = os.path.dirname(abspath) - # Instantiate the class interfacing input with tuplotgui executable - inp_to_dat = DatGenerator(os.path.join(dname, "bin/tuplotgui_nc"), - "../Input/TuPlot.inp", - 1, - "../Output", - 'TuPlot') - # Run the tuplotgui executable - inp_to_dat.run() - + # Run the method that deals with instantiating the dataclass storing the needed + # information for the plotting executable to be run. The corresponding executable + # is run afterwards and the paths to the output .dat and .plt files, stored in the + # returned object, are updated. + inp_to_dat = DatGenerator.init_DatGenerator_and_run_exec( + plotexec_path=os.path.join(dname, "bin/tuplotgui_nc"), + inp_path="../tests/input/TuPlot.inp", + plots_num=1, + cwd="../tests/output", + output_files_name='TuPlot') case 2: print("Testing the interface to .mic file") - # Instantiate the PliReader class - plireader = PliReader("../Input/rodcd.pli") - # Extract the information from the .pli file - plireader.extract_sim_info() + # Extract the information from the .pli file and instantiate the 'PliReader' class + plireader = PliReader.init_PliReader("../Input/rodcd.pli") # Instantiate the MicReader class micreader = MicReader(os.path.dirname(plireader.pli_path) + os.sep + plireader.mic_path) @@ -671,11 +766,9 @@ def read_tu_data(self, record_length: int, axial_steps: int, sta_dataset_length: case 3: # PliReader case print("Testing the interface to the .pli file") - # Instantiate the PliReader class - plireader = PliReader("../Input/rodcd.pli") + # Extract the information from the .pli file and instantiate the 'PliReader' class + plireader = PliReader.init_PliReader("../Input/rodcd.pli") print("Path to the .pli file: " + plireader.pli_path) - # Extract the information from the .pli file - plireader.extract_sim_info() print(plireader.opt_dict) print(plireader.mic_path, plireader.mic_recordLength) @@ -684,10 +777,8 @@ def read_tu_data(self, record_length: int, axial_steps: int, sta_dataset_length: case 4: print("Testing the interface to .mac file") - # Instantiate the PliReader class - plireader = PliReader("../Input/rodcd.pli") - # Extract the information from the .pli file - plireader.extract_sim_info() + # Extract the information from the .pli file and instantiate the 'PliReader' class + plireader = PliReader.init_PliReader("../Input/rodcd.pli") # Instantiate the MacReader class macreader = MacReader(os.path.dirname(plireader.pli_path) + os.sep + plireader.mac_path, @@ -698,10 +789,8 @@ def read_tu_data(self, record_length: int, axial_steps: int, sta_dataset_length: case 5: print("Testing the interface to .sta file") - # Instantiate the PliReader class - plireader = PliReader("../Input/TuStatCase/222r007n.pli") - # Extract the information from the .pli file - plireader.extract_sim_info() + # Extract the information from the .pli file and instantiate the 'PliReader' class + plireader = PliReader.init_PliReader("../Input/rodcd.pli") # Instantiate the StaReader class stareader = StaReader(os.path.dirname(plireader.pli_path) + os.sep + plireader.sta_path,