diff --git a/HyPAT/backup_datafiles/OverviewPlotsSaveFileExample.xlsx b/HyPAT/backup_data_files/OverviewPlotsSaveFileExample.xlsx similarity index 59% rename from HyPAT/backup_datafiles/OverviewPlotsSaveFileExample.xlsx rename to HyPAT/backup_data_files/OverviewPlotsSaveFileExample.xlsx index 35f551c..f326e16 100644 Binary files a/HyPAT/backup_datafiles/OverviewPlotsSaveFileExample.xlsx and b/HyPAT/backup_data_files/OverviewPlotsSaveFileExample.xlsx differ diff --git a/HyPAT/backup_datafiles/brief_explanation_of_datafiles.txt b/HyPAT/backup_data_files/brief_explanation_of_datafiles.txt similarity index 91% rename from HyPAT/backup_datafiles/brief_explanation_of_datafiles.txt rename to HyPAT/backup_data_files/brief_explanation_of_datafiles.txt index ad64471..fe925bd 100644 --- a/HyPAT/backup_datafiles/brief_explanation_of_datafiles.txt +++ b/HyPAT/backup_data_files/brief_explanation_of_datafiles.txt @@ -13,3 +13,5 @@ default_entry_vars and o-ring_data are used for some variables on the Permeation material_data and melting_tempK are used primarily in the Overview Plots tab, although they also provide the information used in the Permeation Estimates menu for sample material persistent_permeation_input_variables is used in the Permeation Plots tab + +persistent_absorption_input_variables is used in the Absorption Plots tab diff --git a/HyPAT/backup_data_files/default_entry_vals_backup.xlsx b/HyPAT/backup_data_files/default_entry_vals_backup.xlsx new file mode 100644 index 0000000..912290a Binary files /dev/null and b/HyPAT/backup_data_files/default_entry_vals_backup.xlsx differ diff --git a/HyPAT/backup_datafiles/material_data_backup.xlsx b/HyPAT/backup_data_files/material_data_backup.xlsx similarity index 58% rename from HyPAT/backup_datafiles/material_data_backup.xlsx rename to HyPAT/backup_data_files/material_data_backup.xlsx index 8d2d1ea..f4e1478 100644 Binary files a/HyPAT/backup_datafiles/material_data_backup.xlsx and b/HyPAT/backup_data_files/material_data_backup.xlsx differ diff --git a/HyPAT/backup_datafiles/melting_tempK_backup.xlsx b/HyPAT/backup_data_files/melting_tempK_backup.xlsx similarity index 76% rename from HyPAT/backup_datafiles/melting_tempK_backup.xlsx rename to HyPAT/backup_data_files/melting_tempK_backup.xlsx index 6b99abb..08628b7 100644 Binary files a/HyPAT/backup_datafiles/melting_tempK_backup.xlsx and b/HyPAT/backup_data_files/melting_tempK_backup.xlsx differ diff --git a/HyPAT/backup_datafiles/o-ring_data_backup.xlsx b/HyPAT/backup_data_files/o-ring_data_backup.xlsx similarity index 53% rename from HyPAT/backup_datafiles/o-ring_data_backup.xlsx rename to HyPAT/backup_data_files/o-ring_data_backup.xlsx index cb45982..6c05850 100644 Binary files a/HyPAT/backup_datafiles/o-ring_data_backup.xlsx and b/HyPAT/backup_data_files/o-ring_data_backup.xlsx differ diff --git a/HyPAT/backup_data_files/persistent_absorption_input_variables_backup.xlsx b/HyPAT/backup_data_files/persistent_absorption_input_variables_backup.xlsx new file mode 100644 index 0000000..9e10d2e Binary files /dev/null and b/HyPAT/backup_data_files/persistent_absorption_input_variables_backup.xlsx differ diff --git a/HyPAT/backup_data_files/persistent_permeation_input_variables_backup.xlsx b/HyPAT/backup_data_files/persistent_permeation_input_variables_backup.xlsx new file mode 100644 index 0000000..3edfdf6 Binary files /dev/null and b/HyPAT/backup_data_files/persistent_permeation_input_variables_backup.xlsx differ diff --git a/HyPAT/backup_datafiles/default_entry_vals_backup.xlsx b/HyPAT/backup_datafiles/default_entry_vals_backup.xlsx deleted file mode 100644 index 08abb8d..0000000 Binary files a/HyPAT/backup_datafiles/default_entry_vals_backup.xlsx and /dev/null differ diff --git a/HyPAT/backup_datafiles/default_entry_vars_backup.xlsx b/HyPAT/backup_datafiles/default_entry_vars_backup.xlsx deleted file mode 100644 index bc49fe4..0000000 Binary files a/HyPAT/backup_datafiles/default_entry_vars_backup.xlsx and /dev/null differ diff --git a/HyPAT/backup_datafiles/persistent_permeation_input_variables_backup.xlsx b/HyPAT/backup_datafiles/persistent_permeation_input_variables_backup.xlsx deleted file mode 100644 index b168ff2..0000000 Binary files a/HyPAT/backup_datafiles/persistent_permeation_input_variables_backup.xlsx and /dev/null differ diff --git a/HyPAT/data_files/default_entry_vals.xlsx b/HyPAT/data_files/default_entry_vals.xlsx new file mode 100644 index 0000000..439b666 Binary files /dev/null and b/HyPAT/data_files/default_entry_vals.xlsx differ diff --git a/HyPAT/data_files/material_data.xlsx b/HyPAT/data_files/material_data.xlsx new file mode 100644 index 0000000..d1fcf8d Binary files /dev/null and b/HyPAT/data_files/material_data.xlsx differ diff --git a/HyPAT/datafiles/melting_tempK.xlsx b/HyPAT/data_files/melting_tempK.xlsx similarity index 62% rename from HyPAT/datafiles/melting_tempK.xlsx rename to HyPAT/data_files/melting_tempK.xlsx index 22e4eb5..a068364 100644 Binary files a/HyPAT/datafiles/melting_tempK.xlsx and b/HyPAT/data_files/melting_tempK.xlsx differ diff --git a/HyPAT/datafiles/o-ring_data.xlsx b/HyPAT/data_files/o-ring_data.xlsx similarity index 53% rename from HyPAT/datafiles/o-ring_data.xlsx rename to HyPAT/data_files/o-ring_data.xlsx index 1b8ecfa..32c7991 100644 Binary files a/HyPAT/datafiles/o-ring_data.xlsx and b/HyPAT/data_files/o-ring_data.xlsx differ diff --git a/HyPAT/data_files/persistent_absorption_input_variables.xlsx b/HyPAT/data_files/persistent_absorption_input_variables.xlsx new file mode 100644 index 0000000..935c9c2 Binary files /dev/null and b/HyPAT/data_files/persistent_absorption_input_variables.xlsx differ diff --git a/HyPAT/datafiles/persistent_permeation_input_variables.xlsx b/HyPAT/data_files/persistent_permeation_input_variables.xlsx similarity index 53% rename from HyPAT/datafiles/persistent_permeation_input_variables.xlsx rename to HyPAT/data_files/persistent_permeation_input_variables.xlsx index 0832138..e7249f3 100644 Binary files a/HyPAT/datafiles/persistent_permeation_input_variables.xlsx and b/HyPAT/data_files/persistent_permeation_input_variables.xlsx differ diff --git a/HyPAT/datafiles/default_entry_vals.xlsx b/HyPAT/datafiles/default_entry_vals.xlsx deleted file mode 100644 index b879117..0000000 Binary files a/HyPAT/datafiles/default_entry_vals.xlsx and /dev/null differ diff --git a/HyPAT/datafiles/default_entry_vars.xlsx b/HyPAT/datafiles/default_entry_vars.xlsx deleted file mode 100644 index c9de0fd..0000000 Binary files a/HyPAT/datafiles/default_entry_vars.xlsx and /dev/null differ diff --git a/HyPAT/datafiles/material_data.xlsx b/HyPAT/datafiles/material_data.xlsx deleted file mode 100644 index a68f9c8..0000000 Binary files a/HyPAT/datafiles/material_data.xlsx and /dev/null differ diff --git a/HyPAT/main.py b/HyPAT/main.py index b15308f..99063a2 100644 --- a/HyPAT/main.py +++ b/HyPAT/main.py @@ -1,10 +1,10 @@ -""" Hydrogen Permeation Analysis Tool (HyPAT) +""" Hydrogen Permeation and Absorption Tool (HyPAT) Python Interpreter: 3.8, 3.9, or 3.10 - Python Packages: matplotlib, pandas, numpy, tkmacosx, mplcursors, scipy, and openpyxl + Python Packages: Matplotlib, Pandas, NumPy, tkmacosx, mplcursors, SciPy, and openpyxl - compatible: macOS and Windows 10 """ + compatible: macOS 12 and Windows 10 """ from source_code.application import Application diff --git a/HyPAT/source_code/absorption_plots.py b/HyPAT/source_code/absorption_plots.py new file mode 100644 index 0000000..41449f2 --- /dev/null +++ b/HyPAT/source_code/absorption_plots.py @@ -0,0 +1,2339 @@ +""" Code for the Absorption Plots tab in HyPAT """ +import matplotlib +matplotlib.use("TkAgg") # backend of matplotlib. needs to be here +from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk +import tkinter as tk +from tkinter import ttk +import numpy as np +import pandas as pd +from tkinter.filedialog import askdirectory, asksaveasfilename +from tkinter.messagebox import showerror +import matplotlib.pyplot as plt +import os +from .data_storage import Widgets, FormatLabel, LoadingScreen +from scipy.optimize import curve_fit +import mplcursors # adds the hover-over feature to labels. This library has good documentation +import openpyxl +import platform # allows for Mac vs. Windows adaptions +# make certain warnings appear as errors, allowing them to be caught using a try/except clause +import warnings +from scipy.optimize import OptimizeWarning +warnings.simplefilter("error", OptimizeWarning) # used during curve_fit + + +class AbsorptionPlots(tk.Frame): + + def __init__(self, parent, storage, *args, **kwargs): + super().__init__(parent, *args, **kwargs) + # container for important variables + self.storage = storage + self.datafiles = {} # Store Excel files + self.inputs = {} + self.header = [] # Container for titles of each column in your data file that will be used + self.needed_cols = [] # Container that will hold the column number for each needed data set + self.converters_dict = {} # Container for functions for converting data into correct units + + # store widgets + self.widgets = Widgets() + self.add_text0 = self.widgets.add_text0 + self.add_text2 = self.widgets.add_text2 + self.add_entry = self.widgets.add_entry + self.add_entry3 = self.widgets.add_entry3 + + # to get 2 rows over 3, we need two more frames + self.top_frame = tk.Frame(self, bd=10) + self.top_frame.grid(row=0, column=0, sticky="nsew") + self.bottom_frame = tk.Frame(self, bd=10) + self.bottom_frame.grid(row=1, column=0, sticky="nsew") + + self.frame = tk.LabelFrame(self.top_frame, bd=8) + self.frame.grid(row=0, column=0, sticky="nsew") + + # arrange frames in tab + self.rowconfigure((0, 1), weight=1) + self.columnconfigure((0, 1), weight=1) + self.top_frame.rowconfigure(0, weight=1) + self.top_frame.columnconfigure((0, 1), weight=1) + self.bottom_frame.rowconfigure(0, weight=1) + self.bottom_frame.columnconfigure((0, 1, 2), weight=1) + + self.directory = "" + self.loading_data = False # Keep track of whether data is currently being loaded by this tab + self.refreshing = False # If false, ask for a directory when select_file gets called + self.file_type = "" + self.time_type = 0 # Used to determine which converter function to use for the time instrument. + self.days = -1 # How many days have passed during the experiment (used for datetime conversion to seconds) + self.this_date_time = 0 # Current datetime (used for datetime conversion to seconds) + self.init_time = 0 # Initial datetime in seconds (used for datetime conversion to seconds) + self.extra_time = 0 # Extra time to be accounted for (used for datetime conversion to seconds) + self.error_texts = "" # Text variable for storing the uncertainty texts that may come up when files are loaded + self.troubleshooting_df = pd.DataFrame() # DataFrame to contain variables when troubleshooting + + self.num_GasT0 = 1 # Number of TCs measuring GasT initial + self.num_GasT = 1 # Number of TCs measuring GasT after the isolation valve has opened + + # When determining uncertainty, the program will pick the max of the calculated statistical uncertainty, the + # constant uncertainty, and the proportional uncertainty. These are containers for the constant and + # proportional uncertainties of each instrument + self.GasT_cerr = {} # degrees C, i.e., +/- 2.2 degrees C (constant uncertainty) + self.GasT_perr = {} # percentage, i.e., +/- 0.75% of total measurement (in Celsius) (proportional uncertainty) + self.SampT_cerr = {} + self.SampT_perr = {} + self.Pres_cerr = {} # Pa (constant uncertainty) + self.Pres_perr = {} # percentage, (proportional uncertainty) + + # variables for input and uncertainty of inputs + self.sample_thickness = tk.DoubleVar(value=0.2) + self.sample_thickness_err = tk.DoubleVar(value=round(self.sample_thickness.get()*0.05, 13)) + self.Vs_cm3 = tk.DoubleVar(value=5.8e-2) # Volume of initial container + self.Vs_cm3_err = tk.DoubleVar(value=round(self.Vs_cm3.get() * 0.005, 13)) + self.Vic_cm3 = tk.DoubleVar(value=379.0) # Volume of initial container + self.Vic_cm3_err = tk.DoubleVar(value=2.3) # round(self.Vic_cm3.get() * 0.005, 13)) + self.Vsc_cm3 = tk.DoubleVar(value=66.4) # cm^3 Volume of sample container + self.Vsc_cm3_err = tk.DoubleVar(value=0.6) # round(self.Vsc_cm3.get() * 0.005, 13)) + self.ms_g = tk.DoubleVar(value=2) # g Mass of sample + self.ms_g_err = tk.DoubleVar(value=round(self.ms_g.get() * 0.05, 13)) + # todo Uncomment self.rhos_gcm3 and self.rho_gcm3_err here and elsewhere, then connect them to the code so they + # do things and update when needed + # self.rhos_gcm3 = tk.DoubleVar(value=round(self.ms_g.get() / self.Vs_cm3.get(), 13)) # g/cm^3 density of sample + # self.rhos_gcm3_err = tk.DoubleVar(value=round((self.ms_g.get() / self.Vs_cm3.get()) * + # np.sqrt((self.ms_g_err.get() / self.ms_g.get()) ** 2 + # + (self.Vs_cm3_err.get() / self.Vs_cm3.get())** 2), 13)) + self.exp_type = tk.StringVar(value="Single") + self.molar_mass = tk.DoubleVar(value=50.9440) # g/mol default molar_mass + # Variables for determining initial and final values + self.e_tol = tk.DoubleVar(value=self.storage.tol.get()) # Tolerance for equilibrium + self.e_t_del = tk.DoubleVar(value=0) # Minimum time after t0 before looking for eq. + self.i_range = {} # Dict for holding the range used in finding initial values for each file + self.e_range = {} # Dict for holding the range used in finding eq and eq value for each file + self.gen_i_range = tk.IntVar(value=self.storage.gen_dp_range.get()) # Default range to find initial vals over + self.gen_e_range = tk.IntVar(value=self.storage.gen_dp_range.get()) # Default range to find eq and eq vals over + + # absorption variables + self.p_num = {} # number of pressures + self.t0 = {} # time when isolation valve opens (s) + self.t_e = {} # time when equilibrium pressure is achieved (s) + self.Phi = {} # permeability + self.Phi_err = {} + self.Tg0 = {} # Temperature of gas at t0 (using an average over time and instruments) (K) + self.Tg_e = {} # Temperature of gas at t_e (using an average over time and instruments) (K) + self.Tg_e_err = {} + self.artists = [] # store points for Perm/Dif/Sol vs. Temp graph + self.Ts0 = {} # Sample temperature at t0 (using an average over time and instruments) (K) + self.Ts_e = {} # Sample temperature at t_e (using an average over time and instruments) (K) + self.Ts_e_err = {} + self.pr0_avg = {} # Pressure at t0 (using an average) (Pa) + self.pr_e_avg = {} # Pressure at t_e (using an average) (Pa) + self.pr_e_err = {} + self.ns0 = {} # Initial moles absorbed by sample + self.ns0_err = {} + self.ns_e = {} # Total moles absorbed by sample + self.ns_e_err = {} + self.ns_t = {} # Number of moles in sample as a function of time + self.HM = {} # Hydrogen to metal atoms ratio + self.HM_err = {} + + # diffusivity variables + self.D = {} + self.D_err = {} + self.A = {} # proportionality constant + self.dt = {} # additive time constant + self.D_time = {} # time over which D is calculated + self.lhs = {} # for the diffusivity optimization comparison + self.rhs = {} # analytical solution (non-fit) + self.rhs_cf = {} # curve_fit + + # solubility variables + self.Ks = {} + self.Ks_err = {} + + # text variables for labels + self.pr_e_label = tk.DoubleVar(value=0) # Equilibrium Pressure + self.pr_e_err_label = tk.DoubleVar(value=0) + self.ns_e_label = tk.DoubleVar(value=0) # Moles absorbed + self.ns_e_err_label = tk.DoubleVar(value=0) + self.eq_t_label = tk.DoubleVar(value=0) # Time to equilibrium + self.eq_t_err_label = tk.DoubleVar(value=0) + self.Phi_label = tk.DoubleVar(value=0) # Permeability + self.Phi_err_label = tk.DoubleVar(value=0) + self.D_label = tk.DoubleVar(value=0) # Diffusivity + self.D_err_label = tk.DoubleVar(value=0) + self.K_label = tk.DoubleVar(value=0) # Solubility + self.K_err_label = tk.DoubleVar(value=0) + self.p_num_label = tk.IntVar(value=0) # Number of pressures in an isotherm + + # add frames and buttons to view important variable + + entry_row = 0 + self.add_entry3(self, self.frame, variable=self.inputs, key="ls", text="Sample Thickness: l", + subscript="s", tvar1=self.sample_thickness, tvar2=self.sample_thickness_err, + update_function=lambda tvar, var_type, key: self.update_function(tvar, var_type, key), + units="[mm]", row=entry_row) + entry_row += 1 + + self.add_entry3(self, self.frame, variable=self.inputs, key="ms_g", text="Sample Mass: m", + subscript="s", tvar1=self.ms_g, tvar2=self.ms_g_err, + update_function=lambda tvar, var_type, key: self.update_function(tvar, var_type, key), + units="[g]", row=entry_row) + entry_row += 1 + + self.add_entry3(self, self.frame, variable=self.inputs, key="Vs_cm3", text="Sample Volume: V", + subscript="s", tvar1=self.Vs_cm3, tvar2=self.Vs_cm3_err, units="[cm\u00B3]", # "[cm^3]" + update_function=lambda tvar, var_type, key: self.update_function(tvar, var_type, key), + row=entry_row) + entry_row += 1 + + self.add_entry3(self, self.frame, variable=self.inputs, key="Vic_cm3", text="Initial Container Volume: V", + subscript="ic", tvar1=self.Vic_cm3, tvar2=self.Vic_cm3_err, units="[cm\u00B3]", # "[cm^3]" + update_function=lambda tvar, var_type, key: self.update_function(tvar, var_type, key), + row=entry_row) + entry_row += 1 + + self.add_entry3(self, self.frame, variable=self.inputs, key="Vsc_cm3", text="Sample Container Volume: V", + subscript="sc", tvar1=self.Vsc_cm3, tvar2=self.Vsc_cm3_err, units="[cm\u00B3]", # "[cm^3]" + update_function=lambda tvar, var_type, key: self.update_function(tvar, var_type, key), + row=entry_row) + entry_row += 1 + + label_row = entry_row + # todo Uncomment self.rhos_gcm3 and self.rho_gcm3_err stuff here and elsewhere, then connect them to the code so + # they do things and update when needed + # self.add_text2(self.frame, text="Sample Density: \u03C1", subscript="s", tvar1=self.rhos_gcm3, + # tvar2=self.rhos_gcm3_err, units="[g/cm\u00B3]", + # row=label_row) # No "_label" in rho's name because rho doesn't change based on file + # label_row += 1 + self.add_text2(self.frame, text="Equilibrium Pressure: P", subscript="e", tvar1=self.pr_e_label, + tvar2=self.pr_e_err_label, units="[Pa]", row=label_row) + label_row += 1 + self.add_text2(self.frame, text="Moles Absorbed by Sample: n", subscript="se", tvar1=self.ns_e_label, + tvar2=self.ns_e_err_label, units="[mol]", row=label_row) + label_row += 1 + self.add_text2(self.frame, text="Time to Equilibrium: t", subscript="e", tvar1=self.eq_t_label, + tvar2=self.eq_t_err_label, units="[s]", row=label_row) + label_row += 1 + self.add_text2(self.frame, text="Permeability: \u03A6", subscript="", tvar1=self.Phi_label, + tvar2=self.Phi_err_label, + units="[mol m\u207b\u00b9 s\u207b\u00b9 Pa\u207b\u2070\u1427\u2075]", # "[mol/msPa^0.5]" + row=label_row) + label_row += 1 + self.add_text2(self.frame, text="Diffusivity: D", subscript="", tvar1=self.D_label, tvar2=self.D_err_label, + units="[m\u00b2 s\u207b\u00b9]", row=label_row) + label_row += 1 + self.add_text2(self.frame, text="Solubility: K", subscript="s", tvar1=self.K_label, tvar2=self.K_err_label, + units="[mol m\u207b\u00B3 Pa\u207b\u2070\u1427\u2075]", row=label_row) + label_row += 1 + + menu_row = label_row + self.add_text0(self.frame, text="Current File:", subscript="", row=menu_row) + # menu to choose which file to display + self.current_file = tk.StringVar(value='No files yet') + self.filemenu = tk.OptionMenu(self.frame, self.current_file, self.current_file.get()) + self.filemenu.grid(row=menu_row, column=1, columnspan=4, sticky='ew') + self.current_file.trace_add("write", self.generate_plots) + + # menu to choose which measurement (P, D, K) to display in bottom left graph + self.current_variable = tk.StringVar(value='Solubility') + self.add_text0(self.frame, text="Current Measurement:", subscript="", row=menu_row + 1) + self.PDK_menu = tk.OptionMenu(self.frame, self.current_variable, 'Solubility', 'Diffusivity', 'Permeability') + self.PDK_menu.grid(row=menu_row + 1, column=1, columnspan=4, sticky='ew') + self.current_variable.trace_add("write", self.update_PDK_plot) + + sidecol = 5 + self.add_text0(self.frame, text="Experiment Type:", subscript="", row=0, column=sidecol, sticky='w') + self.experiment_type_menu = tk.OptionMenu(self.frame, self.exp_type, *list(["Single", "Isotherm"])) + self.experiment_type_menu.config(bg="yellow") + self.experiment_type_menu.grid(row=1, column=sidecol, columnspan=4, + sticky="ew") + + self.add_text0(self.frame, text="Sample Molar Mass:", subscript="", row=2, column=sidecol, sticky="w") + mm_frame = tk.Frame(self.frame) # Frame for the molar mass + mm_frame.grid(row=3, column=sidecol, sticky="w") + self.add_entry(self, mm_frame, variable=self.inputs, key="molar_mass", text="", subscript="", + units="[g/mol]", tvar=self.molar_mass, row=1, + command=lambda tvar, variable, key: self.storage.check_for_number(tvar, variable, key)) + mm_frame.columnconfigure((0, 2), weight=1) + self.inputs['molar_mass'].config(width=10) + + self.npframe = tk.Frame(self.frame) + self.npframe.grid(row=4, column=sidecol, sticky="w") + self.add_text0(self.npframe, text="Number of Pressures:", subscript="", row=0, sticky="w") + FormatLabel(self.npframe, textvariable=self.p_num_label, borderwidth=1, relief="ridge", + ).grid(row=0, column=1, sticky="w") + tk.Label(self.npframe, text=" ").grid(row=0, column=2, sticky='w') # Give the label some right-side padding + self.npframe.columnconfigure(0, weight=1) + + button_row = 6 + ttk.Style(self).configure('Tight.TButton', width="") # Remove the extra space from the button + + self.b0 = ttk.Button(self.frame, text='Choose folder', command=self.select_file) + self.b0.grid(row=button_row, column=sidecol, sticky="w") + button_row += 1 + b1 = ttk.Button(self.frame, text='Refresh', command=self.refresh_graphs, style="Tight.TButton") + b1.grid(row=button_row, column=sidecol, sticky="w") + button_row += 1 + settings_b = ttk.Button(self.frame, text='Settings', command=self.adjust_persistent_vars, style="Tight.TButton") + settings_b.grid(row=button_row, column=sidecol, sticky="w") + button_row += 1 + self.coord_b = ttk.Button(self.frame, text="Enable Coordinates", command=self.toggle_coordinates) + self.coord_b.grid(row=button_row, column=sidecol, sticky="w") + button_row += 1 + b2 = ttk.Button(self.frame, text='Close popout plots', command=lambda: plt.close('all')) + b2.grid(row=button_row, column=sidecol, sticky="w") + button_row += 1 + b3 = ttk.Button(self.frame, text='Export to Excel', command=self.export_data) + b3.grid(row=button_row, column=sidecol, sticky="w") + button_row += 1 + b4 = ttk.Button(self.frame, text='Save current figures', command=self.save_figures) + b4.grid(row=button_row, column=sidecol, sticky="w") + button_row += 1 + + # make all the rows in the top left frame have the same size and change size at the same rate + self.frame.rowconfigure(tuple(range(menu_row + 2)), weight=1, uniform="row") + + # Configure how the columns squish so that only the text box columns squish + self.frame.columnconfigure((0, 2, 4, 5), weight=1) + # self.frame.columnconfigure(tuple(range(6)), weight=1) # option for squishing all columns instead + + # Store the "set_message" functions + self.message_function = {} + + # create bottom left plot + self.ax_title = "Solubility vs. Temperature" + self.ax_xlabel = " Temperature (\u00B0C) " # Space at the beginning and end is b/c on Mac, T & e are too close + self.ax_ylabel = "Solubility (mol m$^{-3}$ Pa$^{-0.5}$)" + self.fig, self.ax, self.canvas, self.toolbar = self.add_plot(self.bottom_frame, + xlabel=self.ax_xlabel, + ylabel=self.ax_ylabel, + title=self.ax_title, + row=0, column=0, axes=[.3, .15, .65, .75]) + # create top right plot + self.ax1_title = "Raw Data" + self.ax1_xlabel = "Time (s)" + self.ax1_ylabel = "Pressure (Pa)" + self.ax12_ylabel = " Temperature (\u00B0C) " # Space at the beginning & end is b/c on Mac, T & e are too close + self.fig1, self.ax1, self.canvas1, self.toolbar1 = self.add_plot(self.top_frame, + xlabel=self.ax1_xlabel, + ylabel=self.ax1_ylabel, + title=self.ax1_title, + row=0, column=1, rowspan=1) + self.ax12 = self.ax1.twinx() + self.ax12.set_ylabel(self.ax12_ylabel) + # The top right plot would flicker every time the order of magnitude of the cursor's location would change, so + # the following line changes the format of the displayed x & y coordinates. The specific format chosen was the + # result of trial and error in conjunction with editing the text of the entry boxes for e_tol and e_t_del + # (which are now accessed by a button). This is kept in case coordinates are changed to on by default again. + self.ax12.format_coord = lambda x, y: "({:3g}, ".format(x) + "{:3g})".format(y) + + # Create bottom middle plot + self.ax2_title = "Pressure-Composition-Temperature" + self.ax2_xlabel = "Composition (H/M)" + self.ax2_ylabel = "Pressure (Pa)" + self.fig2, self.ax2, self.canvas2, self.toolbar2 = self.add_plot(self.bottom_frame, + xlabel=self.ax2_xlabel, + ylabel=self.ax2_ylabel, + title=self.ax2_title, + row=0, column=1, axes=[.15, .15, .7, .75]) + + # Create bottom right plot + self.ax3_title = "Diffusivity Optimization Comparison" + self.ax3_xlabel = "Time (s)" + self.ax3_ylabel = r"$(~n_{\mathrm{t}} - n_{\mathrm{0}}~)~/~(~n_{\mathrm{inf}} - n_{\mathrm{0}}~)$" + self.fig3, self.ax3, self.canvas3, self.toolbar3 = self.add_plot(self.bottom_frame, + xlabel=self.ax3_xlabel, + ylabel=self.ax3_ylabel, + title=self.ax3_title, + row=0, column=2, axes=[.15, .15, .75, .75]) + + # Turn off coordinates to avoid layout="constrained" causing the plots to shift constantly + self.ax.format_coord = lambda x, y: '' + self.ax1.format_coord = lambda x, y: '' + self.ax12.format_coord = lambda x, y: '' + self.ax2.format_coord = lambda x, y: '' + self.ax3.format_coord = lambda x, y: '' + + # Counteracts a bug of layout="constrained" which causes four plots to be generated but not shown until a + # Popout Plot button is clicked: + plt.close("all") + + def toggle_coordinates(self): + """ Toggles between being able to see coordinates while hovering over plots """ + if self.coord_b.config('text')[-1] == 'Enable Coordinates': + # Turn on coordinates + self.ax.format_coord = lambda x, y: "({:3g}, ".format(x) + "{:3g})".format(y) + self.ax1.format_coord = lambda x, y: "({:3g}, ".format(x) + "{:3g})".format(y) + self.ax12.format_coord = lambda x, y: "({:3g}, ".format(x) + "{:3g})".format(y) + self.ax2.format_coord = lambda x, y: "({:3g}, ".format(x) + "{:3g})".format(y) + self.ax3.format_coord = lambda x, y: "({:3g}, ".format(x) + "{:3g})".format(y) + # Turn on status bar + self.toolbar.set_message = self.message_function[self.ax] + self.toolbar1.set_message = self.message_function[self.ax1] + self.toolbar2.set_message = self.message_function[self.ax2] + self.toolbar3.set_message = self.message_function[self.ax3] + self.coord_b.config(text='Disable Coordinates') # Toggle the text so next time, it goes to the "else" part + else: + # Turn off coordinates + self.ax.format_coord = lambda x, y: '' + self.ax1.format_coord = lambda x, y: '' + self.ax12.format_coord = lambda x, y: '' + self.ax2.format_coord = lambda x, y: '' + self.ax3.format_coord = lambda x, y: '' + # Turn off status bar + self.toolbar.set_message("") + self.toolbar1.set_message("") + self.toolbar2.set_message("") + self.toolbar3.set_message("") + self.toolbar.set_message = lambda s: "" + self.toolbar1.set_message = lambda s: "" + self.toolbar2.set_message = lambda s: "" + self.toolbar3.set_message = lambda s: "" + self.coord_b.config(text='Enable Coordinates') # Toggle the text so next time, it goes to the "if" part + self.canvas.draw() + self.toolbar.update() + + def update_function(self, tvar, var_type, key): + """ Checks if the user entry is a number (float or int). If not, revert the entered string to its prior state. + Wanted to have this functionality in the widgets class, but it wasn't working. + :param tvar: + :param var_type: + :param key: + :return: """ + # Check to make sure the entry is a float + try: + svar = float(self.inputs[key + var_type].get()) # string var for formatting + except ValueError: + # i.e. user typed a word or left it blank + tk.messagebox.showwarning("Invalid Entry", "Please enter a number.") + svar = tvar.get() # reset to what it was before the invalid entry was typed + + # update the entry to be nicely formatted and update the variable to be what was entered (or reset the variable) + if key in self.inputs.keys() and key+"_err" in self.inputs.keys(): + if var_type == "var": + self.inputs[key].delete(0, "end") + self.inputs[key].insert(0, "{:.2e}".format(svar)) + else: + self.inputs[key+"_err"].delete(0, "end") + self.inputs[key+"_err"].insert(0, "{:.2e}".format(svar)) + tvar.set(svar) + return True + + def add_plot(self, parent, xlabel='', ylabel='', title='', row=0, column=0, rowspan=1, axes=[.1, .15, .8, .75]): + """ Create a plot according to variables passed in (parent frame, x-label, y-label, title, row, and column). + axes are kept in case layout="constrained" has problems. """ + # location of main plot + frame = tk.Frame(parent) + frame.grid(row=row, column=column, rowspan=rowspan, sticky="nsew") + """ Note, the below line is to allow plots to behave better when not at optimal size. See details here: + https://matplotlib.org/stable/tutorials/intermediate/constrainedlayout_guide.html + The above website says the following as of 10/16/2021: + "Currently Constrained Layout is experimental. The behaviour and API are subject to change, + or the whole functionality may be removed without a deprecation period..." + As of 9/6/22, according to https://matplotlib.org/stable/api/prev_api_changes/api_changes_3.5.0.html there + was an update. This source says to use layout="constrained": https://matplotlib.org/stable/api/figure_api.html + """ + fig, ax = matplotlib.pyplot.subplots(layout="constrained") + # If something's wrong with layout="constrained", comment out the above line and uncomment the two lines below + # fig = Figure() + # ax = fig.add_axes(axes) # [left, bottom, width, height] + + # Configure plots + ax.tick_params(direction='in') + ax.set_xlabel(xlabel) + ax.set_ylabel(ylabel) + ax.set_title(title) + canvas = FigureCanvasTkAgg(fig, master=frame) + canvas.draw() + + toolbar = NavigationToolbar2Tk(canvas, frame) + # add button to pop out the plot + b = tk.Button(master=toolbar, text="Popout Plot", command=lambda x=ax: self.popout_plot(x)) + b.pack(side="left", padx=1) + + # Add a button that allows user input of various equilibrium variables + if title == 'Raw Data': + entry_frame = tk.Frame(toolbar) + entry_frame.pack(side="left", padx=1) + b = tk.Button(entry_frame, text='Equilibrium Variables', command=self.adjust_e_vars) + b.grid(row=0, column=4, sticky="w") + + # Store and turn off the capability to update the status bar + self.message_function[ax] = toolbar.set_message + toolbar.set_message = lambda s: "" + + toolbar.update() + canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=True) + + return fig, ax, canvas, toolbar + + def refresh_graphs(self, loading_warning=True): + """ Sets self.refreshing = True so that, when select_file is called, HyPAT doesn't ask for a directory. + Calls select_file, which reloads all the data using filenames previously uploaded and plots them """ + if self.loading_data and loading_warning: + proceed = tk.messagebox.askyesno(title="Loading Data", + message="Warning: Data is currently loading. Proceeding may cause " + + "unexpected or erroneous results. Do you wish to proceed?") + if not proceed: + return + self.refreshing = True + self.select_file() + + def select_file(self): + """ Facilitates loading data and plotting the absorption tab plots """ + if self.loading_data and not self.refreshing: + proceed = tk.messagebox.askyesno(title="Loading Data", + message="Warning: Data is currently loading. Proceeding may cause " + + "unexpected or erroneous results. Do you wish to proceed?") + if not proceed: + return + + # If select_file was called because the user is choosing a new folder (if self.refreshing=False), + # then ask them for a directory from which to get data. If not, set self.refreshing back to false and + # continue with the previously loaded data + if not self.refreshing: + # Save to a temp variable in case user cancels loading a new folder + temp_dir = askdirectory(initialdir=os.path.dirname(__file__)) + if temp_dir: + self.directory = temp_dir + else: + self.refreshing = False + temp_dir = True # temp_dir is truthy unless the user cancels loading the data + + if self.directory and temp_dir: + self.options = [file for file in os.listdir(self.directory) if (os.path.splitext(file)[1] == '.xls' or + os.path.splitext(file)[1] == '.xlsx')] + if not self.options: + tk.messagebox.showwarning(title="Missing Files", + message="Please select a folder with data in the XLS or XLSX format.") + return + + self.loading_data = True + try: + self.get_persistent_vars() # Loads variables from a data file for use in analysis + except Exception as e: # If there is a problem while trying to load the persistent variables + # If you want to see the traceback as part of the error text, change the below value to true + want_traceback = True + if want_traceback: + import traceback + full_traceback = "\n" + traceback.format_exc() + else: + full_traceback = "" + + showerror('Loading Error', 'Unknown Error while obtaining persistent variables. No files were' + + ' processed. The following exception was raised: "' + str(e) + '".' + + full_traceback + '\n\n') + self.loading_data = False + return + + self.error_texts = "" # Reset error_texts each time a folder is loaded + self.datafiles.clear() # Clear the dictionary to avoid old data points appearing in PDK plot + + # Create the progress bar + pb_len = len(self.options) + pb = LoadingScreen(self) + pb.add_progress_bar(pb_len) + + # loop through files, test to make sure they will work, then process them + i = 0 + i2 = 0 + while i < len(self.options): # While loop required so that files don't get skipped when a pop is needed + filename = self.options[i] + i += 1 + i2 += 1 + + # check if the file is good + status = self.check_file(os.path.join(self.directory, filename)) + + pname = "" # Text to be added to the filename when using isotherms as a delineator + # Process the data by making the various calculations + if status: + # Reset these variables for each loadable file + self.time_type = 0 + self.days = -1 + self.extra_time = 0 + + self.p_num[filename] = 1 # Default number of pressures for each file + pcount = 0 # Keep track of which pressure in an isotherm is being processed + old_pname = "" # Store filename without the pressure number delineator + # Keep processing data until all the pressures in the isotherm are processed + while pcount < self.p_num[filename]: + try: # Catch most other errors that can happen while processing the data + pcount += 1 + # Keep track of pressure number delineators + if self.exp_type.get() == "Isotherm": + pname = " p#{}".format(pcount) + old_pname = " p#{}".format(pcount - 1) + + # Load the data first time through; otherwise, truncate the old data + if pcount == 1: + self.datafiles[filename + pname] = \ + self.extract_data(os.path.join(self.directory, filename)) + self.options.pop(self.options.index(filename)) # Remove old filename from the options + else: # Takes only the data after t0 + self.datafiles[filename + pname] = \ + (self.datafiles[filename + old_pname])[self.t0[filename + old_pname]:] + # Ensure the index is properly labeled + self.datafiles[filename + pname].reset_index(drop=True, inplace=True) + i += 1 + + self.options.insert(i - 1, filename + pname) # Add in the properly named option + + # Find row number where the Isolation Valve is 1st opened after being closed (aka, the start + # of the experiment). Done in a try/except clause in case no opening is detected + try: + _t0 = np.where((self.datafiles[filename + pname])['Isolation Valve'] == 0)[0] + closed = np.where(np.diff(_t0) > 1)[0] + if not closed.any(): # if the isolation valve wasn't closed again after being opened... + # ...set t0 to be the data point after the last zero in the file + self.t0[filename + pname] = _t0[-1] + 1 + else: # else, set t0 to the data point after the last zero before the valve is opened + self.t0[filename + pname] = _t0[closed[0]] + 1 + + # If the user is submitting an isotherm, and either there are data points after the + # last data point where the valve is closed, or there more than one separate + # places where the valve is closed after being opened (meaning there are multiple + # places where the valve is open) + if self.exp_type.get() == "Isotherm" and \ + (len((self.datafiles[filename + pname])['Isolation Valve']) > _t0[-1] + 1 or + len(closed) > 1): + self.p_num[filename] += 1 + except IndexError: + self.error_texts += "Loading Error with file " + filename + pname + \ + ". Incorrect Isolation Valve format.\n\n" + self.datafiles.pop(filename + pname) + status = False + if status: # Ensure status hasn't changed since original definition + self.calculate_solubility(filename + pname, self.datafiles[filename + pname]) + self.calculate_diffusivity(filename + pname, self.datafiles[filename + pname]) + self.calculate_permeability(filename + pname, self.datafiles[filename + pname]) + except Exception as e: + # If you want to see the traceback as part of the error text, change the below value to true + want_traceback = True + if want_traceback: + import traceback + full_traceback = "\n" + traceback.format_exc() + else: + full_traceback = "" + + self.error_texts += "Unknown Error with file " + filename + pname + '. The following' + \ + ' exception was raised: "' + str(e) + '".' + full_traceback + '\n\n' + try: + self.datafiles.pop(filename + pname) + except KeyError: + self.error_texts += "Note: " + filename + pname + ' was never successfully loaded.' + \ + '\n\n' + status = False + break + if not status: # If something is wrong with the file or with analyzing the file + self.options.pop(self.options.index(filename + pname)) + i -= 1 + # Update the progress bar + try: + pb.update_progress_bar(100 // pb_len, i2) + except tk.TclError: # if the user closed the progress bar window + # clear out remaining files + while i < len(self.options): + self.options.pop(i) + tk.messagebox.showwarning("Loading Canceled", "Loading has been canceled.") + self.error_texts += "Warning: Not all files in selected folder were analyzed.\n\n" + break + pb.destroy() # Close the progress bar + + self.update_dataframe() + self.update_option_menu() + # If data has been loaded into self.troubleshooting_df, save it to an XLSX file for analysis + if len(self.troubleshooting_df.index) != 0: + # This dataframe can be generated anywhere in the processing and then turned into an XLSX here + self.troubleshooting_df.to_excel("Troubleshooting File.xlsx") + print("TROUBLESHOOTING FILE SAVED") + self.troubleshooting_df = pd.DataFrame() # Reset the dataframe after exporting + + + try: + self.current_file.set(self.options[0]) + except IndexError: + self.error_texts += "No files were able to be processed.\n\n" + self.current_file.set('No files yet') + + if self.error_texts: + # Create a new popup window + popup = tk.Tk() + popup.wm_title("Error Messages") + + from tkinter import font # Change the default font for text boxes to TkDefaultFont + + # Create a textbox with the error text in it + errortext = tk.Text(popup, font=font.nametofont("TkFixedFont"), + background=self.frame.cget("background"), padx=50, pady=50) + errortext.insert("insert", self.error_texts) + errortext.configure(state="disabled") # Turn off editing the text + errortext.grid(row=0, column=0, sticky="nsew", padx=2, pady=2) + + # Create a scrollbar for the text box + err_scrollbar = tk.Scrollbar(popup, command=errortext.yview) + err_scrollbar.grid(row=0, column=1, sticky="nsew") + errortext.configure(wrap=tk.WORD, yscrollcommand=err_scrollbar.set) + + self.loading_data = False + + def check_file(self, filename): + """ method to check Excel files for any problems, and to move on if there is an issue. + :param filename: + :return: """ + self.file_type = os.path.splitext(filename)[1] + # Try to load data from the file. If an error is thrown, return false + try: + if self.file_type == ".xls": + # This is faster for large files, but isn't working on .xlsx file formats + data = pd.read_csv(filename, sep='\t', header=None) + elif self.file_type == ".xlsx": + # openpyxl supports .xlsx file formats. According to documentation, engine=None should + # support xlsx and xls, but it doesn't seem to work for xls as of 11/10/2021 + data = pd.read_excel(filename, header=None, engine="openpyxl") + else: # This shouldn't ever get called if the program is working right + self.error_texts += "Loading Error with file " + filename + ". This file type, " + self.file_type + \ + ", is unsupported.\n\n" + return False + return True + except Exception as e: + self.error_texts += "Loading Error with file " + filename + ". HyPAT was unable to read the file." + \ + ' The following exception was raised: "' + str(e) + '".\n\n' + return False + + def update_option_menu(self): + """ update file selecting menu when data is loaded """ + menu = self.filemenu["menu"] + menu.delete(0, "end") + for string in self.options: + menu.add_command(label=string, command=lambda value=string: self.current_file.set(value)) + self.b0.config(text='Choose new folder') + + def update_dataframe(self): + """ Update the dataframe that is used for filling the Excel sheet and plotting the data in overview plots """ + # this will recreate the dataframe each time + df = pd.DataFrame() + for filename in self.options: + df = pd.concat([df, pd.DataFrame( + {"Gas Temperature [K]": self.Tg_e[filename], + "Gas Temperature Uncertainty [K]": self.Tg_e_err[filename], + "Sample Temperature [K]": self.Ts_e[filename], + "Sample Temperature Uncertainty [K]": self.Ts_e_err[filename], + "Pressure [Pa]": self.pr_e_avg[filename], + "Pressure Uncertainty [Pa]": self.pr_e_err[filename], + "Permeability [mol m^-1 s^-1 Pa^-0.5]": self.Phi[filename], + "Permeability Uncertainty [mol m^-1 s^-1 Pa^-0.5]": self.Phi_err[filename], + "Diffusivity [m^2 s^-1]": self.D[filename], + "Diffusivity Uncertainty [m^2 s^-1]": self.D_err[filename], + "Solubility [mol m^-3 Pa^-0.5]": self.Ks[filename], + "Solubility Uncertainty [mol m^-3 Pa^-0.5]": self.Ks_err[filename], + "Sample Composition [H/M]": self.HM[filename], + "Sample Composition Uncertainty [H/M]": self.HM_err[filename], + "Proportionality Constant A": self.A[filename], + "Additive Time Constant dt [s]": self.dt[filename] + }, index=[filename] + )]) + self.storage.TransportParameters = df + self.storage.ATransportParameters = df + + def export_data(self): + """ Loads dataframe containing the information for each material into an Excel sheet """ + if self.loading_data: + proceed = tk.messagebox.askyesno(title="Loading Data", + message="Warning: Data is currently loading. Proceeding may cause " + + "unexpected or erroneous results. Do you wish to proceed?") + if not proceed: + return + + if self.current_file.get() != "No files yet": + + save_filename = asksaveasfilename(initialdir=os.path.dirname(__file__), initialfile="test", + defaultextension="*.xlsx") + if save_filename != '': + self.storage.ATransportParameters.to_excel(save_filename) + + else: + showerror(message='Please load data first.') + + def l2n(self, col): + """ Take Excel column names (such as J or AR) and return which column number they are (0-indexed)""" + col_num = 0 + for i, l in enumerate(reversed(col)): + col_num += (ord(l) - 64) * (26 ** i) + return col_num - 1 + + def unit_conversion(self, value, multi, add): # todo This seems to do things cell by cell. Maybe find another way? + """ Returns the linearly adjusted input according to multi and add """ + if value == "": + return float("NaN") + try: + new_val = float(value) * multi + add + except ValueError: + self.error_texts += "Data Extraction Warning. The following was submitted to HyPAT for conversion" + \ + ' into correct units: "' + str(value) + '". This triggered a ValueError. The' + \ + " dataframe location corresponding to this term has been set to NaN. No further" \ + " information is available.\n\n" + return float("NaN") + return new_val + + def get_seconds(self, date_time): + """ Converts a datetime variable into seconds (excluding the date) """ + if self.days == -1: # Equals -1 if this is the first term in the column being loaded. Resets with each file + self.this_date_time = date_time + self.init_time = date_time.hour * 3600 + date_time.minute * 60 + date_time.second + \ + date_time.microsecond * 10 ** -6 + last_date_time = self.this_date_time + self.this_date_time = date_time + try: + # Check to see if the day has changed between one cell and the next + self.days = round(date_time.day - last_date_time.day, 5) # round to avoid precision errors + # If the day has changed, store the time that has passed in a new variable to be added to the new time + if self.days != 0: + self.extra_time += 86400 + value = date_time.hour * 3600 + date_time.minute * 60 + date_time.second + date_time.microsecond * 10 ** -6 + \ + self.extra_time - self.init_time + except AttributeError: + # This is expected if the cell is blank or otherwise not a datetime. NaNs sometimes trigger other errors + # that HyPAT is more ready to handle than this AttributeError + value = float("NaN") + return value + + def convert_time(self, time_term, multi, add): + """ Checks to see if the time-instrument's data is formatted in datetime or cumulative time passed and sets the + converter function accordingly """ + if time_term == "": + return float("NaN") + if self.time_type == 0: # Resets to zero before each file is loaded + try: + dummy = float(time_term) + self.time_type = 1 + except TypeError: + # TypeError is expected if time_term is a datetime + self.time_type = 2 + except ValueError: + self.error_texts += "Data Extraction Warning. The following was submitted to HyPAT for conversion" + \ + ' into seconds: "' + str(time_term) + '". This triggered a ValueError.' + \ + " The dataframe location corresponding to this term has been set to NaN." + \ + " No further information is available.\n\n" + return float("NaN") + if self.time_type == 1: + # If the time is in cumulative time passed format, do a linear transform according to multi and add + return self.unit_conversion(time_term, multi, add) + elif self.time_type == 2: + # If the time is in datetime format, convert to cumulative seconds passed + return self.get_seconds(time_term) + + def get_persistent_vars(self): + """ Read data from persistent_absorption_input_variables.xlsx. This data is critical to processing the + data files loaded in by the user. """ + # Open up the persistent variable file for reading + pv_filename = os.path.join('data_files', 'persistent_absorption_input_variables.xlsx') + pv_wb = openpyxl.load_workbook(pv_filename) + + self.num_GasT0 = pv_wb['Numbers']['C2'].value # Number of TCs measuring GasT initial + self.num_GasT = pv_wb['Numbers']['C3'].value # Number of TCs measuring GasT after isolation valve is opened + + # Reset lists in case this is the second time they are loaded + self.header = [] + self.needed_cols = [] + # Start the lists/dicts with time + self.header.append('t') # Generic name for data from this instrument + self.needed_cols.append(self.l2n(pv_wb['MiscInfo']['A2'].value)) # Number corresponding to which column this instrument's data is in + self.converters_dict[self.needed_cols[-1]] = \ + lambda input, multi=pv_wb['MiscInfo']['A3'].value, add=pv_wb['MiscInfo']['A4'].value: \ + self.convert_time(input, multi, add) # Function to convert the instrument's data to correct units + + # Read data for each instrument that measures GasT into a dataframe, then save that info in convenient locations + GasT_info = pd.read_excel(pv_filename, sheet_name="GasT", header=0) + for GasT_inst in GasT_info.keys(): # Each column header corresponds with a TC measuring GasT + self.header.append(GasT_inst) # Generic name for the instrument + self.needed_cols.append(self.l2n(GasT_info[GasT_inst][0])) # Column number + self.converters_dict[self.needed_cols[-1]] = \ + lambda input, multi=GasT_info[GasT_inst][1], add=GasT_info[GasT_inst][2]: \ + self.unit_conversion(input, multi, add) # Function for unit conversion + self.GasT_cerr[GasT_inst] = GasT_info[GasT_inst][3] # constant uncertainty + self.GasT_perr[GasT_inst] = GasT_info[GasT_inst][4] # proportional uncertainty + + # Repeat the above for other variables + # Sample temperature + SampT_info = pd.read_excel(pv_filename, sheet_name="SampT", header=0) + for SampT_inst in SampT_info.keys(): + self.header.append(SampT_inst) + self.needed_cols.append(self.l2n(SampT_info[SampT_inst][0])) + self.converters_dict[self.needed_cols[-1]] = \ + lambda input, multi=SampT_info[SampT_inst][1], add=SampT_info[SampT_inst][2]: \ + self.unit_conversion(input, multi, add) + self.SampT_cerr[SampT_inst] = SampT_info[SampT_inst][3] + self.SampT_perr[SampT_inst] = SampT_info[SampT_inst][4] + + # Pressure + Pres_info = pd.read_excel(pv_filename, sheet_name="Pres", header=0) + for Pres_inst in Pres_info.keys(): + self.header.append(Pres_inst) + self.needed_cols.append(self.l2n(Pres_info[Pres_inst][0])) + self.converters_dict[self.needed_cols[-1]] = \ + lambda input, multi=Pres_info[Pres_inst][1], add=Pres_info[Pres_inst][2]: \ + self.unit_conversion(input, multi, add) + self.Pres_cerr[Pres_inst] = Pres_info[Pres_inst][3] + self.Pres_perr[Pres_inst] = Pres_info[Pres_inst][4] + + # Isolation Valve shouldn't need more than one instrument, so don't need a for loop. Also don't need uncertainty + self.header.append("Isolation Valve") + self.needed_cols.append(self.l2n(pv_wb['MiscInfo']['B2'].value)) + self.converters_dict[self.needed_cols[-1]] = \ + lambda input, multi=pv_wb['MiscInfo']['B3'].value, add=pv_wb['MiscInfo']['B4'].value: \ + round(self.unit_conversion(input, multi, add)) + + # Variable for which row in Excel sheet to start obtaining data from (0-indexed) + self.starting_row = pv_wb['MiscInfo']['C2'].value + # Variable for how many rows at the end of the Excel sheet to not read + self.footer_rows = pv_wb['MiscInfo']['D2'].value + + def n_of_cols(self, filename): + """ Given a .xls or .xlsx filename, return the number of columns in that file""" + if self.file_type == ".xls": + # This is faster for large files, but isn't working on .xlsx file formats + df = pd.read_csv(filename, sep='\t', header=None, skiprows=self.starting_row, skipfooter=self.footer_rows) + else: # assume .xlsx file + # openpyxl supports .xlsx file formats. According to documentation, engine=None should + # support xlsx and xls, but it doesn't seem to work for xls as of 11/10/2021 + df = pd.read_excel(filename, header=None, engine="openpyxl", skiprows=self.starting_row, + skipfooter=self.footer_rows) + return len(df.columns) + + def extract_data(self, filename): + """ Extract data from given file """ + # Attempt to extract data from the file. If it fails because of a ValueError or IndexError, enter into a while + # loop that can help the user fix the problem so the program can proceed + try: + if self.file_type == ".xls": + # This is faster for large files, but isn't working on .xlsx file formats + Data = pd.read_csv(filename, sep='\t', header=None, usecols=self.needed_cols, + converters=self.converters_dict, skiprows=self.starting_row, + skipfooter=self.footer_rows, engine='python') + else: # assume .xlsx file + # openpyxl supports .xlsx Excel file formats. According to documentation, engine=None should + # support xlsx and xls, but it doesn't seem to work for xls as of 11/10/2021 + Data = pd.read_excel(filename, header=None, engine="openpyxl", usecols=self.needed_cols, + converters=self.converters_dict, skiprows=self.starting_row, + skipfooter=self.footer_rows) + # https://stackoverflow.com/questions/6618515/sorting-list-based-on-values-from-another-list + new_header = [x for _, x in sorted(zip(self.needed_cols, self.header))] + Data.columns = new_header + except (ValueError, IndexError): + # todo When there's a column name that references a column not in the data sheet, the following appears: + # FutureWarning: Defining usecols with out of bounds indices is deprecated and will raise a ParserError in a future version. + # return self._reader.parse( + # Unfortunately, ParserError doesn't seem to be recognized as an exception right now (late April / + # early May 2022), so this will need to be delt with later + + # Next line fixes some IndexErrors that occur when loading XLSX files if you give an incorrect column name + # for the isolation valve or if you give a column name referencing a column outside the datasheet + self.converters_dict.clear() + new_pvars = False # Whether new persistent variables (and self.converters_dict) have been obtained + + num_of_cols = self.n_of_cols(filename) + while len(set(self.needed_cols)) < len(self.needed_cols) or \ + not all(x < num_of_cols for x in self.needed_cols): + # check to make sure all values in the list are less than the length of the Excel sheet + # https://www.geeksforgeeks.org/python-check-if-all-the-values-in-a-list-are-less-than-a-given-value/ + if not all(x < num_of_cols for x in self.needed_cols): + tk.messagebox.showwarning("Data Reading Error", + "Please ensure all instruments have column names that correspond to " + + "columns in the Excel sheet.") + self.adjust_persistent_vars(retry=True) # gives user a chance to fix the column names + new_pvars = True + + # check to make sure every column name is unique + if len(set(self.needed_cols)) < len(self.needed_cols): + tk.messagebox.showwarning("Data Reading Error", + "Please ensure all instruments have unique column names") + self.adjust_persistent_vars(retry=True) # gives user a chance to fix the column names + new_pvars = True + + # Make sure self.converters_dict is filled since it was cleared above + if not new_pvars: + self.get_persistent_vars() + + # Now the errors are handled, extract the data from the Excel file and load it into a dataframe + if self.file_type == ".xls": + Data = pd.read_csv(filename, sep='\t', header=None, usecols=self.needed_cols, + converters=self.converters_dict, skiprows=self.starting_row, + skipfooter=self.footer_rows, engine='python') + else: # assume .xlsx file + Data = pd.read_excel(filename, header=None, engine="openpyxl", usecols=self.needed_cols, + converters=self.converters_dict, skiprows=self.starting_row, + skipfooter=self.footer_rows) + # https://stackoverflow.com/questions/6618515/sorting-list-based-on-values-from-another-list + new_header = [x for _, x in sorted(zip(self.needed_cols, self.header))] + Data.columns = new_header + + n = len(Data) + # calculate numerical derivative of pressure + deriv = np.zeros(n) + for i in range(n - 1): + deriv[i] = ((Data.loc[i + 1, 'Pres'] - Data.loc[i, 'Pres']) / + (Data.loc[i + 1, 't'] - Data.loc[i, 't'])) + Data['dPres'] = deriv.tolist() + # rearrange last two columns so 'Pres' and 'dPres' are adjacent + cols = Data.columns.tolist() + cols = cols[:-2] + [cols[-1]] + [cols[-2]] + Data = Data[cols] + return Data + + def adjust_persistent_vars(self, retry=False): + """ Function for calling the window for adjusting persistent variables for loading absorption data, + then update everything if something was changed """ + loading_warn = True + if self.loading_data and not retry: + proceed = tk.messagebox.askyesno(title="Loading Data", + message="Warning: Data is currently loading. Proceeding may cause " + + "unexpected or erroneous results. Do you wish to proceed?") + if not proceed: + return + else: + loading_warn = False + + self.update() + # get location of gui + x = self.winfo_rootx() + y = self.winfo_rooty() + # Call the "Adjust Persistent Variables" window and, if something was changed, refresh graphs and variables + d = AdjustPVars(storage=self.storage, pos=(x, y)).show_pv_window() + if d and not retry: + self.refresh_graphs(loading_warning=loading_warn) # if a warning has already been given don't give it again + # retry = True if adjust_persistent_vars is called but only the persistent variables need to be updated + # (as opposed to updating the graphs as well) + if d and retry: + self.get_persistent_vars() # Loads data from the absorption input file-format file + + def adjust_e_vars(self): + """ Function for calling the window for adjusting equilibrium variables for loading absorption data, + then update everything if desired """ + popup = tk.Toplevel(self) + popup.wm_title("Adjust Equilibrium Variables") + + # Place the popup window near the cursor + pos_right = self.winfo_pointerx() + pos_down = self.winfo_pointery() + popup.geometry("+{}+{}".format(pos_right, pos_down)) + + entry_frame = tk.Frame(popup) + entry_frame.pack(side="left", padx=1) + # Tolerance for determining equilibrium + self.add_entry(popup, entry_frame, self.inputs, key="e_tol", + text="Tolerance for Equilibrium:", subscript='', units="Pa s\u207b\u00b2", + tvar=self.e_tol, formatting=True, ent_w=8, in_window=True, + command=lambda tvar, variable, key, formatting, pf: + self.storage.check_for_number(tvar, variable, key, formatting, pf)) + # Minimum time to let pass after t0 before checking for equilibrium + self.add_entry(popup, entry_frame, self.inputs, key="e_t_del", + text="Delay until Equilibrium:", subscript='', units="s", + tvar=self.e_t_del, ent_w=8, row=1, in_window=True, + command=lambda tvar, variable, key, pf: + self.storage.check_for_number(tvar, variable, key, False, pf)) # "false" to say no formatting + # Number of data points used in determining the initial values + self.add_entry(popup, entry_frame, self.inputs, key="i_range", + text="Initial Values Range:", subscript='', units="data points", + tvar=self.gen_i_range, ent_w=8, row=3, in_window=True, + command=lambda tvar, variable, key, pf: + self.storage.check_for_number(tvar, variable, key, False, pf)) + # Number of data points used in determining the equilibrium and max time and final values + self.add_entry(popup, entry_frame, self.inputs, key="e_range", + text="Equilibrium Range:", subscript='', units="data points", + tvar=self.gen_e_range, ent_w=8, row=4, in_window=True, + command=lambda tvar, variable, key, pf: + self.storage.check_for_number(tvar, variable, key, False, pf)) + + button_row = 5 + b1 = ttk.Button(entry_frame, text='Close & Refresh', command=lambda: self.close_and_refresh(popup)) + b1.grid(row=button_row, column=0, sticky="ew") + b2 = ttk.Button(entry_frame, text='Refresh', command=self.refresh_graphs) + b2.grid(row=button_row, column=2, sticky="ew") + + def close_and_refresh(self, win): + """ Accepts window argument, then closes that window after refreshing the graphs of the Absorption Plots tab """ + self.refresh_graphs() + win.destroy() + + def calculate_solubility(self, filename, data): + """ Calculate Solubility using loaded data """ + R = self.storage.R * 1000 # J/mol K + Vs = self.Vs_cm3.get() * 10 ** (-6) # m^3 + Vs_err = self.Vs_cm3_err.get() * 10 ** (-6) # m^3 + V_ic = self.Vic_cm3.get() * 10 ** (-6) # m^3 + V_ic_err = self.Vic_cm3_err.get() * 10 ** (-6) # m^3 + V_sc = self.Vsc_cm3.get() * 10 ** (-6) - Vs # m^3 Note the correction for the sample's volume + V_sc_err = np.sqrt((self.Vsc_cm3_err.get() * 10 ** (-6)) ** 2 + Vs_err ** 2) # m^3 + V_totc = V_ic + V_sc # m^3, total container + V_totc_err = np.sqrt(V_ic_err ** 2 + V_sc_err ** 2) # m^3 + + # Set the init range to the general init range (which is editable by the user) or to something that works better + if self.gen_i_range.get() < self.t0[filename]: + self.i_range[filename] = self.gen_i_range.get() + else: + self.i_range[filename] = self.t0[filename] - 1 + self.error_texts += "Initial Range Warning with file " + filename + ". The user-input initial range" + \ + " exceeded the limit of " + str(self.t0[filename]) + " data points. Initial range" + \ + " set to " + str(self.i_range[filename]) + " data points.\n\n" + # Set the eq range to the general eq range (which is editable by the user) or to something that works better + if self.gen_e_range.get() < len(data['Pres']) - self.t0[filename]: + self.e_range[filename] = self.gen_e_range.get() + else: + self.e_range[filename] = len(data['Pres']) - self.t0[filename] - 2 + self.error_texts += "Equilibrium Warning with file " + filename + ". The user-input equilibrium" + \ + " range exceeded the limit of " + str(len(data['Pres']) - self.t0[filename]) + \ + " data points. Equilibrium range set to " + str(self.e_range[filename]) + \ + " data points.\n\n" + + # determine row number when equilibrium pressure is achieved, checking to ensure it is before the end of + # the file and after the minimum time delay + t0 + dPres = data['dPres'].rolling(window=self.e_range[filename], center=True, min_periods=1).mean() + # List of all terms in abs(dPres) which are more than 5e-3 (allowing for a slight change during equilibrium) + nonzero_dPres = np.where(abs(dPres) > 5E-3)[0] + # Minimum terms in a row required to determine if the pressure drop off or other big change was reached + min_seq = max(self.e_range[filename] // 2 - 1, 1) + # variable to store the last point before experiment ends (and/or pressure changes and/or isolation valve opens) + e_time_max = len(dPres) + + # Check for valve being closed after it was opened and set the max data point accordingly + IVclosed = np.where(((self.datafiles[filename])['Isolation Valve'])[self.t0[filename]:] == 0)[0] + if IVclosed.any(): + e_time_max = (IVclosed[0] + self.t0[filename]) + + past_change = False + # Find where the pressure changes because a valve was opened or fail to find such a point + for count, value in enumerate(nonzero_dPres[:len(nonzero_dPres) - min_seq]): + if e_time_max > value > (self.t0[filename]): # if the value is in the range for looking for equilibrium, + # ...check to see if we are past the initial drop in pressure + if np.diff(nonzero_dPres[count:count + 2]) > 2 and not past_change: + past_change = True + # ...check for a sequence post the initial drop that is min_seq long in which abs(dPres) is >5e-3 + if sum(np.diff(nonzero_dPres[count:count + min_seq])) <= min_seq and past_change: + e_time_max = nonzero_dPres[count] # set the max time to when the pressure changes + break + + e_del = np.where(data.loc[self.t0[filename]:, 't'] > self.e_t_del.get() + data.loc[self.t0[filename], 't'] + + 0.000001)[0] # Added .000001 to ensure precision errors don't lead to e_del being 0 + try: + # Set the time delay to be the minimum number of data points away from t0 that still is further than + # the user's input time delay + e_del = e_del[0] - 1 # -1 to compensate for the > sign in e_del's def and in later checks + except IndexError: # This is expected if e_del is empty, generally because self.e_t_del is too long in some way + # Set e_del to the largest delay that still works + e_del = e_time_max - (self.e_range[filename] + self.t0[filename] + 2) + self.error_texts += "Equilibrium Warning with file " + filename + ". The user-input time delay" + \ + " exceeded the limit of " + \ + str(round(data.loc[e_time_max - (self.e_range[filename] + 2), 't'] - + data.loc[self.t0[filename], 't'], 3)) + " s. Time delay set to " + \ + str(round(data.loc[e_del + self.t0[filename], 't'] - + data.loc[self.t0[filename], 't'], 3)) + " s.\n\n" + if e_del > e_time_max - (self.e_range[filename] + self.t0[filename] + 2): + # In case e_del produces a usable data point but that data point is past an abrupt downturn in pressure or + # is past the valve closing, + e_del = e_time_max - (self.e_range[filename] + self.t0[filename] + 2) + self.error_texts += "Equilibrium Warning with file " + filename + ". The user-input time delay" + \ + " exceeded the limit of " + \ + str(round(data.loc[e_time_max - (self.e_range[filename] + 2), 't'] - + data.loc[self.t0[filename], 't'], 3)) + " s. Time delay set to " + \ + str(round(data.loc[e_del + self.t0[filename], 't'] - + data.loc[self.t0[filename], 't'], 3)) + " s.\n\n" + # Find the data point at which an equilibrium is reached + ddPres = pd.Series((dPres.loc[2:].to_numpy() - dPres.loc[:len(dPres) - 3].to_numpy()) / + (data.loc[2:, 't'].to_numpy() - data.loc[:len(dPres) - 3, 't'].to_numpy())) + ddPres = ddPres.rolling(window=self.e_range[filename], center=True, min_periods=1).mean() + zeros = np.where(abs(ddPres) < self.e_tol.get())[0] # Find points where ddPres is approximately zero + zeros = [z for z in zeros if self.t0[filename] + e_del < z < e_time_max - min_seq - 2] + # If no equilibrium was found with the user-input tolerance, loop until find an equilibrium using new_e_tol + new_e_tol = self.e_tol.get() + while not zeros and new_e_tol < self.e_tol.get() * 100000: + new_e_tol = float("{:.2e}".format(new_e_tol * 5)) + zeros = np.where(abs(ddPres) < new_e_tol)[0] + zeros = [z for z in zeros if self.t0[filename] + e_del < z < e_time_max - min_seq - 2] + if new_e_tol != self.e_tol.get() and zeros: + self.error_texts += "Equilibrium Warning with file " + filename + ". HyPAT was unable to find an" + \ + " equilibrium using the user-input tolerance " + str(self.e_tol.get()) + \ + ". New tolerance was set to {:.2e}".format(new_e_tol) + \ + ", which successfully determined a time for the beginning of the equilibrium.\n\n" + elif new_e_tol != self.e_tol.get(): + zeros = [min(self.t0[filename] + e_del + 1, e_time_max - min_seq - 3)] # Added a +1 because e_del can be zero now, so this forces t_e != t0 + self.error_texts += "Equilibrium Error with file " + filename + \ + ". HyPAT was unable to find equilibrium using the user-input tolerance " + \ + str(self.e_tol.get()) + " or the computationally set tolerance " + \ + "{:.2e}".format(new_e_tol) + ". Equilibrium time was set to " + \ + str(round(data.loc[zeros[0], 't'], 3)) + \ + " s, which is equal to either the starting time plus the user-input" + \ + " time delay until equilibrium or the maximum usable time, whichever is lower.\n\n" + self.t_e[filename] = zeros[0] + + # Get the initial gas temperature using an average of arbitrarily many TCs + Tg_ivals = pd.DataFrame() + for i in range(1, self.num_GasT0 + 1): + Tg_ivals['GasT' + str(i)] = data.loc[:, 'GasT' + str(i)] + Tg_imean = Tg_ivals.mean(axis=1) + self.Tg0[filename] = Tg_imean.loc[self.t0[filename] - self.i_range[filename]:self.t0[filename] - 1].mean() + \ + self.storage.standard_temp # K + # Get the equilibrium gas temperature using an average of arbitrarily many TCs + Tg_fvals = pd.DataFrame() + for i in range(1, self.num_GasT + 1): + Tg_fvals['GasT' + str(i)] = data.loc[:, 'GasT' + str(i)] + Tg_fmean = Tg_fvals.mean(axis=1) + self.Tg_e[filename] = Tg_fmean.loc[self.t_e[filename] + 1:self.t_e[filename] + + self.e_range[filename]].mean() + self.storage.standard_temp # K + + # calculate average pressures + self.pr0_avg[filename] = \ + data.loc[self.t0[filename] - self.i_range[filename]:self.t0[filename] - 1, 'Pres'].mean() + self.pr_e_avg[filename] = \ + data.loc[self.t_e[filename] + 1: self.t_e[filename] + self.e_range[filename], 'Pres'].mean() + + # Obtain values from previous pressure for calculations + if self.exp_type.get() == "Isotherm" and filename[-1] != "1": + # Get the filename of the previous pressure + pressure_number = str(int(filename[-1]) - 1) + last_filename = filename[:-1] + pressure_number + # Get info on number of moles absorbed in the sample previously + self.ns0[filename] = self.ns_e[last_filename] # Initial number of moles in sample at t0 + self.ns0_err[filename] = self.ns_e_err[last_filename] + # Calculate number of moles in sample container when isolation valve is closed + pr_sc0 = self.pr_e_avg[last_filename] # Initial pressure in sample container + pr_sc0_err = self.pr_e_err[last_filename] + T_sc0 = self.Tg_e[last_filename] # Initial temperature in sample container + T_sc0_err = self.Tg_e_err[last_filename] + n_sc0 = pr_sc0 * V_sc / R / T_sc0 # Initial number of moles in sample container + n_sc0_err = abs(n_sc0) * np.sqrt((pr_sc0_err / pr_sc0) ** 2 + (T_sc0_err / T_sc0) ** 2 + + (V_sc_err / V_sc) ** 2) + else: + # Assume there weren't previous experiments + self.ns0[filename] = 0 # Initial number of moles in sample + self.ns0_err[filename] = 0 + n_sc0 = 0 # Initial number of moles in sample container + n_sc0_err = 0 + + # calculate moles + n_ci0 = self.pr0_avg[filename] * V_ic / R / self.Tg0[filename] # Initial number of moles in initial container + nc0 = n_ci0 + n_sc0 # Total number of moles at start + nc_e = self.pr_e_avg[filename] * V_totc / R / self.Tg_e[filename] + self.ns_e[filename] = nc0 - nc_e + self.ns0[filename] + + # Pressure as a function of time (Pa) + pr_t = data.loc[self.t0[filename] + 1:self.t_e[filename] + self.e_range[filename], 'Pres'].to_numpy() + # Moles in the sample as a function of time (mol) + self.ns_t[filename] = nc0 - pr_t * V_totc / R / \ + (Tg_fmean.loc[self.t0[filename] + 1:self.t_e[filename] + self.e_range[filename]].to_numpy() + + self.storage.standard_temp) + self.ns0[filename] + + # calculate solubility + self.Ks[filename] = self.ns_e[filename] / Vs / np.sqrt(self.pr_e_avg[filename]) + + # Calculate the uncertainty in solubility + + # Find uncertainty of Tg0 when there are arbitrarily many TCs + Tg0_errs = [] + for i in range(1, self.num_GasT0 + 1): + Tg0_errs.append((max(data.loc[self.t0[filename] - self.i_range[filename]:self.t0[filename] - 1, 'GasT' + str(i)].std(), + self.GasT_cerr['GasT' + str(i)], + (self.Tg0[filename] - self.storage.standard_temp) * + self.GasT_perr['GasT' + str(i)] / 100)) ** 2) + Tg0_err = np.sqrt(sum(Tg0_errs)) / self.num_GasT0 + # Find uncertainty of Tg_e when there are arbitrarily many TCs + Tge_errs = [] + for i in range(1, self.num_GasT + 1): + Tge_errs.append((max(data.loc[self.t_e[filename] + 1:self.t_e[filename] + self.e_range[filename], 'GasT' + str(i)].std(), + self.GasT_cerr['GasT' + str(i)], + (self.Tg_e[filename] - self.storage.standard_temp) * + self.GasT_perr['GasT' + str(i)] / 100)) ** 2) + Tge_err = np.sqrt(sum(Tge_errs)) / self.num_GasT + + # Pressure uncertainties + Pres0_err = max(data.loc[self.t0[filename] - self.i_range[filename]:self.t0[filename] - 1, 'Pres'].std(), + self.Pres_cerr["Pres"], self.pr0_avg[filename] * self.Pres_perr["Pres"] / 100) + Pres_e_err = max(data.loc[self.t_e[filename] + 1:self.t_e[filename] + self.e_range[filename], 'Pres'].std(), + self.Pres_cerr["Pres"], self.pr_e_avg[filename] * self.Pres_perr["Pres"] / 100) + # Moles uncertainties + n_ci0_err = abs(nc0) * np.sqrt((Pres0_err / self.pr0_avg[filename]) ** 2 + (V_ic_err / V_ic) ** 2 + + (Tg0_err / self.Tg0[filename]) ** 2) + nc0_err = np.sqrt(n_ci0_err ** 2 + n_sc0_err ** 2) + nce_err = abs(nc_e) * np.sqrt((Pres_e_err / self.pr_e_avg[filename]) ** 2 + (V_totc_err / V_totc) ** 2 + + (Tge_err / self.Tg_e[filename]) ** 2) + nse_err = np.sqrt(nc0_err ** 2 + nce_err ** 2 + self.ns0_err[filename] ** 2) + + # Solubility uncertainty + self.Ks_err[filename] = abs(self.Ks[filename]) * np.sqrt( + (nse_err / self.ns_e[filename]) ** 2 + (Vs_err / Vs) ** 2 + (Pres_e_err / 2 / self.pr_e_avg[filename]) ** 2) + + # Uncomment to have a data file created at the end of processing. You can also change this df to match your need + # self.troubleshooting_df = pd.concat([self.troubleshooting_df, pd.DataFrame( + # {"index": filename[-1], + # "Ks": self.Ks[filename], + # "Ks_err": self.Ks_err[filename], + # "Ks_err/Ks": self.Ks_err[filename]/self.Ks[filename], + # "nse": self.ns_e[filename], + # "nse_err": nse_err, + # "nc0": nc0, + # "nc0_err**2": nc0_err**2, + # "nc_e": nc_e, + # "nce_err**2": nce_err**2, + # "pr0_avg": self.pr0_avg[filename], + # "Pres0_err": Pres0_err, + # "(Pres0_err/pr0_avg) ** 2": (Pres0_err / self.pr0_avg[filename]) ** 2, + # "pr_e_avg": self.pr_e_avg[filename], + # "Pres_e_err": Pres_e_err, + # "(Pres_e_err/pr_e_avg) ** 2": (Pres_e_err / self.pr_e_avg[filename]) ** 2 + # }, index=[filename] + # )]) + + # Store uncertainties + self.ns_e_err[filename] = nse_err + self.Tg_e_err[filename] = Tge_err + self.pr_e_err[filename] = Pres_e_err + + # Data for pressure vs. composition graph + NA = self.storage.Na # number of atoms per mole, the Avogadro constant + a_num = self.ms_g.get() * NA / self.molar_mass.get() # number of atoms of metal + H_num = 2 * self.ns_e[filename] * NA # Number of Hydrogen atoms in the sample + self.HM[filename] = H_num / a_num # Hydrogen to metal atoms ratio + self.HM_err[filename] = abs(self.HM[filename]) * np.sqrt((self.ms_g_err.get()/self.ms_g.get()) ** 2 + + (nse_err / self.ns_e[filename]) ** 2) + + # Uncomment to have a data file created at the end of processing. You can also change this df to match your need + # self.troubleshooting_df = pd.concat( + # [self.troubleshooting_df, pd.DataFrame( + # {"index": filename[-1], + # "HM": self.HM[filename], + # "HM_err": self.HM_err[filename], + # "HM_err/HM": self.HM_err[filename] / self.HM[filename], + # "ns_e": self.ns_e[filename], + # "nse_err": nse_err, + # "(nse_err/ns_e)**2": (nse_err / self.ns_e[filename]) ** 2, + # }, index=[filename] + # )]) + + # Get temp from sample TC + self.Ts0[filename] = data.loc[self.t0[filename] - self.i_range[filename]:self.t0[filename] - 1, 'SampT'].mean() + \ + self.storage.standard_temp + self.Ts_e[filename] = data.loc[self.t_e[filename] + 1:self.t_e[filename] + self.e_range[filename], 'SampT'].mean() + \ + self.storage.standard_temp + self.Ts_e_err[filename] = max(data.loc[self.t_e[filename] + 1:self.t_e[filename] + self.e_range[filename], 'SampT'].std(), + self.SampT_cerr["SampT"], self.Ts_e[filename] * self.SampT_perr["SampT"] / 100) + + def calculate_diffusivity(self, filename, data): + """ Calculate Diffusivity using loaded data """ + debug = False + + # Prepare for analysis + sl = self.sample_thickness.get() * 0.001 # converted to meters + sl_err = self.sample_thickness_err.get() * 0.001 # converted to meters # todo Find a way to use this + self.D_time[filename] = data.loc[self.t0[filename] + 1:self.t_e[filename] + self.e_range[filename], 't'] - \ + data.loc[self.t0[filename], 't'] + h = data.loc[1, 't'] - data.loc[0, 't'] + + # Prepare data for fitting + self.lhs[filename] = (self.ns_t[filename] - self.ns0[filename]) / (self.ns_e[filename] - self.ns0[filename]) + + # Calculate D without fitting for an initial guess. Source: Crank, pg238-239 + halfway_point = int(np.argmin(abs(self.lhs[filename] - 0.5))) + if halfway_point == 1: + self.error_texts += "Curve Fit Warning with file " + filename + \ + ". Half of the total absorbed hydrogen was absorbed after roughly one time step" + \ + ". The time step may be too large to calculate the diffusivity accurately.\n\n" + elif halfway_point < 1: + halfway_point = 1 + self.error_texts += "Curve Fit Warning with file " + filename + \ + ". Half of the total absorbed hydrogen was absorbed after less than one time step" + \ + ". The time step may be too large to calculate the diffusivity accurately.\n\n" + halfway_t = self.D_time[filename][halfway_point + self.t0[filename] + 1] # +t0+1 because of indexing + prop_const = -np.log(np.pi ** 2 / 16 - (1 / 9)*(np.pi ** 2 / 16) ** 9) / np.pi ** 2 # Proportionality constant + D = prop_const * sl ** 2 / halfway_t # Initial guess for diffusivity + self.rhs[filename] = 1 - sum([(8 / ((2 * n + 1) ** 2 * np.pi ** 2)) * np.exp( + -D * (2 * n + 1) ** 2 * np.pi ** 2 * (self.D_time[filename]) / + (4 * (sl / 2) ** 2)) for n in range(0, 20)]) + + # Function for optimizing D using curve fit + def f(xdata, D_opt, dt_opt, A_opt): + rhs = 1 - sum([(8 / ((2 * n + 1) ** 2 * np.pi ** 2)) * + np.exp(-D_opt * (2 * n + 1) ** 2 * np.pi ** 2 * (xdata + dt_opt) / (4 * (sl / 2) ** 2)) + for n in range(0, 20)]) # sl/2 because the book's equation goes from -l to l, not 0 to l + return rhs * A_opt + + # Attempt to optimize D using curve fit and the above function. Show a warning if it fails + try: # todo look into making this a weighted curve fit + popt, pcov = curve_fit(f, self.D_time[filename], self.lhs[filename], p0=[D, 0, 1], xtol=D*1e-3, + bounds=([0, -min(self.D_time[filename]), -1000], [10, 10*h, 1000])) + except RuntimeError: + self.error_texts += "Curve Fit Error with file " + filename + \ + ". Curve fit unable to find optimal diffusivity parameters.\n\n" + NaN = float("NaN") + popt = [D, 0, 1] + pcov = np.array([[NaN, NaN, NaN], [NaN, NaN, NaN], [NaN, NaN, NaN]]) + except OptimizeWarning: + self.error_texts += "Curve Fit Warning with file " + filename + \ + ". Curve fit unable to find covariance of the diffusivity parameters.\n\n" + NaN = float("NaN") + popt = [D, 0, 1] + pcov = np.array([[NaN, NaN, NaN], [NaN, NaN, NaN], [NaN, NaN, NaN]]) + # Get calculated uncertainty + perr = np.sqrt(np.diag(pcov)) + self.D_err[filename] = perr[0] + + if debug: + print('\n', filename) + print("New D, dt, A:", popt) + print('pcov:\n', pcov) + print("perr:", perr) + + # Store D, dt, A and rhs_cf for use elsewhere in program + self.D[filename], self.dt[filename], self.A[filename] = popt + self.rhs_cf[filename] = self.A[filename] * (1 - sum([(8 / ((2 * n + 1) ** 2 * np.pi ** 2)) * np.exp( + -self.D[filename] * (2 * n + 1) ** 2 * np.pi ** 2 * (self.D_time[filename] + self.dt[filename]) / + (4 * (sl / 2) ** 2)) for n in range(0, 20)])) + + if debug: + plt.figure() + plt.plot(self.lhs[filename][1:], label='lhs') + plt.plot(np.array(self.rhs_cf[filename][1:]), label='rhs') + plt.title(filename + " curve fit") + plt.legend() + plt.show() + + def calculate_permeability(self, filename, data): + """ Calculate Permeability using calculated Solubility and Diffusivity """ + self.Phi[filename] = self.D[filename] * self.Ks[filename] + # get the uncertainty + self.Phi_err[filename] = self.Phi[filename] * np.sqrt( + (self.Ks_err[filename] / self.Ks[filename]) ** 2 + (self.D_err[filename] / self.D[filename]) ** 2) + + def generate_plots(self, *args): + """ Create the absorption tab plots """ + filename = self.current_file.get() + if self.current_file.get() == "No files yet": + return + + data = self.datafiles[filename] + + # clear plots + self.ax.clear() + self.ax1.clear() + self.ax12.clear() + self.ax2.clear() + self.ax3.clear() + + # Permeability/Diffusivity/Solubility vs. Temperature (bottom left graph) + self.PDK_plot(self.ax) + + # Pressure vs. Time (top right graph) + self.pressure_time_plot(data, filename, self.fig1, self.ax1, self.ax12) + + # Pressure vs. Composition (bottom middle graph) (color coded by temperature) + self.pres_comp_temp_plot(self.fig2, self.ax2) + + # Diffusivity comparison plots for optimization (bottom right graph) + self.comparison_plot(filename, self.ax3) + + self.canvas.draw() + self.toolbar.update() + self.canvas1.draw() + self.toolbar1.update() + self.canvas2.draw() + self.toolbar2.update() + self.canvas3.draw() + self.toolbar3.update() + + # update labels + self.pr_e_label.set(self.pr_e_avg[filename]) + self.ns_e_label.set(self.ns_e[filename]) + self.eq_t_label.set(data.loc[self.t_e[filename], 't'] - data.loc[self.t0[filename], 't']) + self.Phi_label.set(self.Phi[filename]) + self.D_label.set(self.D[filename]) + self.K_label.set(self.Ks[filename]) + self.pr_e_err_label.set(self.pr_e_err[filename]) + self.ns_e_err_label.set(self.ns_e_err[filename]) + # todo The following error assumes 1 sigma of confidence that t0 and t_e are within one data point of the + # correct value for t0 or t_e, respectively. A better estimate may be needed. + t0_s_err = (data.loc[self.t0[filename] + 1, 't'] - data.loc[self.t0[filename] - 1, 't']) / 2 + te_s_err = (data.loc[self.t_e[filename] + 1, 't'] - data.loc[self.t_e[filename] - 1, 't']) / 2 + self.eq_t_err_label.set(np.sqrt(t0_s_err ** 2 + te_s_err ** 2)) + self.Phi_err_label.set(self.Phi_err[filename]) + self.D_err_label.set(self.D_err[filename]) + self.K_err_label.set(self.Ks_err[filename]) + if self.exp_type.get() == "Isotherm": + fname = (filename.rsplit(maxsplit=1))[0] # Remove the " p#_" by removing info after a whitespace + else: + fname = filename + self.p_num_label.set(self.p_num[fname]) + + def update_PDK_plot(self, *args): + """ Clear and replot the bottom left (PDK) plot """ + if self.current_file.get() == "No files yet": + return + self.ax.clear() + self.PDK_plot(self.ax) + self.canvas.draw() + self.toolbar.update() + + def popout_plot(self, ax): + """ creates the selected plot using the regular popout from matplotlib """ + + if self.loading_data: + proceed = tk.messagebox.askyesno(title="Loading Data", + message="Warning: Data is currently loading. Proceeding may cause " + + "unexpected or erroneous results. Do you wish to proceed?") + if not proceed: + return + + filename = self.current_file.get() + if filename != "No files yet": + data = self.datafiles[filename] + plot = ax.get_title() + plt.ioff() # this sometimes lets us use the navigation buttons in the toolbar # todo is this line needed? + + # Do things depending on which plot is going to be the popout plot + if plot == self.ax_title: + # PDK plot + fig, axis = plt.subplots(layout="constrained") + self.PDK_plot(axis) + + elif plot == self.ax1_title: + # Pressure vs. Time + fig, ax1 = plt.subplots(layout="constrained") + ax12 = ax1.twinx() + self.pressure_time_plot(data, filename, fig, ax1, ax12) + + elif plot == self.ax2_title: + # Pressure vs. Composition + fig, ax2 = plt.subplots(layout="constrained") + self.pres_comp_temp_plot(fig, ax2) + + elif plot == self.ax3_title: + # diffusivity comparison plots for optimization + fig, ax3 = plt.subplots(layout="constrained") + self.comparison_plot(filename, ax3) + + else: + showerror(message="Please select a data folder first.") + plt.show() + + def pressure_time_plot(self, data, filename, fig1, ax1, ax12): + """ Creates the pressure vs. time (top right) plot """ + # Plot the point where the isolation valve opens and where equilibrium is determined + ax1.plot(data.loc[self.t0[filename], 't'], data.loc[self.t0[filename], 'Pres'], 'yo', label="t$_0$") + ax1.plot(data.loc[self.t_e[filename], 't'], data.loc[self.t_e[filename], 'Pres'], 'mo', label="t$_e$") + + # Plot pressure and set up axes for it + ax1.plot(data['t'], data['Pres'], '.', label='Pressure') + ax1.set_title(self.ax1_title) + ax1.set_xlabel(self.ax1_xlabel) + ax1.set_ylabel(self.ax1_ylabel) + + # Plot temperatures and set up axes for them + base_colors = ['#1f77b4', '#ff7f0e', '#d62728', '#2ca02c', '#9467bd', + '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf'] # Red moved from fourth to third + color_index = 2 # skip blue, orange, and red + for i in range(1, max(self.num_GasT0, self.num_GasT) + 1): + color_index += 1 # Move to next color in the color list + color_index %= 10 # Cycle back to beginning if reached the end of the color list + ax12.plot(data['t'], data['GasT' + str(i)], label='GasT' + str(i), color=base_colors[color_index]) + ax12.set_ylabel(self.ax12_ylabel) + + # Make sure ticks are in the right spot + ax1.yaxis.set_ticks_position('left') + ax12.yaxis.set_ticks_position('right') + ax1.xaxis.set_ticks_position('bottom') + + # Generate and plot the red initial line (showing which values are used in initial calculations) + # and green equilibrium line (showing which values are used in final calculations) + xi = data.loc[self.t0[filename] - self.i_range[filename]:self.t0[filename] - 1, 't'] + yi = data.loc[self.t0[filename] - self.i_range[filename]:self.t0[filename] - 1, 'Pres'] + x_e = data.loc[self.t_e[filename] + 1:self.t_e[filename] + self.e_range[filename], 't'] + y_e = data.loc[self.t_e[filename] + 1:self.t_e[filename] + self.e_range[filename], 'Pres'] + ax1.plot(xi, yi, color='red', label='Initial Points') + ax1.plot(x_e, y_e, color='lime', label='Equilibrium Points') + + # Ensure all plots are on the legend. Simply using fig1.legend(...) doesn't get rid of old legends, + # causing them to stack up in a way I can't figure out how to remove. + # https://stackoverflow.com/questions/5484922/secondary-axis-with-twinx-how-to-add-to-legend/47370214#47370214 + pt_lines, pt_labels = ax1.get_legend_handles_labels() + pt_lines2, pt_labels2 = ax12.get_legend_handles_labels() + ax12.legend(pt_lines + pt_lines2, pt_labels + pt_labels2, loc="upper left", bbox_to_anchor=(0, 1), + bbox_transform=ax1.transAxes, framealpha=0.5) + + def pres_comp_temp_plot(self, fig2, ax2): + """ Creates the pressure vs. composition (bottom middle) plot and color codes using temperature """ + # Group data files according to their temperature to the nearest temperature divisible by 5 + # todo Perhaps allow this to round to tens in the case that the scatter of sample temp is larger than 2.5 deg C + approx_temps = {} + for filename in self.datafiles.keys(): + approx_temps[filename] = round(round(2 * (self.Ts_e[filename] - self.storage.standard_temp), -1) / 2) + # Round to ten: + # approx_temps[filename] = round(self.Ts_e[filename] - self.storage.standard_temp, -1) + # Sort the grouped temperatures + # https://www.geeksforgeeks.org/python-sort-python-dictionaries-by-key-or-value/ + sorted_temps = sorted(approx_temps.items(), key=lambda kv: (kv[1], kv[0])) + sorted_names = [] # Store sorted filenames + # Assign colors to data files according to their group + old_temp = 0 + base_colors = ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd', + '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf'] + color_index = -1 + tempe_color = {} # Temperature color + legend_plots = [] + for i, tup in enumerate(sorted_temps): + new_temp = tup[1] + if new_temp != old_temp: # if the temperature of the latest datafile is different from the last datafile + color_index += 1 # Move to next color in the color list + color_index %= 10 # Cycle back to beginning if reached the end of the color list + legend_plots.append([i, str(new_temp) + " \u00B0C"]) # Keep track of which artists will be in the legend + tempe_color[tup[0]] = base_colors[color_index] # Set what color the datapoint will be + old_temp = new_temp + sorted_names.append(tup[0]) + + artists = [] + for filename in sorted_names: + y = self.pr_e_avg[filename] # Pa + x = self.HM[filename] # Hydrogen to metal atoms ratio + T = self.Ts_e[filename] - self.storage.standard_temp # degrees C + label = f'{filename}\nComposition={x:.3e} H/M' + \ + f'\nTemperature={T:.3e} \u00B0C\nPressure={y:.3e} Pa' + + a = ax2.errorbar(x, y, yerr=self.pr_e_err[filename], + xerr=self.HM_err[filename], marker='o', label=label, color=tempe_color[filename]) + artists.append(a) + + # set labels on the points + mplcursors.cursor(artists, hover=mplcursors.HoverMode.Transient).connect( + "add", lambda sel: sel.annotation.set_text(sel.artist.get_label())) + + # Set up axes + ax2.set_title(self.ax2_title) + ax2.set_xlabel(self.ax2_xlabel) + ax2.set_ylabel(self.ax2_ylabel) + ax2.semilogy() + # Make sure ticks are in the right spot + ax2.yaxis.set_ticks_position('left') + ax2.xaxis.set_ticks_position('bottom') + + # [[x[0] for x in legend_plots]] gets the list of integers since np.array(legend_plots)[:, 0] produces an + # array of strings, and strings can't be used as indexes. + # np.array(ax2.lines) turns the list of artists into an array so that a list can be used as indexes to pick out + # the first artist for each color/temperature and only use those in the legend. + # np.array(legend_plots)[:, 1] produces the labels for the legend + ax2.legend(np.array(ax2.lines)[[x[0] for x in legend_plots]], np.array(legend_plots)[:, 1], + loc="lower right", bbox_to_anchor=(1, 0), bbox_transform=ax2.transAxes, framealpha=0.5) + + def comparison_plot(self, filename, ax3): + """ Creates the diffusivity optimization comparison (bottom right) plot """ + # Diffusivity comparison plots for optimization + ax3.plot(self.D_time[filename][1:], self.lhs[filename][1:], '.', label='Experimental Data', ) + ax3.plot(self.D_time[filename][1:], self.rhs[filename][1:], '--', label='D: Initial Guess') + ax3.plot(self.D_time[filename][1:], self.rhs_cf[filename][1:], '--', label='D: Optimized Fit') + ax3.set_title(self.ax3_title) + # Set up axes + ax3.set_xlabel(self.ax3_xlabel) + ax3.set_ylabel(self.ax3_ylabel) + ax3.yaxis.set_ticks_position('left') + ax3.xaxis.set_ticks_position('bottom') + ax3.legend(framealpha=0.5) + + def PDK_plot(self, ax): + """ Creates the PDK (bottom left) plot for calculated properties of different files """ + plot = self.current_variable.get() + artists = [] + self.ax_title = plot + " vs. Temperature" + + # set y labels to get units right + if plot == "Permeability": + self.ax_ylabel = plot + " (mol m$^{-1}$ s$^{-1}$ Pa$^{-0.5}$)" + elif plot == "Diffusivity": + self.ax_ylabel = plot + " (m$^2$ s$^{-1}$)" + elif plot == "Solubility": + self.ax_ylabel = plot + " (mol m$^{-3}$ Pa$^{-0.5}$)" + + for filename in self.datafiles.keys(): + x = self.Ts0[filename] - self.storage.standard_temp + p = self.pr_e_avg[filename] # pressure + # y values change based on plot type + if plot == "Permeability": + y = self.Phi[filename] + yerr = self.Phi_err[filename] + label = f'{filename}\n{plot}={y:.3e}' + r' mol m$^{-1}\,$s$^{-1}\,$Pa$^{-0.5}$' + \ + f'\nTemperature={x:.3e} \u00B0C\nPressure={p:.3e} Pa' + elif plot == "Diffusivity": + y = self.D[filename] + yerr = self.D_err[filename] + label = f'{filename}\n{plot}={y:.3e}' + r' m$^{2}\,$s$^{-1}$' + \ + f'\nTemperature={x:.3e} \u00B0C\nPressure={p:.3e} Pa' + elif plot == "Solubility": + y = self.Ks[filename] + yerr = self.Ks_err[filename] + label = f'{filename}\n{plot}={y:.3e}' + r' mol m$^{-3}\,$Pa$^{-0.5}$' + \ + f'\nTemperature={x:.3e} \u00B0C\nPressure={p:.3e} Pa' + else: + # This shouldn't be possible + showerror("Selection Error", "Error, you should have selected Permeability, Diffusivity, Solubility.") + y = self.Phi[filename] + yerr = self.Phi_err[filename] + label = f'{filename}\n{plot}={y:.3e}' + r' mol m$^{-1}\,$s$^{-1}\,$Pa$^{-0.5}$' + \ + f'\nTemperature={x:.3e} K\nPressure={p:.3e} Pa' + + a = ax.errorbar(x, y, yerr=yerr, marker='o', label=label) + artists.append(a) + + # set labels on the points + mplcursors.cursor(artists, hover=mplcursors.HoverMode.Transient).connect( + "add", lambda sel: sel.annotation.set_text(sel.artist.get_label())) + + ax.semilogy() + # ax.minorticks_off() + + ax.set_title(self.ax_title) + ax.set_ylabel(self.ax_ylabel) + ax.set_xlabel(self.ax_xlabel) + ax.yaxis.set_ticks_position('left') + ax.xaxis.set_ticks_position('bottom') + + def save_figures(self): + """ Saves figures into a folder """ + if self.loading_data: + proceed = tk.messagebox.askyesno(title="Loading Data", + message="Warning: Data is currently loading. Proceeding may cause " + + "unexpected or erroneous results. Do you wish to proceed?") + if not proceed: + return + + filename = asksaveasfilename(initialdir=os.path.dirname(__file__), initialfile="figures") + if filename != '': + # check for extension and remove it + directory = os.path.splitext(filename)[0] # if no extension the second element will be blank ('dir', '') + # create the directory. Need to check if it already exists + try: + os.mkdir(directory) + # save the figures + self.fig.savefig(os.path.join(directory, self.ax_title)) + self.fig1.savefig(os.path.join(directory, self.ax1_title)) + self.fig2.savefig(os.path.join(directory, self.ax2_title)) + self.fig3.savefig(os.path.join(directory, self.ax3_title)) + except FileExistsError: + ans = tk.messagebox.askyesno( + message="The directory already exists. Do you wish add the figures to this directory?") + if ans: + # save the figures + self.fig.savefig(os.path.join(directory, self.ax_title)) + self.fig1.savefig(os.path.join(directory, self.ax1_title)) + self.fig2.savefig(os.path.join(directory, self.ax2_title)) + self.fig3.savefig(os.path.join(directory, self.ax3_title)) + + +class AdjustPVars(tk.Toplevel): + """ popup window used to change many of the variables used in loading absorption data. """ + + def __init__(self, storage, pos, *args, **kwargs): + super().__init__(*args, **kwargs) + + # import custom widgets + widgets = Widgets() + self.add_entry = widgets.add_entry + + self.title("Adjust Persistent Variables") + # self.resizable(width=False, height=False) + self.minsize(400, 200) + + # gui_x/y values determined by running self.updateidletasks() at the end of self.__init__ and then printing size + gui_x = 1429 + gui_y = 742 + if platform.system() == 'Darwin': + width = 900 # width gets scaled again later if number of TCs measuring GasT has changed + height = 424 + extra_width = 205 # width to add for each new TC beyond three + else: + width = 710 + height = 330 + extra_width = 165 + pos_right = int(pos[0] + (gui_x - width) / 2) + pos_down = int(pos[1] + (gui_y - height) / 3) + self.geometry("{}x{}+{}+{}".format(width, height, pos_right, pos_down)) + + # Treat closing the window via the "x" button the same as if "cancel" was clicked + self.protocol("WM_DELETE_WINDOW", self.close_pv_win) + + self.storage = storage + self.changed = False # Boolean of whether edits have been made that require plot refreshing + + # Get the path for the persistent variable file + self.pv_filename = os.path.join('data_files', 'persistent_absorption_input_variables.xlsx') + + # Read the Excel sheets into dataframes for easier access + self.numbers_info = pd.read_excel(self.pv_filename, sheet_name="Numbers", header=0) + self.misc_info = pd.read_excel(self.pv_filename, sheet_name="MiscInfo", header=0) + self.GasT_info = pd.read_excel(self.pv_filename, sheet_name="GasT", header=0) + self.SampT_info = pd.read_excel(self.pv_filename, sheet_name="SampT", header=0) + self.Pres_info = pd.read_excel(self.pv_filename, sheet_name="Pres", header=0) + + # Pull out variables for display and potential editing + + # Number of TCs measuring gas temperature + self.num_GasT0 = tk.DoubleVar(value=self.numbers_info['GasT'][0]) + self.num_GasT = tk.DoubleVar(value=self.numbers_info['GasT'][1]) + + # Method of scaling the window width according to number of TCs measuring GasT. At num_GasT0 = 10, + # things are still fine. Potentially add a scrollbar instead. + if max(self.num_GasT0.get(), self.num_GasT.get()) > 2: + width += 90 + int(extra_width * (max(self.num_GasT0.get(), self.num_GasT.get()) - 3)) + self.geometry("{}x{}+{}+{}".format(width, height, pos_right, pos_down)) + + # time + self.col_t = tk.StringVar(value=self.misc_info['t'][0]) # column name + self.m_t = tk.DoubleVar(value=self.misc_info['t'][1]) # "m" part of unit converter, as in mx + b + self.b_t = tk.DoubleVar(value=self.misc_info['t'][2]) # "b" part of unit converter + self.cerr_t = tk.DoubleVar(value=self.misc_info['t'][3]) # constant uncertainty + self.perr_t = tk.DoubleVar(value=self.misc_info['t'][4]) # proportional uncertainty + + # TCs measuring gas temperature + self.col_GasT = {} + self.m_GasT = {} + self.b_GasT = {} + self.cerr_GasT = {} # constant uncertainty + self.perr_GasT = {} # proportional uncertainty + + for GasT_inst in self.GasT_info.keys(): + self.col_GasT[GasT_inst] = tk.StringVar(value=self.GasT_info[GasT_inst][0]) + self.m_GasT[GasT_inst] = tk.DoubleVar(value=self.GasT_info[GasT_inst][1]) + self.b_GasT[GasT_inst] = tk.DoubleVar(value=self.GasT_info[GasT_inst][2]) + self.cerr_GasT[GasT_inst] = tk.DoubleVar(value=self.GasT_info[GasT_inst][3]) + self.perr_GasT[GasT_inst] = tk.DoubleVar(value=self.GasT_info[GasT_inst][4]) + + # Sample temperature + self.col_SampT = tk.StringVar(value=self.SampT_info['SampT'][0]) + self.m_SampT = tk.DoubleVar(value=self.SampT_info['SampT'][1]) + self.b_SampT = tk.DoubleVar(value=self.SampT_info['SampT'][2]) + self.cerr_SampT = tk.DoubleVar(value=self.SampT_info['SampT'][3]) + self.perr_SampT = tk.DoubleVar(value=self.SampT_info['SampT'][4]) + + # Pressure + self.col_Pres = tk.StringVar(value=self.Pres_info['Pres'][0]) + self.m_Pres = tk.DoubleVar(value=self.Pres_info['Pres'][1]) + self.b_Pres = tk.DoubleVar(value=self.Pres_info['Pres'][2]) + self.cerr_Pres = tk.DoubleVar(value=self.Pres_info['Pres'][3]) + self.perr_Pres = tk.DoubleVar(value=self.Pres_info['Pres'][4]) + + # Isolation valve + self.col_IV = tk.StringVar(value=self.misc_info['Isolation Valve'][0]) + self.m_IV = tk.DoubleVar(value=self.misc_info['Isolation Valve'][1]) + self.b_IV = tk.DoubleVar(value=self.misc_info['Isolation Valve'][2]) + + # Row at which the program starts reading data + self.starting_row = tk.IntVar(value=self.misc_info['Starting Row'][0]) + # Rows at the end of the file which the program doesn't read + self.footer_rows = tk.IntVar(value=self.misc_info['Rows in Footer'][0]) + + # store entries + self.inputs = {} + + # create frames + self.create_labels() + self.create_inputs() + + submit_button = ttk.Button(self, text="Submit", command=self.submit) + submit_button.grid(row=14, column=7, columnspan=2, sticky='e') + + quit_button = ttk.Button(self, text="Cancel", command=self.close_pv_win) + quit_button.grid(row=14, column=9, columnspan=2, sticky='w') + + parent = tk.Frame(self) + parent.grid(row=14, column=0, columnspan=7, sticky="nsew", pady=5) + self.add_entry(self, parent, variable=self.inputs, key='num_GasT0', text="Number of TCs measuring initial GasT:", + subscript='', ent_w=3, tvar=self.num_GasT0, units='', row=0, column=0, in_window=True, + command=lambda tvar, variable, key, pf: self.one_to_max(tvar, variable, key, pf)) + self.add_entry(self, parent, variable=self.inputs, key='num_GasT', text="Number of TCs measuring GasT:", + subscript='', ent_w=3, tvar=self.num_GasT, units='', row=0, column=4, in_window=True, + command=lambda tvar, variable, key, pf: self.one_to_max(tvar, variable, key, pf)) + + def one_to_max(self, tvar, variable, key, pf=None): + """ Ensure the user entry is greater than 1. If not, set it equal to 1. """ + self.storage.check_for_number(tvar, variable, key, parent_frame=pf) + maxTCnum = 8 # Max number of TCs to allow + # Either set the entry to 1 if less than 1, to int(maxTCnum) if more than int(maxTCnum), or to the user's entry + if key in variable.keys() and tvar.get() < 1: + tk.messagebox.showwarning("Invalid Entry", "Please enter a number greater than or equal to 1.") + self.deiconify() + variable[key].delete(0, "end") + variable[key].insert(0, "1") + tvar.set(1) + elif key in variable.keys() and tvar.get() > int(maxTCnum): + # Keeps the user from having to type out too many unique column names if something like "100" gets submitted + tk.messagebox.showwarning("Invalid Entry", 'Please enter a number less than or equal to ' + + '{}.\n\n'.format(int(maxTCnum)) + 'If more than ' + + '{} TCs are needed,'.format(int(maxTCnum)) + ' simply search ' + + 'for "maxTCnum" in the source code and change the ' + + '{} in its definition '.format(int(maxTCnum)) + + 'to your desired number.') + self.deiconify() + variable[key].delete(0, "end") + variable[key].insert(0, "{}".format(int(maxTCnum))) + tvar.set(int(maxTCnum)) + else: + variable[key].delete(0, "end") + variable[key].insert(0, "{}".format(int(tvar.get()))) + tvar.set(int(tvar.get())) + return True + + def create_labels(self): + """ Creates intro text to Absorption Plots tab's Adjust Persistent Variables window """ + parent = tk.Frame(self) + parent.grid(row=0, column=0, columnspan=10, sticky="nsew") + tk.Label(parent, text='Choose how the program will handle your uploaded data. ' + + 'Click the "?" for symbol information:').grid(sticky="nsew", columnspan=2) + + # Create a nicely formatted help button + s = ttk.Style() + s.configure('Bold.TButton', font=('Helvetica', 10, 'bold')) + help_button = ttk.Button(parent, text=" ? ", width=3, style='Bold.TButton', command=self.show_help) + help_button.grid(row=0, column=10) + + def show_help(self): + """ Calls the class for creating the help window """ + x = self.winfo_rootx() + y = self.winfo_rooty() + apv_width = self.winfo_width() + apv_height = self.winfo_height() + APSettingsHelp(pos=(x, y), size=(apv_width, apv_height)).show_help_window() + + def check_col_names(self, tvar, variable, key): + """ Checks to make sure the entered string is made up of only letters""" + if variable[key].get().isalpha(): + svar = variable[key].get().upper() # set the string to uppercase + else: + tk.messagebox.showwarning("Invalid Entry", "Please enter a string of letters.") + self.deiconify() + svar = tvar.get() # reset to what it was before the invalid entry was typed + + # Either set the entry back to what it was (if not a string of letters) or to the string of (uppercase) letters + if key in variable.keys(): + variable[key].delete(0, "end") + variable[key].insert(0, "{}".format(svar)) + tvar.set(svar) + return True + + def create_inputs(self): + """Create the many entry boxes this window requires""" + parent = self + entry_w = 8 # Width of each entry box in this window + entry_row = 1 # First row of the entries + + entry_col = 0 + tk.Label(parent, text="t [s]").grid(row=entry_row + 0, column=1) + self.add_entry(self, parent, variable=self.inputs, key="col_t", text="col", innersubscript="t", innertext=":", + subscript="", tvar=self.col_t, units="", ent_w=entry_w, + row=entry_row + 1, column=entry_col, + command=lambda tvar, variable, key: self.check_col_names(tvar, variable, key)) + self.add_entry(self, parent, variable=self.inputs, key="m_t", text="m", innersubscript="t", innertext=":", + subscript="", tvar=self.m_t, units="[s/?]", ent_w=entry_w, + row=entry_row + 2, column=entry_col, in_window=True, + command=lambda tvar, variable, key, pf: self.storage.check_for_number(tvar, variable, key, + parent_frame=pf)) + self.add_entry(self, parent, variable=self.inputs, key="b_t", text="b", innersubscript="t", innertext=":", + subscript="", tvar=self.b_t, units="[s]", ent_w=entry_w, + row=entry_row + 3, column=entry_col, in_window=True, + command=lambda tvar, variable, key, pf: self.storage.check_for_number(tvar, variable, key, + parent_frame=pf)) + self.add_entry(self, parent, variable=self.inputs, key="cerr_t", text="cerr", innersubscript="t", innertext=":", + subscript="", tvar=self.cerr_t, units="[s]", ent_w=entry_w, + row=entry_row + 4, column=entry_col, in_window=True, + command=lambda tvar, variable, key, pf: self.storage.check_for_number(tvar, variable, key, + parent_frame=pf)) + self.add_entry(self, parent, variable=self.inputs, key="perr_t", text="perr", innersubscript="t", innertext=":", + subscript="", tvar=self.perr_t, units="[%]", ent_w=entry_w, + row=entry_row + 5, column=entry_col, in_window=True, + command=lambda tvar, variable, key, pf: self.storage.check_for_number(tvar, variable, key, + parent_frame=pf)) + + entry_col += 3 + tk.Label(parent, text="Pres [Pa]").grid(row=entry_row + 0, column=entry_col + 1) + self.add_entry(self, parent, variable=self.inputs, key="col_Pres", text="col", innersubscript="Pres", + innertext=":", subscript="", tvar=self.col_Pres, units="", ent_w=entry_w, + row=entry_row + 1, column=entry_col, + command=lambda tvar, variable, key: self.check_col_names(tvar, variable, key)) + self.add_entry(self, parent, variable=self.inputs, key="m_Pres", text="m", innersubscript="Pres", + innertext=":", subscript="", tvar=self.m_Pres, units="[Pa/?]", ent_w=entry_w, + row=entry_row + 2, column=entry_col, in_window=True, + command=lambda tvar, variable, key, pf: self.storage.check_for_number(tvar, variable, key, + parent_frame=pf)) + self.add_entry(self, parent, variable=self.inputs, key="b_Pres", text="b", innersubscript="Pres", + innertext=":", subscript="", tvar=self.b_Pres, units="[Pa]", ent_w=entry_w, + row=entry_row + 3, column=entry_col, in_window=True, + command=lambda tvar, variable, key, pf: self.storage.check_for_number(tvar, variable, key, + parent_frame=pf)) + self.add_entry(self, parent, variable=self.inputs, key="cerr_Pres", text="cerr", innersubscript="Pres", + innertext=":", subscript="", tvar=self.cerr_Pres, units="[Pa]", ent_w=entry_w, + row=entry_row + 4, column=entry_col, in_window=True, + command=lambda tvar, variable, key, pf: self.storage.check_for_number(tvar, variable, key, + parent_frame=pf)) + self.add_entry(self, parent, variable=self.inputs, key="perr_Pres", text="perr", innersubscript="Pres", + innertext=":", subscript="", tvar=self.perr_Pres, units="[%]", ent_w=entry_w, + row=entry_row + 5, column=entry_col, in_window=True, + command=lambda tvar, variable, key, pf: self.storage.check_for_number(tvar, variable, key, + parent_frame=pf)) + + entry_col += 3 + tk.Label(parent, text="Isolation Valve [1/0]").grid(row=entry_row + 0, column=entry_col + 1) + self.add_entry(self, parent, variable=self.inputs, key="col_IV", text="col", innersubscript="IV", + innertext=":", subscript="", tvar=self.col_IV, units="", ent_w=entry_w, + row=entry_row + 1, column=entry_col, + command=lambda tvar, variable, key: self.check_col_names(tvar, variable, key)) + self.add_entry(self, parent, variable=self.inputs, key="m_IV", text="m", innersubscript="IV", + innertext=":", subscript="", tvar=self.m_IV, units="", ent_w=entry_w, + row=entry_row + 2, column=entry_col, in_window=True, + command=lambda tvar, variable, key, pf: self.storage.check_for_number(tvar, variable, key, + parent_frame=pf)) + self.add_entry(self, parent, variable=self.inputs, key="b_IV", text="b", innersubscript="IV", + innertext=":", subscript="", tvar=self.b_IV, units="", ent_w=entry_w, + row=entry_row + 3, column=entry_col, in_window=True, + command=lambda tvar, variable, key, pf: self.storage.check_for_number(tvar, variable, key, + parent_frame=pf)) + + entry_col += 3 + tk.Label(parent, text="Starting Row").grid(row=entry_row + 0, column=entry_col + 1) + self.add_entry(self, parent, variable=self.inputs, key="Starting Row", text="", innersubscript="", + innertext="", subscript="", tvar=self.starting_row, units="", ent_w=entry_w, + row=entry_row + 1, column=entry_col, in_window=True, + command=lambda tvar, variable, key, pf: self.storage.check_for_number(tvar, variable, key, + parent_frame=pf)) + + tk.Label(parent, text="Rows in Footer").grid(row=entry_row + 2, column=entry_col + 1) + self.add_entry(self, parent, variable=self.inputs, key="Footer Rows", text="", innersubscript="", + innertext="", subscript="", tvar=self.footer_rows, units="", ent_w=entry_w, + row=entry_row + 3, column=entry_col, in_window=True, + command=lambda tvar, variable, key, pf: self.storage.check_for_number(tvar, variable, key, + parent_frame=pf)) + + # Second row of instruments + + row2entries = entry_row + 7 + entry_col = -3 # Reset to far left (noting that it will start with a += 3) + tk.Label(parent, text="").grid(row=row2entries-1, column=0) # Add some space between the rows + + # Add entries for arbitrarily many instruments measuring GasT + for GasT_inst in self.GasT_info.keys(): + entry_col += 3 + tk.Label(parent, text="{} [\u00B0C]".format(GasT_inst)).grid(row=row2entries, column=entry_col + 1) + self.add_entry(self, parent, variable=self.inputs, key="col_{}".format(GasT_inst), text="col", + innersubscript="{}".format(GasT_inst), innertext=":", + subscript="", tvar=self.col_GasT[GasT_inst], units="", ent_w=entry_w, + row=row2entries + 1, column=entry_col, + command=lambda tvar, variable, key: self.check_col_names(tvar, variable, key)) + self.add_entry(self, parent, variable=self.inputs, key="m_{}".format(GasT_inst), text="m", + innersubscript="{}".format(GasT_inst), innertext=":", + subscript="", tvar=self.m_GasT[GasT_inst], units="[\u00B0C/?]", ent_w=entry_w, + row=row2entries + 2, column=entry_col, in_window=True, + command=lambda tvar, variable, key, pf: self.storage.check_for_number(tvar, variable, key, + parent_frame=pf)) + self.add_entry(self, parent, variable=self.inputs, key="b_{}".format(GasT_inst), text="b", + innersubscript="{}".format(GasT_inst), innertext=":", + subscript="", tvar=self.b_GasT[GasT_inst], units="[\u00B0C]", ent_w=entry_w, + row=row2entries + 3, column=entry_col, in_window=True, + command=lambda tvar, variable, key, pf: self.storage.check_for_number(tvar, variable, key, + parent_frame=pf)) + self.add_entry(self, parent, variable=self.inputs, key="cerr_{}".format(GasT_inst), text="cerr", + innersubscript="{}".format(GasT_inst), innertext=":", + subscript="", tvar=self.cerr_GasT[GasT_inst], units="[\u00B0C]", ent_w=entry_w, + row=row2entries + 4, column=entry_col, in_window=True, + command=lambda tvar, variable, key, pf: self.storage.check_for_number(tvar, variable, key, + parent_frame=pf)) + self.add_entry(self, parent, variable=self.inputs, key="perr_{}".format(GasT_inst), text="perr", + innersubscript="{}".format(GasT_inst), innertext=":", + subscript="", tvar=self.perr_GasT[GasT_inst], units="[%]", ent_w=entry_w, + row=row2entries + 5, column=entry_col, in_window=True, + command=lambda tvar, variable, key, pf: self.storage.check_for_number(tvar, variable, key, + parent_frame=pf)) + + # Sample temperature + entry_col += 3 + tk.Label(parent, text="SampT [\u00B0C]").grid(row=row2entries, column=entry_col + 1) + self.add_entry(self, parent, variable=self.inputs, key="col_SampT", text="col", innersubscript="SampT", + innertext=":", subscript="", tvar=self.col_SampT, units="", ent_w=entry_w, + row=row2entries + 1, column=entry_col, + command=lambda tvar, variable, key: self.check_col_names(tvar, variable, key)) + self.add_entry(self, parent, variable=self.inputs, key="m_SampT", text="m", innersubscript="SampT", + innertext=":", subscript="", tvar=self.m_SampT, units="[\u00B0C/?]", ent_w=entry_w, + row=row2entries + 2, column=entry_col, in_window=True, + command=lambda tvar, variable, key, pf: self.storage.check_for_number(tvar, variable, key, + parent_frame=pf)) + self.add_entry(self, parent, variable=self.inputs, key="b_SampT", text="b", innersubscript="SampT", + innertext=":", subscript="", tvar=self.b_SampT, units="[\u00B0C]", ent_w=entry_w, + row=row2entries + 3, column=entry_col, in_window=True, + command=lambda tvar, variable, key, pf: self.storage.check_for_number(tvar, variable, key, + parent_frame=pf)) + self.add_entry(self, parent, variable=self.inputs, key="cerr_SampT", text="cerr", innersubscript="SampT", + innertext=":", subscript="", tvar=self.cerr_SampT, units="[\u00B0C]", ent_w=entry_w, + row=row2entries + 4, column=entry_col, in_window=True, + command=lambda tvar, variable, key, pf: self.storage.check_for_number(tvar, variable, key, + parent_frame=pf)) + self.add_entry(self, parent, variable=self.inputs, key="perr_SampT", text="perr", innersubscript="SampT", + innertext=":", subscript="", tvar=self.perr_SampT, units="[%]", ent_w=entry_w, + row=row2entries + 5, column=entry_col, in_window=True, + command=lambda tvar, variable, key, pf: self.storage.check_for_number(tvar, variable, key, + parent_frame=pf)) + + def submit(self): + """Updates the persistent variables with user input""" + + # Create a list that contains all non-column name entries in GasT for checking for NaNs + GasT_vals = list(self.m_GasT.values()) + list(self.b_GasT.values()) + \ + list(self.cerr_GasT.values()) + list(self.perr_GasT.values()) + + # List of all column names for checking for uniqueness + col_list = [self.col_t.get(), self.col_SampT.get(), self.col_Pres.get(), self.col_IV.get()] + for val in self.col_GasT.values(): + col_list.append(val.get()) + # check to make sure no numerical variable is assigned "nan" + if sum(map(np.isnan, [self.m_t.get(), self.b_t.get(), self.cerr_t.get(), self.perr_t.get(), + self.m_SampT.get(), self.b_SampT.get(), self.cerr_SampT.get(), self.perr_SampT.get(), + self.m_Pres.get(), self.b_Pres.get(), self.cerr_Pres.get(), self.perr_Pres.get(), + self.m_IV.get(), self.b_IV.get(), self.starting_row.get(), self.footer_rows.get(), + self.num_GasT0.get(), self.num_GasT.get()])) + \ + any(np.isnan(val.get()) for val in GasT_vals) > 0: + tk.messagebox.showwarning(title="Missing Entry", + message="Please fill in remaining boxes before continuing.") + self.deiconify() + + elif len(set(col_list)) < len(col_list): # Check to make sure all column names are unique + tk.messagebox.showwarning(title="Duplicate Column", + message="Please ensure all column names are unique.") + self.deiconify() + + else: + ans = tk.messagebox.askokcancel( + title="Confirmation", + message="Do you wish to save the variables as they are to the source file?") + if ans: + # Redefine dataframes to account for changes + self.misc_info['t'] = [self.col_t.get(), self.m_t.get(), self.b_t.get(), + self.cerr_t.get(), self.perr_t.get()] + for GasT_inst in self.GasT_info.keys(): + self.GasT_info[GasT_inst] = [self.col_GasT[GasT_inst].get(), self.m_GasT[GasT_inst].get(), + self.b_GasT[GasT_inst].get(), + self.cerr_GasT[GasT_inst].get(), self.perr_GasT[GasT_inst].get()] + self.SampT_info['SampT'] = [self.col_SampT.get(), self.m_SampT.get(), self.b_SampT.get(), + self.cerr_SampT.get(), self.perr_SampT.get()] + self.Pres_info['Pres'] = [self.col_Pres.get(), self.m_Pres.get(), self.b_Pres.get(), + self.cerr_Pres.get(), self.perr_Pres.get()] + self.misc_info['Isolation Valve'] = [self.col_IV.get(), self.m_IV.get(), + self.b_IV.get(), np.nan, np.nan] + self.misc_info['Starting Row'] = [self.starting_row.get(), np.nan, np.nan, np.nan, np.nan] + self.misc_info['Rows in Footer'] = [self.footer_rows.get(), np.nan, np.nan, np.nan, np.nan] + # Create or remove data from dataframe so that it has the max number of instruments measuring GasT + # out of self.num_GasT0 and self.num_GasT. Note that this can handle non-integer values + # of self.num_GasT0 and self.num_GasT + max_num = max(self.numbers_info['GasT'][0], self.numbers_info['GasT'][1]) + while max_num > max(self.num_GasT0.get(), self.num_GasT.get()): + del self.GasT_info['GasT' + str(max_num)] + max_num -= 1 + while max_num < max(self.num_GasT0.get(), self.num_GasT.get()): + self.GasT_info['GasT' + str(max_num + 1)] = [self.col_GasT['GasT1'].get(), 1.0, 0.0, 0.0, 0.0] + max_num += 1 + self.numbers_info.loc[0, 'GasT'] = self.num_GasT0.get() + self.numbers_info.loc[1, 'GasT'] = self.num_GasT.get() + + # overwrite the old source file with updated dataframes + with pd.ExcelWriter(self.pv_filename, engine="openpyxl", if_sheet_exists='new', mode='a') as writer: + workBook = writer.book + + # remove's shouldn't be required, but if_sheet_exists='replace' is malfunctioning as of 11/18/21 + workBook.remove(workBook['Numbers']) + workBook.remove(workBook['MiscInfo']) + workBook.remove(workBook['GasT']) + workBook.remove(workBook['SampT']) + workBook.remove(workBook['Pres']) + + # Write updated dataframes to the Excel sheets + self.numbers_info.to_excel(writer, sheet_name='Numbers', index=False) + self.misc_info.to_excel(writer, sheet_name='MiscInfo', index=False) + self.GasT_info.to_excel(writer, sheet_name='GasT', index=False) + self.SampT_info.to_excel(writer, sheet_name='SampT', index=False) + self.Pres_info.to_excel(writer, sheet_name='Pres', index=False) + writer.save() + + self.changed = True + self.destroy() # close the pop-up window + else: + self.deiconify() # bring back the pop-up window + + def close_pv_win(self): + """ Checks for unsaved updates to persistent variables """ + + # Create new dataframes to compare against + temp_misc_info = self.misc_info.copy() + temp_GasT_info = self.GasT_info.copy() + temp_SampT_info = self.SampT_info.copy() + temp_Pres_info = self.Pres_info.copy() + temp_numbers_info = self.numbers_info.copy() + + # update new dataframes with current entries + temp_misc_info['t'] = [self.col_t.get(), self.m_t.get(), self.b_t.get(), + self.cerr_t.get(), self.perr_t.get()] + for GasT_inst in self.GasT_info.keys(): + temp_GasT_info[GasT_inst] = [self.col_GasT[GasT_inst].get(), self.m_GasT[GasT_inst].get(), + self.b_GasT[GasT_inst].get(), + self.cerr_GasT[GasT_inst].get(), self.perr_GasT[GasT_inst].get()] + temp_SampT_info['SampT'] = [self.col_SampT.get(), self.m_SampT.get(), self.b_SampT.get(), + self.cerr_SampT.get(), self.perr_SampT.get()] + temp_Pres_info['Pres'] = [self.col_Pres.get(), self.m_Pres.get(), self.b_Pres.get(), + self.cerr_Pres.get(), self.perr_Pres.get()] + temp_misc_info['Isolation Valve'] = [self.col_IV.get(), self.m_IV.get(), self.b_IV.get(), np.nan, np.nan] + temp_misc_info['Starting Row'] = [self.starting_row.get(), np.nan, np.nan, np.nan, np.nan] + temp_misc_info['Rows in Footer'] = [self.footer_rows.get(), np.nan, np.nan, np.nan, np.nan] + temp_numbers_info['GasT'] = [int(self.num_GasT0.get()), int(self.num_GasT.get())] # "int" is so HyPAT recognizes when there was no change + + # List of all column names for checking for uniqueness + col_list = [self.col_t.get(), self.col_SampT.get(), self.col_Pres.get(), self.col_IV.get()] + for val in self.col_GasT.values(): + col_list.append(val.get()) + + # Check to see if any changes have been made and if there are any duplicate column names + if temp_misc_info.equals(self.misc_info) and temp_GasT_info.equals(self.GasT_info) and \ + temp_SampT_info.equals(self.SampT_info) and \ + temp_Pres_info.equals(self.Pres_info) and temp_numbers_info.equals(self.numbers_info)\ + and len(set(col_list)) == len(col_list): + self.destroy() + elif len(set(col_list)) < len(col_list): # If there are duplicate column names + tk.messagebox.showwarning(title="Duplicate Column", + message="Please ensure all column names are unique.") + self.deiconify() + else: # If changes have been made, check with user before closing + ans = tk.messagebox.askokcancel( + title="Confirmation", + message="Do you wish close without saving changes?") + if ans: + self.destroy() + else: + self.deiconify() + + def show_pv_window(self): + """ This allows the window to open and then, when closed, trigger the main gui to update the plots if + changes were made """ + self.deiconify() + self.wait_window() + + return self.changed + + +class APSettingsHelp(tk.Toplevel): + """ popup box to display information about the variables that can be edited. """ + + def __init__(self, pos, size, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.title("Help for Absorption's Persistent Variables Settings") + # self.resizable(width=False, height=False) + self.minsize(400, 200) + + if platform.system() == 'Darwin': + width = 900 + height = 500 + else: + width = 650 + height = 430 + pos_right = int(pos[0] + (size[0] - width) / 2) + pos_down = int(pos[1] + (size[1] - height) / 2) + self.geometry("{}x{}+{}+{}".format(width, height, pos_right, pos_down)) + + self.create_text_boxes() + + def create_text_boxes(self): + """ Create the instructions that appear in this help window """ + # Get fonts ready for use + from tkinter import font + tfont = font.nametofont("TkDefaultFont").copy() + btfont, bbtfont, bb2tfont = tfont.copy(), tfont.copy(), tfont.copy() + btfont.config(weight="bold") + bbtfont.config(weight="bold", underline=True) + + # Create a text widget for each symbol, along with one (" ") for the header + td = {} + tdt = {} + for item in (" ", "t [s]: ", "Pres [Pa]: ", "Isolation Valve [1/0]: ", "Starting Row: ", "Rows in Footer: ", + "GasT [\u00B0C]: ", "SampT [\u00B0C]: ", "col: ", "m: ", "b: ", "cerr: ", "perr: "): + td[item] = tk.Text(self, borderwidth=0, background=self.cget("background"), spacing3=1) + tdt[item + "text"] = tk.Text(self, borderwidth=0, background=self.cget("background"), spacing3=1) + + # Add in formatting options + for symbol in td: + td[symbol].tag_configure("bold", justify="right", font=btfont) + tdt[symbol + "text"].tag_configure("bold_underline", font=bbtfont) + + # Insert text into right-side widgets + tdt[" text"].insert("insert", "Symbols", "bold_underline") # Header + tdt["t [s]: text"].insert("insert", "Time. HyPAT will treat this column either as a column of datetimes or " + + "as cumulative time passed since start of data recording, " + + "depending on whether the first entry in the column is a datetime or a float/int. " + + "The variables 'm' and 'b,' described further down, " + + "are only applied to the time data if the data is composed of floats.") + tdt["Pres [Pa]: text"].insert("insert", "Pressure.") + tdt["Isolation Valve [1/0]: text"].insert("insert", "The data describing whether the isolation valve is " + + "closed (~0) or open (not ~0). Start of absorption is " + + "measured from when the valve first goes from closed to " + + "open.") + tdt["Starting Row: text"].insert("insert", "The row at which the program starts reading data from the Excel " + + "sheet (0-indexed). HyPAT assumes the data file has no header, so " + + "use this to start analysis after the header.") + tdt["Rows in Footer: text"].insert("insert", "How many rows at the bottom of the file to ignore.") + tdt["GasT [\u00B0C]: text"].insert("insert", "The thermocouples (TCs) that measure the gas's temperature. " + + "The number of TCs can be set arbitrarily via " + + "'Number of TCs measuring GasT0' and 'Number of TCs measuring " + + "GasT,' each of which start counting with GasT1.") + tdt["SampT [\u00B0C]: text"].insert("insert", "The thermocouple that measures the sample temperature.") + tdt["col: text"].insert("insert", "Reference column for corresponding quantity in raw data file.") + tdt["m: text"].insert("insert", "Every entry in the column of data is converted according to m*x + b, " + + "where x is the entry in the column. Use this to convert the column's data " + + "to SI units.") + tdt["b: text"].insert("insert", "See entry for 'm' above.") + tdt["cerr: text"].insert("insert", "Constant uncertainty of the instrument, e.g., +/- 2\u00B0C. " + + "When calculating the uncertainty caused by each instrument, the " + + "application chooses the largest out of the constant uncertainty, the " + + "proportional uncertainty (see below), and the calculated statistical " + + "uncertainty.") + tdt["perr: text"].insert("insert", "Proportional uncertainty of the instrument, e.g., " + + "+/- 0.75% of the measurement (in \u00B0C).") + + # Configure each text widget + for i, symbol in enumerate(td): + # Set width (and other things) of left side + if platform.system() == 'Darwin': + divisor = 9 + addend = 2 + divisor2 = 9 + addend2 = 0 + else: + divisor = 7 + addend = 6 + divisor2 = 6 + addend2 = 0 + # btfont.measure() says how many pixels text+subscript would take to write out. + # For Windows, //7 takes that number and turns it into how many zeros it would take to be that long because + # one zero takes up 7 pixels in this font. The +6 adds one more character to make up for // rounding down + # and 5 more characters to make room. + # For Mac, setting divisor to 9 and addend to 2 seems to look good, but it's unclear why those numbers + # work well + # The unit of width is characters, specifically 0s, not pixels, which is why "Isolation Valve [1/0]" is + # needed. Those words are the longest text on the left side. + td[symbol].configure(width=btfont.measure("Isolation Valve [1/0]") // divisor + addend, + wrap=tk.WORD, height=1) + + # Set height of each right-side widget according to how many lines it will take to display + text_width = btfont.measure(tdt[symbol + "text"].get("1.0", 'end-1c')) // divisor2 + addend2 + text_height = 1 + while text_width >= tdt[symbol + "text"].cget("width"): # Check if the text width > widget's width. + text_height += 1 + text_width -= tdt[symbol + "text"].cget("width") + tdt[symbol + "text"].configure(wrap=tk.WORD, height=text_height) + + td[symbol].insert("insert", symbol, "bold") # Insert text into the left-side widgets + + td[symbol].grid(row=i, column=0, sticky="nse") + tdt[symbol + "text"].grid(row=i, column=1, sticky="nsew") + + # Make the text un-editable (to user and to program) + td[symbol].configure(state="disabled") + tdt[symbol + "text"].configure(state="disabled") + + def show_help_window(self): + """ This allows the window to open correctly """ + self.deiconify() + self.wait_window() + return diff --git a/HyPAT/source_code/application.py b/HyPAT/source_code/application.py index 86f3fbc..ed15dca 100644 --- a/HyPAT/source_code/application.py +++ b/HyPAT/source_code/application.py @@ -7,6 +7,7 @@ from . import permeation_estimates from . import overview_plots from . import permeation_plots +from . import absorption_plots class Application(tk.Tk): @@ -14,16 +15,16 @@ class Application(tk.Tk): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Change the default font for text boxes to TkDefaultFont. This mostly affects - # the help window of the permeation_plot.py's settings, but may affect other places + # the help window of the permeation_plot.py's and absorption_plot.py's settings, but may affect other places default_font = font.nametofont("TkFixedFont") default_font.configure(family=font.nametofont("TkDefaultFont").cget("family"), size=font.nametofont("TkDefaultFont").cget("size")) - self.title("Hydrogen Permeation Analysis Tool") + self.title("Hydrogen Permeation and Absorption Tool") self.resizable(width=None, height=None) self.minsize(900, 600) - # size of gui based of size of widgets + # size of gui based off size of widgets # this could be calculated later but would result in the gui appearing then moving to the center. width = 1429 height = 750 @@ -34,7 +35,7 @@ def __init__(self, *args, **kwargs): self.geometry("{}x{}+{}+{}".format(width, height, pos_right, pos_down)) # Ensure the program stops when the window is closed (counteracts a bug brought in from using - # constrained_layouts = True in the permeation_plots.py code) + # constrained_layouts = True in the permeation_plots.py and absorption_plots.py code) self.protocol("WM_DELETE_WINDOW", self.quit_me) self.storage = data_storage.Storage() @@ -48,7 +49,7 @@ def __init__(self, *args, **kwargs): def quit_me(self): """ Ensure the program ends when window is closed """ - # This counteracts a bug brought in by constrained_layout=True in the permeation_plots code + # This counteracts a bug brought in by constrained_layout=True in the permeation_plots and absorption_plots code # Function found at https://stackoverflow.com/a/55206851 self.quit() self.destroy() @@ -57,7 +58,9 @@ def load_pages(self): """ Load the GUI with the pages containing the calculations """ front_page = permeation_estimates.InputForm(self.notebook, self.storage) # Permeation estimates experiment_plots_page = permeation_plots.PermeationPlots(self.notebook, self.storage) # Permeation plots + absorption_plots_page = absorption_plots.AbsorptionPlots(self.notebook, self.storage) # Absorption plots overview_plots_page = overview_plots.Plots(self.notebook, self.storage) # Theoretical plots (Overview) self.notebook.add(front_page, text="Permeation Estimates") self.notebook.add(experiment_plots_page, text="Permeation Plots") + self.notebook.add(absorption_plots_page, text="Absorption Plots") self.notebook.add(overview_plots_page, text="Overview Plots") diff --git a/HyPAT/source_code/data_storage.py b/HyPAT/source_code/data_storage.py index 462da79..3963ef4 100644 --- a/HyPAT/source_code/data_storage.py +++ b/HyPAT/source_code/data_storage.py @@ -4,7 +4,7 @@ import numpy as np import pandas as pd import os -import platform # allows for Mac vs Windows adaptions +import platform # allows for Mac vs. Windows adaptions class Storage: @@ -16,21 +16,21 @@ def __init__(self): # Create dataframes to store values for updating source spreadsheets. # First the material_data spreadsheet - filename = os.path.join('datafiles', 'material_data.xlsx') + filename = os.path.join('data_files', 'material_data.xlsx') self.material_data = pd.read_excel(filename, header=[0, 1], engine="openpyxl") # openpyxl supports .xlsx but not .xls # set up the index using material names self.material_data.set_index(('Unnamed: 0_level_0', "Material"), inplace=True) self.material_data.rename_axis("Material", axis="index", inplace=True) # Next for the melting_tempK file - melt_filename = os.path.join('datafiles', 'melting_tempK.xlsx') + melt_filename = os.path.join('data_files', 'melting_tempK.xlsx') self.melting_tempK = pd.read_excel(melt_filename, engine="openpyxl") self.melting_tempK.set_index("Material", inplace=True) # Read the o-ring data off an Excel sheet into a convenient dataframe. # (1/2, 1/4 VCR data comes from https://www.swagelok.com/downloads/webcatalogs/en/ms-01-24.pdf, # page 17 Silver Plated Nonretained) - self.oring_filename = os.path.join('datafiles', 'o-ring_data.xlsx') + self.oring_filename = os.path.join('data_files', 'o-ring_data.xlsx') self.oring_info = pd.read_excel(self.oring_filename, header=0, index_col=0) self.oring_info_4file = self.oring_info.copy() # Duplicate used exclusively for saving to the o-ring file @@ -67,7 +67,7 @@ def __init__(self): self.sample_material.trace_add("write", self.update_properties) # Read the file for some default values - self.defaults_filename = os.path.join('datafiles', 'default_entry_vals.xlsx') + self.defaults_filename = os.path.join('data_files', 'default_entry_vals.xlsx') self.defaults_info = pd.read_excel(self.defaults_filename, header=0) # Calibrated Leak Rate @@ -91,8 +91,8 @@ def __init__(self): self.t_L = tk.DoubleVar(value=0) # Estimated time lag self.Phi = tk.DoubleVar(value=0) # Estimated permeability self.flux = tk.DoubleVar(value=0) # Estimated molar flux - self.flux_atoms = tk.DoubleVar(value=0) # Estimated atomic flux - self.Q = tk.DoubleVar(value=0) # Estimated permeation rate + self.flux_atoms = tk.DoubleVar(value=0) # Estimated atomic flux + self.Q = tk.DoubleVar(value=0) # Estimated molecular permeation rate self.del_sP = tk.DoubleVar(value=0) # Estimated rate of pressure increase, Torr/s self.del_sP_Pa = tk.DoubleVar(value=0) # /\, Pa/s self.sP_final = tk.DoubleVar(value=0) # Estimated final secondary side pressure, Torr @@ -104,13 +104,15 @@ def __init__(self): # tolerance used for finding steady state during permeation data analysis self.tol = tk.DoubleVar(value=1e-6) - # minimum number of seconds following t0 before finding a steady state - self.t_del = tk.DoubleVar(value=100) - # default number of data points used in determining leak and/or steady state + # minimum number of seconds following t0 before finding a steady state or equilibrium + self.t_del = tk.DoubleVar(value=100) # Note, as of 5/28/22, this isn't used in absorption_plots.py + # default number of data points used in determining leak, steady state, and/or equilibrium self.gen_dp_range = tk.IntVar(value=30) - # store the permeation calculations - self.PermeationParameters = pd.DataFrame() + # store the transport properties calculations + self.TransportParameters = pd.DataFrame() + self.PTransportParameters = pd.DataFrame() + self.ATransportParameters = pd.DataFrame() def connect_variables(self): """ Set up variables so that functions are triggered if they are changed""" @@ -146,10 +148,10 @@ def load_data(): diffusivity and solubility. Use that information to calculate those respective number for permeability. Read in the melting temp if available. Combine this all into a dataframe and return that dataframe """ # read in diffusivity and solubility data - # todo It'd be nice to have the user select a file similar to "SaveFileExample.xlsx" in the datafiles folder + # todo It'd be nice to have the user select a file similar to "SaveFileExample.xlsx" in the data_files folder # then if two of the three quantities (diffusivity, solubility, permeability) are present, the third # will be calculated. This can be similar to how it is done in overview_plots.py - EditMaterials class. - filename = os.path.join('datafiles', 'material_data.xlsx') + filename = os.path.join('data_files', 'material_data.xlsx') df = pd.read_excel(filename, header=[0, 1], engine="openpyxl") # openpyxl supports .xlsx file format, not .xls # set up the index using material names df.set_index(('Unnamed: 0_level_0', "Material"), inplace=True) @@ -170,7 +172,7 @@ def round_to_13(df_col): df[("Permeability", "max. temp. [K]")] = [min((df[col[3]][i], df[col[7]][i])) for i in range(len(df))] # read in melting temp data - melt_filename = os.path.join('datafiles', 'melting_tempK.xlsx') + melt_filename = os.path.join('data_files', 'melting_tempK.xlsx') df2 = pd.read_excel(melt_filename, engine="openpyxl") df2.set_index("Material", inplace=True) diff --git a/HyPAT/source_code/overview_plots.py b/HyPAT/source_code/overview_plots.py index 6703e75..8788b9b 100644 --- a/HyPAT/source_code/overview_plots.py +++ b/HyPAT/source_code/overview_plots.py @@ -32,7 +32,7 @@ def __init__(self, parent, storage, *args, **kwargs): self.index = list(self.storage.data.index) self.columns = list(self.storage.data.columns) self.item = {} # Container for storing whether a material is selected for plotting - self.exp_data = DataFrame() # Container for data received from permeation_plots + self.exp_data = DataFrame() # Container for data received from permeation_plots or absorption_plots self.start = -1 self.plotted_once = False @@ -49,9 +49,9 @@ def __init__(self, parent, storage, *args, **kwargs): '#8c564b', '#e377c2', '#7f7f7f', '#bcbd22', '#17becf'] self.current_base_color_idx = 0 # for default plotting self.artists1, self.artists2 = [], [] - self.experimental_data0 = {} # Container for the diffusivity from each file loaded into permeation_plots - self.experimental_data1 = {} # Container for the solubility from each file loaded into permeation_plots - self.experimental_data2 = {} # Container for the permeability from each file loaded into permeation_plots + self.experimental_data0 = {} # Container for diffusivity from each file loaded into permeation/absorption_plots + self.experimental_data1 = {} # Container for solubility from each file loaded into permeation/absorption_plots + self.experimental_data2 = {} # Container for permeability from each file loaded into permeation/absorption_plots # make the frames self.create_materials_frame() @@ -114,13 +114,14 @@ def create_materials_frame(self): self.rowconfigure(0, weight=1) self.columnconfigure(1, weight=1) - def plot_permeation(self): - """ plot/remove the experimental permeation data in the overview plots """ + def plot_trans_props(self): + """ plot/remove the experimental permeation/absorption data in the overview plots """ - # Store the old dataframe for when we need to remove old data after new data is selected in permeation_plots + # Store the old dataframe for when we need to remove old data after new data is selected in + # permeation/absorption_plots old_data = self.exp_data # Get the new dataframe from the latest selected folder - self.exp_data = self.storage.PermeationParameters + self.exp_data = self.storage.TransportParameters if self.exp_data.empty and old_data.empty: # If no data has been loaded into HyPAT return @@ -129,7 +130,7 @@ def plot_permeation(self): if self.exp_data.empty: return - # Plot data from permeation_plots tab + # Plot data from permeation/absorption_plots tab for file in self.exp_data.index: self.experimental_data2[file] = self.ax[2].plot( 1000/self.exp_data.loc[file, "Sample Temperature [K]"], @@ -156,7 +157,7 @@ def plot_permeation(self): self.toolbar.update() def toggle_multiple_labels(self): - """ Toggles between being able to see labels while hovering over lines vs being able to see many labels + """ Toggles between being able to see labels while hovering over lines vs. being able to see many labels by clicking on the lines """ if self.b2.config('text')[-1] == 'Enable Multiple Labels': mplcursors.Cursor.remove(self.cursor) # Removes the possibility of double labels @@ -177,7 +178,8 @@ def fit_to_Arrhenius_eqn(self, new_data=False): Arrhenius fit to determine the pre-exponential factors and activation energies """ from scipy.optimize import curve_fit - # define data for analysis according to whether it comes from the permeation plots tab or a user-selected file + # define data for analysis according to whether it comes from the permeation/absorption plots tab or + # a user-selected file if self.exp_data.empty or self.b.config('text')[-1] == 'Add Experimental Data' or new_data: # If there is nothing in the dataframe that holds data or if the button for displaying experimental data is # toggled so that no data is currently displayed or if the button was clicked to select a new file... @@ -191,6 +193,7 @@ def fit_to_Arrhenius_eqn(self, new_data=False): # Read in variables try: T = data["Sample Temperature [K]"] + T_unc = data["Sample Temperature Uncertainty [K]"] P_var = data["Permeability [mol m^-1 s^-1 Pa^-0.5]"] P_unc = data["Permeability Uncertainty [mol m^-1 s^-1 Pa^-0.5]"] D_var = data["Diffusivity [m^2 s^-1]"] @@ -201,9 +204,14 @@ def fit_to_Arrhenius_eqn(self, new_data=False): except KeyError: # This is expected if the file doesn't have the correct headers tk.messagebox.showerror(title="Arrhenius Fit Error", message='File must be formatted like the files generated' - ' by the "Export to Excel" button in the "Permeation Plots" tab."') + ' by the "Export to Excel" button in the "Permeation Plots" tab or' + ' in the "Absorption Plots" tab.') return + # Min and max temperatures, degrees Celsius + Tmin, Tmax = round(min(T) - self.storage.standard_temp, 12), round(max(T) - self.storage.standard_temp, 12) + Tmin_unc, Tmax_unc = T_unc[T.argmin()], T_unc[T.argmax()] # Uncertainties in min and max temps + # The Arrhenius function def f(temp, energy, variable): return variable * np.exp(-energy / (R * temp)) @@ -247,7 +255,8 @@ def f(temp, energy, variable): "Pre-exponential factor for permeability: {:.2e} +/- {:.2e}".format(P_fit[1], P_uncert[1]) + \ " [mol m\u207b\u00b9 s\u207b\u00b9 Pa\u207b\u2070\u1427\u2075]\n" + \ "Activation energy for permeability: {:.2e} +/- {:.2e}".format(P_fit[0], P_uncert[0]) + \ - " [kJ mol\u207b\u00b9]" + " [kJ mol\u207b\u00b9]\n\n" + \ + "Temperature Range: {} [\u00B0C] to {}".format(round(Tmin), round(Tmax)) + " [\u00B0C]" # Create a textbox with the text in it from tkinter import font @@ -256,7 +265,7 @@ def f(temp, energy, variable): else: twidth = 75 fittext = tk.Text(popup, font=font.nametofont("TkDefaultFont"), background=self.mats_frame.cget("background"), - padx=10, pady=10, spacing2=10, width=twidth, height=11) + padx=10, pady=10, spacing2=10, width=twidth, height=13) fittext.insert("insert", fit_texts) fittext.configure(state="disabled") # Turn off editing the text fittext.grid(row=0, column=0, sticky="nsew", padx=2, pady=2) @@ -268,24 +277,28 @@ def f(temp, energy, variable): command=lambda loading=True: self.fit_to_Arrhenius_eqn(loading)) b1.grid(row=0, column=0) b2 = tk.Button(master=Arrhenius_button_frame, text="Save to Excel", - command=lambda pf=P_fit, pu=P_uncert, df=D_fit, du=D_uncert, sf=S_fit, su=S_uncert: - self.save_Arrhenius_to_excel(pf, pu, df, du, sf, su)) + command=lambda pf=P_fit, pu=P_uncert, df=D_fit, du=D_uncert, sf=S_fit, su=S_uncert, + tmin=Tmin, tmax=Tmax, tminu=Tmin_unc, tmaxu=Tmax_unc: + self.save_Arrhenius_to_excel(pf, pu, df, du, sf, su, tmin, tmax, tminu, tmaxu)) b2.grid(row=0, column=1) - def save_Arrhenius_to_excel(self, P_fit, P_uncert, D_fit, D_uncert, S_fit, S_uncert): + def save_Arrhenius_to_excel(self, P_fit, P_uncert, D_fit, D_uncert, S_fit, S_uncert, Tmin, Tmax, Tmin_unc, Tmax_unc): """ Save the pre-exponential factors and activation energies determined via Arrhenius fit to Excel """ filename = asksaveasfilename(initialdir=os.path.dirname(__file__), defaultextension=".xlsx") if filename != '': - data = {'Value': [D_fit[1], D_fit[0], S_fit[1], S_fit[0], P_fit[1], P_fit[0]], - 'Uncertainty': [D_uncert[1], D_uncert[0], S_uncert[1], S_uncert[0], P_uncert[1], P_uncert[0]], + data = {'Value': [D_fit[1], D_fit[0], S_fit[1], S_fit[0], P_fit[1], P_fit[0], Tmin, Tmax], + 'Uncertainty': [D_uncert[1], D_uncert[0], S_uncert[1], S_uncert[0], + P_uncert[1], P_uncert[0], Tmin_unc, Tmax_unc], 'Units': ['[m\u00b2 s\u207b\u00b9]', '[kJ mol\u207b\u00b9]', '[mol m\u207b\u00B3 Pa\u207b\u2070\u1427\u2075]', '[kJ mol\u207b\u00b9]', - '[mol m\u207b\u00b9 s\u207b\u00b9 Pa\u207b\u2070\u1427\u2075]', '[kJ mol\u207b\u00b9]']} + '[mol m\u207b\u00b9 s\u207b\u00b9 Pa\u207b\u2070\u1427\u2075]', '[kJ mol\u207b\u00b9]', + '[\u00B0C]', '[\u00B0C]']} # Creates pandas DataFrame. df = DataFrame(data, index=['Pre-exponential factor for diffusivity:', 'Activation energy for diffusivity:', 'Pre-exponential factor for solubility:', 'Activation energy for solubility:', - 'Pre-exponential factor for permeability:', 'Activation energy for permeability:']) + 'Pre-exponential factor for permeability:', 'Activation energy for permeability:', + 'Minimum Sample Temperature:', 'Maximum Sample Temperature:']) df.to_excel(filename) def reset_scroll_region(self, event=None): @@ -411,7 +424,10 @@ def create_plot_frame(self, row=0, column=0): self.fig = Figure() self.ax = [self.fig.add_subplot(1, 3, i) for i in range(1, 4)] # The below line helps keep the graphs looking nice when the window gets made small. Note that tight_layout is - # an experimental feature as of 10/16/2021 and may be changed unpredictably. + # an experimental feature as of 10/16/2021 and may be changed unpredictably. + # As of 9/6/22, according to https://matplotlib.org/stable/api/prev_api_changes/api_changes_3.5.0.html there + # was an update. This source says set_tight_layout is still fine: + # https://matplotlib.org/stable/api/figure_api.html self.fig.set_tight_layout("True") # Format axes @@ -434,7 +450,7 @@ def create_plot_frame(self, row=0, column=0): self.toolbar = NavigationToolbar2Tk(self.canvas, plot_frame) # add the buttons to the toolbar - self.b = tk.Button(master=self.toolbar, text="Add Experimental Data", command=self.plot_permeation) + self.b = tk.Button(master=self.toolbar, text="Add Experimental Data", command=self.plot_trans_props) self.b.pack(side="left", padx=1) self.b2 = tk.Button(master=self.toolbar, text="Enable Multiple Labels", command=self.toggle_multiple_labels) self.b2.pack(side="left", padx=1) @@ -511,7 +527,7 @@ def celsius_to_positions(labels): self.ax[i].set_xlim(self.xmin, self.max) # top axis - self.axC[i].set_xlabel(u"Temperature, T (\u2103)") + self.axC[i].set_xlabel(" Temperature, T (\u00B0C) ") # Space at the start&end b/c on Mac, T&e are too close self.axC[i].set_xlim(self.xmin, self.max) # redo the top ticks so that they line up with proper positions for Celsius @@ -580,7 +596,7 @@ def __init__(self, storage, pos, *args, **kwargs): self.add_entry = widgets.add_entry self.title("Add or Edit Materials") - self.resizable(width=False, height=False) + # self.resizable(width=False, height=False) self.minsize(400, 170) # gui_x/y values determined by running self.updateidletasks() at the end of self.__init__ and then printing size @@ -679,7 +695,7 @@ def create_inputs(self): tk.Label(parent, text="Solubility").grid(row=0, column=4) self.add_entry(self, parent, variable=self.inputs, key="K0", text="K", innersubscript="0", innertext=":", subscript="", tvar=self.K0, - units=u"[mol m\u207b\u00B3 Pa\u207b\u2070\u1427\u2075]", # mol m^-3 Pa^-0.5 + units="[mol m\u207b\u00B3 Pa\u207b\u2070\u1427\u2075]", # mol m^-3 Pa^-0.5 row=1, column=3, in_window=True, command=lambda tvar, variable, key, pf: self.calc_DKP(tvar, variable, key, pf)) self.add_entry(self, parent, variable=self.inputs, key="E_K", text="E", innersubscript="K", innertext=":", @@ -694,12 +710,12 @@ def create_inputs(self): command=lambda tvar, variable, key, pf: self.calc_tmax(tvar, variable, key, pf)) tk.Label(parent, text="Permeability").grid(row=0, column=7) - self.add_entry(self, parent, variable=self.inputs, key="P0", text="P", innersubscript="0", innertext=":", + self.add_entry(self, parent, variable=self.inputs, key="P0", text="\u03A6", innersubscript="0", innertext=":", subscript="", tvar=self.P0, # units=[mol m^-1 s^-1 Pa^-0.5] - units=u"[mol m\u207b\u00b9 s\u207b\u00b9 Pa\u207b\u2070\u1427\u2075]", + units="[mol m\u207b\u00b9 s\u207b\u00b9 Pa\u207b\u2070\u1427\u2075]", row=1, column=6, in_window=True, command=lambda tvar, variable, key, pf: self.calc_DKP(tvar, variable, key, pf)) - self.add_entry(self, parent, variable=self.inputs, key="E_P", text="E", innersubscript="P", innertext=":", + self.add_entry(self, parent, variable=self.inputs, key="E_P", text="E", innersubscript="\u03A6", innertext=":", subscript="", tvar=self.E_P, units="[kJ mol\u207b\u00b9]", # [kJ mol^-1] row=2, column=6, in_window=True, command=lambda tvar, variable, key, pf: self.calc_E(tvar, variable, key, pf)) @@ -1002,7 +1018,7 @@ def submit(self): self.K0.get(), self.E_K.get(), self.tmin_s.get(), self.tmax_s.get()] # overwrite the old source file with the updated dataframe - material_filename = os.path.join('datafiles', 'material_data.xlsx') + material_filename = os.path.join('data_files', 'material_data.xlsx') self.storage.material_data.to_excel(material_filename) # This next bit is needed because to_excel formats named indexes poorly import openpyxl @@ -1015,7 +1031,7 @@ def submit(self): # the melting temperatures source file, then update that source file. if not np.isnan(self.tmelt.get()): self.storage.melting_tempK.loc[self.material.get()] = [self.tmelt.get()] - self.storage.melting_tempK.to_excel(os.path.join('datafiles', 'melting_tempK.xlsx')) + self.storage.melting_tempK.to_excel(os.path.join('data_files', 'melting_tempK.xlsx')) self.changed = True self.destroy() @@ -1050,7 +1066,7 @@ def remove_material(self): self.storage.material_data.drop(index=self.material.get(), inplace=True) # overwrite the old source file with the updated dataframe - material_filename = os.path.join('datafiles', 'material_data.xlsx') + material_filename = os.path.join('data_files', 'material_data.xlsx') self.storage.material_data.to_excel(material_filename) # This next bit is needed because to_excel formats named indexes poorly import openpyxl @@ -1063,7 +1079,7 @@ def remove_material(self): # updating the melting temperatures source file, then update that source file. if self.material.get() in self.storage.melting_tempK.index: self.storage.melting_tempK.drop(index=self.material.get(), inplace=True) - self.storage.melting_tempK.to_excel(os.path.join('datafiles', 'melting_tempK.xlsx')) + self.storage.melting_tempK.to_excel(os.path.join('data_files', 'melting_tempK.xlsx')) self.changed = True self.destroy() # Closes Add/Edit Materials window diff --git a/HyPAT/source_code/permeation_estimates.py b/HyPAT/source_code/permeation_estimates.py index d8595d9..8237b2f 100644 --- a/HyPAT/source_code/permeation_estimates.py +++ b/HyPAT/source_code/permeation_estimates.py @@ -499,7 +499,7 @@ def submit_4orings(self): [self.D_ring.get(), self.d_ring.get(), self.x_ring.get()] # overwrite the old source file with the updated dataframe - oring_filename = os.path.join('datafiles', 'o-ring_data.xlsx') + oring_filename = os.path.join('data_files', 'o-ring_data.xlsx') self.storage.oring_info_4file.to_excel(oring_filename) self.orings_changed = True @@ -532,7 +532,7 @@ def remove_oring(self): if self.ring.get() in self.storage.oring_info_4file.index: # Check that the O-ring's in this dataframe self.storage.oring_info_4file.drop(index=self.ring.get(), inplace=True) # overwrite the old source file with the updated dataframe - oring_filename = os.path.join('datafiles', 'o-ring_data.xlsx') + oring_filename = os.path.join('data_files', 'o-ring_data.xlsx') self.storage.oring_info_4file.to_excel(oring_filename) self.orings_changed = True @@ -575,7 +575,7 @@ def submit_4default(self): self.storage.defaults_info.loc[0, "Secondary Side Volume [cc]"] = self.sV.get() # overwrite the old source file with the updated dataframe - default_entry_vars_filename = os.path.join('datafiles', 'default_entry_vals.xlsx') + default_entry_vars_filename = os.path.join('data_files', 'default_entry_vals.xlsx') self.storage.defaults_info.to_excel(default_entry_vars_filename, index=False) self.defaults_changed = True diff --git a/HyPAT/source_code/permeation_plots.py b/HyPAT/source_code/permeation_plots.py index 7d87138..87bfeef 100644 --- a/HyPAT/source_code/permeation_plots.py +++ b/HyPAT/source_code/permeation_plots.py @@ -12,10 +12,10 @@ import os from .data_storage import Widgets, LoadingScreen from scipy.optimize import curve_fit -import mplcursors # adds the hover over feature. This library has good documentation +import mplcursors # adds the hover-over feature to labels. This library has good documentation import openpyxl -import platform # allows for Mac vs Windows adaptions -# make certain warnings appear as errors so they can be caught using a try/except clause +import platform # allows for Mac vs. Windows adaptions +# make certain warnings appear as errors, allowing them to be caught using a try/except clause import warnings from scipy.optimize import OptimizeWarning warnings.simplefilter("error", OptimizeWarning) @@ -54,7 +54,7 @@ def __init__(self, parent, storage, *args, **kwargs): self.add_entry = self.widgets.add_entry self.add_entry3 = self.widgets.add_entry3 - # to get 2 rows over three we need two more frames + # to get 2 rows over 3, we need two more frames self.top_frame = tk.Frame(self, bd=10) self.top_frame.grid(row=0, column=0, sticky="nsew") self.bottom_frame = tk.Frame(self, bd=10) @@ -66,6 +66,7 @@ def __init__(self, parent, storage, *args, **kwargs): self.inputs = {} self.directory = "" + self.loading_data = False # Keep track of whether data is currently being loaded by this tab self.refreshing = False # If false, ask for a directory when select_file gets called self.file_type = "" self.time_type = 0 # Used to determine which converter function to use for the time instrument @@ -88,8 +89,8 @@ def __init__(self, parent, storage, *args, **kwargs): self.volume = tk.DoubleVar(value=(self.storage.defaults_info["Secondary Side Volume [cc]"][0]) * 10 ** -6) self.ss_tol = tk.DoubleVar(value=self.storage.tol.get()) # Tolerance for finding the steady state self.ss_t_del = tk.DoubleVar(value=self.storage.t_del.get()) # Minimum time after t0 before looking for ss - self.leak_range = {} # Dictionary for holding the range used in determining leaks for each file - self.ss_range = {} # Dictionary for holding the range used in determining leaks for each file + self.leak_range = {} # Dict for holding the range used in finding leaks and initial values for each file + self.ss_range = {} # Dict for holding the range used in finding ss and ss values for each file self.gen_leak_range = tk.IntVar(value=self.storage.gen_dp_range.get()) # Default range to determine leak over self.gen_ss_range = tk.IntVar(value=self.storage.gen_dp_range.get()) # Default range to determine ss over # default area is the inner area of the default O-ring @@ -107,9 +108,9 @@ def __init__(self, parent, storage, *args, **kwargs): # permeation variables self.t0 = {} # time when isolation valve opens (s) self.tss = {} # time when steady state pressure is achieved (s) - self.pleak = {} # linear fit for leak rate - self.pss = {} # linear fit for steady state - self.Prate = {} # steady state permeation rates (pss minus pleak) + self.pleak_lf = {} # linear fit for leak rate + self.pss_lf = {} # linear fit for steady state + self.Prate = {} # steady state permeation rates (pss_lf minus pleak_lf) self.Prate_err = {} self.F = {} # flux self.F_err = {} @@ -119,7 +120,7 @@ def __init__(self, parent, storage, *args, **kwargs): self.Tg0 = {} # Temperature of gas at t0 (using an average over time and instruments) (K) self.Tgss = {} # Temperature of gas at tss (using an average over time and instruments) (K) self.Tgss_err = {} - self.artists = [] # store points for Perm/Dif/Sol vs Temp graph + self.artists = [] # store points for Perm/Dif/Sol vs. Temp graph self.Tsamp0 = {} # Sample temperature at t0 (using an average over time and instruments) (K) self.Tsampss = {} # Sample temperature at tss (using an average over time and instruments) (K) self.Tsampss_err = {} @@ -131,12 +132,14 @@ def __init__(self, parent, storage, *args, **kwargs): # diffusivity variables self.intercept = {} self.tlag = {} - self.D = {} + self.D = {} # diffusivity self.D_err = {} + self.A = {} # proportionality constant + self.dt = {} # additive time constant self.D_time = {} # time over which D is calculated self.lhs = {} # for the diffusivity optimization comparison - self.rhs = {} - self.rhs_cf = {} # curve_fit (for debugging) + self.rhs = {} # time-lag method + self.rhs_cf = {} # curve_fit # solubility variables self.Ks = {} @@ -157,33 +160,33 @@ def __init__(self, parent, storage, *args, **kwargs): # add frames and buttons to view important variables entry_row = 1 - self.add_entry3(self, self.frame, variable=self.inputs, key="sl", text="Sample Thickness: sl", - subscript="", tvar1=self.sample_thickness, tvar2=self.sample_thickness_err, + self.add_entry3(self, self.frame, variable=self.inputs, key="ls", text="Sample Thickness: l", + subscript="s", tvar1=self.sample_thickness, tvar2=self.sample_thickness_err, update_function=lambda tvar, var_type, key: self.update_function(tvar, var_type, key), units="[mm]", row=entry_row) self.add_entry3(self, self.frame, variable=self.inputs, key="A_perm", text="Permeation Surface Area: A", subscript="perm", tvar1=self.A_perm, tvar2=self.A_perm_err, update_function=lambda tvar, var_type, key: self.update_function(tvar, var_type, key), - units=u"[m\u00b2]", row=entry_row + 1) # "[m^2]" + units="[m\u00b2]", row=entry_row + 1) # "[m^2]" - self.add_entry3(self, self.frame, variable=self.inputs, key="V", text="Secondary Side Volume:", - subscript="", tvar1=self.volume, tvar2=self.volume_err, units=u"[m\u00B3]", # "[m^3]" + self.add_entry3(self, self.frame, variable=self.inputs, key="V", text="Secondary Side Volume: V", + subscript="sec", tvar1=self.volume, tvar2=self.volume_err, units="[m\u00B3]", # "[m^3]" update_function=lambda tvar, var_type, key: self.update_function(tvar, var_type, key), row=entry_row + 2) label_row = 4 - self.add_text2(self.frame, text="Steady State Permeation Rate:", subscript="", tvar1=self.Prate_label, + self.add_text2(self.frame, text="Permeation Rate: dP/dt", subscript="", tvar1=self.Prate_label, tvar2=self.Prate_err_label, units="[Pa s\u207b\u00b9]", row=label_row) - self.add_text2(self.frame, text="Molar Flux:", subscript="", tvar1=self.F_label, tvar2=self.F_err_label, - units=u"[mol m\u207b\u00b2 s\u207b\u00b9]", row=label_row + 1) # "[mol/m^2 s]" - self.add_text2(self.frame, text="Permeability:", subscript="", tvar1=self.Phi_label, tvar2=self.Phi_err_label, - units=u"[mol m\u207b\u00b9 s\u207b\u00b9 Pa\u207b\u2070\u1427\u2075]", # "[mol/msPa^0.5]" + self.add_text2(self.frame, text="Molar Flux: J", subscript="inf", tvar1=self.F_label, tvar2=self.F_err_label, + units="[mol m\u207b\u00b2 s\u207b\u00b9]", row=label_row + 1) # "[mol/m^2 s]" + self.add_text2(self.frame, text="Permeability: \u03A6", subscript="", tvar1=self.Phi_label, tvar2=self.Phi_err_label, + units="[mol m\u207b\u00b9 s\u207b\u00b9 Pa\u207b\u2070\u1427\u2075]", # "[mol/msPa^0.5]" row=label_row + 2) - self.add_text2(self.frame, text="Diffusivity:", subscript="", tvar1=self.D_label, tvar2=self.D_err_label, - units=u"[m\u00b2 s\u207b\u00b9]", row=label_row + 3) - self.add_text2(self.frame, text="Solubility:", subscript="", tvar1=self.K_label, tvar2=self.K_err_label, - units=u"[mol m\u207b\u00B3 Pa\u207b\u2070\u1427\u2075]", row=label_row + 4) + self.add_text2(self.frame, text="Diffusivity: D", subscript="", tvar1=self.D_label, tvar2=self.D_err_label, + units="[m\u00b2 s\u207b\u00b9]", row=label_row + 3) + self.add_text2(self.frame, text="Solubility: K", subscript="s", tvar1=self.K_label, tvar2=self.K_err_label, + units="[mol m\u207b\u00B3 Pa\u207b\u2070\u1427\u2075]", row=label_row + 4) button_row = 10 self.b0 = ttk.Button(self.frame, text='Choose folder', command=self.select_file) @@ -197,11 +200,11 @@ def __init__(self, parent, storage, *args, **kwargs): b2 = ttk.Button(self.frame, text='Close popout plots', command=lambda: plt.close('all')) b2.grid(row=button_row + 1, column=1, sticky="ew") - b3 = ttk.Button(self.frame, text='Export to Excel', command=self.export_data) - b3.grid(row=button_row + 1, column=3, sticky="ew") + self.coord_b = ttk.Button(self.frame, text="Enable Coordinates", command=self.toggle_coordinates) + self.coord_b.grid(row=button_row + 1, column=3, sticky="ew") - b4 = ttk.Button(self.frame, text='Save current figures', command=self.save_figures) - b4.grid(row=button_row + 1, column=4, sticky="w") + b3 = ttk.Button(self.frame, text='Save current figures', command=self.save_figures) + b3.grid(row=button_row + 1, column=4, sticky="w") menu_row = button_row + 2 self.add_text0(self.frame, text="Current File:", subscript="", row=menu_row) @@ -216,12 +219,18 @@ def __init__(self, parent, storage, *args, **kwargs): self.add_text0(self.frame, text="Current Measurement:", subscript="", row=menu_row + 1) self.PDK_menu = tk.OptionMenu(self.frame, self.current_variable, 'Permeability', 'Diffusivity', 'Solubility', 'Flux') - self.PDK_menu.grid(row=menu_row + 1, column=1, columnspan=4, sticky='ew') + self.PDK_menu.grid(row=menu_row + 1, column=1, columnspan=3, sticky='ew') self.current_variable.trace_add("write", self.update_PDK_plot) + b4 = ttk.Button(self.frame, text='Export to Excel', command=self.export_data) + b4.grid(row=menu_row + 1, column=4, sticky="ew") + + # Store the "set_message" functions + self.message_function = {} + # create bottom left plot - self.ax_title = "Permeability vs Temperature" - self.ax_xlabel = "Temperature (\u2103)" + self.ax_title = "Permeability vs. Temperature" + self.ax_xlabel = " Temperature (\u00B0C) " # Space at the beginning and end is b/c on Mac, T & e are too close self.ax_ylabel = "Permeability (mol m$^{-1}\, $s$^{-1}\, $Pa$^{-0.5}$)" self.fig, self.ax, self.canvas, self.toolbar = self.add_plot(self.bottom_frame, xlabel=self.ax_xlabel, @@ -229,7 +238,7 @@ def __init__(self, parent, storage, *args, **kwargs): title=self.ax_title, row=0, column=0, axes=[.3, .15, .65, .75]) # create top right plot - self.ax1_title = "Pressure vs Time" + self.ax1_title = "Pressure vs. Time" self.ax1_xlabel = "Time (s)" self.ax1_ylabel = "Secondary Pressure (Pa)" self.ax12_ylabel = "Primary Pressure (Pa)" @@ -243,14 +252,14 @@ def __init__(self, parent, storage, *args, **kwargs): # The top right plot would flicker every time the order of magnitude of the cursor's location would change, so # the following line changes the format of the displayed x & y coordinates. The specific format chosen was the # result of trial and error in conjunction with editing the text of the entry boxes for ss_tol and ss_t_del - # (which are now accessed by a button) - self.ax12.format_coord = lambda x, y: "({:3g}, ".format(y) + "{:3g})".format(x) + # (which are now accessed by a button). This is kept in case coordinates are changed to on by default again. + self.ax12.format_coord = lambda x, y: "({:3g}, ".format(x) + "{:3g})".format(y) # Create bottom middle plot - self.ax2_title = "Permeability vs Time" + self.ax2_title = "Permeability vs. Time" self.ax2_xlabel = "Time (s)" self.ax2_ylabel = r"Permeability (mol m$^{-1}\, $s$^{-1}\, $Pa$^{-0.5}$)" - self.ax22_ylabel = u"Sample Temperature (\u2103)" + self.ax22_ylabel = "Sample Temperature (\u00B0C)" self.fig2, self.ax2, self.canvas2, self.toolbar2 = self.add_plot(self.bottom_frame, xlabel=self.ax2_xlabel, ylabel=self.ax2_ylabel, @@ -262,17 +271,62 @@ def __init__(self, parent, storage, *args, **kwargs): # Create bottom right plot self.ax3_title = "Diffusivity Optimization Comparison" self.ax3_xlabel = "Time (s)" - self.ax3_ylabel = r"(J$_t$ - J$_0$)/(J$_{inf}$ - J$_0$)" + self.ax3_ylabel = r"$(~J_{\mathrm{t}} - J_{\mathrm{0}}~)~/~(~J_{\mathrm{inf}} - J_{\mathrm{0}}~)$" self.fig3, self.ax3, self.canvas3, self.toolbar3 = self.add_plot(self.bottom_frame, xlabel=self.ax3_xlabel, ylabel=self.ax3_ylabel, title=self.ax3_title, row=0, column=2, axes=[.15, .15, .75, .75]) - # Counteracts a bug of constrained_layout=True which causes four plots to be generated but not shown until a + # Turn off coordinates to avoid layout="constrained" causing the plots to shift constantly + self.ax.format_coord = lambda x, y: '' + self.ax1.format_coord = lambda x, y: '' + self.ax12.format_coord = lambda x, y: '' + self.ax2.format_coord = lambda x, y: '' + self.ax22.format_coord = lambda x, y: '' + self.ax3.format_coord = lambda x, y: '' + + # Counteracts a bug of layout="constrained" which causes four plots to be generated but not shown until a # Popout Plot button is clicked plt.close("all") + def toggle_coordinates(self): + """ Toggles between being able to see coordinates while hovering over plots """ + if self.coord_b.config('text')[-1] == 'Enable Coordinates': + # Turn on coordinates + self.ax.format_coord = lambda x, y: "({:3g}, ".format(x) + "{:3g})".format(y) + self.ax1.format_coord = lambda x, y: "({:3g}, ".format(x) + "{:3g})".format(y) + self.ax12.format_coord = lambda x, y: "({:3g}, ".format(x) + "{:3g})".format(y) + self.ax2.format_coord = lambda x, y: "({:3g}, ".format(x) + "{:3g})".format(y) + self.ax22.format_coord = lambda x, y: "({:3g}, ".format(x) + "{:3g})".format(y) + self.ax3.format_coord = lambda x, y: "({:3g}, ".format(x) + "{:3g})".format(y) + # Turn on status bar + self.toolbar.set_message = self.message_function[self.ax] + self.toolbar1.set_message = self.message_function[self.ax1] + self.toolbar2.set_message = self.message_function[self.ax2] + self.toolbar3.set_message = self.message_function[self.ax3] + self.coord_b.config(text='Disable Coordinates') # Toggle the text so next time, it goes to the "else" part + else: + # Turn off coordinates + self.ax.format_coord = lambda x, y: '' + self.ax1.format_coord = lambda x, y: '' + self.ax12.format_coord = lambda x, y: '' + self.ax2.format_coord = lambda x, y: '' + self.ax22.format_coord = lambda x, y: '' + self.ax3.format_coord = lambda x, y: '' + # Turn off status bar + self.toolbar.set_message("") + self.toolbar1.set_message("") + self.toolbar2.set_message("") + self.toolbar3.set_message("") + self.toolbar.set_message = lambda s: "" + self.toolbar1.set_message = lambda s: "" + self.toolbar2.set_message = lambda s: "" + self.toolbar3.set_message = lambda s: "" + self.coord_b.config(text='Enable Coordinates') # Toggle the text so next time, it goes to the "if" part + self.canvas.draw() + self.toolbar.update() + def update_function(self, tvar, var_type, key): """ Checks if the user entry is a number (float or int). If not, revert the entered string to its prior state. Wanted to have this functionality in the widgets class, but it wasn't working. @@ -301,7 +355,7 @@ def update_function(self, tvar, var_type, key): def add_plot(self, parent, xlabel='', ylabel='', title='', row=0, column=0, rowspan=1, axes=[.1, .15, .8, .75]): """ Create a plot according to variables passed in (parent frame, x-label, y-label, title, row, and column). - axes are kept in case constrained_layout has problems. """ + axes are kept in case layout="constrained" has problems. """ # location of main plot frame = tk.Frame(parent) frame.grid(row=row, column=column, rowspan=rowspan, sticky="nsew") @@ -310,9 +364,11 @@ def add_plot(self, parent, xlabel='', ylabel='', title='', row=0, column=0, rows The above website says the following as of 10/16/2021: "Currently Constrained Layout is experimental. The behaviour and API are subject to change, or the whole functionality may be removed without a deprecation period..." + As of 9/6/22, according to https://matplotlib.org/stable/api/prev_api_changes/api_changes_3.5.0.html there + was an update. This source says to use layout="constrained": https://matplotlib.org/stable/api/figure_api.html """ - fig, ax = matplotlib.pyplot.subplots(constrained_layout=True) - # If something goes wrong with contrained_layout, comment out the above line and uncomment the two lines below + fig, ax = matplotlib.pyplot.subplots(layout="constrained") + # If something's wrong with layout="constrained", comment out the above line and uncomment the two lines below # fig = Figure() # ax = fig.add_axes(axes) # [left, bottom, width, height] @@ -331,34 +387,55 @@ def add_plot(self, parent, xlabel='', ylabel='', title='', row=0, column=0, rows b.pack(side="left", padx=1) # Add a button that allows user input of various steady state variables - if title == 'Pressure vs Time': + if title == 'Pressure vs. Time': entry_frame = tk.Frame(toolbar) entry_frame.pack(side="left", padx=1) b = tk.Button(entry_frame, text='Steady State Variables', command=self.adjust_ss_vars) b.grid(row=0, column=4, sticky="w") + # Store and turn off the capability to update the status bar + self.message_function[ax] = toolbar.set_message + toolbar.set_message = lambda s: "" + toolbar.update() canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=True) return fig, ax, canvas, toolbar - def refresh_graphs(self): + def refresh_graphs(self, loading_warning=True): """ Sets self.refreshing = True so that, when select_file is called, HyPAT doesn't ask for a directory. Calls select_file, which reloads all the data using filenames previously uploaded and plots them """ + if self.loading_data and loading_warning: + proceed = tk.messagebox.askyesno(title="Loading Data", + message="Warning: Data is currently loading. Proceeding may cause " + + "unexpected or erroneous results. Do you wish to proceed?") + if not proceed: + return self.refreshing = True self.select_file() def select_file(self): """ Facilitates loading data and plotting the permeation tab plots """ + if self.loading_data and not self.refreshing: + proceed = tk.messagebox.askyesno(title="Loading Data", + message="Warning: Data is currently loading. Proceeding may cause " + + "unexpected or erroneous results. Do you wish to proceed?") + if not proceed: + return + # If select_file was called because the user is choosing a new folder (if self.refreshing=False), # then ask them for a directory from which to get data. If not, set self.refreshing back to false and # continue with the previously loaded data if not self.refreshing: - self.directory = askdirectory(initialdir=os.path.dirname(__file__)) + # Save to a temp variable in case user cancels loading a new folder + temp_dir = askdirectory(initialdir=os.path.dirname(__file__)) + if temp_dir: + self.directory = temp_dir else: self.refreshing = False + temp_dir = True # temp_dir is truthy unless the user cancels loading the data - if self.directory: + if self.directory and temp_dir: self.options = [file for file in os.listdir(self.directory) if (os.path.splitext(file)[1] == '.xls' or os.path.splitext(file)[1] == '.xlsx')] if not self.options: @@ -366,8 +443,25 @@ def select_file(self): message="Please select a folder with data.") return + self.loading_data = True + try: + self.get_persistent_vars() # Loads variables from a data file for use in analysis + except Exception as e: + # If you want to see the traceback as part of the error text, change the below value to true + want_traceback = False + if want_traceback: + import traceback + full_traceback = "\n" + traceback.format_exc() + else: + full_traceback = "" + + showerror('Loading Error', 'Unknown Error while obtaining persistent variables. No files were' + + ' processed. The following exception was raised: "' + str(e) + '".' + + full_traceback + '\n\n') + self.loading_data = False + return + self.datafiles.clear() # Clear the dictionary to avoid old data points appearing in PDK plot - self.get_persistent_vars() # Loads data from the permeation input file-format file self.error_texts = "" # Reset error_texts each time a folder is loaded # Create the progress bar @@ -377,9 +471,11 @@ def select_file(self): # loop through files, test to make sure they will work, then process them i = 0 + i2 = 0 while i < len(self.options): # While loop required so that files don't get skipped when a pop is needed filename = self.options[i] i += 1 + i2 += 1 # check if the file is good status = self.check_file(os.path.join(self.directory, filename)) @@ -391,13 +487,19 @@ def select_file(self): self.days = -1 self.extra_time = 0 - try: # Catch most other errors than can happen while processing the data + try: # Catch most other errors that can happen while processing the data self.datafiles[filename] = self.extract_data(os.path.join(self.directory, filename)) # Find row number where the Isolation Valve is 1st opened after being closed (aka, the start of # the experiment). Done in a try/except clause in case no opening is registered try: - self.t0[filename] = np.where((self.datafiles[filename])['Isolation Valve'] == 0)[0][-1] + 1 + _t0 = np.where((self.datafiles[filename])['Isolation Valve'] == 0)[0] + closed = np.where(np.diff(_t0) > 1)[0] + if not closed.any(): # if the isolation valve wasn't closed again after being opened... + # ...set t0 to be the data point after the last zero in the file + self.t0[filename] = _t0[-1] + 1 + else: # else, set t0 to be the data point after the last zero before the valve is opened + self.t0[filename] = _t0[closed[0]] + 1 except IndexError: self.error_texts += "Loading Error with file " + filename + \ ". Incorrect Isolation Valve format.\n\n" @@ -410,7 +512,7 @@ def select_file(self): self.calculate_solubility(filename, self.datafiles[filename]) except Exception as e: # If you want to see the traceback as part of the error text, change the below value to true - want_traceback = False + want_traceback = True if want_traceback: import traceback full_traceback = "\n" + traceback.format_exc() @@ -419,15 +521,25 @@ def select_file(self): self.error_texts += "Unknown Error with file " + filename + '. The following exception was' + \ ' raised: "' + str(e) + '".' + full_traceback + '\n\n' - self.datafiles.pop(filename) + try: + self.datafiles.pop(filename) + except KeyError: + self.error_texts += "Note: " + filename + ' was never successfully loaded.' + '\n\n' status = False if not status: # If something is wrong with the file or with analyzing the file self.options.pop(self.options.index(filename)) i -= 1 # Update the progress bar - pb.update_progress_bar(100//pb_len, i) - + try: + pb.update_progress_bar(100 // pb_len, i2) + except tk.TclError: # if the user closed the progress bar window + # clear out remaining files + while i < len(self.options): + self.options.pop(i) + tk.messagebox.showwarning("Loading Canceled", "Loading has been canceled.") + self.error_texts += "Warning: Not all files in selected folder were analyzed.\n\n" + break pb.destroy() # Close the progress bar self.update_dataframe() @@ -456,6 +568,7 @@ def select_file(self): err_scrollbar = tk.Scrollbar(popup, command=errortext.yview) err_scrollbar.grid(row=0, column=1, sticky="nsew") errortext.configure(wrap=tk.WORD, yscrollcommand=err_scrollbar.set) + self.loading_data = False def check_file(self, filename): """ method to check Excel files for any problems, and to move on if there is an issue. @@ -494,7 +607,7 @@ def update_dataframe(self): # this will recreate the dataframe each time df = pd.DataFrame() for filename in self.options: - df = df.append(pd.DataFrame( + df = pd.concat([df, pd.DataFrame( {"Gas Temperature [K]": self.Tgss[filename], "Gas Temperature Uncertainty [K]": self.Tgss_err[filename], "Sample Temperature [K]": self.Tsampss[filename], @@ -509,18 +622,28 @@ def update_dataframe(self): "Diffusivity Uncertainty [m^2 s^-1]": self.D_err[filename], "Solubility [mol m^-3 Pa^-0.5]": self.Ks[filename], "Solubility Uncertainty [mol m^-3 Pa^-0.5]": self.Ks_err[filename], + "Proportionality Constant A": self.A[filename], + "Additive Time Constant dt [s]": self.dt[filename] }, index=[filename] - )) - self.storage.PermeationParameters = df + )]) + self.storage.TransportParameters = df + self.storage.PTransportParameters = df def export_data(self): """ Loads dataframe containing the information for each material into an Excel sheet """ + if self.loading_data: + proceed = tk.messagebox.askyesno(title="Loading Data", + message="Warning: Data is currently loading. Proceeding may cause " + + "unexpected or erroneous results. Do you wish to proceed?") + if not proceed: + return + if self.current_file.get() != "No files yet": save_filename = asksaveasfilename(initialdir=os.path.dirname(__file__), initialfile="test", defaultextension="*.xlsx") if save_filename != '': - self.storage.PermeationParameters.to_excel(save_filename) + self.storage.PTransportParameters.to_excel(save_filename) else: showerror(message='Please load data first.') @@ -534,36 +657,58 @@ def l2n(self, col): def unit_conversion(self, value, multi, add): # todo This seems to do things cell by cell. Maybe find another way? """ Returns the linearly adjusted input according to multi and add """ - return float(value) * multi + add + if value == "": + return float("NaN") + try: + new_val = float(value) * multi + add + except ValueError: + self.error_texts += "Data Extraction Warning. The following was submitted to HyPAT for conversion" + \ + " into seconds: " + str(value) + ". This triggered a ValueError. The dataframe " + \ + " location corresponding to this term has been set to NaN. No further information" \ + " is available.\n\n" + return float("NaN") + return new_val def get_seconds(self, date_time): """ Converts a datetime variable into seconds (excluding the date) """ - # Check to see if the day has changed between one cell and the next - if self.days == -1: # Equals -1 iff this is the first term in the column being loaded. Resets with each file + if self.days == -1: # Equals -1 if this is the first term in the column being loaded. Resets with each file self.this_date_time = date_time self.init_time = date_time.hour * 3600 + date_time.minute * 60 + date_time.second + \ date_time.microsecond * 10 ** -6 last_date_time = self.this_date_time self.this_date_time = date_time - self.days = round(date_time.day - last_date_time.day, 14) # round to avoid numerical errors - # If the day has changed, store the time that has passed in a new variable to be added to the new time - if self.days != 0: - self.extra_time += last_date_time.hour * 3600 + last_date_time.minute * 60 + last_date_time.second + \ - last_date_time.microsecond * 10 ** -6 - - return date_time.hour * 3600 + date_time.minute * 60 + date_time.second + date_time.microsecond * 10 ** -6 + \ - self.extra_time - self.init_time + try: + # Check to see if the day has changed between one cell and the next + self.days = round(date_time.day - last_date_time.day, 5) # round to avoid precision errors + # If the day has changed, store the time that has passed in a new variable to be added to the new time + if self.days != 0: + self.extra_time += 86400 + value = date_time.hour * 3600 + date_time.minute * 60 + date_time.second + date_time.microsecond * 10 ** -6 + \ + self.extra_time - self.init_time + except AttributeError: + # This is expected if the cell is blank or otherwise not a datetime. NaNs sometimes trigger other errors + # that HyPAT is more ready to handle than this AttributeError + value = float("NaN") + return value def convert_time(self, time_term, multi, add): """ Checks to see if the time-instrument's data is formatted in datetime or cumulative time passed and sets the converter function accordingly """ - if self.time_type == 0: # Equals 0 iff this is the first term in the column being loaded. Resets with each file + if time_term == "": + return float("NaN") + if self.time_type == 0: # Resets to zero before each file is loaded try: dummy = float(time_term) self.time_type = 1 except TypeError: # TypeError is expected if time_term is a datetime self.time_type = 2 + except ValueError: + self.error_texts += "Data Extraction Warning. The following was submitted to HyPAT for conversion" + \ + " into seconds: " + str(time_term) + ". This triggered a ValueError. The dataframe " + \ + " location corresponding to this term has been set to NaN. No further information" \ + " is available.\n\n" + return float("NaN") if self.time_type == 1: # If the time is in cumulative time passed format, do a linear transform according to multi and add return self.unit_conversion(time_term, multi, add) @@ -573,9 +718,9 @@ def convert_time(self, time_term, multi, add): def get_persistent_vars(self): """ Read data from persistent_permeation_input_variables.xlsx. This data is critical to processing the - datafiles loaded in by the user. """ + data files loaded in by the user. """ # Open up the persistent variable file for reading - pv_filename = os.path.join('datafiles', 'persistent_permeation_input_variables.xlsx') + pv_filename = os.path.join('data_files', 'persistent_permeation_input_variables.xlsx') pv_wb = openpyxl.load_workbook(pv_filename) self.num_GasT = pv_wb['Numbers']['C2'].value # Number of TCs measuring GasT @@ -644,39 +789,56 @@ def get_persistent_vars(self): # Variable for which row in Excel sheet to start obtaining data from (0-indexed) self.starting_row = pv_wb['MiscInfo']['C2'].value + # Variable for how many rows at the end of the Excel sheet to not read (0-indexed) + self.footer_rows = pv_wb['MiscInfo']['D2'].value def n_of_cols(self, filename): """ Given a .xls or .xlsx filename, return the number of columns in that file""" if self.file_type == ".xls": # This is faster for large files, but isn't working on .xlsx file formats - df = pd.read_csv(filename, sep='\t', header=None, skiprows=self.starting_row) + df = pd.read_csv(filename, sep='\t', header=None, skiprows=self.starting_row, skipfooter=self.footer_rows) else: # assume .xlsx file # openpyxl supports .xlsx file formats. According to documentation, engine=None should # support xlsx and xls, but it doesn't seem to work for xls as of 11/10/2021 - df = pd.read_excel(filename, header=None, engine="openpyxl", skiprows=self.starting_row) + df = pd.read_excel(filename, header=None, engine="openpyxl", skiprows=self.starting_row, + skipfooter=self.footer_rows) return len(df.columns) def extract_data(self, filename): """ Extract data from given file """ - # Attempt to extract data from the file. If it fails because of a ValueError, enter into a while loop that - # that can help the user fix the problem so the program can proceed + # Attempt to extract data from the file. If it fails because of a ValueError or IndexError, enter into a while + # loop that can help the user fix the problem so the program can proceed try: if self.file_type == ".xls": # This is faster for large files, but isn't working on .xlsx file formats Data = pd.read_csv(filename, sep='\t', header=None, usecols=self.needed_cols, - converters=self.converters_dict, skiprows=self.starting_row) + converters=self.converters_dict, skiprows=self.starting_row, + skipfooter=self.footer_rows, engine='python') else: # assume .xlsx file # openpyxl supports .xlsx Excel file formats. According to documentation, engine=None should # support xlsx and xls, but it doesn't seem to work for xls as of 11/10/2021 Data = pd.read_excel(filename, header=None, engine="openpyxl", usecols=self.needed_cols, - converters=self.converters_dict, skiprows=self.starting_row) + converters=self.converters_dict, skiprows=self.starting_row, + skipfooter=self.footer_rows) # https://stackoverflow.com/questions/6618515/sorting-list-based-on-values-from-another-list new_header = [x for _, x in sorted(zip(self.needed_cols, self.header))] Data.columns = new_header - except ValueError: + except (ValueError, IndexError): + # todo When there's a column name that references a column not in the data sheet, the following appears: + # FutureWarning: Defining usecols with out of bounds indices is deprecated and will raise a ParserError in a future version. + # return self._reader.parse( + # Unfortunately, ParserError doesn't seem to be recognized as an exception right now (late April / + # early May 2022), so this will need to be delt with later + + # Next line fixes some IndexErrors that occur when loading XLSX files if you give an incorrect column name + # for the isolation valve or if you give a column name referencing a column outside the datasheet + self.converters_dict.clear() + new_pvars = False # Whether new persistent variables (and self.converters_dict) have been obtained + num_of_cols = self.n_of_cols(filename) while len(set(self.needed_cols)) < len(self.needed_cols) or \ not all(x < num_of_cols for x in self.needed_cols): + # check to make sure all values in the list are less than the length of the Excel sheet # https://www.geeksforgeeks.org/python-check-if-all-the-values-in-a-list-are-less-than-a-given-value/ if not all(x < num_of_cols for x in self.needed_cols): @@ -684,23 +846,28 @@ def extract_data(self, filename): "Please ensure all instruments have column names that correspond to " + "columns in the Excel sheet.") self.adjust_persistent_vars(retry=True) # gives user a chance to fix the column names + new_pvars = True # check to make sure every column name is unique if len(set(self.needed_cols)) < len(self.needed_cols): tk.messagebox.showwarning("Data Reading Error", "Please ensure all instruments have unique column names") self.adjust_persistent_vars(retry=True) # gives user a chance to fix the column names + new_pvars = True + + # Make sure self.converters_dict is filled since it was cleared above + if not new_pvars: + self.get_persistent_vars() # Now that the errors are handled, extract the data from the Excel file and load it into a dataframe if self.file_type == ".xls": - # This is faster for large files, but isn't working .xlsx file formats Data = pd.read_csv(filename, sep='\t', header=None, usecols=self.needed_cols, - converters=self.converters_dict, skiprows=self.starting_row) + converters=self.converters_dict, skiprows=self.starting_row, + skipfooter=self.footer_rows, engine='python') else: # assume .xlsx file - # openpyxl supports .xlsx Excel file formats. According to documentation, engine=None should - # support xlsx and xls, but it doesn't seem to work for xls as of 11/10/2021 Data = pd.read_excel(filename, header=None, engine="openpyxl", usecols=self.needed_cols, - converters=self.converters_dict, skiprows=self.starting_row) + converters=self.converters_dict, skiprows=self.starting_row, + skipfooter=self.footer_rows) # https://stackoverflow.com/questions/6618515/sorting-list-based-on-values-from-another-list new_header = [x for _, x in sorted(zip(self.needed_cols, self.header))] Data.columns = new_header @@ -723,14 +890,24 @@ def extract_data(self, filename): def adjust_persistent_vars(self, retry=False): """ Function for calling the window for adjusting persistent variables for loading permeation data, then update everything if something was changed """ + loading_warn = True + if self.loading_data and not retry: + proceed = tk.messagebox.askyesno(title="Loading Data", + message="Warning: Data is currently loading. Proceeding may cause " + + "unexpected or erroneous results. Do you wish to proceed?") + if not proceed: + return + else: + loading_warn = False + self.update() # get location of gui x = self.winfo_rootx() y = self.winfo_rooty() - # Call the Adjust Persistent Variables window and, if something was changed, refresh graphs and variables + # Call the "Adjust Persistent Variables" window and, if something was changed, refresh graphs and variables d = AdjustPVars(storage=self.storage, pos=(x, y)).show_pv_window() if d and not retry: - self.refresh_graphs() + self.refresh_graphs(loading_warning=loading_warn) # if a warning has already been given don't give it again # retry = True if adjust_persistent_vars is called but only the persistent variables need to be updated # (as opposed to updating the graphs as well) if d and retry: @@ -841,7 +1018,7 @@ def calculate_permeability(self, filename, data): " data points.\n\n" # determine the leak rate. self.leak_range points before opening, not including t0 (note that .loc is inclusive) - self.pleak[filename] = \ + self.pleak_lf[filename] = \ self.get_rates(data.loc[self.t0[filename] - self.leak_range[filename]:self.t0[filename] - 1, 't'], data.loc[self.t0[filename] - self.leak_range[filename]:self.t0[filename] - 1, 'SecP']) @@ -862,11 +1039,12 @@ def calculate_permeability(self, filename, data): # Convert the user's input time delay into a location in the array of data that doesn't depend on delta t # This line creates an array containing only the locations of times that are eligible to be a steady state - ss_del = np.where(data.loc[self.t0[filename]:, 't'] > self.ss_t_del.get() + data.loc[self.t0[filename], 't'])[0] + ss_del = np.where(data.loc[self.t0[filename]:, 't'] > self.ss_t_del.get() + data.loc[self.t0[filename], 't'] + + 0.000001)[0] # Added .00001 to ensure precision errors don't lead to e_del being 0 try: # Set the time delay to be the minimum number of data points away from t0 that still is further than # the user's input time delay - ss_del = ss_del[0] + ss_del = ss_del[0] - 1 # -1 to compensate for the > sign in e_del's def and in later checks except IndexError: # This is expected if ss_del is empty # Set ss_del to the largest delay that still works ss_del = ss_time_max - (self.ss_range[filename] + self.t0[filename] + 2) @@ -877,7 +1055,7 @@ def calculate_permeability(self, filename, data): str(round(data.loc[ss_del + self.t0[filename], 't'] - data.loc[self.t0[filename], 't'], 3)) + " s.\n\n" if ss_del > ss_time_max - (self.ss_range[filename] + self.t0[filename] + 2): - # In case ss_del produces a useable data point but that data point is past an abrupt downturn in pressure + # In case ss_del produces a usable data point but that data point is past an abrupt downturn in pressure ss_del = ss_time_max - (self.ss_range[filename] + self.t0[filename] + 2) self.error_texts += "Steady State Warning with file " + filename + ". The user-input time delay" + \ " exceeded the limit of " + \ @@ -886,36 +1064,41 @@ def calculate_permeability(self, filename, data): str(round(data.loc[ss_del + self.t0[filename], 't'] - data.loc[self.t0[filename], 't'], 3)) + " s.\n\n" + # Find the data point at which an equilibrium is reached ddSecP = pd.Series((dSecP.loc[2:].to_numpy() - dSecP.loc[:len(dSecP) - 3].to_numpy()) / (data.loc[2:, 't'].to_numpy() - data.loc[:len(dSecP) - 3, 't'].to_numpy())) ddSecP = ddSecP.rolling(window=self.ss_range[filename], center=True, min_periods=1).mean() zeros = np.where(abs(ddSecP) < self.ss_tol.get())[0] - zeros = [z for z in zeros if self.t0[filename] + ss_del < z < ss_time_max - self.ss_range[filename]] + zeros = [z for z in zeros if self.t0[filename] + ss_del < z < ss_time_max - min_seq - 2] # If no steady state was found with the user-input tolerance, loop until find a steady state using new_ss_tol new_ss_tol = self.ss_tol.get() while not zeros and new_ss_tol < self.ss_tol.get() * 100000: - new_ss_tol *= 5 + new_ss_tol = float("{:.2e}".format(new_ss_tol * 5)) zeros = np.where(abs(ddSecP) < new_ss_tol)[0] - zeros = [z for z in zeros if self.t0[filename] + ss_del < z < ss_time_max - self.ss_range[filename]] + zeros = [z for z in zeros if self.t0[filename] + ss_del < z < ss_time_max - min_seq - 2] if new_ss_tol != self.ss_tol.get() and zeros: self.error_texts += "Steady State Warning with file " + filename + ". HyPAT was unable to find a steady" + \ " state using the user-input tolerance " + str(self.ss_tol.get()) + \ - ". New tolerance was set to " + str(round(new_ss_tol, 14)) + \ - ", which successfully determined a time for beginning of the steady state.\n\n" + ". New tolerance was set to {:.2e}".format(new_ss_tol) + \ + ", which successfully determined a time for the beginning of the steady state.\n\n" elif new_ss_tol != self.ss_tol.get(): - zeros = [min(self.t0[filename] + ss_del, ss_time_max - self.ss_range[filename] - 1)] + zeros = [min(self.t0[filename] + ss_del + 1, ss_time_max - min_seq - 3)] self.error_texts += "Steady State Error with file " + filename + \ ". HyPAT was unable to find steady state using the user-input tolerance " + \ str(self.ss_tol.get()) + " or the computationally set tolerance " + \ - str(round(new_ss_tol, 14)) + ". Steady state time was set to " + \ + "{:.2e}".format(new_ss_tol) + ". Steady state time was set to " + \ str(round(data.loc[zeros[0], 't'], 3)) + \ " s, which is equal to either the starting time plus the user-input" + \ " time delay until steady state or the maximum usable time, whichever is lower.\n\n" - self.tss[filename] = zeros[0] # change ss_tol pe to address steady state issues - self.pss[filename] = self.get_rates(data.loc[self.tss[filename] + 1:self.tss[filename] + self.ss_range[filename], 't'], - data.loc[self.tss[filename] + 1:self.tss[filename] + self.ss_range[filename], 'SecP']) + # Set steady state time + self.tss[filename] = zeros[0] + # determine steady state permeation rates - self.Prate[filename] = self.pss[filename]["slope"] - self.pleak[filename]["slope"] # Pa/s + self.pss_lf[filename] = self.get_rates(data.loc[self.tss[filename] + 1: + self.tss[filename] + self.ss_range[filename], 't'], + data.loc[self.tss[filename] + 1: + self.tss[filename] + self.ss_range[filename], 'SecP']) + self.Prate[filename] = self.pss_lf[filename]["slope"] - self.pleak_lf[filename]["slope"] # Pa/s # Get the temperature using an average of arbitrarily many TCs Tg_vals = pd.DataFrame() @@ -930,8 +1113,8 @@ def calculate_permeability(self, filename, data): self.storage.standard_temp # K # convert to molar flow rate mol/s using ideal gas law - Nrate = (self.pss[filename]["slope"] * self.volume.get()) / (R * self.Tgss[filename]) - \ - (self.pleak[filename]["slope"] * self.volume.get()) / (R * self.Tg0[filename]) + Nrate = (self.pss_lf[filename]["slope"] * self.volume.get()) / (R * self.Tgss[filename]) - \ + (self.pleak_lf[filename]["slope"] * self.volume.get()) / (R * self.Tg0[filename]) # convert to molar flux self.F[filename] = Nrate / self.A_perm.get() @@ -947,7 +1130,7 @@ def calculate_permeability(self, filename, data): np.sqrt(self.SecP_average[filename])) # Calculate the uncertainty in permeability - # We need uncertainty from the following: sl, SecP, PrimP, A_perm, pss[slope], pleak[slope], V, Tgss, Tg0 + # We need uncertainty from the following: sl, SecP, PrimP, A_perm, pss_lf[slope], pleak_lf[slope], V, Tgss, Tg0 # Find uncertainty of Tg0 when there are arbitrarily many TCs Tg0_errs = [] @@ -973,14 +1156,14 @@ def calculate_permeability(self, filename, data): self.PrimP_cerr["PrimP"], self.PrimP_average[filename] * self.PrimP_perr["PrimP"] / 100) # Rate uncertainty - Prate_err = np.sqrt(self.pss[filename]['slope error'] ** 2 + self.pleak[filename]['slope error'] ** 2) - Nrate_err = 1 / R * np.sqrt((abs(self.pss[filename]['slope'] * self.volume.get() / self.Tgss[filename]) * - np.sqrt((self.pss[filename]['slope error'] / self.pss[filename]['slope']) ** 2 + + Prate_err = np.sqrt(self.pss_lf[filename]['slope error'] ** 2 + self.pleak_lf[filename]['slope error'] ** 2) + Nrate_err = 1 / R * np.sqrt((abs(self.pss_lf[filename]['slope'] * self.volume.get() / self.Tgss[filename]) * + np.sqrt((self.pss_lf[filename]['slope error'] / self.pss_lf[filename]['slope']) ** 2 + (Tgss_err / self.Tgss[filename]) ** 2 + (self.volume_err.get() / self.volume.get()) ** 2) ) ** 2 + - (abs(self.pleak[filename]['slope'] * self.volume.get() / self.Tg0[filename]) * - np.sqrt((self.pleak[filename]['slope error'] / self.pleak[filename]['slope']) ** 2 + + (abs(self.pleak_lf[filename]['slope'] * self.volume.get() / self.Tg0[filename]) * + np.sqrt((self.pleak_lf[filename]['slope error'] / self.pleak_lf[filename]['slope']) ** 2 + (Tg0_err / self.Tg0[filename]) ** 2 + (self.volume_err.get() / self.volume.get()) ** 2) ) ** 2 @@ -996,7 +1179,7 @@ def calculate_permeability(self, filename, data): (np.sqrt(self.PrimP_average[filename]) - np.sqrt( self.SecP_average[filename])) ) ** 2 - ) + ) # store uncertainties self.Prate_err[filename] = Prate_err @@ -1006,17 +1189,18 @@ def calculate_permeability(self, filename, data): self.SecP_err[filename] = SecP_err self.PrimP_err[filename] = PrimP_err - # Data for permeability vs time graph - self.Perm_Plot[filename] = ((dSecP-self.pleak[filename]["slope"]) * self.volume.get() * sl) / \ + # Data for permeability vs. time graph + self.Perm_Plot[filename] = ((dSecP - self.pleak_lf[filename]["slope"]) * self.volume.get() * sl) / \ (((data['PrimP']) ** 0.5 - ( data['SecP']) ** 0.5) * R * ( - self.Tg_mean + 273.15) * self.A_perm.get()) + self.Tg_mean + self.storage.standard_temp) * self.A_perm.get()) - # get temp from sample TC when we assess the leak + # get temp from sample TC self.Tsamp0[filename] = data.loc[self.t0[filename] - self.leak_range[filename]: - self.t0[filename] - 1, 'SampT'].mean() + 273.15 + self.t0[filename] - 1, 'SampT'].mean() + self.storage.standard_temp self.Tsampss[filename] = data.loc[self.tss[filename] + 1: - self.tss[filename] + self.ss_range[filename], 'SampT'].mean() + 273.15 + self.tss[filename] + self.ss_range[filename], 'SampT'].mean() + \ + self.storage.standard_temp self.Tsampss_err[filename] = max(data.loc[self.tss[filename] + 1: self.tss[filename] + self.ss_range[filename], 'SampT'].std(), self.SampT_cerr["SampT"], @@ -1028,9 +1212,9 @@ def calculate_diffusivity(self, filename, data): R = self.storage.R * 1000 # J/mol K # Prep for the time-lag method - x_intercept = (self.pss[filename]["intercept"] - self.pleak[filename]["intercept"]) / \ - (self.pleak[filename]["slope"] - self.pss[filename]["slope"]) - y_intercept = self.pleak[filename]["slope"] * x_intercept + self.pleak[filename]["intercept"] + x_intercept = (self.pss_lf[filename]["intercept"] - self.pleak_lf[filename]["intercept"]) / \ + (self.pleak_lf[filename]["slope"] - self.pss_lf[filename]["slope"]) + y_intercept = self.pleak_lf[filename]["slope"] * x_intercept + self.pleak_lf[filename]["intercept"] self.intercept[filename] = (x_intercept, y_intercept) self.tlag[filename] = x_intercept - data.loc[self.t0[filename], 't'] if self.tlag[filename] < 0: @@ -1064,9 +1248,9 @@ def calculate_diffusivity(self, filename, data): print('\n', filename) print("mean:", np.mean(data.loc[self.t0[filename] - self.leak_range[filename]: self.t0[filename] - 1, 'dSecP'])) - print("leak rate:", self.pleak[filename]["slope"]) + print("leak rate:", self.pleak_lf[filename]["slope"]) print("max:", np.max(Jt)) # large in 1505 - print("Steady State Rate:", self.pss[filename]["slope"]) + print("Steady State Rate:", self.pss_lf[filename]["slope"]) print("Diffusivity guess:", D) # before optimizing @@ -1083,20 +1267,21 @@ def calculate_diffusivity(self, filename, data): plt.figure(filename) plt.plot(self.rhs[filename][1:], label='no optimization') plt.plot(self.lhs[filename][1:], label='lhs') + plt.show() # optimizing using curve fit - should just be a wrapper around least squares h = data.loc[1, 't'] - data.loc[0, 't'] - def f(xdata, D, dt, A): + def f(xdata, D_opt, dt_opt, A_opt): # Function for optimizing D using curve fit rhs = 1 + 2 * sum( - [(-1) ** n * np.exp(-D * n ** 2 * np.pi ** 2 * (xdata + dt) / sl ** 2) for n in range(1, 20)]) - return rhs / A - + [(-1) ** n * np.exp(-D_opt * n ** 2 * np.pi ** 2 * (xdata + dt_opt) / sl ** 2) for n in range(1, 20)]) + return rhs * A_opt # Attempt to optimize D using curve fit and the above function. Show a warning if it fails due to RuntimeError try: if not skipD: # If not skipping the calculation of D + # todo look into making this a weighted curve fit popt, pcov = curve_fit(f, self.D_time[filename], self.lhs[filename], p0=[D, 0, 1], xtol=D * 1e-3, bounds=([0, -min(self.D_time[filename]), -1000], [10, 10*h, 1000])) else: @@ -1106,22 +1291,23 @@ def f(xdata, D, dt, A): except RuntimeError: self.error_texts += "Curve Fit Error with file " + filename + \ ". Curve fit unable to find optimal diffusivity parameters.\n\n" - popt = [D, 1, 1] - pcov = np.array([[0, 0, 0], [0, 0, 0], [0, 0, 0]]) + NaN = float("NaN") + popt = [D, 0, 1] + pcov = np.array([[NaN, NaN, NaN], [NaN, NaN, NaN], [NaN, NaN, NaN]]) except OptimizeWarning: self.error_texts += "Curve Fit Warning with file " + filename + \ ". Curve fit unable to find covariance of the diffusivity parameters. D has been" + \ " set to the diffusivity calculated by the time-lag method.\n\n" - popt = [D, 1, 1] - pcov = np.array([[0, 0, 0], [0, 0, 0], [0, 0, 0]]) + NaN = float("NaN") + popt = [D, 0, 1] + pcov = np.array([[NaN, NaN, NaN], [NaN, NaN, NaN], [NaN, NaN, NaN]]) perr = np.sqrt(np.diag(pcov)) self.D_err[filename] = perr[0] - - # Store D and rhs_cf for use elsewhere in program - self.D[filename], dt, A = popt - self.rhs_cf[filename] = 1 + 2 * sum( - [(-1) ** n * np.exp(-self.D[filename] * n ** 2 * np.pi ** 2 * (self.D_time[filename] + dt) / sl ** 2) - for n in range(1, 20)]) + # Store D, dt, A, and rhs_cf for use elsewhere in program + self.D[filename], self.dt[filename], self.A[filename] = popt + self.rhs_cf[filename] = self.A[filename] * (1 + 2 * sum( + [(-1) ** n * np.exp(-self.D[filename] * n ** 2 * np.pi ** 2 * (self.D_time[filename] + self.dt[filename]) / + sl ** 2) for n in range(1, 20)])) if debug: lhs = A * (Jt - J0) / (Jinf - J0) plt.figure() @@ -1159,18 +1345,18 @@ def generate_plots(self, *args): self.ax22.clear() self.ax3.clear() - # Permeability/Diffusivity/Solubility vs Temperature and Flux vs Pressure (bottom left graph) + # Permeability/Diffusivity/Solubility vs. Temperature and Flux vs. Pressure (bottom left graph) self.PDK_plot(self.ax) - # Pressure vs Time (top right graph) + # Pressure vs. Time (top right graph) self.pressure_time_plot(data, filename, self.fig1, self.ax1, self.ax12) - # Permeation vs Time (bottom middle graph) + # Permeation vs. Time (bottom middle graph) self.perm_time_plot(data, filename, self.fig2, self.ax2, self.ax22) # Diffusivity comparison plots for optimization (bottom right graph) self.comparison_plot(filename, self.ax3) - # time may be one second off, but the index is about the same as the time array used earlier. + self.canvas.draw() self.toolbar.update() self.canvas1.draw() @@ -1203,6 +1389,13 @@ def update_PDK_plot(self, *args): def popout_plot(self, ax): """ creates the selected plot using the regular popout from matplotlib """ + if self.loading_data: + proceed = tk.messagebox.askyesno(title="Loading Data", + message="Warning: Data is currently loading. Proceeding may cause " + + "unexpected or erroneous results. Do you wish to proceed?") + if not proceed: + return + filename = self.current_file.get() if filename != "No files yet": data = self.datafiles[filename] @@ -1212,24 +1405,24 @@ def popout_plot(self, ax): # Do things depending on which plot is going to be the popout plot if plot == self.ax_title: # PDK plot - fig, axis = plt.subplots() + fig, axis = plt.subplots(layout="constrained") self.PDK_plot(axis) elif plot == self.ax1_title: - # Pressure vs Time - fig, ax1 = plt.subplots() + # Pressure vs. Time + fig, ax1 = plt.subplots(layout="constrained") ax12 = ax1.twinx() self.pressure_time_plot(data, filename, fig, ax1, ax12) elif plot == self.ax2_title: - # Permeation vs Time - fig, ax2 = plt.subplots() + # Permeation vs. Time + fig, ax2 = plt.subplots(layout="constrained") ax22 = ax2.twinx() self.perm_time_plot(data, filename, fig, ax2, ax22) elif plot == self.ax3_title: # diffusivity comparison plots for optimization - fig, ax3 = plt.subplots() + fig, ax3 = plt.subplots(layout="constrained") self.comparison_plot(filename, ax3) else: @@ -1237,9 +1430,13 @@ def popout_plot(self, ax): plt.show() def pressure_time_plot(self, data, filename, fig1, ax1, ax12): - """ Creates the pressure vs time (top right) plot """ + """ Creates the pressure vs. time (top right) plot """ + # Plot the point where the isolation valve opens and where steady state is determined + ax1.plot(data.loc[self.t0[filename], 't'], data.loc[self.t0[filename], 'SecP'], 'yo', label="t$_0$") + ax1.plot(data.loc[self.tss[filename], 't'], data.loc[self.tss[filename], 'SecP'], 'mo', label="t$_{ss}$") + # Plot secondary pressure and set up axes for it - ax1.plot(data['t'], data['SecP'], label='Secondary Pressure') + ax1.plot(data['t'], data['SecP'], '.', label='Secondary Pressure') ax1.set_title(self.ax1_title) ax1.set_xlabel(self.ax1_xlabel) ax1.set_ylabel(self.ax1_ylabel) @@ -1248,14 +1445,24 @@ def pressure_time_plot(self, data, filename, fig1, ax1, ax12): ax12.plot(data['t'], data['PrimP'], color='orange', label='Primary Pressure') ax12.set_ylabel(self.ax12_ylabel) - # Generate and plot the red leak line and green steady state line - xleak = data.loc[self.t0[filename] + 1:self.t0[filename] + self.leak_range[filename], 't'] - yleak = self.pleak[filename]["slope"] * xleak + self.pleak[filename]["intercept"] + # Generate and plot the red leak line (showing which values are used in initial calculations) + # and green steady state line (showing which values are used in final calculations) + xleak = data.loc[self.t0[filename] - self.leak_range[filename]:self.t0[filename] - 1, 't'] + yleak = self.pleak_lf[filename]["slope"] * xleak + self.pleak_lf[filename]["intercept"] xss = data.loc[self.tss[filename] + 1:self.tss[filename] + self.ss_range[filename], 't'] - yss = self.pss[filename]["slope"] * xss + self.pss[filename]["intercept"] + yss = self.pss_lf[filename]["slope"] * xss + self.pss_lf[filename]["intercept"] ax1.plot(xleak, yleak, color='red', label='Leak') ax1.plot(xss, yss, color='lime', label='Steady State') - # Plot the point that would be the intersection of the leak line and steady state line if they were extended + # Extrapolate the lines further + xleak_ex = np.array([data.loc[self.t0[filename] - self.leak_range[filename], 't'], + (self.intercept[filename])[0]]) + yleak_ex = self.pleak_lf[filename]["slope"] * xleak_ex + self.pleak_lf[filename]["intercept"] + xss_ex = np.array([(self.intercept[filename])[0], + data.loc[self.tss[filename] + self.ss_range[filename], 't']]) + yss_ex = self.pss_lf[filename]["slope"] * xss_ex + self.pss_lf[filename]["intercept"] + ax1.plot(xleak_ex, yleak_ex, ":", color='red') + ax1.plot(xss_ex, yss_ex, ":", color='lime') + # Plot a point at the intersection of the leak line and steady state line ax1.plot(*self.intercept[filename], 'o') # Make sure ticks are in the right spot @@ -1263,10 +1470,16 @@ def pressure_time_plot(self, data, filename, fig1, ax1, ax12): ax12.yaxis.set_ticks_position('right') ax1.xaxis.set_ticks_position('bottom') - fig1.legend(loc="lower right", bbox_to_anchor=(1, 0), bbox_transform=ax1.transAxes) + # Ensure all plots are on the legend. Simply using fig1.legend(...) doesn't get rid of old legends, + # causing them to stack up in a way I can't figure out how to remove. + # https://stackoverflow.com/questions/5484922/secondary-axis-with-twinx-how-to-add-to-legend/47370214#47370214 + pt_lines, pt_labels = ax1.get_legend_handles_labels() + pt_lines2, pt_labels2 = ax12.get_legend_handles_labels() + ax12.legend(pt_lines + pt_lines2, pt_labels + pt_labels2, loc="lower right", bbox_to_anchor=(1, 0), + bbox_transform=ax1.transAxes, framealpha=0.5) def perm_time_plot(self, data, filename, fig2, ax2, ax22): - """ Creates the permeability vs time (bottom middle) plot """ + """ Creates the permeability vs. time (bottom middle) plot """ # Plot the permeability over time ax2.plot(data.loc[self.t0[filename]:, 't'], self.Perm_Plot[filename].iloc[self.t0[filename]:], label="RT Perm") @@ -1274,7 +1487,7 @@ def perm_time_plot(self, data, filename, fig2, ax2, ax22): ax2.set_xlabel(self.ax2_xlabel) ax2.set_ylabel(self.ax2_ylabel) - # Plot temperature vs time and set relevant axes + # Plot temperature vs. time and set relevant axes ax22.plot(data.loc[self.t0[filename]:, 't'], data.loc[self.t0[filename]:, 'SampT'], color='orange', label='Sample T') ax22.set_ylabel(self.ax22_ylabel) @@ -1292,7 +1505,13 @@ def perm_time_plot(self, data, filename, fig2, ax2, ax22): # dipping way below 0. Added the top limit was because autoscaling was failing once the bottom limit was added. ax2.set_ylim(bottom=0, top=1.25*self.Phi[filename]) - fig2.legend(loc="lower right", bbox_to_anchor=(1, 0), bbox_transform=ax2.transAxes) + # Ensure all plots are on the legend. Simply using fig2.legend(...) doesn't get rid of old legends, + # causing them to stack up in a way I can't figure out how to remove. + # https://stackoverflow.com/questions/5484922/secondary-axis-with-twinx-how-to-add-to-legend/47370214#47370214 + pt_lines, pt_labels = ax2.get_legend_handles_labels() + pt_lines2, pt_labels2 = ax22.get_legend_handles_labels() + ax22.legend(pt_lines + pt_lines2, pt_labels + pt_labels2, loc="lower right", bbox_to_anchor=(1, 0), + bbox_transform=ax2.transAxes, framealpha=0.5) def comparison_plot(self, filename, ax3): """ Creates the diffusivity optimization comparison (bottom right) plot """ @@ -1305,7 +1524,7 @@ def comparison_plot(self, filename, ax3): ax3.set_ylabel(self.ax3_ylabel) ax3.yaxis.set_ticks_position('left') ax3.xaxis.set_ticks_position('bottom') - ax3.legend() + ax3.legend(framealpha=0.5) def PDK_plot(self, ax): """ Creates the PDK (bottom left) plot for calculated properties of different files """ @@ -1314,12 +1533,12 @@ def PDK_plot(self, ax): # Set plot title if plot == "Flux": - self.ax_title = plot + " vs Pressure" + self.ax_title = plot + " vs. Pressure" self.ax.loglog() else: - self.ax_title = plot + " vs Temperature" + self.ax_title = plot + " vs. Temperature" - # store as a local variable to allow for a different xlabel in the flux vs pressure plot + # store as a local variable to allow for a different xlabel in the flux vs. pressure plot xlabel = self.ax_xlabel # set y labels to get units right @@ -1341,17 +1560,17 @@ def PDK_plot(self, ax): y = self.Phi[filename] yerr = self.Phi_err[filename] label = f'{filename}\n{plot}={y:.3e}' + r' mol m$^{-1}\,$s$^{-1}\,$Pa$^{-0.5}$' + \ - f'\nTemperature={x:.3e} \u2103\nPressure={p:.3e} Pa' + f'\nTemperature={x:.3e} \u00B0C\nPressure={p:.3e} Pa' elif plot == "Diffusivity": y = self.D[filename] yerr = self.D_err[filename] label = f'{filename}\n{plot}={y:.3e}' + r' m$^{2}\,$s$^{-1}$' + \ - f'\nTemperature={x:.3e} \u2103\nPressure={p:.3e} Pa' + f'\nTemperature={x:.3e} \u00B0C\nPressure={p:.3e} Pa' elif plot == "Solubility": y = self.Ks[filename] yerr = self.Ks_err[filename] label = f'{filename}\n{plot}={y:.3e}' + r' mol m$^{-3}\,$Pa$^{-0.5}$' + \ - f'\nTemperature={x:.3e} \u2103\nPressure={p:.3e} Pa' + f'\nTemperature={x:.3e} \u00B0C\nPressure={p:.3e} Pa' elif plot == "Flux": y = self.F[filename] yerr = self.F_err[filename] @@ -1360,7 +1579,7 @@ def PDK_plot(self, ax): f'\nPressure={p:.3e} Pa' else: # This shouldn't be possible - print("Error, you should have selected Permeability, Diffusivity, Solubility, or Flux.") + showerror("Selection Error", "Error, you should have selected Permeability, Diffusivity, Solubility.") y = self.Phi[filename] yerr = self.Phi_err[filename] label = f'{filename}\n{plot}={y:.3e}' + r' mol m$^{-1}\,$s$^{-1}\,$Pa$^{-0.5}$' + \ @@ -1384,6 +1603,13 @@ def PDK_plot(self, ax): def save_figures(self): """ Saves figures into a folder """ + if self.loading_data: + proceed = tk.messagebox.askyesno(title="Loading Data", + message="Warning: Data is currently loading. Proceeding may cause " + + "unexpected or erroneous results. Do you wish to proceed?") + if not proceed: + return + filename = asksaveasfilename(initialdir=os.path.dirname(__file__), initialfile="figures") if filename != '': # check for extension and remove it @@ -1418,7 +1644,7 @@ def __init__(self, storage, pos, *args, **kwargs): self.add_entry = widgets.add_entry self.title("Adjust Persistent Variables") - self.resizable(width=False, height=False) + # self.resizable(width=False, height=False) self.minsize(400, 200) # gui_x/y values determined by running self.updateidletasks() at the end of self.__init__ and then printing size @@ -1443,7 +1669,7 @@ def __init__(self, storage, pos, *args, **kwargs): self.changed = False # Boolean of whether edits have been made that require plot refreshing # Get the path for the persistent variable file - self.pv_filename = os.path.join('datafiles', 'persistent_permeation_input_variables.xlsx') + self.pv_filename = os.path.join('data_files', 'persistent_permeation_input_variables.xlsx') # Read the Excel sheets into dataframes for easier access self.numbers_info = pd.read_excel(self.pv_filename, sheet_name="Numbers", header=0) @@ -1459,7 +1685,7 @@ def __init__(self, storage, pos, *args, **kwargs): # Method of scaling the window width according to number of TCs measuring GasT. At num_GasT = 10, # things are still fine. Potentially add a scrollbar instead. if self.num_GasT.get() > 2: - width += 80 + int(extra_width * (self.num_GasT.get() - 3)) + width += int(extra_width * (self.num_GasT.get() - 2)) self.geometry("{}x{}+{}+{}".format(width, height, pos_right, pos_down)) # time @@ -1510,7 +1736,9 @@ def __init__(self, storage, pos, *args, **kwargs): self.b_IV = tk.DoubleVar(value=self.misc_info['Isolation Valve'][2]) # Row at which the program starts reading data - self.starting_row = tk.DoubleVar(value=self.misc_info['Starting Row'][0]) + self.starting_row = tk.IntVar(value=self.misc_info['Starting Row'][0]) + # Rows at the end of the file which the program doesn't read + self.footer_rows = tk.IntVar(value=self.misc_info['Rows in Footer'][0]) # store entries self.inputs = {} @@ -1535,16 +1763,26 @@ def __init__(self, storage, pos, *args, **kwargs): def one_or_more(self, tvar, variable, key, pf=None): """ Ensure the user entry is greater than 1. If not, set it equal to 1. """ self.storage.check_for_number(tvar, variable, key, parent_frame=pf) - # Either set the entry to 1 if less than 1 or to the user's entry + maxTCnum = 8 # Max number of TCs to allow + # Either set the entry to 1 if less than 1, to int(maxTCnum) if more than int(maxTCnum), or to the user's entry if key in variable.keys() and tvar.get() < 1: tk.messagebox.showwarning("Invalid Entry", "Please enter a number greater than or equal to 1.") self.deiconify() variable[key].delete(0, "end") variable[key].insert(0, "1") tvar.set(1) - # elif tvar.get() != int(tvar.get()): - # tk.messagebox.showwarning("Invalid Entry", "Please enter an integer.") - # self.deiconify() + elif key in variable.keys() and tvar.get() > int(maxTCnum): + # Keeps the user from having to type out too many unique column names if something like "100" gets submitted + tk.messagebox.showwarning("Invalid Entry", 'Please enter a number less than or equal to ' + + '{}.\n\n'.format(int(maxTCnum)) + 'If more than ' + + '{} TCs are needed,'.format(int(maxTCnum)) + ' simply search ' + + 'for "maxTCnum" in the source code and change the ' + + '{} in its definition '.format(int(maxTCnum)) + + 'to your desired number.') + self.deiconify() + variable[key].delete(0, "end") + variable[key].insert(0, "{}".format(int(maxTCnum))) + tvar.set(int(maxTCnum)) else: variable[key].delete(0, "end") variable[key].insert(0, "{}".format(int(tvar.get()))) @@ -1692,13 +1930,6 @@ def create_inputs(self): command=lambda tvar, variable, key, pf: self.storage.check_for_number(tvar, variable, key, parent_frame=pf)) - tk.Label(parent, text="Starting Row").grid(row=entry_row + 4, column=entry_col + 1) - self.add_entry(self, parent, variable=self.inputs, key="Starting Row", text="", innersubscript="", - innertext="", subscript="", tvar=self.starting_row, units="", ent_w=entry_w, - row=entry_row + 5, column=entry_col, in_window=True, - command=lambda tvar, variable, key, pf: self.storage.check_for_number(tvar, variable, key, - parent_frame=pf)) - # Second row of instruments row2entries = entry_row + 7 @@ -1708,7 +1939,7 @@ def create_inputs(self): # Add entries for arbitrarily many instruments measuring GasT for GasT_inst in self.GasT_info.keys(): entry_col += 3 - tk.Label(parent, text="{} [\u2103]".format(GasT_inst)).grid(row=row2entries, column=entry_col + 1) + tk.Label(parent, text="{} [\u00B0C]".format(GasT_inst)).grid(row=row2entries, column=entry_col + 1) self.add_entry(self, parent, variable=self.inputs, key="col_{}".format(GasT_inst), text="col", innersubscript="{}".format(GasT_inst), innertext=":", subscript="", tvar=self.col_GasT[GasT_inst], units="", ent_w=entry_w, @@ -1716,19 +1947,19 @@ def create_inputs(self): command=lambda tvar, variable, key: self.check_col_names(tvar, variable, key)) self.add_entry(self, parent, variable=self.inputs, key="m_{}".format(GasT_inst), text="m", innersubscript="{}".format(GasT_inst), innertext=":", - subscript="", tvar=self.m_GasT[GasT_inst], units="[\u2103/?]", ent_w=entry_w, + subscript="", tvar=self.m_GasT[GasT_inst], units="[\u00B0C/?]", ent_w=entry_w, row=row2entries + 2, column=entry_col, in_window=True, command=lambda tvar, variable, key, pf: self.storage.check_for_number(tvar, variable, key, parent_frame=pf)) self.add_entry(self, parent, variable=self.inputs, key="b_{}".format(GasT_inst), text="b", innersubscript="{}".format(GasT_inst), innertext=":", - subscript="", tvar=self.b_GasT[GasT_inst], units="[\u2103]", ent_w=entry_w, + subscript="", tvar=self.b_GasT[GasT_inst], units="[\u00B0C]", ent_w=entry_w, row=row2entries + 3, column=entry_col, in_window=True, command=lambda tvar, variable, key, pf: self.storage.check_for_number(tvar, variable, key, parent_frame=pf)) self.add_entry(self, parent, variable=self.inputs, key="cerr_{}".format(GasT_inst), text="cerr", innersubscript="{}".format(GasT_inst), innertext=":", - subscript="", tvar=self.cerr_GasT[GasT_inst], units="[\u2103]", ent_w=entry_w, + subscript="", tvar=self.cerr_GasT[GasT_inst], units="[\u00B0C]", ent_w=entry_w, row=row2entries + 4, column=entry_col, in_window=True, command=lambda tvar, variable, key, pf: self.storage.check_for_number(tvar, variable, key, parent_frame=pf)) @@ -1741,23 +1972,23 @@ def create_inputs(self): # Sample temperature entry_col += 3 - tk.Label(parent, text="SampT [\u2103]").grid(row=row2entries, column=entry_col + 1) + tk.Label(parent, text="SampT [\u00B0C]").grid(row=row2entries, column=entry_col + 1) self.add_entry(self, parent, variable=self.inputs, key="col_SampT", text="col", innersubscript="SampT", innertext=":", subscript="", tvar=self.col_SampT, units="", ent_w=entry_w, row=row2entries + 1, column=entry_col, command=lambda tvar, variable, key: self.check_col_names(tvar, variable, key)) self.add_entry(self, parent, variable=self.inputs, key="m_SampT", text="m", innersubscript="SampT", - innertext=":", subscript="", tvar=self.m_SampT, units="[\u2103/?]", ent_w=entry_w, + innertext=":", subscript="", tvar=self.m_SampT, units="[\u00B0C/?]", ent_w=entry_w, row=row2entries + 2, column=entry_col, in_window=True, command=lambda tvar, variable, key, pf: self.storage.check_for_number(tvar, variable, key, parent_frame=pf)) self.add_entry(self, parent, variable=self.inputs, key="b_SampT", text="b", innersubscript="SampT", - innertext=":", subscript="", tvar=self.b_SampT, units="[\u2103]", ent_w=entry_w, + innertext=":", subscript="", tvar=self.b_SampT, units="[\u00B0C]", ent_w=entry_w, row=row2entries + 3, column=entry_col, in_window=True, command=lambda tvar, variable, key, pf: self.storage.check_for_number(tvar, variable, key, parent_frame=pf)) self.add_entry(self, parent, variable=self.inputs, key="cerr_SampT", text="cerr", innersubscript="SampT", - innertext=":", subscript="", tvar=self.cerr_SampT, units="[\u2103]", ent_w=entry_w, + innertext=":", subscript="", tvar=self.cerr_SampT, units="[\u00B0C]", ent_w=entry_w, row=row2entries + 4, column=entry_col, in_window=True, command=lambda tvar, variable, key, pf: self.storage.check_for_number(tvar, variable, key, parent_frame=pf)) @@ -1767,6 +1998,21 @@ def create_inputs(self): command=lambda tvar, variable, key, pf: self.storage.check_for_number(tvar, variable, key, parent_frame=pf)) + entry_col += 3 + tk.Label(parent, text="Starting Row").grid(row=row2entries + 0, column=entry_col + 1) + self.add_entry(self, parent, variable=self.inputs, key="Starting Row", text="", innersubscript="", + innertext="", subscript="", tvar=self.starting_row, units="", ent_w=entry_w, + row=row2entries + 1, column=entry_col, in_window=True, + command=lambda tvar, variable, key, pf: self.storage.check_for_number(tvar, variable, key, + parent_frame=pf)) + + tk.Label(parent, text="Rows in Footer").grid(row=row2entries + 2, column=entry_col + 1) + self.add_entry(self, parent, variable=self.inputs, key="Footer Rows", text="", innersubscript="", + innertext="", subscript="", tvar=self.footer_rows, units="", ent_w=entry_w, + row=row2entries + 3, column=entry_col, in_window=True, + command=lambda tvar, variable, key, pf: self.storage.check_for_number(tvar, variable, key, + parent_frame=pf)) + def submit(self): """Updates the persistent variables with user input""" @@ -1785,8 +2031,8 @@ def submit(self): self.m_SampT.get(), self.b_SampT.get(), self.cerr_SampT.get(), self.perr_SampT.get(), self.m_PrimP.get(), self.b_PrimP.get(), self.cerr_PrimP.get(), self.perr_PrimP.get(), self.m_SecP.get(), self.b_SecP.get(), self.cerr_SecP.get(), self.perr_SecP.get(), - self.m_IV.get(), self.b_IV.get(), self.starting_row.get(), self.num_GasT.get()])) + \ - any(np.isnan(val.get()) for val in GasT_vals) > 0: + self.m_IV.get(), self.b_IV.get(), self.starting_row.get(), self.footer_rows.get(), + self.num_GasT.get()])) + any(np.isnan(val.get()) for val in GasT_vals) > 0: tk.messagebox.showwarning(title="Missing Entry", message="Please fill in remaining boxes before continuing.") self.deiconify() @@ -1817,6 +2063,7 @@ def submit(self): self.misc_info['Isolation Valve'] = [self.col_IV.get(), self.m_IV.get(), self.b_IV.get(), np.nan, np.nan] self.misc_info['Starting Row'] = [self.starting_row.get(), np.nan, np.nan, np.nan, np.nan] + self.misc_info['Rows in Footer'] = [self.footer_rows.get(), np.nan, np.nan, np.nan, np.nan] # Create or remove data from dataframe so that it has the same number of instruments measuring GasT # as self.num_GasT. Note that this can handle non-integer values of self.numGasT @@ -1880,6 +2127,7 @@ def close_pv_win(self): self.cerr_SecP.get(), self.perr_SecP.get()] temp_misc_info['Isolation Valve'] = [self.col_IV.get(), self.m_IV.get(), self.b_IV.get(), np.nan, np.nan] temp_misc_info['Starting Row'] = [self.starting_row.get(), np.nan, np.nan, np.nan, np.nan] + temp_misc_info['Rows in Footer'] = [self.footer_rows.get(), np.nan, np.nan, np.nan, np.nan] temp_numbers_info['GasT'] = [int(self.num_GasT.get())] # "int" is so HyPAT recognizes when there was no change # List of all column names for checking for uniqueness @@ -1923,7 +2171,7 @@ def __init__(self, pos, size, *args, **kwargs): super().__init__(*args, **kwargs) self.title("Help for Permeation's Persistent Variables Settings") - self.resizable(width=False, height=False) + # self.resizable(width=False, height=False) self.minsize(400, 200) if platform.system() == 'Darwin': @@ -1951,7 +2199,8 @@ def create_text_boxes(self): td = {} tdt = {} for item in (" ", "t [s]: ", "PrimP [Pa]: ", "SecP [Pa]: ", "Isolation Valve [1/0]: ", "Starting Row: ", - "GasT [\u2103]: ", "SampT [\u2103]: ", "col: ", "m: ", "b: ", "cerr: ", "perr: "): + "Rows in Footer: ", "GasT [\u00B0C]: ", "SampT [\u00B0C]: ", "col: ", "m: ", "b: ", "cerr: ", + "perr: "): td[item] = tk.Text(self, borderwidth=0, background=self.cget("background"), spacing3=1) tdt[item + "text"] = tk.Text(self, borderwidth=0, background=self.cget("background"), spacing3=1) @@ -1970,28 +2219,29 @@ def create_text_boxes(self): tdt["PrimP [Pa]: text"].insert("insert", "Primary side pressure.") tdt["SecP [Pa]: text"].insert("insert", "Secondary side pressure.") tdt["Isolation Valve [1/0]: text"].insert("insert", "The data describing whether the isolation valve is " + - "closed (~0) or open (not ~0). Start of permeation is " + - "measured from when the valve first goes from closed to " + - "open.") + "closed (~0) or open (not ~0). Start of permeation is " + + "measured from when the valve first goes from closed to " + + "open.") tdt["Starting Row: text"].insert("insert", "The row at which the program starts reading data from the Excel " + - "sheet (0-indexed).") - tdt["GasT [\u2103]: text"].insert("insert", "The thermocouples that measure the temperature of the gas. " + - "There can be arbitrarily number of thermocouples selected in " + - 'Number of TCs measuring GasT.') - tdt["SampT [\u2103]: text"].insert("insert", "The thermocouple that measures the sample temperature.") - tdt["col: text"].insert("insert", "Reference column for variable in raw data file. " + - "The application is programmed for Excel files with no header.") + "sheet (0-indexed). HyPAT assumes the data file has no header, so " + + "use this to start analysis after the header.") + tdt["Rows in Footer: text"].insert("insert", "How many rows at the bottom of the file to ignore.") + tdt["GasT [\u00B0C]: text"].insert("insert", "The thermocouples (TCs) that measure the gas's temperature. " + + "The number of TCs can be set arbitrarily via " + + "'Number of TCs measuring GasT.'") + tdt["SampT [\u00B0C]: text"].insert("insert", "The thermocouple that measures the sample temperature.") + tdt["col: text"].insert("insert", "Reference column for corresponding quantity in raw data file.") tdt["m: text"].insert("insert", "Every entry in the column of data is converted according to m*x + b, " + - "where x is the entry in the column. Use this to convert the column's data to " + - "SI units.") + "where x is the entry in the column. Use this to convert the column's data " + + "to SI units.") tdt["b: text"].insert("insert", "See entry for 'm' above.") - tdt["cerr: text"].insert("insert", "Constant uncertainty of the instrument, e.g., +/- 2\u2103. " + + tdt["cerr: text"].insert("insert", "Constant uncertainty of the instrument, e.g., +/- 2\u00B0C. " + "When calculating the uncertainty caused by each instrument, the " + "application chooses the largest out of the constant uncertainty, the " + "proportional uncertainty (see below), and the calculated statistical " + "uncertainty.") tdt["perr: text"].insert("insert", "Proportional uncertainty of the instrument, e.g., " + - "+/- 0.75% of the measurement (in \u2103).") + "+/- 0.75% of the measurement (in \u00B0C).") # Configure each text widget for i, symbol in enumerate(td): @@ -2018,7 +2268,6 @@ def create_text_boxes(self): wrap=tk.WORD, height=1) # Set height of each right-side widget according to how many lines it will take to display - text_width = btfont.measure(tdt[symbol + "text"].get("1.0", 'end-1c')) // divisor2 + addend2 text_height = 1 while text_width >= tdt[symbol + "text"].cget("width"): # Check if the text width > widget's width. diff --git a/README.md b/README.md index cdc65b9..2b3c2fb 100644 --- a/README.md +++ b/README.md @@ -1,93 +1,188 @@ -# Hydrogen Permeation Analysis Tool (HyPAT) +# Hydrogen Permeation and Absorption Tool (HyPAT v2.0.0) ## About -The Hydrogen Permeation Analysis Tool (HyPAT) is an application that streamlines data analysis for gas-driven permeation through metals using the build-up in closed volume method to measure the following hydrogen transport properties: permeability, diffusivity, and solubility. A built-in literature database provides direct comparison for experimental results as well as timescale and permeation rate estimates for experiment preparation based on the user-input parameters. -Find more details about HyPAT V1.0.0 on the SoftwareX publication ["HyPAT: A GUI for high-throughput gas-driven hydrogen permeation data analysis"](https://doi.org/10.1016/j.softx.2022.101284). +The Hydrogen Permeation and Absorption Tool (HyPAT v2.0.0) is an application that streamlines data analysis for hydrogen +permeation and absorption experiments to measure the following hydrogen transport properties: permeability, diffusivity, +and solubility. Specifically, HyPAT analyzes data from gas-driven permeation through metals using the build-up in closed +volume method and data from absorption experiments using a Sieverts'-type apparatus. A built-in literature database +provides direct comparison for experimental results as well as timescale and permeation rate estimates for experiment +preparation based on the user-input parameters. + +Find more details about HyPAT v1.0.0 in the SoftwareX publication ["HyPAT: A GUI for high-throughput gas-driven hydrogen permeation data analysis."](https://doi.org/10.1016/j.softx.2022.101284) A publication on HyPAT v2.0.0 is in progress. + ## Description -The Hydrogen Permeation Analysis Tool (HyPAT) provides a convenient application that allows the user to prepare experiment estimates, quickly analyze multiple data files, and compare results to literature values based on gas-driven permeation through metals using the build-up method. +The Hydrogen Permeation and Absorption Tool (HyPAT) provides a convenient application that allows the user to +prepare permeation experiment estimates, quickly analyze multiple data files, and compare results to literature values. Features: * Calculates the hydrogen transport properties of permeability, diffusivity, and solubility from experimental pressure-rise permeation data. +* Calculates the hydrogen transport properties of permeability, diffusivity, and solubility from experimental absorption data obtained from a Sieverts'-type apparatus. * Batchwise data analysis of multiple files. * Direct comparison of measured hydrogen transport properties to known literature values. -* Predicts the permeation rates and diffusion times of experiments. +* Predicts the permeation rates and diffusion times of permeation experiments. * Easily customizable to adapt to user needs and experimental setups. ## Examples of usage of HyPAT -### Usage: Data Analysis with “Permeation Plots" Tab +### Usage: Data Analysis with "Permeation Plots" Tab -This tab calculates the hydrogen transport properties permeability, diffusivity, and solubility from experimental permeation data using pressure build-up in a secondary side closed volume while allowing for batchwise data analysis of multiple files. The required user inputs and associated errors are highlighted in yellow. Thickness (mm) is the only required sample property. The experimental apparatus parameters are the secondary side volume (m3) and permeation surface area (m2). +This tab calculates the hydrogen transport properties permeability, diffusivity, and solubility from experimental +permeation data using pressure build-up in a secondary side closed volume while allowing for batchwise data analysis of +multiple files. The required user inputs and associated errors are highlighted in yellow. Thickness (mm) is the only +required sample property. The experimental apparatus parameters are the secondary side volume (m3) and +permeation surface area (m2). -Clicking the “Steady State Variables” button immediately beneath the top-right plot allows the user to edit the tolerance for determining steady state, the delay until steady state, and the number of points used to find the leak rate and steady state rate. The tolerance is the value at which the second derivative of the secondary side pressure with respect to time (∂^2 P/∂t^2) is approximated to be zero. The delay until steady state is the minimum number of seconds to wait after the isolation valve is opened until initiating analysis to detect steady state. The leak range is the number of data points prior to opening the isolation valve used to determine the leak rate and initial values. The steady state range is the number of data points used to determine final values and when the permeation rate has reached a steady state. +Clicking the "Steady State Variables" button beneath the top-right plot allows the user to edit the +tolerance for determining steady state, the delay until steady state, and the number of points used to find the leak +rate and steady state rate. The tolerance is the value at which the second derivative of the secondary side pressure +with respect to time (∂2P/∂t2) is approximated to be zero. The delay until steady state is the +minimum number of seconds to wait after the isolation valve is opened until initiating analysis to detect steady state. +The leak range is the number of data points prior to opening the isolation valve used to determine the leak rate and +initial values. The steady state range is the number of data points used to determine final values and when the +permeation rate has reached a steady state. -Click the “Choose folder” button to select the folder containing experimental data. The graphs will then self-populate. Note: Only XLS and XLSX files are processed by the program; all other files are ignored. HyPAT assumes data is organized such that the data of each instrument (or set of instruments) has its own column in the Excel sheet without a header. +Click the "Choose folder" button to select the folder containing experimental data. The graphs will then self-populate. +Note: Only XLS and XLSX files are processed by the program; all other files are ignored. HyPAT assumes data is organized +such that the data of each instrument (or set of instruments) has its own column in the Excel sheet without a header. -The bottom left graph displays the permeability calculated from each data set plotted against temperature. Use the “Current Measurement” drop-down menu to change to the diffusivity, solubility, or flux calculated from each file plotted against temperature (or pressure, in the case of flux). +The bottom left graph displays the permeability calculated from each data set plotted against temperature. Use the +"Current Measurement" drop-down menu to change to the diffusivity, solubility, or flux calculated from each file plotted +against temperature (or against pressure, in the case of flux). -Also visible are a pressure versus time plot, a permeability versus time plot, and a comparison of different diffusivity optimizations plot, all using data from the first file in the selected folder. Use the “Current File” drop-down menu to use data from a different file for these plots. +Also visible are a pressure versus time plot, a permeability versus time plot, and a comparison of different diffusivity +optimizations plot, all using data from the first file in the selected folder. Use the "Current File" drop-down menu to +use data from a different file for these plots. -By clicking the “Settings” button, you gain the ability to edit some of how the program processes the data. Specifically, the following parameters are editable: +By clicking the "Settings" button, you gain the ability to edit some of how the program processes the data. +Specifically, the following parameters are editable: * The columns of the Excel sheet from which the program reads data for each instrument, * The row at which the program starts reading data from the Excel sheet, +* The number of rows at the end of the Excel sheet to ignore when reading data, * The number of thermocouples used to measure the gas temperature, -* Conversion factors for each instrument to allow the user to update for appropriate unit conversions, +* Conversion factors for each instrument to facilitate appropriate unit conversions, +* Constant and proportional errors of each instrument. + +### Usage: Data Analysis with "Absorption Plots" Tab + +The "Absorption Plots" tab calculates the hydrogen transport properties permeability, +diffusivity, and solubility from experimental absorption data obtained using a Sieverts'-type apparatus. HyPAT does this +while allowing for batchwise data analysis of multiple files. The required user inputs and associated errors are +highlighted in yellow. Thickness (mm), mass (g), volume (cm3), and molar mass (g mol-1) are the +required sample properties. The required experimental apparatus parameters are the initial container volume +(cm3) and the sample container volume (cm3). HyPAT also requires the user to submit whether the +experiment type is "Single," meaning each data file has the sample measured at only one pressure, or "Isotherm," +meaning at least one data file has the sample measured at multiple pressures along an isotherm. + +Clicking the "Equilibrium Variables" button immediately beneath the top-right plot allows the user to edit the +tolerance for determining equilibrium, the delay until equilibrium, the number of points used to find the equilibrium, +and the number of data points used to find initial and equilibrium values. The tolerance is the value at which the +second derivative of the pressure with respect to time (∂2P/∂t2) is approximated to be zero. +The delay until equilibrium is the minimum number of seconds to wait after the isolation valve is opened until +initiating analysis to detect equilibrium. The initial values range is the number of data points prior to opening the +isolation valve used to determine the initial values. The equilibrium range is the number of data points used to +determine final values and when the absorption rate has reached an equilibrium. + +Click the "Choose folder" button to select the folder containing experimental data. The graphs will then self-populate. +Note: Only XLS and XLSX files are processed by the program; all other files are ignored. HyPAT assumes data is organized +such that the data of each instrument (or set of instruments) has its own column in the Excel sheet without a header. + +The bottom left graph displays the solubility calculated from each data set plotted against temperature. Use the +"Current Measurement" drop-down menu to change to the diffusivity or permeability calculated from each file plotted +against temperature. The bottom middle graph displays the final pressure of each data set plotted against the final +composition. The points are color coded according to final sample temperature, yielding a +pressure-composition-temperature (PCT) plot. + +Also visible are a raw data plot and a comparison of different diffusivity optimizations plot, each using data from the +first file in the selected folder. Use the "Current File" drop-down menu to use data from a different file for the raw +data and diffusivity comparison plots. + +By clicking the "Settings" button, you gain the ability to edit some of how the program processes the data. +Specifically, the following parameters are editable: + +* The columns of the Excel sheet from which the program reads data for each instrument, +* The row at which the program starts reading data from the Excel sheet, +* The number of rows at the end of the Excel sheet to ignore when reading data, +* The number of thermocouples used to measure the gas temperature initially and after the isolation valve is opened, +* Conversion factors for each instrument to facilitate appropriate unit conversions, * Constant and proportional errors of each instrument. ### Usage: Comparison with Literature Values with "Overview Plots" Tab -The “Overview Plots” tab allows quick comparison to literature values of diffusivity, solubility, and permeability taken from M. Shimada (2020). The user can select and remove materials displayed in these overview plots using the materials column. Materials can be added to, modified, or removed from the database with the “Add/Edit Material” button. +The "Overview Plots" tab allows quick comparison to literature values of diffusivity, solubility, and permeability +taken from M. Shimada (2020). The user can select and remove materials displayed in these overview plots using the +materials column. Materials can be added to, modified, or removed from the database with the "Add/Edit Material" button. -The button labelled “Arrhenius Fit” calculates the pre-exponential factors and activation energies of diffusivity, solubility, and permeability for a material. +The button labelled "Arrhenius Fit" calculates the pre-exponential factors and activation energies of diffusivity, +solubility, and permeability for a material. Reference: -M. Shimada, “Tritium Transport in Fusion Reactor Materials” in Comprehensive Nuclear Materials 6, Second Edition, Elsevier (2020); https://doi.org/10.1016/B978-0-12-803581-8.11754-0 +M. Shimada, "Tritium Transport in Fusion Reactor Materials" in Comprehensive Nuclear Materials 6, Second Edition, +Elsevier (2020); https://doi.org/10.1016/B978-0-12-803581-8.11754-0 -### Usage: Predict Outcomes with "Permeation Estimates” Tab +### Usage: Predict Outcomes with "Permeation Estimates" Tab -This tab provides predictions for permeation experiments based on specified user inputs. The required user inputs are highlighted in yellow. The sample properties are thickness (mm) and material. The experimental apparatus parameters are O-ring/sealing method, calibrated leak rate (mol s-1 Torr-1), secondary side volume (cc), and accumulation time (hr). The specific test parameters are sample temperature (°C) and primary side pressure (Torr). +The "Permeation Estimates" tab provides predictions for permeation experiments based on specified user inputs. The required user inputs are +highlighted in yellow. The sample properties are thickness (mm) and material. The experimental apparatus parameters are +O-ring/sealing method, calibrated leak rate (mol s-1 Torr-1), secondary side volume (cc), and +accumulation time (hr). The specific test parameters are sample temperature (°C) and primary calculated partial pressure +(Torr). -The results reported in the "FINAL OUTPUT” section include estimated time-lag (s), molecular permeation rate (mol s-1), and final secondary side pressure (Torr). +The results reported in the "FINAL OUTPUT" section include estimated time-lag (s), permeation rate +(mol s-1), and final secondary pressure (Torr). -By clicking the “Settings” button, O-rings can be added, edited, or removed. The default values for calibrated leak rate and secondary side volume can also be edited there. The options for sample material can be edited using the “Add/Edit Material” button on the "Overview Plots" tab. +By clicking the "Settings" button, O-rings can be added, edited, or removed. The default values for calibrated leak rate +and secondary side volume can also be edited there. The options for sample material can be edited using the "Add/Edit +Material" button on the "Overview Plots" tab. ## Installation Instructions -1. Select the button “Code” near the top of this directory -2. Select “Download ZIP” from the dropdown menu that appears -3. Extract the downloaded file to a folder of your choice -4. Open the command line, then move to the directory “HyPAT” within the downloaded folder +1. Select the button "Code" near the top of this directory. +2. Select "Download ZIP" from the dropdown menu that appears. +3. Extract the downloaded file to a folder of your choice. +4. Open the command line, then move to the directory "HyPAT" within the downloaded folder. 5. Run the following code to open the application: python main.py + +* Note: + * Your system may require the following code instead: python3 main.py + * These instructions assume you already have Python and the required Python packages installed. -Required Python Packages: matplotlib, pandas, numpy, tkmacosx, mplcursors, scipy, and openpyxl +Python Interpreter: 3.8, 3.9, or 3.10 + +Python Packages: Matplotlib, Pandas, NumPy, tkmacosx, mplcursors, SciPy, and openpyxl Compatible: macOS and Windows 10 ## Plans - A new tab is currently under development to analyze hydrogen absorption data in a Sieverts’ type apparatus. - - ## Contribution Guidelines +There are no current updates planned for HyPAT. + +## Contribution Guidelines - If you notice a feature that you want or a bug that needs to be fixed, please contact Chase Taylor or Thomas Fuerst. +If you notice a feature that you want or a bug that needs to be fixed, please contact Chase Taylor or Thomas Fuerst. - ## Authors and Acknowledgement - -Joseph Watkins +## Authors and Acknowledgement George Evans +Joseph Watkins + Thomas Fuerst Chase Taylor -Joey Wakins (https://github.com/joeymwatkins) built most of the original functionality. George Evans (https://github.com/GeorgeEvans0) cleaned up errors, made various visual fixes, and added functionality. Thomas Fuerst (https://github.com/FuerstT) was the primary supervisor over this project, with Chase Taylor (https://github.com/taylchas) as secondary supervisor. +Joey Watkins (https://github.com/joeymwatkins) built most of the original functionality. George Evans +(https://github.com/GeorgeEvans0) added the "Absorption Plots" tab, cleaned up errors, made visual changes, and +added functionality. Thomas Fuerst (https://github.com/FuerstT) was the primary supervisor over this project, with Chase +Taylor (https://github.com/taylchas) as secondary supervisor. -This work was prepared for the U.S. Department of Energy, Office of Fusion Energy Sciences, under the DOE Idaho Field Office contract number DE-AC07–05ID14517. +This work was prepared for the U.S. Department of Energy, Office of Fusion Energy Sciences, under the DOE Idaho Field +Office contract number DE-AC07–05ID14517. ## License -Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the file “LICENSE” for the specific language governing permissions and limitations under the License. +Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an +"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the file "LICENSE" for the +specific language governing permissions and limitations under the License. diff --git a/Vanadium Absorption Pseudo-Data/V Absorption Pseudo-Data, 5 pressures, 100 C.xlsx b/Vanadium Absorption Pseudo-Data/V Absorption Pseudo-Data, 5 pressures, 100 C.xlsx new file mode 100644 index 0000000..b9409ee Binary files /dev/null and b/Vanadium Absorption Pseudo-Data/V Absorption Pseudo-Data, 5 pressures, 100 C.xlsx differ diff --git a/Vanadium Absorption Pseudo-Data/V Absorption Pseudo-Data, 5 pressures, 250 C.xlsx b/Vanadium Absorption Pseudo-Data/V Absorption Pseudo-Data, 5 pressures, 250 C.xlsx new file mode 100644 index 0000000..d03fe98 Binary files /dev/null and b/Vanadium Absorption Pseudo-Data/V Absorption Pseudo-Data, 5 pressures, 250 C.xlsx differ diff --git a/Vanadium Absorption Pseudo-Data/V Absorption Pseudo-Data, 5 pressures, 500 C.xlsx b/Vanadium Absorption Pseudo-Data/V Absorption Pseudo-Data, 5 pressures, 500 C.xlsx new file mode 100644 index 0000000..dcd5e58 Binary files /dev/null and b/Vanadium Absorption Pseudo-Data/V Absorption Pseudo-Data, 5 pressures, 500 C.xlsx differ diff --git a/Vanadium Absorption Pseudo-Data/V Absorption Pseudo-Data, 5 pressures, 750 C.xlsx b/Vanadium Absorption Pseudo-Data/V Absorption Pseudo-Data, 5 pressures, 750 C.xlsx new file mode 100644 index 0000000..00a93cc Binary files /dev/null and b/Vanadium Absorption Pseudo-Data/V Absorption Pseudo-Data, 5 pressures, 750 C.xlsx differ