diff --git a/betsi/gui.py b/betsi/gui.py index 8ae6d3d..39e9df4 100755 --- a/betsi/gui.py +++ b/betsi/gui.py @@ -89,7 +89,7 @@ def import_file(self): ## file_path = QFileDialog.getOpenFileName( ## self, 'Select File', os.getcwd(), '*.csv')[0] file_path = QFileDialog.getOpenFileName( - self, 'Select File', os.getcwd(), "*.csv *.aif *.txt")[0] + self, 'Select File', os.getcwd(), "*.csv *.aif *.txt *.XLS")[0] self.betsi_widget.target_filepath = file_path self.betsi_widget.populate_table(csv_path=file_path) print(f'Imported file: {Path(file_path).name}') @@ -103,7 +103,7 @@ def dragEnterEvent(self, e): # accept files only for now. ##if all(dt == 'file' for dt in drag_type) and all(ext == '.csv' for ext in extensions): - if all(dt == 'file' for dt in drag_type) and all(ext in ['.csv', '.aif', '.txt'] for ext in extensions): + if all(dt == 'file' for dt in drag_type) and all(ext in ['.csv', '.aif', '.txt', '.XLS'] for ext in extensions): e.accept() elif len(drag_type) == 1 and os.path.isdir(paths[0]): e.ignore() @@ -118,7 +118,7 @@ def dropEvent(self, e): # Single path to csv file ##if len(paths) == 1 and Path(paths[0]).suffix == '.csv': - if len(paths) == 1 and Path(paths[0]).suffix in ['.csv', '.aif', '.txt']: + if len(paths) == 1 and Path(paths[0]).suffix in ['.csv', '.aif', '.txt', '.XLS']: self.betsi_widget.target_filepath = paths[0] self.betsi_widget.populate_table(csv_path=paths[0]) print(f'Imported file: {Path(paths[0]).name}') @@ -188,9 +188,9 @@ def __init__(self, parent=None): # add a group box containing controls self.criteria_box = QGroupBox("BET area selection criteria") - self.criteria_box.setMaximumWidth(500) - self.criteria_box.setMaximumHeight(800) - self.criteria_box.setMinimumWidth(400) + self.criteria_box.setMaximumWidth(700) + self.criteria_box.setMaximumHeight(1000) + self.criteria_box.setMinimumWidth(500) #self.criteria_box.setMinimumHeight(650) self.min_points_label = QLabel(self.criteria_box) self.min_points_label.setText('Minimum number of points in the linear region: [3,10]') @@ -205,14 +205,14 @@ def __init__(self, parent=None): self.rouq2_tick = QCheckBox("Rouquerol criterion 2: Positive C") self.rouq3_tick = QCheckBox("Rouquerol criterion 3: Pressure in linear range") self.rouq4_tick = QCheckBox("Rouquerol criterion 4: Error in %, [5,75]") - self.rouq5_tick = QCheckBox("Rouquerol criterion 5: End at the knee") + self.rouq5_tick = QCheckBox("BETSI criterion: End at the knee") self.rouq4_edit = QLineEdit() self.rouq4_edit.setMaximumWidth(75) self.rouq4_slider = QSlider(QtCore.Qt.Horizontal) self.adsorbate_label = QLabel('Adsorbate:') self.adsorbate_combo_box = QComboBox() - self.adsorbate_combo_box.addItems(["N2", "Ar", "Kr", "Xe", "CO2", "Custom"]) + self.adsorbate_combo_box.addItems(["N2", "Ar", "Kr", "Xe", "Custom"]) self.adsorbate_cross_section_label = QLabel('Cross sectional area (nm2):') self.adsorbate_cross_section_edit = QLineEdit() self.adsorbate_cross_section_edit.setMaximumWidth(75) @@ -223,9 +223,17 @@ def __init__(self, parent=None): """Hints: - For convenience, it is best to first set your desired criteria before importing the input file. - Drag and drop the input file into the BETSI window. - - Valid input file formats: *.csv, *.txt, *.aif + - Valid input file formats: + # Adsorption Information File: *.aif + # Two-column data files: *.csv, *.txt + # Micromeritics: *.XLS + - Valid value range for parameters are given in brackets "[ ]" - Make sure to read the warnings that may pop up after BET calculation. - After the first run, by modifiying any of the parameters above, the calculations will rerun automatically. + - Regarding the minimum number of points, Rouquerol suggested 10 points, but you can lower the number if the data has insufficient number of points. + - Units: "Relative pressure" is dimensionless and "Quantity adsorbed" is in (cm\u00B3 STP/g). + - When the calculation is done, by clicking on any points on the "Filtered BET Areas" plot, all the plots will change to the corresponding selected points range. + - If the calculation takes longer than expected, bear with it. In case you see "Not responding", it means it is still running, otherwise the software would crash. """) self.note_label.setStyleSheet("background-color: lightblue") self.note_label.setMargin(10) @@ -411,7 +419,7 @@ def run_calculation(self): ## assert self.target_filepath, "You must provide a csv file before calling run." if not self.target_filepath: - warnings = "You must provide an input file (e.g. *.csv, *.txt, *.aif) before calling run. Press \"Clear\" and try again. Please refer to the \"Hints\" box for a quick guide." + warnings = "You must provide an input file (e.g. *.csv, *.txt, *.aif, *.XLS) before calling run. Press \"Clear\" and try again. Please refer to the \"Hints\" box for a quick guide." information = "" self.show_dialog(warnings, information) return @@ -599,7 +607,8 @@ def analyse_directory(self, dir_path): csv_paths = Path(dir_path).glob('*.csv') aif_paths = Path(dir_path).glob('*.aif') txt_paths = Path(dir_path).glob('*.txt') - input_file_paths = (*csv_paths, *aif_paths, txt_paths) + XLS_paths = Path(dir_path).glob('*.XLS') + input_file_paths = (*csv_paths, *aif_paths, *txt_paths, *XLS_paths) ##for file_path in csv_paths: for file_path in input_file_paths: @@ -629,7 +638,7 @@ def set_defaults(self): self.rouq2_tick.setCheckState(True) self.rouq3_tick.setCheckState(True) self.rouq4_tick.setCheckState(True) - self.rouq5_tick.setCheckState(False) + self.rouq5_tick.setCheckState(True) # the ticks can only be on or off - Not sure why I need to do this every time, but doesn't matter self.rouq1_tick.setTristate(False) @@ -772,7 +781,7 @@ def clean_table(self): self.results_table.setColumnCount(2) self.results_table.setRowCount(0) self.results_table.setHorizontalHeaderLabels( - ['Relative pressure (p/p\u2080)', 'Quantity adsorbed (cm\u00B3/g)']) + ['Relative pressure (p/p\u2080)', 'Quantity adsorbed (cm\u00B3 STP/g)']) self.results_table.setColumnWidth(0, 250) self.results_table.setColumnWidth(1, 250) diff --git a/betsi/lib.py b/betsi/lib.py index 6215691..883a5a2 100755 --- a/betsi/lib.py +++ b/betsi/lib.py @@ -175,9 +175,10 @@ def distance_to_pchip(_s,_mono): self.pc_error[i, j] = ( self.error[i, j] / self.corresponding_pressure_pchip[i,j]) * 100. - # ROUQUEROL CRITERIA 5. Linear region must end at the knee - if j == self.knee_index: - self.rouq5[i, j] = 1 + ### ROUQUEROL CRITERIA 5. Linear region must end at the knee + ##if j == self.knee_index: + ## self.rouq5[i, j] = 1 + self.rouq5[i, j] = 1 class BETFilterAppliedResults: """ @@ -209,9 +210,12 @@ def __init__(self, bet_result, **kwargs): max_perc_error = kwargs.get('max_perc_error', 20) filter_mask = filter_mask * (bet_result.pc_error < max_perc_error) - if kwargs.get('use_rouq5', False): + ##if kwargs.get('use_rouq5', False): + ## filter_mask = filter_mask * bet_result.rouq5 + if kwargs.get('use_rouq5', True): filter_mask = filter_mask * bet_result.rouq5 + self.use_rouq5 = kwargs.get('use_rouq5', True) adsorbate = kwargs.get('adsorbate', "N2") if adsorbate == "Custom": @@ -272,8 +276,11 @@ def __init__(self, bet_result, **kwargs): filtered_pcerrors = bet_result.pc_error + 1000.0 * (1 - filter_mask) knee_filtered_pcerrors = bet_result.pc_error + \ 1000.0 * (1 - knee_filter) - min_i, min_j = np.unravel_index( - np.argmin(knee_filtered_pcerrors), filtered_pcerrors.shape) + + if self.use_rouq5: + min_i, min_j = np.unravel_index(np.argmin(knee_filtered_pcerrors), filtered_pcerrors.shape) + else: + min_i, min_j = np.unravel_index(np.argmin(filtered_pcerrors), filtered_pcerrors.shape) self.min_i = min_i self.min_j = min_j diff --git a/betsi/utils.py b/betsi/utils.py index 4f0ef31..c88424b 100755 --- a/betsi/utils.py +++ b/betsi/utils.py @@ -3,12 +3,13 @@ """ from pathlib import Path +import pandas as pd import numpy as np from scipy.interpolate import splrep, PchipInterpolator, pchip_interpolate def get_data(input_file): - """Read pressure and Nitrogen uptake data from file. Asserts pressures in units of bar. + """Read pressure and adsorbate uptake data from file. Asserts pressures in units of bar. Args: input_file: Path the path to the input file @@ -19,7 +20,9 @@ def get_data(input_file): input_file = Path(input_file) if str(input_file).find('.txt') != -1 or str(input_file).find('.aif') != -1 or str(input_file).find('.csv') != -1: - pressure, q_adsorbed = get_data_for_txt_aif_csv(str(input_file)) + pressure, q_adsorbed = get_data_for_txt_aif_csv_two_columns(str(input_file)) + elif str(input_file).find('.XLS') != -1: + pressure, q_adsorbed = get_data_from_micromeritics_xls(str(input_file)) else: pressure = np.array([]) q_adsorbed = np.array([]) @@ -84,7 +87,7 @@ def get_fitted_spline(pressure, q_adsorbed): Args: pressure: Array of relative pressure values. - q_adsorbed: Array of Nitrogen uptake values. + q_adsorbed: Array of adsorbate uptake values. Returns: tck tuple of spline parameters. @@ -96,7 +99,7 @@ def get_pchip_interpolation(pressure,q_adsorbed): Args: pressure: Array of relative pressure values - q_adsorbed: Array of Nitrogen uptake values + q_adsorbed: Array of adsorbate uptake values Returns: Pchip parameters @@ -108,9 +111,9 @@ def isotherm_pchip_reconstruction(pressure, q_adsorbed, num_of_interpolated_poin calculate BET area for difficult isotherms Args: pressure: Array of relative pressure values - q_adsorbed: Array of Nitrogen uptake values + q_adsorbed: Array of adsorbate uptake values - Returns: Array of interpolated nitrogen uptake values + Returns: Array of interpolated adsorbate uptake values """ ## x_range = np.linspace(pressure[0], pressure[len(pressure) -1 ], 500) @@ -144,8 +147,8 @@ def isotherm_pchip_reconstruction(pressure, q_adsorbed, num_of_interpolated_poin return pressure_new, q_adsorbed_new -def get_data_for_txt_aif_csv(input_file): - """ Read pressure and Nitrogen uptake data from file if the file extension is *.txt or *.aif. +def get_data_for_txt_aif_csv_two_columns(input_file): + """ Read pressure and adsorbate uptake data from file if the file extension is *.txt or *.aif. this function will be called in the get_data() function, and should not be called alone anywhere in the code as it might cause some errors. @@ -220,4 +223,92 @@ def get_data_for_txt_aif_csv(input_file): pressure = np.array(pressure) q_adsorbed = np.array(q_adsorbed) + return pressure, q_adsorbed + +def get_data_from_micromeritics_xls(input_file): + """ Read pressure and adsorbate uptake data from Micromeritics output files with *.XLS extension + this function will be called in the get_data() function, and should not be called alone anywhere in the code + as it might cause some errors. + + Args: + input_file: String of the path to the input file + + Returns: + Pressure and Quantity adsorbed. + """ + + # pressure = [] + # q_adsorbed = [] + + excel_file = pd.ExcelFile(input_file) + + if len(excel_file.sheet_names) == 1: + excel_df = pd.read_excel(input_file, sheet_name=0) + column_num = 0 + for column in excel_df.columns: + if len(excel_df.loc[excel_df[column]=='Isotherm Linear Plot']) > 0: + keyword_column_name = column + keyword_index = excel_df.loc[excel_df[column]=='Isotherm Linear Plot'].index[0] + + adsorption_data_start_index = 0 + for row_num in range(keyword_index+1, len(excel_df.index)): + if excel_df[keyword_column_name][row_num] == "Relative Pressure (p/p°)": + adsorption_data_start_index = row_num + 1 + + nan_indexes_in_column = np.array(excel_df.loc[excel_df[column].isnull()].index) + adsorption_data_stop_index = nan_indexes_in_column[np.argmax(nan_indexes_in_column > adsorption_data_start_index)] - 1 + + if adsorption_data_stop_index < adsorption_data_start_index: + adsorption_data_stop_index = len(excel_df.index) - 1 + + if adsorption_data_start_index > 0: + pressure = np.array(excel_df[keyword_column_name][adsorption_data_start_index:adsorption_data_stop_index+1]) + q_adsorbed = np.array(excel_df[excel_df.columns[column_num+1]][adsorption_data_start_index:adsorption_data_stop_index+1]) + else: + pressure = np.array([]) + q_adsorbed = np.array([]) + + break + column_num += 1 + + elif len(excel_file.sheet_names) == 0: + pressure = np.array([]) + q_adsorbed = np.array([]) + else: + try: + excel_df = pd.read_excel(input_file, sheet_name="Isotherm Linear Plot") + column_num = 0 + for column in excel_df.columns: + if len(excel_df.loc[excel_df[column]=='Isotherm Linear Plot']) > 0: + keyword_column_name = column + keyword_index = excel_df.loc[excel_df[column]=='Isotherm Linear Plot'].index[0] + + adsorption_data_start_index = 0 + for row_num in range(keyword_index+1, len(excel_df.index)): + if excel_df[keyword_column_name][row_num] == "Relative Pressure (p/p°)": + adsorption_data_start_index = row_num + 1 + + nan_indexes_in_column = np.array(excel_df.loc[excel_df[column].isnull()].index) + adsorption_data_stop_index = nan_indexes_in_column[np.argmax(nan_indexes_in_column > adsorption_data_start_index)] - 1 + + if adsorption_data_stop_index < adsorption_data_start_index: + adsorption_data_stop_index = len(excel_df.index) - 1 + + if adsorption_data_start_index > 0: + pressure = np.array(excel_df[keyword_column_name][adsorption_data_start_index:adsorption_data_stop_index+1]) + q_adsorbed = np.array(excel_df[excel_df.columns[column_num+1]][adsorption_data_start_index:adsorption_data_stop_index+1]) + else: + pressure = np.array([]) + q_adsorbed = np.array([]) + + break + column_num += 1 + + except ValueError: + pressure = np.array([]) + q_adsorbed = np.array([]) + + pressure = np.float64(pressure) + q_adsorbed = np.float64(q_adsorbed) + return pressure, q_adsorbed \ No newline at end of file diff --git a/executables/BETSI_v1.1.0_linux.zip b/executables/BETSI_v1.1.0_linux.zip deleted file mode 100755 index 4f0caa9..0000000 --- a/executables/BETSI_v1.1.0_linux.zip +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f25ad645d971bc43a1db4d56448c496afe16f30eddd755758c679940ff5c26f3 -size 117476105 diff --git a/executables/BETSI_v1.1.0_mac.zip b/executables/BETSI_v1.1.0_mac.zip deleted file mode 100755 index 0c2f6ec..0000000 --- a/executables/BETSI_v1.1.0_mac.zip +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:5b35ff9472175426e78f020f93c094c932c8bf1f206b5de0af2e8e8c82b1d3a7 -size 234884121 diff --git a/executables/BETSI_v1.1.0_windows.exe b/executables/BETSI_v1.1.0_windows.exe index bdb9d4d..51dd1fc 100755 --- a/executables/BETSI_v1.1.0_windows.exe +++ b/executables/BETSI_v1.1.0_windows.exe @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:debcf883e16c8b789db076fa2d7d7f18eb7b171f90a844553b15ad1a3f1b7d86 -size 183088923 +oid sha256:d0d973d7b1461fe02cfd1e82bafa5d7e634076dfd09dcb86378551e48cbb5a47 +size 183182879 diff --git a/executables/BETSI_v1_1_0_linux.zip b/executables/BETSI_v1_1_0_linux.zip new file mode 100755 index 0000000..05aa0f8 --- /dev/null +++ b/executables/BETSI_v1_1_0_linux.zip @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9fe6a2109a0c40e5fef4be93cf4fefb0f510a77fa996e09f3f8470581c0553d4 +size 117570162 diff --git a/executables/BETSI_v1_1_0_mac.zip b/executables/BETSI_v1_1_0_mac.zip new file mode 100755 index 0000000..c756c7a --- /dev/null +++ b/executables/BETSI_v1_1_0_mac.zip @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2eff4fd62f6050c8bb5b9744d2cb9cb68f2d9c479337dc6c2b6dec047f2fde67 +size 117573850 diff --git a/requirements.txt b/requirements.txt index 89b4541..e0a66e6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,4 +4,5 @@ matplotlib==3.2.2 PyQt5==5.9.2 pandas==1.1.5 seaborn==0.11.0 -statsmodels==0.12.1 \ No newline at end of file +statsmodels==0.12.1 +xlrd==2.0.1