Skip to content

Commit

Permalink
new feature: can read Micromeritics xls file as input
Browse files Browse the repository at this point in the history
  • Loading branch information
MRAlizadeh-mra51 committed Apr 25, 2023
1 parent 1fee95e commit c4bb7d5
Show file tree
Hide file tree
Showing 9 changed files with 144 additions and 36 deletions.
35 changes: 22 additions & 13 deletions betsi/gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}')
Expand All @@ -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()
Expand All @@ -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}')
Expand Down Expand Up @@ -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]')
Expand All @@ -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 (nm<sup>2</sup>):')
self.adsorbate_cross_section_edit = QLineEdit()
self.adsorbate_cross_section_edit.setMaximumWidth(75)
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)

Expand Down
19 changes: 13 additions & 6 deletions betsi/lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
"""
Expand Down Expand Up @@ -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":
Expand Down Expand Up @@ -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

Expand Down
107 changes: 99 additions & 8 deletions betsi/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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([])
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
3 changes: 0 additions & 3 deletions executables/BETSI_v1.1.0_linux.zip

This file was deleted.

3 changes: 0 additions & 3 deletions executables/BETSI_v1.1.0_mac.zip

This file was deleted.

4 changes: 2 additions & 2 deletions executables/BETSI_v1.1.0_windows.exe
Git LFS file not shown
3 changes: 3 additions & 0 deletions executables/BETSI_v1_1_0_linux.zip
Git LFS file not shown
3 changes: 3 additions & 0 deletions executables/BETSI_v1_1_0_mac.zip
Git LFS file not shown
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ matplotlib==3.2.2
PyQt5==5.9.2
pandas==1.1.5
seaborn==0.11.0
statsmodels==0.12.1
statsmodels==0.12.1
xlrd==2.0.1

0 comments on commit c4bb7d5

Please sign in to comment.