diff --git a/.github/workflows/windows-app.yml b/.github/workflows/windows-app.yml index 3f4ffa7..c2b210a 100644 --- a/.github/workflows/windows-app.yml +++ b/.github/workflows/windows-app.yml @@ -21,7 +21,7 @@ jobs: run: | pip install -r requirements.txt pip install pyinstaller - - name: Build application and ru Inno Setup + - name: Build application and run Inno Setup run: | python -m PyInstaller --onefile --noconsole --icon="icon\app_icon.tiff" --name="ThermLUM" --uac-admin start_app.py iscc ".\ThermLUM.iss" @@ -31,8 +31,8 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - tag_name: 1.0.0 #VERSION - release_name: Release 1.0.0 #VERSION + tag_name: 1.1.0 #VERSION + release_name: Release 1.1.0 #VERSION draft: false prerelease: false - name: Upload Release Asset @@ -46,7 +46,7 @@ jobs: asset_name: ThermLUMsetup.exe asset_content_type: application/x-msdownload - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 with: name: ThermLUMsetup.exe path: ./dist/ThermLUMsetup.exe diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cb98d20 --- /dev/null +++ b/.gitignore @@ -0,0 +1,45 @@ +# Byte-compiled / optimized / DLL files +*.py[cod] +*$py.class +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec +# Installer logs +pip-log.txt +pip-delete-this-directory.txt +# Translations +*.mo +*.pot +# Distribution / packaging +.Python +/build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST +# VS +.idea +*.xml \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 13566b8..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/ThermLUM.iss b/ThermLUM.iss index 69a0a71..8478517 100644 --- a/ThermLUM.iss +++ b/ThermLUM.iss @@ -2,7 +2,7 @@ ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! #define MyAppName "ThermLUM" -#define MyAppVersion "1.0.0" +#define MyAppVersion "1.1.0" #define MyAppPublisher "Multifunctional Luminescent Materials Group" #define MyAppExeName "ThermLUM.exe" #define MyAppAssocName MyAppName + " File" diff --git a/core.py b/core.py deleted file mode 100644 index 66ccf70..0000000 --- a/core.py +++ /dev/null @@ -1,31 +0,0 @@ -# from creation import new - -# file_path = r"C:\Users\hu3rt\Desktop\[Ln(2,2'-bpdo)4][Cr(CN)6]\EuCr_22bpdo_dT\Exc_spectra_T_map_30to300K_em614nm_0.5nm.txt" - -# thermap = new(file_path, 'test') -# print(thermap.normalize(500.)) - - - -# test = pd.read_csv(file, skiprows=2) -# -# temps_row = test[test.iloc[:, 0] == 'Temp'] -# print(test['Labels']) - -from scipy.signal import savgol_filter -import numpy as np - -# Generate some noisy data -x = np.linspace(0, 10, 100) -y = np.sin(x) + np.random.normal(0, 0.1, 100) - -# Apply Savitzky-Golay filter with window size 5 and polynomial order 2 -y_filtered = savgol_filter(y, window_length=5, polyorder=2, delta=0.1) - -# Plot original and filtered data -import matplotlib.pyplot as plt - -plt.plot(x, y, label='Noisy Data') -plt.plot(x, y_filtered, label='Filtered Data') -plt.legend() -plt.show() \ No newline at end of file diff --git a/creation.py b/creation.py index 9786896..942d26c 100644 --- a/creation.py +++ b/creation.py @@ -12,7 +12,7 @@ # GNU General Public License for more details. import pandas as pd import h5py -from os import path +from os import path, remove def new(file_path, hdf_name): file_directory, file_name = path.split(file_path) with open(file_path, "r") as file: @@ -49,5 +49,10 @@ def new(file_path, hdf_name): group.create_dataset(f'temperatures_{hdf_name}', data=temps) hdf_file.close() + if path.exists(path.join(file_directory, f'description_{file_name.split('.')[0]}')): + remove(path.join(file_directory, f'description_{file_name.split('.')[0]}')) + if path.exists(path.join(file_directory, f'_temp_data)_{file_name.split('.')[0]}.csv')): + remove(path.join(file_directory, f'_temp_data)_{file_name.split('.')[0]}.csv')) + from thermmap_object import ThermMap return ThermMap(hdf_file_path) diff --git a/fitting_functions.py b/fitting_functions.py index e73be52..1566fdb 100644 --- a/fitting_functions.py +++ b/fitting_functions.py @@ -85,4 +85,4 @@ def coth_vibration_error(x, delta0, ev, b, delta0_error, ev_error, b_error): 'Exponential decay': exponential_decay_error, 'coth': coth_vibration_error } - +#NEWLINE diff --git a/gui.py b/gui.py index 76dfdf2..018a029 100644 --- a/gui.py +++ b/gui.py @@ -21,8 +21,10 @@ from PyQt6.QtWidgets import QDialog, QSpinBox, QDialogButtonBox, QLabel, QWidget, QVBoxLayout, \ QApplication, QMainWindow, QFileDialog, QHBoxLayout, QGridLayout, QAbstractSpinBox, QComboBox, \ - QFormLayout, QDoubleSpinBox, QPushButton, QStackedLayout, QSizePolicy + QFormLayout, QDoubleSpinBox, QPushButton, QStackedLayout, QSizePolicy, QCheckBox, QButtonGroup, QGroupBox from PyQt6.QtGui import QIcon +from PyQt6.QtCore import Qt + import matplotlib as mpl # import matplotlib after PyQt6 from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg @@ -152,7 +154,7 @@ def __init__(self, thermmap, parent=None): self.button_box.accepted.connect(self.accept) self.button_box.rejected.connect(self.reject) - layout_ribbon.addWidget(QLabel("To correctly determine error smooth data in a way that removes noise but do not obscure any peaks.\n Smoothing options of Savitzky-Goloy filter"), 0, 0) + layout_ribbon.addWidget(QLabel("To correctly determine error smooth data in a way that removes noise but do not obscure any peaks.\nSmoothing options of Savitzky-Goloy filter"), 0, 0) layout_ribbon.addWidget(QLabel("Window length"), 1, 0) layout_ribbon.addWidget(self.window_length_widget, 1, 1) layout_ribbon.addWidget(QLabel("Polynomial order"), 2, 0) @@ -162,7 +164,38 @@ def __init__(self, thermmap, parent=None): layout_ribbon.addWidget(QLabel("Sum of all residuals should be as close to 0 as posible for correct error determination"), 4, 0) layout_ribbon.addWidget(QLabel("Sum of all residuals:"), 5, 0) layout_ribbon.addWidget(self.residula_widget, 5, 1) - layout_ribbon.addWidget(self.button_box, 6, 0, 6, 1) + + + layout_ribbon.addWidget(QLabel("Detector error's setettings:"), 6, 0) + # Constant error option + self.constant_detector_error_checkbox = QCheckBox('Constant error') + layout_ribbon.addWidget(self.constant_detector_error_checkbox, 7, 0) + self.constant_detector_error_spinbox = QSpinBox( + minimum=1, + maximum=1000000, + ) + self.constant_detector_error_spinbox.setValue(250) + self.constant_detector_error_spinbox.setKeyboardTracking(False) + self.constant_detector_error_spinbox.setMinimumWidth(150) + self.constant_detector_error_spinbox.setButtonSymbols(QAbstractSpinBox.ButtonSymbols.NoButtons) + layout_ribbon.addWidget(self.constant_detector_error_spinbox, 7, 1) + self.constant_detector_error_checkbox.stateChanged.connect(lambda: self.handle_detector_checkbox(self.constant_detector_error_checkbox)) + + # Smooth error option + self.smoothed_detector_error_checkbox = QCheckBox('Determined from smoothing') + layout_ribbon.addWidget(self.smoothed_detector_error_checkbox, 8, 0) + self.smoothed_detector_error_checkbox.stateChanged.connect(lambda: self.handle_detector_checkbox(self.smoothed_detector_error_checkbox)) + + + layout_ribbon.addWidget(QLabel("Function error's setettings:"), 9, 0) + function_error_group = QButtonGroup() + self.easy_function_error_checkbox = QCheckBox('Fit - exp diffrence error') + layout_ribbon.addWidget(self.easy_function_error_checkbox, 10, 0) + + function_error_group.addButton(self.easy_function_error_checkbox) + + + layout_ribbon.addWidget(self.button_box, 11, 0, 11, 1) layout_outer.addLayout(layout_plot, 0, 0) @@ -172,6 +205,18 @@ def __init__(self, thermmap, parent=None): self.update() self.setLayout(layout_outer) + def handle_detector_checkbox(self, selected_checkbox): + # Uncheck other checkboxes + for checkbox in [self.constant_detector_error_checkbox, self.smoothed_detector_error_checkbox]: + if checkbox != selected_checkbox: + checkbox.blockSignals(True) # Temporarily block signals to prevent recursion + checkbox.setChecked(False) + checkbox.blockSignals(False) # Re-enable signals + if self.constant_detector_error_checkbox.isChecked(): + self.constant_detector_error_spinbox.setEnabled(True) + else: + self.constant_detector_error_spinbox.setEnabled(False) + def update(self, _=None): window_length = self.window_length_widget.value() polyorder = self.polyorder_widget.value() @@ -237,8 +282,6 @@ def __init__(self, *args, **kwargs): filters, selectedFilter) settings['recently_opened_folder'] = path.split(file_path)[0] - with open('./settings.json', 'r+') as settings_json: - settings_json.write(json.dumps(settings, indent=0)) self.thermmap = new(file_path, path.split(file_path)[1][:-4]) self.thermmap.get_data() @@ -277,8 +320,10 @@ def __init__(self, *args, **kwargs): self.normalization_line = None self.fitting_plot = None self.sensitivity_plot = None - self.error_bar_plot = None - self.temperature_err = None + self.error_bar_plot_detector = None + self.error_bar_plot_function = None + self.temperature_err_detector = None + self.temperature_err_function = None self.cid1 = self.canvas.mpl_connect('button_press_event', self.on_click) self.cid2 = self.canvas.mpl_connect('pick_event', self.on_pick) @@ -319,6 +364,9 @@ def __init__(self, *args, **kwargs): maximum=self.thermmap.data[-1, 0], singleStep=self.thermmap.resolution ) + self.swap_widget = QPushButton('Swap') + self.swap_widget.clicked.connect(self.on_wavelength_switch) + layout_wavelengths_chooser.addRow(QLabel(''), self.swap_widget) self.second_value_widget.setKeyboardTracking(False) self.second_value_widget.setMinimumWidth(150) self.second_value_widget.valueChanged.connect(self.on_second_value_changed) @@ -534,6 +582,12 @@ def on_second_value_changed(self, value): self.second_click = False self.canvas.draw() self.normalized_canvas.draw() + + def on_wavelength_switch(self): + if (self.second_line_position is not None) and (self.first_line_position is not None): + _temp_value = self.first_value_widget.value() + self.first_value_widget.setValue(self.second_line_position) + self.second_value_widget.setValue(_temp_value) def on_normalization_value_changed(self, value): new_value = quantization_to_resolution(value, self.thermmap.resolution) @@ -733,26 +787,43 @@ def start_fitting(self): def determine_error(self): dialog = ErrorDeterminingDialog(self.thermmap, self) if dialog.exec() == QDialog.DialogCode.Accepted: - self.smoothed_data, self.smoothed_residual = dialog.smoothed_data, dialog.smoothed_residual + if dialog.smoothed_detector_error_checkbox.isChecked(): + self.smoothed_data, self.smoothed_residual = dialog.smoothed_data, dialog.smoothed_residual - detector_err = self.thermometric_parameter * sqrt((self.thermmap.general_get_row_of_ydata(self.smoothed_residual, self.first_line_position) / self.thermmap.get_row_of_ydata(self.first_line_position))**2 + + detector_err = sqrt((self.thermmap.general_get_row_of_ydata(self.smoothed_residual, self.first_line_position) / self.thermmap.get_row_of_ydata(self.first_line_position))**2 + (self.thermmap.general_get_row_of_ydata(self.smoothed_residual, self.second_line_position) / self.thermmap.get_row_of_ydata(self.second_line_position))**2) - - # function_err = list(dict_of_fitting_errors_functions.values())[self.fitting_functions_layout.currentIndex()](self.thermometric_parameter, *self.fitted_output_parameters, *self.parameter_errors) - function_err = abs(self.thermometric_parameter - list(dict_of_fitting_functions.values())[self.fitting_functions_layout.currentIndex()](self.thermometric_parameter, *self.fitted_output_parameters)) + elif dialog.constant_detector_error_checkbox.isChecked(): + constant_err = dialog.constant_detector_error_spinbox.value() + detector_err = sqrt((constant_err / self.thermmap.get_row_of_ydata(self.first_line_position))**2 + (constant_err / self.thermmap.get_row_of_ydata(self.second_line_position))**2) + if dialog.easy_function_error_checkbox.isChecked(): + # function_err = list(dict_of_fitting_errors_functions.values())[self.fitting_functions_layout.currentIndex()](self.thermometric_parameter, *self.fitted_output_parameters, *self.parameter_errors) + function_err = abs(self.thermometric_parameter - list(dict_of_fitting_functions.values())[self.fitting_functions_layout.currentIndex()](self.thermometric_parameter, *self.fitted_output_parameters)) / list(dict_of_fitting_functions.values())[self.fitting_functions_layout.currentIndex()](self.thermometric_parameter, *self.fitted_output_parameters) + else: + function_err = 0 total_err = sqrt(detector_err**2 + function_err**2) - self.temperature_err = (total_err / self.thermometric_parameter) * (1 / self.discontinuous_sensitivity) + percent_detector = detector_err / (detector_err + function_err) + + self.temperature_err_detector = (total_err * percent_detector) * (1 / self.discontinuous_sensitivity) + self.temperature_err_function = (total_err * (1 - percent_detector)) * (1 / self.discontinuous_sensitivity) - if self.error_bar_plot is not None: - self.error_bar_plot[0].remove() - self.error_bar_plot = self.fitting_canvas.error_axes.bar( + if self.error_bar_plot_detector is not None: + self.error_bar_plot_detector[0].remove() + self.error_bar_plot_function[0].remove() + self.error_bar_plot_detector = self.fitting_canvas.error_axes.bar( self.thermmap.temperatures, - self.temperature_err, + self.temperature_err_detector, color='#E56B6F', width=8 ) + self.error_bar_plot_function = self.fitting_canvas.error_axes.bar( + self.thermmap.temperatures, + self.temperature_err_function, + color='#6D597A', + width=8, + bottom=self.temperature_err_detector + ) self.fitting_canvas.error_axes.set_ylim(0, 2) self.fitting_canvas.error_axes.set_ylabel(r'Error / $\mathrm{K}$') self.fitting_canvas.draw() @@ -770,9 +841,10 @@ def export_data(self): result_dict['Fit temperature / K'] = self.fit_x result_dict['Fitted parameter'] = self.fitted_output_data result_dict['Relative sensitivity / %K^(-1)'] = self.sensitivity - if self.temperature_err is not None: + if self.temperature_err_detector is not None: result_dict['Error temperature / K'] = self.thermmap.temperatures - result_dict['Temperature error / K'] = self.temperature_err + result_dict['Temperature error (detector) / K'] = self.temperature_err_detector + result_dict['Temperature error (function) / K'] = self.temperature_err_function for index, fitted_parameter in enumerate(signature(list(dict_of_fitting_functions.values())[self.fitting_functions_layout.currentIndex()]).parameters): if index == 0: continue @@ -787,6 +859,11 @@ def export_data(self): self.result.to_csv(filename) + def closeEvent(self, event): + with open('./settings.json', 'w') as settings_json: + settings_json.write(json.dumps(settings, indent=0)) + + def run_gui(): diff --git a/package.json b/package.json index 007f491..7455fd9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ThermLUM", - "version": "1.0.0", + "version": "1.1.0", "dependencies": { } } diff --git a/plotting.py b/plotting.py index a52b6aa..cac1fb6 100644 --- a/plotting.py +++ b/plotting.py @@ -12,7 +12,7 @@ # GNU General Public License for more details. import matplotlib.pyplot as plt import numpy as np - +#EGG def luminescence_dt(data, temperatures, axes: plt.Axes, colormap): counter = 0 diff --git a/requirements.txt b/requirements.txt index 7d34ea9..3ff0136 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,8 @@ -h5py==3.11.0 +h5py matplotlib==3.8.0 -numpy==2.1.2 -pandas==2.2.3 -PyQt6==6.7.1 -PyQt6_sip==13.8.0 -scipy==1.14.1 +numpy +pandas +PyQt6 +PyQt6_sip +scipy +requests diff --git a/start_app.py b/start_app.py index 9f668db..c1fda80 100644 --- a/start_app.py +++ b/start_app.py @@ -10,5 +10,37 @@ # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. +import sys from gui import run_gui +from PyQt6.QtWidgets import QApplication, QWidget, QVBoxLayout, QLabel, QPushButton +from PyQt6.QtGui import QIcon +import requests #type: ignore +from webbrowser import open + +class Launcher(QApplication): + """Checks if installed ThermLUM version is actual. Helps install the latest version. Credit to relACs: + """ + def __init__(self, sys_argv:list[str]): + super(Launcher, self).__init__(sys_argv) + self.window = QWidget() + app_icon: QIcon = QIcon(r'.\icon\app_icon.tiff') + Launcher.setWindowIcon(app_icon) + launcher_layout = QVBoxLayout() + launcher_layout.addWidget(QLabel(f"New ThermLUM version {latest_relase} is available.")) + install_button = QPushButton("Install new version") + install_button.clicked.connect(lambda: open(f"https://github.com/HubertCo22bpdo/ThermLUM/tag/{latest_relase}")) #type: ignore + skip_button: QPushButton = QPushButton("Continue using old version") + skip_button.clicked.connect(Launcher.closeAllWindows) #type: ignore + launcher_layout.addWidget(install_button) + launcher_layout.addWidget(skip_button) + self.window.setLayout(launcher_layout) + self.window.show() + +response = requests.get("https://api.github.com/repos/HubertCo22bpdo/ThermLUM/releases") +if response.ok: + release = "1.1.0" #VERSION + latest_relase= response.json()[0]["tag_name"] + if latest_relase != release: + launcher = Launcher(sys.argv) + launcher.exec() run_gui()