#This is part of the source code for the Paineira Graphical User Interface - Iguape
#The code is distributed under the GNU GPL-3.0 License. Please refer to the main page (https://github.com/cnpem/iguape) for more information
"""
This is the main script for the excution of the Paineira Graphical User Interface, a GUI for visualization and data processing during in situ experiments at Paineira.
In this script, both GUIs used by the program are called and all of the backend functions and processes are defined.
"""
import sys, time, gc, copy, matplotlib, re
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT
import matplotlib.colorbar
import matplotlib.backends.backend_svg
from matplotlib.figure import Figure
import matplotlib.pyplot as plt
from matplotlib.lines import Line2D
from matplotlib.widgets import SpanSelector, Cursor
from matplotlib.cm import ScalarMappable
import matplotlib.font_manager
from matplotlib.colors import LogNorm, PowerNorm, CenteredNorm, Normalize
import numpy as np
import pandas as pd
from Monitor import FolderMonitor
from GUI.iguape_GUI import Ui_MainWindow
from GUI.pk_window import Ui_pk_window
from GUI.export_figure import Ui_Export_Figure
from GUI.filter_gui import Ui_Filter_Dialog
from Monitor import *
if getattr(sys, 'frozen', False):
import pyi_splash #If the program is executed as a pyinstaller executable, import pyi_splash for the Splash Screen
license = 'GNU GPL-3.0 License'
counter.count = 0
fonts_list = [font.name for font in matplotlib.font_manager.fontManager.ttflist]
cmaps = [cmap for cmap in plt.colormaps()]
[docs]
class Window(QMainWindow, Ui_MainWindow):
"""Class for IGUAPE main window. It inherits QMainWindow from PyQt5 and Ui_MainWindow from GUI.iguape_GUI
:param QMainWindow: QMainWindow from PyQt5
:type QMainWindow: QMainWindow
:param Ui_MainWindow: Ui_MainWindow from GUI.iguape_GUI
:type Ui_MainWindow: QMainWindow
"""
[docs]
def __init__(self, parent=None):
"""Constructor for Window class
Args:
parent (optional): Defaults to None.
"""
super().__init__(parent)
self.setupUi(self)
geometry = QGuiApplication.screens()[-1].availableGeometry()
#print(geometry)
self.props_dict = {"Main Axis": {"X_Label": "2θ (°)", "Y_Label": "Intensity (a.u.)", "Cmap_Label": "XRD acquisition order"},
"Peak Fit Axis": {"Peak Position Axis": {"X_Label": "XRD acquisition order", "Y_Label": "Peak Position (°)", "Cmap_Label": "XRD acquisition order"},
"FWHM Axis": {"X_Label": "XRD acquisition order", "Y_Label": "FWHM (°)", "Cmap_Label": "XRD acquisition order"},
"Integrated Area Axis": {"X_Label": "XRD acquisition order", "Y_Label": "Peak Integrated Area (a.u.)", "Cmap_Label": "XRD acquisition order"}},
"Contour Axis": {"X_Label": "2θ (°)", "Y_Label": "XRD acquisition order", "Cmap_Label": "Intensity (a.u.)"},
"Normalization Axis": {"X_Label": "2θ (°)", "Y_Label": "Normalized Intensity (a.u.)", "Cmap_Label": "XRD acquisition order"}}
self.setGeometry(geometry)
self.create_graphs_layout()
self.gc_collector = GarbageCollector()
self.gc_collector.start()
if getattr(sys, 'frozen', False):
pyi_splash.close() #After the GUI initialization, close the Splash Screen
[docs]
def create_graphs_layout(self):
"""Routine to initialize and connect UI elements. All parameters and flags are initiated and UI element's signals are connected to its functions.
"""
self.url_data = {self.paineira_logo: "https://lnls.cnpem.br/facilities/paineira-en/", self.LNLS_logo: "https://lnls.cnpem.br/en/", self.CNPEM_logo: "https://cnpem.br/en/", self.iguape_logo: "https://cnpem.github.io/iguape/"}
for logo in self.url_data.keys():
logo.installEventFilter(self)
#self.XRD_data_layout = QVBoxLayout()
# Creating the main Figure and Layout #
self.fig_main = Figure(dpi=100)
self.gs_main = self.fig_main.add_gridspec(1, 1)
self.ax_main = self.fig_main.add_subplot(self.gs_main[0, 0])
self.fig_main.set_layout_engine('constrained')
self.canvas_main = FigureCanvas(self.fig_main)
self.ax_main.set_xlabel('2θ (°)', fontsize = 15)
self.ax_main.set_ylabel('Intensity (a.u.)', fontsize = 15)
self.ax_main.text(0.5, 0.5, "Biondo Neto, J. L., Cintra Mauricio, J. & Rodella, C. B. (2025). \n J. Appl. Cryst. 58, 1061-1067.",
fontsize=20, color="grey", alpha=0.8, ha="center", va="center", rotation=10)
self.XRD_data_layout.addWidget(self.canvas_main)
#self.XRD_data_tab.setLayout(self.XRD_data_layout)
#self.peak_fit_layout = QVBoxLayout()
#Creating the fitting parameter Figure and Layout#
self.fig_sub = Figure(figsize=(8, 5), dpi=100)
self.gs_sub = self.fig_sub.add_gridspec(1, 3)
self.ax_2theta = self.fig_sub.add_subplot(self.gs_sub[0, 0])
self.ax_area = self.fig_sub.add_subplot(self.gs_sub[0, 2])
self.ax_FWHM = self.fig_sub.add_subplot(self.gs_sub[0, 1])
self.fig_sub.set_layout_engine('constrained')
self.canvas_sub = FigureCanvas(self.fig_sub)
self.peak_fit_layout.addWidget(self.canvas_sub)
self.cursor = None
self.fig_contour = Figure(figsize=(8, 6), dpi = 100)
self.gs_contour = self.fig_contour.add_gridspec(1,1)
self.ax_contour = self.fig_contour.add_subplot(self.gs_contour[0,0])
self.fig_contour.set_layout_engine('constrained')
self.canvas_contour = FigureCanvas(self.fig_contour)
self.ax_contour.set_xlabel('2θ (°)', fontsize = 15)
self.contour_layout.addWidget(self.canvas_contour)
self.fig_norm = Figure(figsize=(8,6), dpi= 100)
self.gs_norm = self.fig_norm.add_gridspec(1,1)
self.ax_norm = self.fig_norm.add_subplot(self.gs_norm[0,0])
self.fig_norm.set_layout_engine('constrained')
self.canvas_norm = FigureCanvas(self.fig_norm)
self.ax_norm.set_xlabel('2θ (°)', fontsize = 15)
self.normalization_layout.addWidget(self.canvas_norm)
#Creating a colormap on the main canvas#
self.cmap = plt.get_cmap("coolwarm")
self.norm = plt.Normalize(vmin=0, vmax=1) # Initial placeholder values for norm #
self.sm = ScalarMappable(cmap=self.cmap, norm=self.norm)
self.sm.set_array([])
self.cax = self.fig_main.colorbar(self.sm, ax=self.ax_main) # Creating the colorbar axes #
self.cax_2 = self.fig_sub.colorbar(self.sm, ax=self.ax_area) # Creating the colorbar axes #
self.cax_3 = self.fig_contour.colorbar(self.sm, ax = self.ax_contour)
self.cax_4 = self.fig_norm.colorbar(self.sm, ax = self.ax_norm)
#Connecting functions to buttons#
self.refresh_button.clicked.connect(self.update_graphs)
self.refresh_button_peak_fit.clicked.connect(self.update_graphs)
self.reset_button.clicked.connect(self.reset_interval)
self.peak_fit_button.clicked.connect(self.select_fit_interval)
self.save_peak_fit_data_button.clicked.connect(self.save_data_frame)
self.contour_button.clicked.connect(self.contour)
self.color_pallete_comboBox.activated.connect(self.on_change_color_pallete)
self.color_pallete_comboBox_2.activated.connect(self.on_change_color_pallete)
#self.checkBox.setChecked(True)
self.checkBox.stateChanged.connect(self.on_change_vline_checkbox)
self.plot_with_temp = False
self.export_window = None
self.selected_interval = None
self.filter_window = None
self.fit_interval = None
self.monitor = None
self.normalize_state = False
self.Q_vector_state = False
self.norms = {'LogNorm': LogNorm(), "PowerNorm": PowerNorm(gamma=0.5), 'CenteredNorm': CenteredNorm(), "LinearNorm": None}
self.folder_selected = False
self.temp_mask_signal = False
self.temp_mask = slice(None)
self.plot_data = pd.DataFrame()
#Create span selector on the main plot#
self.span = SpanSelector(self.ax_main, self.onselect, 'horizontal', useblit=True,
props=dict(alpha=0.3, facecolor='red', capstyle='round'))
self.color_pallete_comboBox.addItems(cmaps)
self.color_pallete_comboBox.setCurrentIndex(44)
self.color_pallete_comboBox_2.addItems(cmaps)
self.color_pallete_comboBox_2.setCurrentIndex(44)
self.peak_fit_layout.addWidget(NavigationToolbar2QT(self.canvas_sub, self))
self.toolbar = NavigationToolbar2QT(self.canvas_main, self)
self.contour_layout.addWidget(NavigationToolbar2QT(self.canvas_contour, self))
self.normalization_layout.addWidget(NavigationToolbar2QT(self.canvas_norm, self))
self.XRD_data_layout.addWidget(self.toolbar)
self.canvas_main.mpl_connect("motion_notify_event", self.on_mouse_move)
self.normalize_button.clicked.connect(self.normalize)
self.actionOpen_New_Folder.triggered.connect(self.select_folder)
self.actionExport_Figure.triggered.connect(self.export_figure)
self.actionAbout.triggered.connect(self.about)
self.actionQ_Vector.triggered.connect(self.on_toggle_Q_vector_action)
self.action2theta.triggered.connect(self.on_toggle_2theta_action)
self.offset_slider.setMinimum(1)
self.offset_slider.setMaximum(99)
self.offset_slider.setValue(90)
self.XRD_measure_order_checkbox.stateChanged.connect(self.measure_order_index)
self.temperature_checkbox.stateChanged.connect(self.temp_index)
self.filter_button.clicked.connect(self.apply_filter)
[docs]
def _open_url(self, url: str):
"""Uses QDesktopServices to open URL from logos.
:param url: url to websites (as strings)
:type url: str
"""
try:
QDesktopServices.openUrl(QUrl(url))
except Exception:
pass
[docs]
def eventFilter(self, source: QLabel, event: QEvent):
"""eventFilter method for logo QLabel. It tracks a mouse press event and calls the :func:`Window._open_url`
:param source: Object name of logo in IGUAPE UI (QLabel)
:type source: QLabel
:param event: QEvent to track mouse click
:type event: QEvent
:return: Boolean value that determines the excution of :func:`Window._open_url`
:rtype: Bool
"""
if event.type() == QEvent.MouseButtonPress:
if source in self.url_data:
url = self.url_data[source]
self._open_url(url)
return True
return super().eventFilter(source, event)
[docs]
def update_graphs(self):
"""Method to update Main Figure (XRD Data tab) and Plotting Parameters Figure (Peak Fitting tab).
"""
try:
QApplication.setOverrideCursor(Qt.WaitCursor)
self.ax_main.clear()
self._update_main_figure()
self._plot_fitting_parameters()
self.canvas_main.draw()
self.canvas_sub.draw()
gc.collect()
self.cax.update_normal(self.sm)
self.cax_2.update_normal(self.sm)
except KeyError as e:
print(f'Please, initialize the monitor! Error: {e}')
QMessageBox.warning(self, '','Please initialize the monitor!')
pass
except AttributeError as e:
print(f'Please, initialize the monitor! Error: {e}')
QMessageBox.warning(self, '','Please initialize the monitor!')
pass
QApplication.restoreOverrideCursor()
[docs]
def _get_mask(self, i: int):
"""
Method for getting the :math:`2\\theta` mask, given the selection of interval by `SpanSelector` in the XRD Data tab.
:param i: index of the XRD pattern
:type i: int
:return: slice object as a mask.
:rtype: slice
"""
if self.selected_interval:
dois_theta = self.read_data(self.plot_data['file_name'][i], Q=self.Q_vector_state)[0]
return (dois_theta >= self.selected_interval[0]) & (dois_theta <= self.selected_interval[1])
return slice(None)
[docs]
def update_colormap(self, color_map_type: str, label:str):
"""
Routine for updating the colormaps and norm used in the XRD Data and PeakFit tabs.
:param color_map_type: Column label of XRD patterns DataFrame. It can be `temp` or `file_index`
:type color_map_type: str
:param label: _description_
:type label: str
"""
self.norm.vmin, self.norm.vmax = min(self.plot_data[color_map_type]), max(self.plot_data[color_map_type])
self.sm.set_norm(self.norm)
self.cax.set_label(label, fontsize = 15)
self.cax_2.set_label(label, fontsize = 15)
#self.cax_3.set_label(label)
self.cmap = plt.get_cmap(self.color_pallete_comboBox.currentText())
self.sm.set_cmap(self.cmap)
gc.collect()
[docs]
def _update_main_figure(self):
"""
Routine to update XRD Data Tab graph. This calls other methods such as update_colormap and plots the selected XRD measures in the main figure.
"""
QApplication.setOverrideCursor(Qt.WaitCursor)
try:
self.plot_data = self.monitor.data_frame[self.temp_mask].reset_index(drop=True) if self.temp_mask_signal else self.monitor.data_frame
except (AttributeError, pd.errors.IndexingError, ValueError):
self.plot_data = self.monitor.data_frame
pass
if self.plot_with_temp:
norm_col = 'temp'
self.update_colormap('temp', self.props_dict["Main Axis"]["Cmap_Label"])
self.min_temp_doubleSpinBox.setRange(min(self.monitor.data_frame['temp']), max(self.monitor.data_frame['temp']))
self.max_temp_doubleSpinBox.setRange(min(self.monitor.data_frame['temp']), max(self.monitor.data_frame['temp']))
self.min_temp_doubleSpinBox.setValue(min(self.plot_data['temp']))
self.max_temp_doubleSpinBox.setValue(max(self.plot_data['temp']))
else:
norm_col = 'file_index'
self.update_colormap('file_index', self.props_dict["Main Axis"]["Cmap_Label"])
self.min_temp_doubleSpinBox.setRange(min(self.monitor.data_frame['file_index']), max(self.monitor.data_frame['file_index']))
self.max_temp_doubleSpinBox.setRange(min(self.monitor.data_frame['file_index']), max(self.monitor.data_frame['file_index']))
self.min_temp_doubleSpinBox.setValue(min(self.plot_data['file_index']))
self.max_temp_doubleSpinBox.setValue(max(self.plot_data['file_index']))
self.spacing = max(self.plot_data['max']) / (100 - self.offset_slider.value())
offset = 0
mask = self._get_mask(0)
for i in range(len(self.plot_data['file_name'])):
color = self.cmap(self.norm(self.plot_data[norm_col][i])) #Selecting the pattern's color based on the colormap
dois_theta, intensity = self.read_data(self.plot_data['file_name'][i], normalize=False, Q=self.Q_vector_state)
if self.plot_with_temp:
temp = re.search(r"Temperature\s*\(([^)]+)\)", self.props_dict["Main Axis"]["Cmap_Label"]).group(1)
label = f'XRD pattern #{self.plot_data["file_index"][i]} - Temperature {self.plot_data["temp"][i]} {temp}'
else:
label = f'XRD pattern #{self.plot_data["file_index"][i]}'
self.ax_main.plot(dois_theta[mask], intensity[mask] + offset, color=color, label=label)
offset += self.spacing
del dois_theta, intensity
self.ax_main.set_xlabel(self.props_dict["Main Axis"]["X_Label"], fontsize = 15)
self.ax_main.set_ylabel(self.props_dict["Main Axis"]["Y_Label"], fontsize = 15)
QApplication.restoreOverrideCursor()
gc.collect()
[docs]
def _plot_fitting_parameters(self):
"""
This method calls :py:meth:`Window._plot_single_peak` or :py:meth:`Window._plot_double_peak`, according to the profile model selected.
"""
if not self.fit_interval:
return
self.ax_2theta.clear()
self.ax_area.clear()
self.ax_FWHM.clear()
if self.fit_interval_window.fit_model == 'PseudoVoigt':
self._plot_single_peak()
else:
self._plot_double_peak()
avxspan = self.ax_main.axvspan(self.fit_interval[0], self.fit_interval[1], color='grey', alpha=0.5, label='Selected Fitting Interval')
self.ax_main.legend(handles=[avxspan], loc='upper right')
# self.canvas_sub.draw()
# gc.collect()
# self.cax_2.update_normal(self.sm)
[docs]
def _plot_single_peak(self):
"""Hidden method to call :py:meth:`Window._plot_parameter` in order to plot fitting parameters of a single peak.
"""
mask = self.temp_mask if self.temp_mask_signal else slice(None)
x_data_type = 'temp' if self.plot_with_temp else 'file_index'
x_label = self.props_dict["Peak Fit Axis"]["FWHM Axis"]["X_Label"]
try:
self._plot_parameter(self.ax_2theta, self.monitor.fit_data[x_data_type].values[mask], self.monitor.fit_data['dois_theta_0'].values[mask], self.props_dict["Peak Fit Axis"]["Peak Position Axis"]["Y_Label"], x_label)#, yerr=self.monitor.fit_data['dois_theta_0_stderr'].values)
self._plot_parameter(self.ax_area, self.monitor.fit_data[x_data_type].values[mask], self.monitor.fit_data['area'].values[mask], self.props_dict["Peak Fit Axis"]["Integrated Area Axis"]["Y_Label"], x_label)#, yerr=self.monitor.fit_data['area_stderr'].values)
self._plot_parameter(self.ax_FWHM, self.monitor.fit_data[x_data_type].values[mask], self.monitor.fit_data['fwhm'].values[mask], self.props_dict["Peak Fit Axis"]["FWHM Axis"]["Y_Label"], x_label)#, yerr=self.monitor.fit_data['fwhm_stderr'].values)
except IndexError:
self._plot_parameter(self.ax_2theta, self.monitor.fit_data[x_data_type].values, self.monitor.fit_data['dois_theta_0'].values, self.props_dict["Peak Fit Axis"]["Peak Position Axis"]["Y_Label"], x_label)#, yerr=self.monitor.fit_data['dois_theta_0_stderr'].values)
self._plot_parameter(self.ax_area, self.monitor.fit_data[x_data_type].values, self.monitor.fit_data['area'].values, self.props_dict["Peak Fit Axis"]["Integrated Area Axis"]["Y_Label"], x_label)#, yerr=self.monitor.fit_data['area_stderr'].values)
self._plot_parameter(self.ax_FWHM, self.monitor.fit_data[x_data_type].values, self.monitor.fit_data['fwhm'].values, self.props_dict["Peak Fit Axis"]["FWHM Axis"]["Y_Label"], x_label)#, yerr=self.monitor.fit_data['fwhm_stderr'].values)
[docs]
def _plot_double_peak(self):
"""Hidden method to call :py:meth:`Window._plot_parameter` in order to plot fitting parameters of a double peak.
"""
mask = self.temp_mask if self.temp_mask_signal else slice(None)
x_data_type = 'temp' if self.plot_with_temp else 'file_index'
x_label = self.props_dict["Peak Fit Axis"]["FWHM Axis"]["X_Label"]
try:
self._plot_parameter(self.ax_2theta, self.monitor.fit_data[x_data_type].values[mask], self.monitor.fit_data['dois_theta_0'].values[mask], self.props_dict["Peak Fit Axis"]["Peak Position Axis"]["Y_Label"], x_label, label=True, color='red')
self._plot_parameter(self.ax_2theta, self.monitor.fit_data[x_data_type].values[mask], self.monitor.fit_data['dois_theta_0_#2'].values[mask], self.props_dict["Peak Fit Axis"]["Peak Position Axis"]["Y_Label"], x_label, label=True, color='red', marker='x')
self._plot_parameter(self.ax_area, self.monitor.fit_data[x_data_type].values[mask], self.monitor.fit_data['area'].values[mask], self.props_dict["Peak Fit Axis"]["Integrated Area Axis"]["Y_Label"], x_label, label=True, color='green')
self._plot_parameter(self.ax_area, self.monitor.fit_data[x_data_type].values[mask], self.monitor.fit_data['area_#2'].values[mask], self.props_dict["Peak Fit Axis"]["Integrated Area Axis"]["Y_Label"], x_label, label=True, color='green', marker='x')
self._plot_parameter(self.ax_FWHM, self.monitor.fit_data[x_data_type].values[mask], self.monitor.fit_data['fwhm'].values[mask], self.props_dict["Peak Fit Axis"]["FWHM Axis"]["Y_Label"], x_label, label = True, color='blue')
self._plot_parameter(self.ax_FWHM, self.monitor.fit_data[x_data_type].values[mask], self.monitor.fit_data['fwhm_#2'].values[mask], self.props_dict["Peak Fit Axis"]["FWHM Axis"]["Y_Label"], x_label, label = True, color='blue', marker='x')
except IndexError:
self._plot_parameter(self.ax_2theta, self.monitor.fit_data[x_data_type].values, self.monitor.fit_data['dois_theta_0'].values, self.props_dict["Peak Fit Axis"]["Peak Position Axis"]["Y_Label"], x_label, label=True, color='red')
self._plot_parameter(self.ax_2theta, self.monitor.fit_data[x_data_type].values, self.monitor.fit_data['dois_theta_0_#2'].values, self.props_dict["Peak Fit Axis"]["Peak Position Axis"]["Y_Label"], x_label, label=True, color='red', marker='x')
self._plot_parameter(self.ax_area, self.monitor.fit_data[x_data_type].values, self.monitor.fit_data['area'].values, self.props_dict["Peak Fit Axis"]["Integrated Area Axis"]["Y_Label"], x_label, label=True, color='green')
self._plot_parameter(self.ax_area, self.monitor.fit_data[x_data_type].values, self.monitor.fit_data['area_#2'].values, self.props_dict["Peak Fit Axis"]["Integrated Area Axis"]["Y_Label"], x_label, label=True, color='green', marker='x')
self._plot_parameter(self.ax_FWHM, self.monitor.fit_data[x_data_type].values, self.monitor.fit_data['fwhm'].values, self.props_dict["Peak Fit Axis"]["FWHM Axis"]["Y_Label"], x_label, label = True, color='blue')
self._plot_parameter(self.ax_FWHM, self.monitor.fit_data[x_data_type].values, self.monitor.fit_data['fwhm_#2'].values, self.props_dict["Peak Fit Axis"]["FWHM Axis"]["Y_Label"], x_label, label = True, color='blue', marker='x')
[docs]
def _plot_parameter(self, ax, x, y, ylabel, xlabel, label=None, color=None, marker='o', yerr = None):
"""
Generic routine to plot xy data in a given axes.
Args:
ax (matplotlib.axes.Axes): A matplotlib Axes object
x (list): x-axis data
y (list): y-axis data
ylabel (str): y-axis label
xlabel (str): x-axis label
label (bool, optional): Displays label. Accepts True or False. Defaults to None.
color (str, optional): matplotlib color. Defaults to None.
marker (str, optional): matplotlib marker. Defaults to 'o'.
yerr (list, optional): y-axis error. Defaults to None.
"""
for i in range(len(x)):
norm_col = 'temp' if self.plot_with_temp else 'file_index'
color = self.cmap(self.norm(x[i]))
ax.plot(x[i], y[i], marker = marker, color = color)
#ax.errorbar(x[i], y[i], ystderr, marker =marker, color = color, capsize=2)
ax.set_xlabel(xlabel, fontsize = 15)
ax.set_ylabel(ylabel, fontsize = 15)
if label:
peak1 = Line2D([0], [0], color='black', marker='o', markersize=5, linestyle="", label='PseudoVoigt #1')
peak2 = Line2D([0], [0], color='black', marker='x', markersize=5, linestyle="", label='PseudoVoigt #2')
ax.legend(handles = [peak1, peak2])
[docs]
def select_folder(self):
"""
Method for selecting a folder conatining the XRD data. In order for IGUAPE to read and monitor the folder, the file iguape_filelist.txt must be present.
"""
folder_path = QFileDialog.getExistingDirectory(self, 'Select the data folder to monitor', '', options=QFileDialog.Options()) # Selection of monitoring folder
if folder_path == "":
return
if self.folder_selected:
self.monitor.data_frame = pd.DataFrame(columns=['file_name', 'temp', 'max', 'file_index'])
self.monitor.fit_data = pd.DataFrame(columns=['dois_theta_0', 'fwhm', 'area', 'temp', 'file_index', 'R-squared'])
#self.monitor = None
self.plot_data = None
print(self.monitor.data_frame, self.monitor.fit_data, self.plot_data)
gc.collect()
counter.count = 0
self.plot_with_temp = False
self.selected_interval = None
self.fit_interval = None
self.folder_selected = False
self.temp_mask_signal = False
#folder_path = QFileDialog.getExistingDirectory(self, 'Select the data folder to monitor', '', options=QFileDialog.Options()) # Selection of monitoring folder
if folder_path:
if not (os.path.exists(os.path.join(folder_path, "iguape_filelist.txt"))):
QMessageBox.warning(self, '','This folder does not contain the iguape_filelist.txt file! Please select a valid folder!')
return
self.folder_selected = True
self.ax_main.clear()
self.ax_contour.clear()
self.ax_2theta.clear()
self.ax_area.clear()
self.ax_FWHM.clear()
self.canvas_main.draw()
self.monitor = FolderMonitor(folder_path=folder_path)
self.monitor.new_data_signal.connect(self.handle_new_data)
self.monitor.start()
gc.collect()
else:
print('No folder selected. Exiting')
[docs]
def handle_new_data(self, new_data):
"""This method is connected to the signal emited by :py:class:`Iguape.Monitor.FolderMonitor`, which delivers a pd.DataFrame containing the XDR data.
Args:
new_data (pd.DataFrame): pandas DataFrame to be concatenated with the existing one
"""
self.plot_data = pd.concat([self.plot_data, new_data], ignore_index=True)
[docs]
def onselect(self, xmin, xmax):
"""This method is passed as argument for the SpanSelector in the XRD Data Tab Graph.
It sets the selected interval for visualization based on the min/max value received for 2theta.
Args:
xmin (float): minimum 2theta value of the interval selected in the XRD Data graph
xmax (float): maximum 2theta value of the interval selected in the XRD Data graph
"""
if not self.monitor:
return
self.selected_interval = (xmin, xmax)
self.update_graphs()
# Reset button function #
[docs]
def reset_interval(self):
"""Method for resetting the 2theta interval in the XRD Data graph to None
"""
self.selected_interval = None
self.update_graphs()
# Peak fit interval selection routine #
[docs]
def select_fit_interval(self):
"""Method for initializing the FitWindow class. This Window has the elements that control the fitting parameters.
"""
if not self.folder_selected:
QMessageBox.warning(self, '','Please initialize the monitor!')
pass
else:
try:
if len(self.plot_data['file_name']) == 0:
print('No data available. Wait for the first measure!')
else:
self.ax_2theta.clear()
self.ax_area.clear()
self.ax_FWHM.clear()
self.fit_interval=None
self.monitor.fit_data = self.monitor.fit_data.iloc[0:0] #Reset fitting data
self.fit_interval_window = FitWindow()
self.fit_interval_window.show()
except AttributeError as e:
print(f'Please, push the Refresh Button! Error: {e}')
QMessageBox.warning(self, '','Please, push the Refresh Button!')
except Exception as e:
print(f'Error: {e}')
[docs]
def on_mouse_move(self, event):
"""This method is connected to the canvas in the XRD Data Tab graph. It shows the temperature/index of the closest XRD difractogram.
Args:
event (str): matplotlib FigureCanvas event.
"""
self.canvas_main.mouse_event = event
x, y = event.xdata, event.ydata
label = None
try:
min_dist = self.spacing #float('inf')
except AttributeError or UnboundLocalError:
pass
if x is not None and y is not None:
# Iterate over all lines in the plot, fiding the closest line point to the mouse.
for line in self.canvas_main.figure.axes[0].get_lines():
x_data, y_data = line.get_xdata(), line.get_ydata()
if len(x_data) > 0:
#Find the closest point to the mouse on the line
idx = np.argmin(np.sqrt((x_data - x) ** 2 + (y_data - y) ** 2))
dist = np.sqrt((x_data[idx] - x) ** 2 + (y_data[idx] - y) ** 2)
#print(f'Distance: {dist}', f'X_curve: {x_data[idx]}', f'Y_Curve: {y_data[idx]}', f'X_mouse: {x}', f'Y_mouse: {y}')
try:
if dist < min_dist:
min_dist = dist
label = line.get_label()
except UnboundLocalError:
pass
#print(f'Distance: {dist}', f'X_curve: {x_data[idx]}', f'Y_Curve: {y_data[idx]}', f'X_mouse: {x}', f'Y_mouse: {y}', f'Cruve label: {label}')
#print(label)
if label is not None and x is not None and y is not None:
s = fr'Label: {label} | {self.props_dict["Main Axis"]["X_Label"]}={x:.2f}, {self.props_dict["Main Axis"]["Y_Label"]}={y:.2e}'
self.toolbar.set_message(s)
else:
try:
s = f"2theta={x:.2f}, Intensity={y:.2e}"
self.toolbar.set_message(s)
except TypeError:
self.toolbar.set_message("")
[docs]
def save_data_frame(self):
"""This method calls two hidden method that generates the DataFrame to be saved. It also writes comments ("#") on the top of the csv file.
"""
try:
options = QFileDialog.Options()
# Select appropriate DataFrame generator based on model and temperature
if self.fit_interval_window.fit_model == 'PseudoVoigt':
df = self._create_single_peak_dataframe()
else:
df = self._create_double_peak_dataframe()
if df is not None:
file_path, _ = QFileDialog.getSaveFileName(self, "Save fitting Data", "", "CSV (*.csv);;All Files (*)", options=options)
if file_path:
with open(file_path, "w+") as f:
f.write("# For more information on each peak fit, please, refer to IGUAPE's terminal window, where you will find a complete report on each fit.\n")
f.write("# If uncertainties were not generated for one or multiple fits, it probably beacause one or more parameters reached a boundary value. For more information refer to lmfit's documentation: https://lmfit.github.io/lmfit-py/faq.html\n")
df.to_csv(f, index=False)
f.close()
except AttributeError as e:
print(f"No data available! Please, initialize the monitor! Error: {e}")
QMessageBox.warning(self, '','Please initialize the monitor!')
except Exception as e:
print(f'Exception {e} encountered')
[docs]
def _create_single_peak_dataframe(self):
"""Method for creating a DataFrame with the results of a PseudoVoigt peak fitting.
Returns:
pd.DataFrame: pandas DataFrame with fitting parameters from a PseudoVoigt single peak fit.
"""
if self.plot_with_temp:
temp_label = 'Cryojet Temperature (K)' if self.monitor.kelvin_sginal else 'Temperature (°C)'
return pd.DataFrame({
temp_label: self.monitor.fit_data['temp'],
self.props_dict["Peak Fit Axis"]["Peak Position Axis"]["Y_Label"]: self.monitor.fit_data['dois_theta_0'],
f'{self.props_dict["Peak Fit Axis"]["Peak Position Axis"]["Y_Label"]} std': self.monitor.fit_data['dois_theta_0_std'],
self.props_dict["Peak Fit Axis"]["Integrated Area Axis"]["Y_Label"]: self.monitor.fit_data['area'],
f'{self.props_dict["Peak Fit Axis"]["Integrated Area Axis"]["Y_Label"]} std': self.monitor.fit_data['area_std'],
self.props_dict["Peak Fit Axis"]["FWHM Axis"]["Y_Label"]: self.monitor.fit_data['fwhm'],
f'{self.props_dict["Peak Fit Axis"]["FWHM Axis"]["Y_Label"]} std': self.monitor.fit_data['fwhm_std'],
'R-squared (R²)': self.monitor.fit_data['R-squared']
})
else:
return pd.DataFrame({
'Measure': self.monitor.fit_data['file_index'],
self.props_dict["Peak Fit Axis"]["Peak Position Axis"]["Y_Label"]: self.monitor.fit_data['dois_theta_0'],
f'{self.props_dict["Peak Fit Axis"]["Peak Position Axis"]["Y_Label"]} std': self.monitor.fit_data['dois_theta_0_std'],
self.props_dict["Peak Fit Axis"]["Integrated Area Axis"]["Y_Label"]: self.monitor.fit_data['area'],
f'{self.props_dict["Peak Fit Axis"]["Integrated Area Axis"]["Y_Label"]} std': self.monitor.fit_data['area_std'],
self.props_dict["Peak Fit Axis"]["FWHM Axis"]["Y_Label"]: self.monitor.fit_data['fwhm'],
f'{self.props_dict["Peak Fit Axis"]["FWHM Axis"]["Y_Label"]} std': self.monitor.fit_data['fwhm_std'],
'R-squared (R²)': self.monitor.fit_data['R-squared']
})
[docs]
def _create_double_peak_dataframe(self):
"""Method for creating a DataFrame with the results of a Split-PseudoVoigt peak fitting.
Returns:
pd.DataFrame: pandas DataFrame with fitting parameters from a Split-PseudoVoigt peak fit.
"""
if self.plot_with_temp:
temp_label = 'Cryojet Temperature (K)' if self.monitor.kelvin_sginal else 'Temperature (°C)'
return pd.DataFrame({
temp_label: self.monitor.fit_data['temp'],
f'{self.props_dict["Peak Fit Axis"]["Peak Position Axis"]["Y_Label"]} #1': self.monitor.fit_data['dois_theta_0'],
f'{self.props_dict["Peak Fit Axis"]["Peak Position Axis"]["Y_Label"]} #1 std': self.monitor.fit_data['dois_theta_0_std'],
f'{self.props_dict["Peak Fit Axis"]["Integrated Area Axis"]["Y_Label"]} #1': self.monitor.fit_data['area'],
f'{self.props_dict["Peak Fit Axis"]["Integrated Area Axis"]["Y_Label"]} #1 std': self.monitor.fit_data['area_std'],
f'{self.props_dict["Peak Fit Axis"]["FWHM Axis"]["Y_Label"]} #1': self.monitor.fit_data['fwhm'],
f'{self.props_dict["Peak Fit Axis"]["FWHM Axis"]["Y_Label"]} #1 std': self.monitor.fit_data['fwhm_std'],
f'{self.props_dict["Peak Fit Axis"]["Peak Position Axis"]["Y_Label"]} #2': self.monitor.fit_data['dois_theta_0_#2'],
f'{self.props_dict["Peak Fit Axis"]["Peak Position Axis"]["Y_Label"]} #2 std': self.monitor.fit_data['dois_theta_0_#2_std'],
f'{self.props_dict["Peak Fit Axis"]["Integrated Area Axis"]["Y_Label"]} #2': self.monitor.fit_data['area_#2'],
f'{self.props_dict["Peak Fit Axis"]["Integrated Area Axis"]["Y_Label"]} #2 std': self.monitor.fit_data['area_#2_std'],
f'{self.props_dict["Peak Fit Axis"]["FWHM Axis"]["Y_Label"]} #2': self.monitor.fit_data['fwhm_#2'],
f'{self.props_dict["Peak Fit Axis"]["FWHM Axis"]["Y_Label"]} #2 std': self.monitor.fit_data['fwhm_#2_std'],
'R-squared (R²)': self.monitor.fit_data["R-squared"]
})
else:
return pd.DataFrame({
'XRD Acquisition Number': self.monitor.fit_data["file_index"],
f'{self.props_dict["Peak Fit Axis"]["Peak Position Axis"]["Y_Label"]} #1': self.monitor.fit_data['dois_theta_0'],
f'{self.props_dict["Peak Fit Axis"]["Peak Position Axis"]["Y_Label"]} #1 std': self.monitor.fit_data['dois_theta_0_std'],
f'{self.props_dict["Peak Fit Axis"]["Integrated Area Axis"]["Y_Label"]} #1': self.monitor.fit_data['area'],
f'{self.props_dict["Peak Fit Axis"]["Integrated Area Axis"]["Y_Label"]} #1 std': self.monitor.fit_data['area_std'],
f'{self.props_dict["Peak Fit Axis"]["FWHM Axis"]["Y_Label"]} #1': self.monitor.fit_data['fwhm'],
f'{self.props_dict["Peak Fit Axis"]["FWHM Axis"]["Y_Label"]} #1 std': self.monitor.fit_data['fwhm_std'],
f'{self.props_dict["Peak Fit Axis"]["Peak Position Axis"]["Y_Label"]} #2': self.monitor.fit_data['dois_theta_0_#2'],
f'{self.props_dict["Peak Fit Axis"]["Peak Position Axis"]["Y_Label"]} #2 std': self.monitor.fit_data['dois_theta_0_#2_std'],
f'{self.props_dict["Peak Fit Axis"]["Integrated Area Axis"]["Y_Label"]} #2': self.monitor.fit_data['area_#2'],
f'{self.props_dict["Peak Fit Axis"]["Integrated Area Axis"]["Y_Label"]} #2 std': self.monitor.fit_data['area_#2_std'],
f'{self.props_dict["Peak Fit Axis"]["FWHM Axis"]["Y_Label"]} #2': self.monitor.fit_data['fwhm_#2'],
f'{self.props_dict["Peak Fit Axis"]["FWHM Axis"]["Y_Label"]} #2 std': self.monitor.fit_data['fwhm_#2_std'],
'R-squared (R²)': self.monitor.fit_data["R-squared"]
})
[docs]
def validate_temp(self, min_value, max_value):
"""Method for validating the temperature value entered in the temperature QDoubleSpinBox. It returns the closest available temperature from the Monitor's DataFrame
Args:
min_value (float): Minimum temperature
max_value (float): Maximum temperature
Returns:
tuple: (min_temp, max_temp) tuple with the closest min/max available temperature
"""
min_temp = min(self.monitor.data_frame['temp'], key=lambda x: abs(x-min_value))
max_temp = min(self.monitor.data_frame['temp'], key=lambda x: abs(x-max_value))
return min_temp, max_temp
[docs]
def apply_temp_mask(self, mask):
"""Method for applying a temperature mask in the XRD measures.
Recieves an array of booleans. The array has a length equal to the number of XRD data read by the :py:class:`Iguape.Monitor.FolderMonitor`.
Args:
mask (np.array): numpy array with booleans as entries
"""
self.temp_mask = np.array(mask)
self.temp_mask_signal = True
self.ax_main.clear()
self.update_graphs()
self.canvas_main.draw()
[docs]
def measure_order_index(self, checked):
"""Method connected to the measure order QCheckBox. This method will change the colormaps that identify the XRD data.
All of the Figures are updated accordingly.
Args:
checked (bool): True or False
"""
if checked:
self.temperature_checkbox.setCheckState(False)
self.plot_with_temp = False
self.min_filter_label.setText('<u>Minimum:</u>')
self.max_filter_label.setText('<u>Maximum:</u>')
label = "XRD acquisition order"
self.props_dict["Main Axis"]["Cmap_Label"] = label
self.props_dict["Normalization Axis"]["Cmap_Label"] = label
self.props_dict["Contour Axis"]["Y_Label"] = label
for key in self.props_dict["Peak Fit Axis"].keys():
self.props_dict["Peak Fit Axis"][key]["X_Label"] = label
self.props_dict["Peak Fit Axis"][key]["Cmap_Label"] = label
self.update_graphs()
else:
self.temperature_checkbox.setCheckable(True)
[docs]
def temp_index(self, checked):
"""Method connected to the measure order QCheckBox. This method will change the colormaps that identify the XRD data.
All of the Figures are updated accordingly.
Args:
checked (bool): True or False
"""
try:
if checked:
if self.monitor.data_frame['temp'][0] != None:
self.XRD_measure_order_checkbox.setCheckState(False)
self.plot_with_temp = True
self.min_filter_label.setText('<u>Minimum Temperature:</u>')
self.max_filter_label.setText('<u>Maximum Temperature:</u>')
if self.monitor.kelvin_sginal:
label = "Temperature (K)"
else:
label = "Temperature (°C)"
self.props_dict["Main Axis"]["Cmap_Label"] = label
self.props_dict["Normalization Axis"]["Cmap_Label"] = label
self.props_dict["Contour Axis"]["Y_Label"] = label
for key in self.props_dict["Peak Fit Axis"].keys():
self.props_dict["Peak Fit Axis"][key]["X_Label"] = label
self.props_dict["Peak Fit Axis"][key]["Cmap_Label"] = label
self.update_graphs()
else:
print("This experiment doesn't make use of temperature!")
self.temperature_checkbox.setCheckState(False)
else:
pass
except AttributeError as e:
self.temperature_checkbox.setCheckState(False)
print(f'Please initizalie the Monitor! Error {e}')
QMessageBox.warning(self, '','Please initialize the monitor!')
[docs]
def read_data(self, path, normalize = False, Q = False):
"""Reads the XRD data using pandas *read_csv* function.
Comments are identified by "#" and the header is identified by the first line.
Args:
path (str):
normalize (bool, optional): If True the XRD intensities will be normalized. Defaults to False.
Q (bool, optional): If True the XRD x-axis will be converted from 2theta to the scattering vector Q. Defaults to False.
Returns:
(theta, intensity): 2theta and Intensity numpy arrays read from the XRD data.
"""
data = pd.read_csv(path, sep = ',', header=0, comment="#")
theta = np.array(data.iloc[:, 0], dtype="float64")
intensity = np.array(data.iloc[:, 1], dtype="float64")
if normalize:
intensity = normalize_array(intensity)
if Q:
theta = calculate_q_vector(win.wavelength, theta)
return theta, intensity
[docs]
def normalize(self):
"""Draws an intensities normalized plot at the Normalization Plot Tab graph.
"""
try:
self.ax_norm.clear()
mask = self._get_mask(0)
offset = 0
spacing = 1 / (100 - self.norm_offset_slider.value())
for i in range(len(self.plot_data['file_name'])):
norm_col = 'temp' if self.plot_with_temp else 'file_index' #Flag for chosing the XRD pattern index
color = self.cmap(self.norm(self.plot_data[norm_col][i])) #Selecting the pattern's color based on the colormap
dois_theta, intensity = self.read_data(self.plot_data['file_name'][i], normalize=True, Q=self.Q_vector_state)
self.ax_norm.plot(dois_theta[mask], intensity[mask] + offset, color=color, label=f'XRD pattern #{self.plot_data["file_index"][i]} - Temperature {self.plot_data["temp"][i]} K' if self.plot_with_temp else f'XRD pattern #{self.plot_data["file_index"][i]}')
offset += spacing
del dois_theta, intensity
if self.plot_with_temp:
self.update_colormap('temp', self.props_dict["Normalization Axis"]["Cmap_Label"])
else:
self.update_colormap('file_index', 'XRD acquisition time')
self.cax_4.set_label(self.props_dict["Normalization Axis"]["Cmap_Label"], fontsize = 15)
self.ax_norm.set_xlabel(self.props_dict["Normalization Axis"]["X_Label"], fontsize = 15)
self.ax_norm.set_ylabel(self.props_dict["Normalization Axis"]["Y_Label"], fontsize = 15)
self.canvas_norm.draw()
gc.collect()
return
except KeyError or Exception as e:
QMessageBox.warning(self, "", "Please, select a folder!")
print(f"Error: {e}")
[docs]
def contour(self):
"""Draws a contour plot at the Contour Plot Tab graph.
"""
if not self.monitor:
return
try:
self.ax_contour.clear()
theta, intensity = self.read_data(self.plot_data['file_name'][0], Q=self.Q_vector_state)
if self.plot_with_temp:
y = np.array(self.plot_data['temp'], dtype="float64")
else:
y = np.array(self.plot_data['file_index'], dtype="float64")
self.ax_contour.set_ylabel(self.props_dict["Contour Axis"]["Y_Label"], fontsize = 15)
mask = self._get_mask(0)
X, Y = np.meshgrid(theta[mask], y)
z = []
for item in self.plot_data['file_name']:
intensity = np.array(self.read_data(item)[1][mask])
z.append(intensity)
z = np.array(z, dtype="float64")
#print(f'Shape X and Y: {np.shape(X)}, {np.shape(Y)}. Z shape: {np.shape(z)}')
norm = self.norms[self.norm_comboBox.currentText()]
colormesh = self.ax_contour.pcolormesh(X, Y, z, cmap = self.color_pallete_comboBox.currentText(), norm = norm, shading='auto')
self.cax_3.update_normal(colormesh)
self.cax_3.set_label(self.props_dict["Contour Axis"]["Cmap_Label"], fontsize = 15)
self.ax_contour.set_xlabel(self.props_dict["Contour Axis"]["X_Label"], fontsize = 15)
colormesh.set_rasterized(True)
self.canvas_contour.draw()
del X, Y, mask, z
gc.collect()
return
except KeyError or Exception as e:
QMessageBox.warning(self, "", "Please, select a folder!")
print(f"Error: {e}")
[docs]
def on_change_color_pallete(self, index):
"""Sets the current item in the QComboBox.
Args:
index (int): Integer representing the index of item in the Color pallete QComboBox.
"""
if not self.monitor:
return
self.color_pallete_comboBox.setCurrentIndex(index)
self.color_pallete_comboBox_2.setCurrentIndex(index)
self.update_graphs()
[docs]
def apply_filter(self):
"""Initializes the :py:class:`FilterWindow` class.
"""
if not self.monitor:
return
try:
self.filter_window = FilterWindow(self.monitor.data_frame.iloc[:, 0:3], self.monitor.kelvin_sginal)
self.filter_window.mask.connect(self.apply_temp_mask)
self.filter_window.show()
except AttributeError or Exception:
QMessageBox.warning(self, "No folder was selected", "Select a folder to monitor!")
[docs]
def on_toggle_Q_vector_action(self):
"""
This is triggred when the Q vector visualization option is selected by the user.
This renders a QDialog for user input of the wavelength and than updates the graphs to plot as function of Q.
"""
dialog = QInputDialog()
dialog.setInputMode(QInputDialog.DoubleInput)
dialog.setLocale(QLocale(QLocale.English, QLocale.UnitedStates))
dialog.setLabelText("Input Wavelength of XRD experiment (Å)")
dialog.setDoubleMinimum(0)
dialog.setDoubleMaximum(100)
dialog.setDoubleStep(1e-5)
dialog.setDoubleDecimals(5)
dialog.setDoubleValue(0)
dialog.setWindowTitle("Wavelenght")
ok = dialog.exec_()
if (not ok) or (dialog.doubleValue() == 0):
self.actionQ_Vector.setChecked(False)
QMessageBox.warning(self, "Invlid Wavelenght", "Please insert a valid wavelength for Q vector computation")
return
else:
self.action2theta.setChecked(False)
self.wavelength = dialog.doubleValue()
self.props_dict["Main Axis"]["X_Label"] = r"Q ($Å^{-1}$)"
self.props_dict["Normalization Axis"]["X_Label"] = r"Q ($Å^{-1}$)"
self.props_dict["Contour Axis"]["X_Label"] = r"Q ($Å^{-1}$)"
self.props_dict["Peak Fit Axis"]["Peak Position Axis"]["Y_Label"] = r"Peak position ($Å^{-1}$)"
self.props_dict["Peak Fit Axis"]["FWHM Axis"]["Y_Label"] = r"$\delta$Q ($Å^{-1}$)"
self.Q_vector_state = True
self.selected_interval = None
#self.update_graphs()
[docs]
def on_toggle_2theta_action(self):
"""
This is triggred when the 2theta visualization option is selected by the user.
This is the default visualization mode. The method updates the graphs to plot as function of 2theta.
"""
self.actionQ_Vector.setChecked(False)
self.action2theta.setChecked(True)
self.props_dict["Main Axis"]["X_Label"] = "2θ (°)"
self.props_dict["Normalization Axis"]["X_Label"] = "2θ (°)"
self.props_dict["Contour Axis"]["X_Label"] = "2θ (°)"
self.props_dict["Peak Fit Axis"]["Peak Position Axis"]["Y_Label"] = "Peak position (°)"
self.props_dict["Peak Fit Axis"]["FWHM Axis"]["Y_Label"] = "FWHM (°)"
self.Q_vector_state = False
[docs]
def on_change_vline_checkbox(self):
"""This method enables a Vertical Line cursor in the XRD Data Tab Figure.
"""
if self.cursor == None:
self.cursor = Cursor(self.ax_main, useblit=True, color='red', linewidth=1, horizOn=False)
else:
self.cursor = None
[docs]
def about(self):
"""Initializes a QMessageBox with some info on IGUAPE.
"""
QMessageBox.about(
self,
"About Iguape",
"<p>This is the Paineira Graphical User Interface</p>"
"<p>- Its usage is resttricted to data acquired via in-situ experiments at Paineira. The software is under the GNU GPL-3.0 License.</p>"
"<p>- For more information, please refer to the <a href='https://github.com/cnpem/iguape'>GitHub</a> page or <a href='https://cnpem.github.io/iguape/'>IGUAPE</a> documentation page</p>"
"<p>- Paineira Beamline</p>"
"<p>- LNLS - CNPEM</p>",
)
[docs]
class GarbageCollector(QThread):
"""Garbage Collector class. Inherits a QThread and calls, every 3 seconds, the collect method of python's garbage collector.
The matpltolib Figures are updated using **for loops**. This consumes a lot of RAM memory. This method frees up some memory space.
Args:
QThread (QThread): QThread class from PyQt5
"""
[docs]
def __init__(self):
"""Constructor
"""
super().__init__()
[docs]
def run(self):
"""Calls the collect method from python's Garbage Collector every 3 seconds.
"""
while True:
gc.collect()
time.sleep(3)
[docs]
class Worker(QThread):
"""QThread that performs peak fitting in the selected XRD data.
Args:
QThread (QThread): QThread class from PyQt5
"""
progress = pyqtSignal(int)
finished = pyqtSignal(float)
error = pyqtSignal(str) # Changed to emit multiple arrays
[docs]
def __init__(self, interval):
r"""Constructor for the peak fitting thread.
Args:
interval (list): List in the form [:math:`2\theta_{i}`, :math:`2\theta_{f}`] where :math:`2\theta_{i}` and :math:`2\theta_{f}` are, respectively,
the initial and final values of the 2theta interval selected to perfor peak fitting.
"""
super().__init__()
self.fit_interval = interval
QApplication.setOverrideCursor(Qt.WaitCursor)
[docs]
def run(self):
r"""Performs peak fitting in the selected XRD data in the selected 2theta interval [:math:`2\theta_{i}`, :math:`2\theta_{f}`]
"""
try:
start = time.time()
pars = None
for i in range(len(win.plot_data['file_name'])):
theta, intensity = win.read_data(win.plot_data['file_name'][i], Q=win.Q_vector_state)
id = [win.plot_data['file_index'][i], win.plot_data['temp'][i]]
if win.fit_interval_window.fit_model == 'PseudoVoigt':
fit = peak_fit(theta, intensity, self.fit_interval, id=id, pars=pars)
try:
dois_theta_std, fwhm_std, area_std = fit[7]['center'].stderr*1, fit[7]['fwhm'].stderr*1, fit[7]['amplitude'].stderr*1
except Exception as e:
print(f"Excepetion {e}")
dois_theta_std, fwhm_std, area_std = float("nan"), float("nan"), float("nan")
new_fit_data = pd.DataFrame({'dois_theta_0': [fit[0]], 'dois_theta_0_std': [dois_theta_std], 'fwhm': [fit[1]],'fwhm_std': [fwhm_std], 'area': [fit[2]], 'area_std': [area_std], 'temp': [win.plot_data['temp'][i]], 'file_index': [win.plot_data['file_index'][i]], 'R-squared': [fit[3]]})
win.monitor.fit_data = pd.concat([win.monitor.fit_data, new_fit_data], ignore_index=True)
pars = fit[7]
progress_value = int((i + 1) / len(win.plot_data['file_name']) * 100)
self.progress.emit(progress_value) # Emit progress signal with percentage
else:
fit = peak_fit_split_gaussian(theta, intensity, self.fit_interval, id=id,height = win.fit_interval_window.height, distance=win.fit_interval_window.distance, prominence=win.fit_interval_window.prominence, pars=pars)
try:
dois_theta_std, fwhm_std, area_std, dois_theta_2_std, fwhm_2_std, area_2_std = fit[7]['cen1'].stderr*1, fit[7]['sigma1'].stderr*2, fit[7]['amp1'].stderr*1, fit[7]['cen2'].stderr*1, fit[7]['sigma2'].stderr*2, fit[7]['amp2'].stderr*1
except Exception as e:
print(f"Exception {e}")
dois_theta_std, fwhm_std, area_std, dois_theta_2_std, fwhm_2_std, area_2_std = float("nan"), float("nan"), float("nan"), float("nan"), float("nan"), float("nan")
new_fit_data = pd.DataFrame({'dois_theta_0': [fit[0][0]], 'dois_theta_0_std': [dois_theta_std], 'dois_theta_0_#2': [fit[0][1]], 'dois_theta_0_#2_std': [dois_theta_2_std],'fwhm': [fit[1][0]], 'fwhm_std': [fwhm_std], 'fwhm_#2': [fit[1][1]], 'fwhm_#2_std': [fwhm_2_std],'area': [fit[2][0]], 'area_std': [area_std],'area_#2': [fit[2][1]], 'area_#2_std': [area_2_std],'temp': [win.plot_data['temp'][i]], 'file_index': [win.plot_data['file_index'][i]], 'R-squared': [fit[3]]})
win.monitor.fit_data =pd.concat([win.monitor.fit_data, new_fit_data], ignore_index=True)
pars = fit[7]
progress_value = int((i + 1) / len(win.plot_data['file_name']) * 100)
self.progress.emit(progress_value) # Emit progress signal with percentage
#self.finished.emit(win.monitor.fit_data['dois_theta_0'], win.monitor.fit_data['area'], win.monitor.fit_data['fwhm']) # Emit finished signal with results
finish = time.time()
self.finished.emit(finish-start)
except Exception as e:
self.error.emit(f'Error during peak fitting: {str(e)}. Please select a new Fit Interval')
print(f'Exception {e}. Please select a new Fit Interval')
[docs]
class FilterWindow(QDialog, Ui_Filter_Dialog):
"""_summary_
Args:
QDialog (QDialog): QDialog class from PyQt5
Ui_Filter_Dialog (QDialog): Custom QDialog class made from QtDesigner
"""
mask = pyqtSignal(list)
[docs]
def __init__(self, data = None, kelvin_signal = None, parent = None):
"""Constructor
This method will initialize the QDialog and render a QListView() in the QDialog Window.
Args:
data (pd.DataFrame, optional): *pandas DataFrame* containing the names of the XRD data files. Defaults to None.
kelvin_signal (Bool, optional): Defines if the temperature unit is K or °C. Defaults to None.
parent (_type_, optional): _description_. Defaults to None.
"""
super().__init__(parent)
self.setupUi(self)
if kelvin_signal:
self.unit = 'K'
else:
self.unit = "°C"
self.data = data
self.setup_df()
self.list = QListView(self)
self.model = CustomListViewModel(self.data, self.unit)
self.list.setModel(self.model)
self.list.setSelectionMode(QListView.ExtendedSelection)
self.verticalLayout.addWidget(self.list)
self.apply_button.clicked.connect(self.apply)
self.select_all_button.clicked.connect(self.set_state_checked)
self.deselect_all_button.clicked.connect(self.set_state_unchecked)
self.toggle_selected_button.clicked.connect(self.set_state_selected)
[docs]
def setup_df(self):
"""Initializes the data as unchecked
"""
checked = np.full_like(np.array(self.data.iloc[:, 0]), False)
self.data['checked'] = checked
return
[docs]
def set_state_checked(self):
"""Sets the state of a given item as checked
"""
for row in range(self.model.rowCount()):
index = self.model.index(row)
self.model.setData(index, Qt.Checked, role=Qt.CheckStateRole)
[docs]
def set_state_unchecked(self):
"""Sets the state of a given item as unchecked
"""
for row in range(self.model.rowCount()):
index = self.model.index(row)
self.model.setData(index, Qt.Unchecked, role=Qt.CheckStateRole)
[docs]
def set_state_selected(self):
"""Sets the state of a given item as selected
"""
selected = self.list.selectedIndexes()
for index in selected:
self.model.setData(index, Qt.Checked, role=Qt.CheckStateRole)
[docs]
def apply(self):
"""Send the maks to IGUAPE's main Window
"""
if True in list(self.model._data.iloc[:, -1]):
self.mask.emit(list(self.model._data.iloc[:, -1]))
self.close()
else:
QMessageBox.warning(win, "No data was selected", "Please, select at least one XRD diffractogram!")
return
[docs]
class CustomListViewModel(QAbstractListModel):
"""Custom QListView made to display the data correctly.
Args:
QAbstractListModel (QAbstractListModel): QAbstractListModel from PyQt5
"""
[docs]
def __init__(self, data = None, unit = None):
"""Constructor
Args:
data (pd.DataFrame, optional): *pandas DataFrame* containing the names of the XRD data files. Defaults to None.. Defaults to None.
unit (str, optional): Temperature unit. Defaults to None.
"""
super().__init__()
self._data = data.copy()
self.unit = unit
[docs]
def rowCount(self, parent=QModelIndex()):
"""Return the length of the *pandas DataFrame*, i.e. the number of columns.
Args:
parent (_type_, optional): _description_. Defaults to QModelIndex().
Returns:
_type_: _description_
"""
return len(self._data)
[docs]
def data(self, index, role=Qt.DisplayRole):
"""
Args:
index (QModelIndex): PyQt5 index for each item
role (Qt.DisplayRolde, optional): PyQt5 Display Role for each item
Returns:
QVariant:
"""
if not index.isValid():
return QVariant()
row = index.row()
if role == Qt.DisplayRole:
entry = f"{self._data.at[row, 'file_name'].split('/')[-1]} | {self._data.at[row, 'temp']} {self.unit} |XRD Acquisition # {self._data.at[row, 'file_index']}"
return entry
elif role == Qt.CheckStateRole:
return Qt.Checked if self._data.at[row, 'checked'] else Qt.Unchecked
return QVariant()
[docs]
def setData(self, index, value, role=Qt.EditRole):
"""Changes the status of an item
Args:
index (QIndexModel): PyQt5 index for each item
value (Qt.CheckState): PyQt5 CheckState of the item
role (Qt.EditRole, optional): Defaults to Qt.EditRole.
Returns:
Bool:
"""
if not index.isValid():
return False
row = index.row()
if role == Qt.CheckStateRole:
self._data.at[row, 'checked'] = (value == Qt.Checked)
self.dataChanged.emit(index, index)
return True
return False
[docs]
def flags(self, index):
"""Defines the properties of each item
Args:
index (QIndexModel): PyQt5 index for each item
Returns:
_type_: _description_
"""
return Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsUserCheckable
[docs]
class ExportWindow(QDialog, Ui_Export_Figure):
"""Custom QDialog for managing Figure export.
Args:
QDialog (QDialog): QDialog class from PyQt5
Ui_Export_Figure (_type_): Custom QDialog mode with QDesigner
"""
[docs]
def __init__(self, figure: Figure, parent=None):
"""Constructor
Args:
figure (matplotlib.figure.Figure): *matplotlib* Figure class
parent (_type_, optional): Defaults to None.
"""
super().__init__(parent)
self.setupUi(self)
self.edit_fig = copy.deepcopy(figure)
#geometry = QGuiApplication.screens()[-1].availableGeometry()
#self.setGeometry(geometry)
#self.setBaseSize(self.w, self.h)
self.edit_fig.set_layout_engine('constrained')
self.axes = self.edit_fig.axes
if len(self.edit_fig.axes) > 2:
#self.xlabel_lineEdit.setEnabled(False)
self.ylabel_lineEdit.setEnabled(False)
#self.cmap_label_lineEdit.setEnabled(False)
self.xlabel_lineEdit.setText(self.axes[0].get_xlabel())
self.ylabel_lineEdit.setText(self.axes[0].get_ylabel())
self.cmap_ax = self.axes[-1]
self.axes.pop()
self.canvas = FigureCanvas(self.edit_fig)
self.cmap_label_lineEdit.setText(self.cmap_ax.get_ylabel())
self.dpi_spinBox.setValue(self.edit_fig.dpi)
self.font_comboBox.addItems(fonts_list)
self.verticalLayout.addWidget(self.canvas)
self.redraw_button.clicked.connect(self.redraw_fig)
self.font_comboBox.currentTextChanged.connect(self.on_change_font_comboBox)
self.label_size_spinBox.valueChanged.connect(self.on_change_label_size_spinBox)
self.label_style_comboBox.currentTextChanged.connect(self.on_change_label_style_comboBox)
self.ticks_size_spinBox.valueChanged.connect(self.on_change_tick_size_spinBox)
self.ticks_style_comboBox.currentTextChanged.connect(self.on_change_tick_style_comboBox)
self.save_fig_button.clicked.connect(self.save_fig)
self.color_pallete.clicked.connect(self.get_color)
self.color_pallete.setFixedSize(QSize(22,22))
self.height = None
self.width = None
self.label_font = {'family': self.font_comboBox.currentText(),
'color': 'black',
'weight': 'normal',
'size': self.label_size_spinBox.value(),
'style': 'normal',
}
self.tick_font = {'family': self.font_comboBox.currentText(),
'color': 'black',
'weight': 'normal',
'size': self.ticks_size_spinBox.value(),
'style': 'normal',
}
[docs]
def get_color(self):
"""Gets the color selected with QColorDialog and sets the button color to it.
"""
color = QColorDialog().getColor()
if color.isValid():
self.label_font['color'] = color.name()
self.color_pallete.setStyleSheet("background-color: %s;" % color.name())
return
[docs]
def on_change_font_comboBox(self, font):
"""Changes the label and tick font.
Args:
font (str): Font for label and ticks
"""
self.label_font['family'] = font
self.tick_font['family'] = font
return
[docs]
def on_change_label_size_spinBox(self, value):
"""Changes the label font size
Args:
value (int): Font size
"""
self.label_font['size'] = value
return
[docs]
def on_change_tick_size_spinBox(self, value):
"""Changes the tick font size
Args:
value (int): Font size
"""
self.tick_font['size'] = value
return
[docs]
def on_change_label_style_comboBox(self, style):
"""Changes the label style
Args:
style (str): Label style (italic/bold/normal)
"""
if style == 'bold':
self.label_font['weight'] = style
return
self.label_font['style'] = style
return
[docs]
def on_change_tick_style_comboBox(self, style):
"""Changes the tick style
Args:
style (str): Tick style (italic/bold/normal)
"""
if style == 'bold':
self.tick_font['weight'] = style
return
self.tick_font['style'] = style
return
[docs]
def redraw_fig(self):
"""Redraws the Figure according to the changes made by the user.
"""
try:
self.height = self.height_doubleSpinBox.value()
self.width = self.width_doubleSpinBox.value()
except Exception as e:
print('{e}')
return
tickfont = matplotlib.font_manager.FontProperties(family=self.tick_font['family'],
style=self.tick_font['style'],
weight=self.tick_font['weight'],
size=self.tick_font['size'])
if len(self.edit_fig.axes) > 2:
for ax in self.axes:
ax.set_xlabel(self.xlabel_lineEdit.text(), fontdict = self.label_font)
ax.set_ylabel(ax.get_ylabel(), fontdict = self.label_font)
for label in ax.get_xticklabels() + ax.get_yticklabels():
label.set_fontproperties(tickfont)
else:
for ax in self.axes:
ax.set_xlabel(self.xlabel_lineEdit.text(), fontdict = self.label_font)
ax.set_ylabel(self.ylabel_lineEdit.text(), fontdict = self.label_font)
for label in ax.get_xticklabels() + ax.get_yticklabels():
label.set_fontproperties(tickfont)
self.cmap_ax.set_ylabel(self.cmap_label_lineEdit.text(), fontdict = self.label_font)
for label in self.cmap_ax.get_yticklabels():
label.set_fontproperties(tickfont)
self.edit_fig.set_size_inches(self.width, self.height)
#self.canvas.resize(int(width*self.edit_fig.dpi), int(height*self.edit_fig.dpi))
#self.setGeometry(0, 0, int(width*self.edit_fig.dpi), int(height*self.edit_fig.dpi))
self.canvas.draw()
gc.collect()
return
[docs]
def save_fig(self):
"""Saves the Figure according to the chosen format.
"""
path = QFileDialog.getSaveFileName(self, "Select Save Path", os.path.expanduser('~'), options=QFileDialog.Options())[0]
if path == "":
QMessageBox.warning(self, 'Saving Error', "Please, select a valid path for your Figure")
return
print(path)
#self.canvas.resize(int(width*self.edit_fig.dpi), int(height*self.edit_fig.dpi))
try: # try-except to handle save when height and width are None
self.edit_fig.set_size_inches(self.width, self.height)
except Exception as e:
pass
self.canvas.draw()
self.edit_fig.savefig(path+f".{self.format_comboBox.currentText()}", dpi = self.dpi_spinBox.value(),
format = self.format_comboBox.currentText(),
bbox_inches = 'tight')
[docs]
class FitWindow(QDialog, Ui_pk_window):
"""QDialog Window to manage the Peak Fitting options.
Args:
QDialog (QDialog): QDialog from PyQt5
Ui_pk_window (QDialog): Custom QDialog made with QDesigner
"""
[docs]
def __init__(self, parent=None):
"""Constructor
Args:
parent (optional): Defaults to None.
"""
super().__init__(parent)
self.setupUi(self)
self.fit_interval= None
self.text = None
self.fit_model = 'PseudoVoigt'
win.monitor.set_fit_model = "PseudoVoigt"
self.distance = 25
self.height = 1e+09
self.prominence = 50
self.setup_layout()
[docs]
def setup_layout(self):
"""Setup the figures layout and connects the widgets to its functions.
"""
self.setWindowTitle('Peak Fit')
self.pk_layout = QVBoxLayout()
self.fig = Figure(figsize=(20,10), dpi=100)
self.ax = self.fig.add_subplot(1,1,1)
theta, intensity = win.read_data(win.plot_data['file_name'][0], Q=win.Q_vector_state)
if win.plot_with_temp:
#theta, intensity = win.read_data(win.plot_data['file_name'][0])
self.ax.plot(theta, intensity,'o', markersize=3, label = 'XRD pattern ' + str(win.plot_data['temp'][0]) + '°C')
#self.ax.plot(win.plot_data['theta'][0], win.plot_data['intensity'][0],'o', markersize=3, label = 'XRD pattern ' + str(win.plot_data['temp'][0]) + '°C')
else:
self.ax.plot(theta, intensity, 'o', markersize=3, label = 'XRD pattern #' + str(win.plot_data['file_index'][0]))
#self.ax.plot(win.plot_data['theta'][0], win.plot_data['intensity'][0], 'o', markersize=3, label = 'XRD pattern #' + str(win.plot_data['file_index'][0]))
self.ax.set_xlabel(win.props_dict["Main Axis"]["X_Label"], fontsize=15)
self.ax.set_ylabel(win.props_dict["Main Axis"]["Y_Label"], fontsize=15)
self.ax.legend(fontsize='small')
self.canvas = FigureCanvas(self.fig)
self.pk_layout.addWidget(self.canvas)
self.pk_layout.addWidget(NavigationToolbar2QT(self.canvas, self))
self.pk_frame.setLayout(self.pk_layout)
self.span = SpanSelector(self.ax, self.onselect, 'horizontal', useblit=True,
props=dict(alpha=0.3, facecolor='red', capstyle='round'))
self.clear_plot_button.clicked.connect(self.clear_plot)
self.pk_button.clicked.connect(self.fit)
self.indexes = [0]
self.shade = False
if win.plot_with_temp:
items_list = [str(item) + '°C' for item in win.plot_data['temp']]
self.xrd_combo_box.addItems(items_list)
else:
items_list = [str(item) for item in win.plot_data['file_index']]
self.xrd_combo_box.addItems(items_list)
self.xrd_combo_box.activated[str].connect(self.onChanged_xrd_combo_box)
self.pk_combo_box.activated[str].connect(self.onChanged_pk_combo_box)
self.bgk_combo_box.activated[str].connect(self.onChanged_bkg_combo_box)
self.preview_button.clicked.connect(self.preview)
self.distance_spinBox.setReadOnly(True)
self.distance_spinBox.valueChanged[int].connect(self.onChanged_distance_spinbox)
self.height_spinBox.setReadOnly(True)
self.height_spinBox.valueChanged[float].connect(self.onChanged_height_spinbox)
self.prominence_spinBox.setReadOnly(True)
self.prominence_spinBox.valueChanged[int].connect(self.onChanged_prominence_spinbox)
[docs]
def onChanged_xrd_combo_box(self, text):
"""Adds a new XRD difractogram to the Figure. If there's already two plotted, it will ask to user to clear the canvas.
Args:
text (str): Text from the QComboBox. It identifies the XRD data.
"""
self.text = text
if len(self.ax.lines) == 2:
QMessageBox.warning(self, '','Warning! It is possible to display only two XRD patterns in this window! Please press the Clear Plot button and select up to 2 XRD patterns to be displayed.')
pass
else:
i = self.xrd_combo_box.currentIndex()
self.indexes.append(i)
theta, intensity = win.read_data(win.plot_data['file_name'][i], Q=win.Q_vector_state)
if win.plot_with_temp:
self.ax.plot(theta, intensity, 'o', markersize=3, label = ('XRD pattern ' + text))
#self.ax.plot(win.plot_data['theta'][i], win.plot_data['intensity'][i], 'o', markersize=3, label = ('XRD pattern ' + text))
else:
self.ax.plot(theta, intensity, 'o', markersize=3, label = ('XRD pattern #' + text))
#self.ax.plot(win.plot_data['theta'][i], win.plot_data['intensity'][i], 'o', markersize=3, label = ('XRD pattern #' + text))
self.ax.set_xlabel("2θ (°)", fontsize = 15)
self.ax.set_ylabel("Intensity (u.a.)", fontsize = 15)
self.ax.legend(fontsize='small')
self.canvas.draw()
[docs]
def onChanged_pk_combo_box(self, text):
"""
Routine for selecting the Peak Fitting Model via the ComboBox.
Parameters
----------
text (str): Text selected on the ComboBox
"""
if text == 'PseudoVoigt Model':
self.fit_model = 'PseudoVoigt'
win.monitor.set_fit_model = 'PseudoVoigt'
self.distance_spinBox.setReadOnly(True)
self.height_spinBox.setReadOnly(True)
self.prominence_spinBox.setReadOnly(True)
elif text == 'Split PseudoVoigt Model - 2x PseudoVoigt':
self.fit_model = '2x PseudoVoigt(SPV)'
win.monitor.set_fit_model = '2x PseudoVoigt(SPV)'
self.distance_spinBox.setReadOnly(False)
self.height_spinBox.setReadOnly(False)
self.prominence_spinBox.setReadOnly(False)
[docs]
def onChanged_bkg_combo_box(self, text):
"""Changes the Background model. Currently only Linear model is applied.
Args:
text (_type_): _description_
"""
if text == 'Linear Model':
self.bkg_model = 'Linear'
else:
self.bkg_model = 'Spline'
[docs]
def onselect(self, xmin, xmax):
"""Sets the selected fit interval as [xmin, xmax]. It also draws a rectangle spaning the interval (axvspan)
Args:
xmin (float): Lower bound of the interval
xmax (flat): Upper bound of the interval
"""
if self.shade:
self.shade.remove()
self.fit_interval = [xmin, xmax]
self.interval_label.setText(f'[{xmin:.3f}, {xmax:.3f}]')
self.shade = self.ax.axvspan(self.fit_interval[0], self.fit_interval[1], color='grey', alpha=0.5, label='Selected Fitting Interval')
self.canvas.draw()
[docs]
def onChanged_distance_spinbox(self, value):
"""Sets the distance to value. Only valid for Spli PseudoVoigt model
Args:
value (int): Minimum distance (in experimental points) between peaks.
"""
self.distance = value
[docs]
def onChanged_height_spinbox(self, value):
"""Sets the height to value (x1e09). Only valid for Spli PseudoVoigt model
Args:
value (float): Minimum height of peaks. This float is multiplied by 1e09.
"""
self.height = value*(1e+09)
[docs]
def onChanged_prominence_spinbox(self, value):
"""Sets the prominence to value. Only valid for Spli PseudoVoigt model
Args:
value (int): Prominence of peaks. Prominence is the difference between the maximum value of the peak and the background of the measure.
"""
self.prominence = value
[docs]
def clear_plot(self):
"""Clears the Figure
"""
self.ax.clear()
self.canvas.draw()
self.indexes.clear()
[docs]
def preview(self):
"""Displays a preview of the Peak Fit in the Figure.
"""
if self.fit_interval == None:
return
if len(self.ax.lines) > 2:
while len(self.ax.lines) > 2:
self.ax.lines[len(self.ax.lines)-1].remove()
if self.fit_model == "PseudoVoigt":
for i in range(len(self.indexes)):
theta, intensity = win.read_data(win.plot_data['file_name'][self.indexes[i]], Q=win.Q_vector_state)
id = [win.plot_data['file_index'][self.indexes[i]], win.plot_data['temp'][self.indexes[i]]]
data = peak_fit(theta, intensity, self.fit_interval, id=id)
best_fit = data[4].best_fit
#dely = data[4].eval_uncertainty(sigma = 3)
if win.plot_with_temp:
self.ax.plot(data[6], best_fit, '--', label = f'Best Fit - {win.plot_data["temp"][self.indexes[i]]} °C')
self.ax.plot(data[6], data[5]['bkg_'], '-', label = f'Background - {win.plot_data["temp"][self.indexes[i]]} °C')
#self.ax.fill_between(data[6],best_fit-dely, best_fit+dely, color='darkslategrey', alpha=0.9, label=r"3-$\sigma$ uncertainty band")
else:
self.ax.plot(data[6], data[4].best_fit, '--', label = f'Best Fit - #{win.plot_data["file_index"][self.indexes[i]]}')
self.ax.plot(data[6], data[5]['bkg_'], '-', label = f'Background - #{win.plot_data["file_index"][self.indexes[i]]}')
#self.ax.fill_between(data[6],best_fit-dely, best_fit+dely, color = "darkslategrey", alpha=0.9, label=r"3-$\sigma$ uncertainty band")
self.ax.legend(fontsize='small')
self.canvas.draw()
else:
for i in range(len(self.indexes)):
try:
theta, intensity = win.read_data(win.plot_data['file_name'][self.indexes[i]], Q=win.Q_vector_state)
id = [win.plot_data['file_index'][self.indexes[i]], win.plot_data['temp'][self.indexes[i]]]
data = peak_fit_split_gaussian(theta, intensity, self.fit_interval, id=id, height = self.height, distance=self.distance, prominence=self.prominence)
best_fit = data[4].best_fit
#dely = data[4].eval_uncertainty(sigma = 3)
if win.plot_with_temp:
self.ax.plot(data[6], data[4].best_fit, '--', label = f'Best Fit - {win.plot_data["temp"][self.indexes[i]]} °C')
self.ax.plot(data[6], data[5]['bkg_'], '-', label = f'Background - {win.plot_data["temp"][self.indexes[i]]} °C')
#self.ax.fill_between(data[6],best_fit-dely, best_fit+dely, color='darkslategrey', alpha=0.9, label=r"3-$\sigma$ uncertainty band")
else:
self.ax.plot(data[6], data[4].best_fit, '--', label = f'Best Fit - #{win.plot_data["file_index"][self.indexes[i]]}')
self.ax.plot(data[6], data[5]['bkg_'], '-', label = f'Background - #{win.plot_data["file_index"][self.indexes[i]]}')
#self.ax.fill_between(data[6],best_fit-dely, best_fit+dely, color='darkslategrey', alpha=0.9, label=r"3-$\sigma$ uncertainty band")
self.ax.legend(fontsize='small')
self.canvas.draw()
except UnboundLocalError as e:
QMessageBox.warning(self, '', 'The value given for distance and/or height for peak search are out of bounds, i.e., it was not possible to find two peaks mtaching the given parameters! Please, try again with different values for distance and height!')
except TypeError as e:
QMessageBox.warning(self, '', 'One or more peak parameters have reached a boundary value! Please check IGUAPE terimnal window for a fit report!')
[docs]
def fit(self):
"""Performs the fit, with the model, interval and data selected in the XRD Data Tab.
"""
if not self.fit_interval:
return
win.monitor.set_fit_interval(self.fit_interval)
win.monitor.set_distance(self.distance)
win.monitor.set_height(self.height)
win.fit_interval = self.fit_interval
win.fit_interval_window.close()
self.progress_dialog = QProgressDialog("Fitting peaks...", "", 0, 100, self)
self.progress_dialog.setWindowModality(Qt.WindowModal)
self.progress_dialog.setAutoClose(True)
self.progress_dialog.show()
self.progress_dialog.setCancelButton(None)
# Start the worker thread for peak fitting
self.worker = Worker(self.fit_interval)
self.worker.progress.connect(self.update_progress)
self.worker.finished.connect(self.peak_fitting_finished)
self.worker.error.connect(self.peak_fitting_error)
self.worker.start()
[docs]
def update_progress(self, value):
"""Updates the QProgressDialog with a percentage progress
Args:
value (int): Progress percentage
"""
self.progress_dialog.setValue(value)
[docs]
def peak_fitting_finished(self, time):
"""Shows QMessageBox when Peak Fitting is done.
Args:
time (float): Time spent during Peak Fit
"""
self.progress_dialog.setValue(100)
QMessageBox.information(self, "Peak Fitting", f"Peak fitting completed successfully! For more information on each fit, check the terminal for the fit report! Elapsed time: {int(time)}s")
win.update_graphs()
self.close()
[docs]
def peak_fitting_error(self, error_message):
"""Shows QMessageBox if there's been an error during peak fitting.
Args:
error_message (str): Error message
"""
self.progress_dialog.cancel()
QMessageBox.warning(self, "Peak Fitting Error", error_message)
self.show()
if __name__ == "__main__":
app = QApplication(sys.argv)
win = Window()
win.show()
sys.exit(app.exec())