diff --git a/Cleopatra/FitExData.py b/Cleopatra/FitExData.py index 89ddc2d..9f5a469 100644 --- a/Cleopatra/FitExData.py +++ b/Cleopatra/FitExData.py @@ -35,7 +35,7 @@ class FitPlotWidget(QWidget): class Fitting(): def __init__(self): - self.ExList = [] + self.dataName_list = [] self.fitOption = [] self.expData = [] @@ -49,7 +49,7 @@ class Fitting(): print(self.headers) def read_expData(self, fileName): - self.ExList = [] + self.dataName_list = [] self.fitOption = [] self.expData = [] @@ -74,7 +74,7 @@ class Fitting(): # Extract dataSet Name dataName = line.split()[1] - self.ExList.append(dataName) + self.dataName_list.append(dataName) # Check for fit option lines elif line.startswith("fit"): @@ -92,14 +92,14 @@ class Fitting(): self.expData.append(np.array(current_data, dtype=float)) # Convert to numpy arrays - self.ExList = np.array(self.ExList) + self.dataName_list = np.array(self.dataName_list) self.expData = [np.array(data) for data in self.expData] # Output the result - print("=========== Number of data set:", len(self.ExList)) - for i in range(0, len(self.ExList)): + print("=========== Number of data set:", len(self.dataName_list)) + for i in range(0, len(self.dataName_list)): print("-------------------------") - print(" ExList:", self.ExList[i]) + print(" ExList:", self.dataName_list[i]) print("Fit Options:", self.fitOption[i]) print(" Data List:\n", self.expData[i]) @@ -127,64 +127,64 @@ class Fitting(): fitTheory_upper = [] para = [] - perr = [] + para_err = [] chi_squared = [] for k in range(nFit): - # Get the cross-section IDs for the current fit option and strip extra spaces - xsecIDStr = self.fitOption[expDataID][k].strip() - xsecID = [int(part.strip()) for part in xsecIDStr.split('+')] if '+' in xsecIDStr else [int(xsecIDStr)] + # Get the cross-section IDs for the current fit option and strip extra spaces + xsecIDStr = self.fitOption[expDataID][k].strip() + xsecID = [int(part.strip()) for part in xsecIDStr.split('+')] if '+' in xsecIDStr else [int(xsecIDStr)] - # Ensure all cross-section IDs are valid - processFlag = True - for id in range(len(xsecID)): - if xsecID[id] >= nXsec: - print(f"Error: Requested Xsec-{xsecID[id]} exceeds the number of available cross-sections ({nXsec})") - processFlag = False - - if processFlag == False : - continue + # Ensure all cross-section IDs are valid + processFlag = True + for id in range(len(xsecID)): + if xsecID[id] >= nXsec: + print(f"Error: Requested Xsec-{xsecID[id]} exceeds the number of available cross-sections ({nXsec})") + processFlag = False + + if processFlag == False : + continue - # Define the fitting function: a weighted sum of the selected data - def fit_func(x, *scale): - y = np.zeros_like(x) - for p, id in enumerate(xsecID): - y += scale[p] * np.interp(x, self.dataX, self.data[id]) - return y + # Define the fitting function: a weighted sum of the selected data + def fit_func(x, *scale): + y = np.zeros_like(x) + for p, id in enumerate(xsecID): + y += scale[p] * np.interp(x, self.dataX, self.data[id]) + return y + lower_bounds = [1e-6] * len(xsecID) # Setting a small positive lower bound + upper_bounds = [np.inf] * len(xsecID) # No upper bound - lower_bounds = [1e-6] * len(xsecID) # Setting a small positive lower bound - upper_bounds = [np.inf] * len(xsecID) # No upper bound + # Perform curve fitting using the fit_func and experimental data with y-errors as weights + popt, pcov = curve_fit(fit_func, x_exp, y_exp, sigma=y_err, absolute_sigma=True, + p0=np.ones(len(xsecID)), # Initial guess for scale parameters + bounds=(lower_bounds, upper_bounds)) - # Perform curve fitting using the fit_func and experimental data with y-errors as weights - popt, pcov = curve_fit(fit_func, x_exp, y_exp, sigma=y_err, absolute_sigma=True, - p0=np.ones(len(xsecID)), # Initial guess for scale parameters - bounds=(lower_bounds, upper_bounds)) + para.append(popt) + perr = np.sqrt(np.diag(pcov))# Standard deviation of the parameters + para_err.append(perr) - para.append(popt) - perr.append(np.sqrt(np.diag(pcov))) # Standard deviation of the parameters + # Get the fitted model values + y_fit = fit_func(x_exp, *popt) + residuals = y_exp - y_fit + chi_squared.append(np.sum((residuals / y_err) ** 2)) - # Get the fitted model values - y_fit = fit_func(x_exp, *popt) - residuals = y_exp - y_fit - chi_squared.append(np.sum((residuals / y_err) ** 2)) + print(f"Fitted scale for fit {k}: {', '.join([f'{x:.3f}' for x in popt])} +/- {', '.join([f'{x:.3f}' for x in perr])} | Chi^2 : {chi_squared[-1]:.4f}") + # print(f"Fitted scale for fit {k}: {popt} +/- {perr} | Chi^2 : {chi_squared[-1]:.4f}") - print(f"Fitted scale for fit {k}: {', '.join([f'{x:.3f}' for x in popt])} +/- {', '.join([f'{x:.3f}' for x in perr[-1]])} | Chi^2 : {chi_squared[-1]:.4f}") - # print(f"Fitted scale for fit {k}: {popt} +/- {perr} | Chi^2 : {chi_squared[-1]:.4f}") + # Append the theoretical fit for this fit option + fitTheory.append(np.zeros_like(self.dataX)) + for p, id in enumerate(xsecID): + fitTheory[-1] += popt[p] * np.interp(self.dataX, self.dataX, self.data[id]) - # Append the theoretical fit for this fit option - fitTheory.append(np.zeros_like(self.dataX)) - for p, id in enumerate(xsecID): - fitTheory[k] += popt[p] * np.interp(self.dataX, self.dataX, self.data[id]) - - # Optionally, you can plot the uncertainty as shaded regions (confidence intervals) - # Create the upper and lower bounds of the theoretical model with uncertainties - fitTheory_upper.append(np.zeros_like(self.dataX)) - fitTheory_lower.append(np.zeros_like(self.dataX)) - - for p, id in enumerate(xsecID): - fitTheory_upper[k] += (popt[p] + perr[p]) * np.interp(self.dataX, self.dataX, self.data[id]) - fitTheory_lower[k] += (popt[p] - perr[p]) * np.interp(self.dataX, self.dataX, self.data[id]) + # Optionally, you can plot the uncertainty as shaded regions (confidence intervals) + # Create the upper and lower bounds of the theoretical model with uncertainties + fitTheory_upper.append(np.zeros_like(self.dataX)) + fitTheory_lower.append(np.zeros_like(self.dataX)) + + for p, id in enumerate(xsecID): + fitTheory_upper[-1] += (popt[p] + perr[p]) * np.interp(self.dataX, self.dataX, self.data[id]) + fitTheory_lower[-1] += (popt[p] - perr[p]) * np.interp(self.dataX, self.dataX, self.data[id]) fig = plt.figure() figure_list.append(fig) @@ -206,13 +206,11 @@ class Fitting(): plt.yscale('log') # Replace plt.title() with plt.text() to position the title inside the plot - plt.text(0.05, 0.05, f'Fit for Exp Data : {self.ExList[expDataID]}', transform=plt.gca().transAxes, + plt.text(0.05, 0.05, f'Fit for Exp Data : {self.dataName_list[expDataID]}', transform=plt.gca().transAxes, fontsize=12, verticalalignment='bottom', horizontalalignment='left', color='black') for i, _ in enumerate(para): - plt.text(0.05, 0.1 + 0.05*i, f"Xsec-{self.fitOption[expDataID][i].strip()}: {', '.join([f'{x:.3f}' for x in para[i]])} +/- {', '.join([f'{x:.3f}' for x in perr[i]])}" , transform=plt.gca().transAxes, + plt.text(0.05, 0.1 + 0.05*i, f"Xsec-{self.fitOption[expDataID][i].strip()}: {', '.join([f'{x:.3f}' for x in para[i]])} +/- {', '.join([f'{x:.3f}' for x in para_err[i]])}" , transform=plt.gca().transAxes, fontsize=12, verticalalignment='bottom', horizontalalignment='left', color=default_colors[i]) - - return figure_list \ No newline at end of file diff --git a/Cleopatra/PlotWindow.py b/Cleopatra/PlotWindow.py index 75c1ab7..e54bf5e 100644 --- a/Cleopatra/PlotWindow.py +++ b/Cleopatra/PlotWindow.py @@ -1,126 +1,164 @@ #!/usr/bin/python3 -import os -import time from PyQt6.QtWidgets import ( QGridLayout, QWidget, QCheckBox ) -from PyQt6.QtCore import QUrl -from PyQt6.QtWebEngineWidgets import QWebEngineView -import plotly.graph_objects as go +import numpy as np + +import matplotlib.pyplot as plt +from matplotlib.backends.backend_qtagg import NavigationToolbar2QT as NavigationToolbar +from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas from ExtractXsecPy import read_DWBA class PlotWindow(QWidget): - def __init__(self, XsecFile): + def __init__(self, windowTitle): super().__init__() - self.setWindowTitle("DWBA Plot") - self.setGeometry(100, 100, 800, 600) + self.setWindowTitle(windowTitle) + self.resize(800, 600) - self.x = [] - self.data = [] - self.headers = [] - self.read_data(XsecFile) + self.default_colors = plt.rcParams['axes.prop_cycle'].by_key()['color'] self.log_scale_checkbox = QCheckBox("Use Log Scale for Y-Axis") self.log_scale_checkbox.setChecked(True) - self.log_scale_checkbox.stateChanged.connect(self.plot_plotly_graph) + self.log_scale_checkbox.stateChanged.connect(self.plot_graph) self.gridline_checkbox = QCheckBox("Show Gridlines") - self.gridline_checkbox.stateChanged.connect(self.plot_plotly_graph) + self.gridline_checkbox.stateChanged.connect(self.plot_graph) self.showMarker_checkBox = QCheckBox("Show Markers") - self.showMarker_checkBox.stateChanged.connect(self.plot_plotly_graph) + self.showMarker_checkBox.stateChanged.connect(self.plot_graph) - self.html_file = None - self.web_view = QWebEngineView() + self.figure, self.ax = plt.subplots() + self.canvas = FigureCanvas(self.figure) + self.toolbar = NavigationToolbar(self.canvas, self) layout = QGridLayout(self) - layout.addWidget(self.showMarker_checkBox, 0, 0) - layout.addWidget(self.log_scale_checkbox, 0, 1) - layout.addWidget(self.gridline_checkbox, 0, 2) - layout.addWidget(self.web_view, 1, 0, 5, 3) + layout.addWidget(self.toolbar, 0, 0, 1, 3) + layout.addWidget(self.showMarker_checkBox, 1, 0) + layout.addWidget(self.log_scale_checkbox, 1, 1) + layout.addWidget(self.gridline_checkbox, 1, 2) + layout.addWidget(self.canvas, 2, 0, 5, 3) - self.plot_plotly_graph() + self.xData = [] + self.yData_list = [] + self.header_list = [] + self.yTitle = "" - def read_data(self,file_path): - self.headers, self.x, self.data = read_DWBA(file_path) + self.x_exp = [] # x positions + self.x_err = [] # x uncertainties (errors) + self.y_exp = [] # y positions + self.y_err = [] # y errors + self.dataName = "" + self.fitOption = [] - def plot_plotly_graph(self): + self.para = [] # fit parameters + self.perr = [] # fit error + self.chi_square = [] # fit Chi-squared - if self.html_file and os.path.exists(self.html_file): - os.remove(self.html_file) - - # Create a Plotly figure - fig = go.Figure() + def set_plot_data(self, xData, yData_list, header_list, yTitle): + self.xData = xData + self.yData_list = yData_list + self.header_list = header_list + self.yTitle = yTitle - if self.showMarker_checkBox.isChecked() : - plotStyle = 'lines+markers' + def set_expData(self, expData, fitOption, dataName_list, ID): + self.x_exp = expData[ID][:, 0] + self.x_err = expData[ID][:, 1] + self.y_exp = expData[ID][:, 2] + self.y_err = expData[ID][:, 3] + self.dataName = dataName_list[ID] + self.fitOption = fitOption[ID] + + def read_Xsec(self, file_path): + headers, dataX, data = read_DWBA(file_path) + self.xData = dataX + self.yData_list = data + self.header_list = headers[1:] + + def set_fitResult(self, para, perr, chi_sq): + self.para = para + self.perr = perr + self.chi_square = chi_sq + + def plot_graph(self): + self.ax.clear() + + plotStyle = '-' if not self.showMarker_checkBox.isChecked() else '-o' + + for i, y in enumerate(self.yData_list): + self.ax.plot(self.xData, y, plotStyle, label=self.header_list[i]) + + self.ax.set_xlabel('Angle_CM [deg]') + self.ax.set_ylabel(self.yTitle) + self.ax.legend(loc='upper right', frameon=True) + + # Apply log scale for y-axis if selected + if self.log_scale_checkbox.isChecked(): + self.ax.set_yscale('log') else: - plotStyle = 'lines' + self.ax.set_yscale('linear') + + self.ax.autoscale(enable=True, axis='x', tight=True) + self.figure.tight_layout() + + def plot_Fit(self): + self.ax.clear() + + self.ax.errorbar(self.x_exp, self.y_exp, xerr=self.x_err, yerr=self.y_err, + fmt='x', label='Experimental Data', color='black', markersize = 15, elinewidth=2) + + self.ax.set_xlabel('Angle_CM [deg]') + self.ax.set_ylabel(self.yTitle) + self.ax.legend(loc='upper right', frameon=True) + + # Apply log scale for y-axis if selected + if self.log_scale_checkbox.isChecked(): + self.ax.set_yscale('log') + else: + self.ax.set_yscale('linear') + + self.ax.autoscale(enable=True, axis='x', tight=True) + self.figure.tight_layout() + + for k in range(len(self.fitOption)): + fitTheory = [] + fitTheory_lower = [] + fitTheory_upper = [] + + xsecIDStr = self.fitOption[k].strip() + xsecID = [int(part.strip()) for part in xsecIDStr.split('+')] if '+' in xsecIDStr else [int(xsecIDStr)] + + fitTheory.append(np.zeros_like(self.xData)) + for p, id in enumerate(xsecID): + fitTheory += self.para[p] * np.interp(self.xData, self.xData, self.yData_list[id]) + + fitTheory_upper.append(np.zeros_like(self.xData)) + fitTheory_lower.append(np.zeros_like(self.xData)) + + for p, id in enumerate(xsecID): + fitTheory_upper += (self.para[p] + self.perr[p]) * np.interp(self.xData, self.xData, self.yData_list[id]) + fitTheory_lower += (self.para[p] - self.perr[p]) * np.interp(self.xData, self.xData, self.yData_list[id]) + + # Replace plt.title() with plt.text() to position the title inside the plot + self.ax.text(0.05, 0.05, f'Fit for Exp Data : {self.dataName}', transform=plt.gca().transAxes, + fontsize=12, verticalalignment='bottom', horizontalalignment='left', color='black') + + for i, fit in enumerate(fitTheory): + self.ax.plot(self.xData, fit, label=f'Chi2:{self.chi_square[i]:.3f} | Xsec:{self.fitOption[i]}') + self.ax.fill_between(self.xData, fitTheory_lower[i], fitTheory_upper[i], alpha=0.2) + + for i, _ in enumerate(self.para): + self.ax.text(0.05, 0.1 + 0.05*i, f"Xsec-{self.fitOption[i].strip()}: {', '.join([f'{x:.3f}' for x in self.para[i]])} +/- {', '.join([f'{x:.3f}' for x in self.perr[i]])}" , + transform=plt.gca().transAxes, fontsize=12, + verticalalignment='bottom', horizontalalignment='left', color=self.default_colors[i]) + + self.canvas.draw_idle() + - # Add traces for each column in data against x - for i, y in enumerate(self.data): - fig.add_trace(go.Scatter(x=self.x, y=y, mode=plotStyle, name=self.headers[i + 1])) # Use headers for names - # Update layout for better presentation - fig.update_layout( - xaxis_title="Angle_CM [Deg]", - yaxis_title="Xsec [mb/sr]", - template="plotly", - plot_bgcolor='rgba(0,0,0,0)', # Set plot background to transparent - paper_bgcolor='rgba(0,0,0,0)', # Set paper background to transparent - legend=dict( - x=1, # X position (1 = far right) - y=1, # Y position (1 = top) - xanchor='right', # Anchor the legend to the right - yanchor='top', # Anchor the legend to the top - bgcolor='rgba(255, 255, 255, 0.5)', # Optional: semi-transparent background for legend - bordercolor='rgba(0, 0, 0, 0.5)', # Optional: border color - borderwidth=1 # Optional: border width - ), - yaxis=dict( - # linecolor='black', # Set y-axis line color to black - type ='log' if self.log_scale_checkbox.isChecked() else 'linear', # Toggle y-axis scale - gridcolor='lightgray', # Set gridline color - gridwidth=1, # Set gridline width (in pixels) - showgrid = self.gridline_checkbox.isChecked() # Toggle gridlines for y-axis - ), - xaxis=dict( - # linecolor='black', # Set x-axis line color to black - gridcolor='lightgray', # Set gridline color - gridwidth=1, # Set gridline width (in pixels) - showgrid = self.gridline_checkbox.isChecked() # Toggle gridlines for x-axis as well - ), - margin=dict(l=40, r=40, t=40, b=40), # Set margins to reduce empty space - # width=800, # Optional: set fixed width for the plot - # height=600 # Optional: set fixed height for the plot - ) - fig.add_shape( - # Line with reference to the plot - type="rect", - xref="paper", - yref="paper", - x0=0, - y0=0, - x1=1.0, - y1=1.0, - line=dict( - color="black", - width=1, - ) - ) - # Save the plot as an HTML file in a temporary location - timestamp = int(time.time() * 1000) # Unique timestamp in milliseconds - html_file = f"/tmp/plotwindow_{timestamp}.html" - fig.write_html(html_file) - self.html_file = html_file # Store for cleanup - self.web_view.setUrl(QUrl.fromLocalFile(html_file)) - def __del__(self): - if os.path.exists(self.html_file): - os.remove(self.html_file) diff --git a/PyGUIQt6/requirements.txt b/PyGUIQt6/requirements.txt index 153fd87..9dc16e5 100644 --- a/PyGUIQt6/requirements.txt +++ b/PyGUIQt6/requirements.txt @@ -5,4 +5,5 @@ PyQt6-WebEngine numpy pandas matplotlib -kaleido \ No newline at end of file +kaleido +scipy \ No newline at end of file