# This file is part of HYLOA - HYsteresis LOop Analyzer.
# Copyright (C) 2024 Francesco Zeno Costanzo
# HYLOA is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# HYLOA is distributed in the hope that it will be useful,
# 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.
# You should have received a copy of the GNU General Public License
# along with HYLOA. If not, see <https://www.gnu.org/licenses/>.
"""
Code with basic routines to correct some loop distortions
"""
import numpy as np
from scipy.optimize import curve_fit
from PyQt5.QtWidgets import QMessageBox
from hyloa.utils.err_format import format_value_error
#================================================#
# Function to save data #
#================================================#
[docs]
def save_corrected_data(dataframes, save_file_combo,
x_up_dest, y_up_dest, x_dw_dest, y_dw_dest,
save_quad_checkbox,
plot_state, logger, window):
'''
Function to save the corrected data in the selected file and columns.
Parameters
----------
dataframes : list of pd.DataFrame
List of dataframes containing loaded data.
save_file_combo : QComboBox
Combo box to select destination data file. The first item should be "No save", followed by the loaded files.
x_up_dest : str
Column name for the destination x-values of the upper branch.
y_up_dest : str
Column name for the destination y-values of the upper branch.
x_dw_dest : str
Column name for the destination x-values of the lower branch.
y_dw_dest : str
Column name for the destination y-values of the lower branch.
save_quad_checkbox : QCheckBox
Checkbox to indicate whether to save quadratic data.
plot_state : dict
Dictionary storing the current plotting state.
logger : logging.Logger
Logger instance for recording events and errors.
window : QWidget
Parent widget used to display error message boxes.
'''
save_choice = save_file_combo.currentIndex() # 0 = No save, >0 => file index adjust
if save_choice == 0:
QMessageBox.information(window, "No Save", "Please select a file to save the corrected data.")
return
else:
# save_file_combo items: ["No save", "File 1", "File 2"...]
save_idx = save_choice - 1
df_dest = dataframes[save_idx]
try:
if plot_state["s_data_up"] is None:
# Save corrected data
df_dest[x_up_dest.currentText()] = plot_state["x_up_corr"]
df_dest[y_up_dest.currentText()] = plot_state["y_up_corr"]
df_dest[x_dw_dest.currentText()] = plot_state["x_dw_corr"]
df_dest[y_dw_dest.currentText()] = plot_state["y_dw_corr"]
logger.info(f"Corrected data saved to file {save_idx + 1} in columns {x_up_dest.currentText()}, {y_up_dest.currentText()}, {x_dw_dest.currentText()}, {y_dw_dest.currentText()}.")
QMessageBox.information(window, "Data Saved", f"Corrected data saved to file {save_idx + 1} in columns {x_up_dest.currentText()}, {y_up_dest.currentText()}, {x_dw_dest.currentText()}, {y_dw_dest.currentText()}.")
elif plot_state["s_data_up"] is not None:
# Save symmetrized data
df_dest[x_up_dest.currentText()] = plot_state["s_data_up"][0]
df_dest[y_up_dest.currentText()] = plot_state["s_data_up"][1]
df_dest[x_dw_dest.currentText()] = plot_state["s_data_dw"][0]
df_dest[y_dw_dest.currentText()] = plot_state["s_data_dw"][1]
if save_quad_checkbox.isChecked():
if plot_state.get("q_data_up") is not None:
_, y_q_up = plot_state["q_data_up"]
# extract name for rotation or ellipticity
name = y_up_dest.currentText()
df_dest[f"{name}_quad"] = y_q_up
if plot_state.get("q_data_dw") is not None:
_, y_q_dw = plot_state["q_data_dw"]
# extract name for rotation or ellipticity
name = y_dw_dest.currentText()
df_dest[f"{name}_quad"] = y_q_dw
logger.info(f"Symmetrized data saved to file {save_idx + 1} in columns {x_up_dest.currentText()}, {y_up_dest.currentText()}, {x_dw_dest.currentText()}, {y_dw_dest.currentText()}.")
QMessageBox.information(window, "Data Saved", f"Symmetrized data saved to file {save_idx + 1} in columns {x_up_dest.currentText()}, {y_up_dest.currentText()}, {x_dw_dest.currentText()}, {y_dw_dest.currentText()}.")
else:
raise Exception("Unexpected state: Neither correction nor symmetrization action performed.")
except Exception as e:
QMessageBox.critical(window, "Error", f"Error saving data:\n{e}")
[docs]
def change_ps(plot_state, window, draw_plot, mode="cp"):
'''
Function to change the plot status deleting the corrected
or the original data
Parameters
----------
plot_state : dict
Dictionary storing the current plotting state.
window : QWidget
Parent widget used to display error message boxes.
draw_plot : callable
Callback function responsible for redrawing the plot.
mode : string, optional, dafult "cp"
if mode="cp" all correction will be remove from the plot
if mode="od" the original data will be removed from the plot
if mode="spl" the splines will be removed from the plot
if mode="sym" the symmetrized data will be removed from the plot
'''
try:
if mode =="cp":
plot_state.update({
"done_corr": False,
"done_spl3": False,
"x_up_corr": None,
"y_up_corr": None,
"e_up" : None,
"x_dw_corr": None,
"y_dw_corr": None,
"e_dw" : None,
"fit_hc_p" : None,
"fit_hc_n" : None,
"fit_rm_p" : None,
"fit_rm_n" : None,
"spline_up": None,
"spline_dw": None
})
draw_plot()
if mode == "od":
plot_state.update({
"x_up" : None,
"y_up" : None,
"x_dw" : None,
"y_dw" : None,
})
draw_plot()
if mode == "spl":
plot_state.update({
"done_spl3": False,
"spline_up": None,
"spline_dw": None
})
draw_plot()
if mode == "sym":
plot_state.update({
"s_data_up" : None,
"s_data_dw" : None,
"q_data_up" : None,
"q_data_dw" : None,
})
draw_plot()
except Exception as e:
QMessageBox.critical(window, "Error", f"Error during flip:\n{e}")
#================================================#
# Function to chek simmetry by flipping a branch #
#================================================#
[docs]
def flip(plot_state, window, draw_plot):
'''
Function to flip a brach to ensure simmetricity of the loop.
Parameters
----------
plot_state : dict
dictionary of the plotted data
window : QWidget
The main window widget.
draw_plot : callable
Function to update the preview
'''
try:
plot_state["flipped"] = not plot_state["flipped"]
draw_plot()
except Exception as e:
QMessageBox.critical(window, "Error", f"Error during flip:\n{e}")
#================================================#
# Function to flip a selected branch #
#================================================#
[docs]
def flip_data(file_combo,
x_up_combo, y_up_combo, x_down_combo, y_down_combo,
data_sel, double_branch, plot_state,
window, logger, draw_plot):
'''
Function to duplicate a branch by flipping it to recontruct a simmetric loop.
Parameters
----------
file_combo : QComboBox
Combo box to select source data file.
x_up_combo : QComboBox
Combo box to select X column for Up branch.
y_up_combo : QComboBox
Combo box to select Y column for Up branch.
x_down_combo : QComboBox
Combo box to select X column for Down branch.
y_down_combo : QComboBox
Combo box to select Y column for Down branch.
data_sel : QComboBox
Combo box to select data type (corrected or original).
double_branch : QComboBox
Combo box to select a branch duplication.
plot_state : dict
Dictionary storing the current plotting state.
window : QWidget
Parent widget used to display error message boxes.
logger : logging.Logger
Logger instance used to record spline computation details.
draw_plot : callable
Callback function responsible for redrawing the plot.
'''
try:
idx_src = file_combo.currentIndex()
selected = data_sel.currentText()
x_up_col = x_up_combo.currentText()
y_up_col = y_up_combo.currentText()
x_dw_col = x_down_combo.currentText()
y_dw_col = y_down_combo.currentText()
#Read data
if selected == "Corrected":
x_up = plot_state["x_up_corr"]
y_up = plot_state["y_up_corr"]
x_dw = plot_state["x_dw_corr"]
y_dw = plot_state["y_dw_corr"]
e_up = plot_state["e_up"]
e_dw = plot_state["e_dw"]
else:
x_up = plot_state["x_up"]
y_up = plot_state["y_up"]
x_dw = plot_state["x_dw"]
y_dw = plot_state["y_dw"]
tail = np.concatenate((y_up[0:25], y_dw[-25:],
y_up[-25:], y_dw[0:25]))
dy_data_err = np.std(tail)
dy_err = (2*np.random.random(x_up.size) - 1) * dy_data_err
e_up = dy_err
e_dw = dy_err
if x_up is None:
QMessageBox.critical(window, "Error", "This can be done only on corrected data.")
return
db = double_branch.currentText()
if db == "No":
# Do nothing if button will be pressed
return
elif db == "Up":
x_dw, y_dw = -np.copy(x_up), -np.copy(y_up)
x_dw, y_dw = x_dw[::-1], y_dw[::-1] # reverse to maintain order
e_dw = np.copy(e_up)[::-1]
elif db == "Down":
x_up, y_up = -np.copy(x_dw), -np.copy(y_dw)
x_up, y_up = x_up[::-1], y_up[::-1] # reverse to maintain order
e_up = np.copy(e_dw)[::-1]
plot_state.update({
"x_up_corr": x_up,
"y_up_corr": y_up,
"e_up" : e_up,
"x_dw_corr": x_dw,
"y_dw_corr": y_dw,
"e_dw" : e_dw,
})
draw_plot()
log_lines = []
log_lines.append(f"Flipped {db} branch in file {idx_src + 1}, columns {x_up_col}/{y_up_col} and {x_dw_col}/{y_dw_col}.")
logger.info("\n".join(log_lines))
except Exception as e:
QMessageBox.critical(window, "Error", f"Error during branch flipping:\n{e}")
#================================================#
# Function to correct field #
#================================================#
[docs]
def apply_shift(data_sel, field_shift_pc_edit, plot_state, window, fit_data, args=(),
logger=None):
'''
Function add a field shift after the corrections
Parameters
----------
field_shift_pc_edit : QLineEdit
Value for field shifting
plot_state : dict
Dictionary storing the current plotting state.
window : QWidget
Parent widget used to display error message boxes.
fit_data : callable
Function for fitting data
args : tuple
Argumets to pass to fit_data
logger : logging.Logger
Logger instance used to record spline computation details.
'''
selected = data_sel.currentText()
#Read data
if selected == "Corrected":
name_xup = "x_up_corr"
name_xdw = "x_dw_corr"
else:
name_xup = "x_up"
name_xdw = "x_dw"
try:
field_shift = float(field_shift_pc_edit.text())
plot_state[name_xup] -= field_shift
plot_state[name_xdw] -= field_shift
try :
fit_data(*args)
logger.info(f"Applied field shift of {field_shift} to data. Updated fits after shift.")
except Exception as e:
QMessageBox.critical(window, "Error", f"Error during fit:\n{e}")
# Return to original values
plot_state[name_xup] += field_shift
plot_state[name_xdw] += field_shift
except Exception as e:
QMessageBox.critical(window, "Error", f"Error applying shift:\n{e}")
#================================================#
# MAIN CORRECTION FUNCTION #
#================================================#
#===================================================================#
# Function to fit for physiscal quantities #
#===================================================================#
[docs]
def fit_data(file_combo,
x_up_combo, y_up_combo, x_down_combo, y_down_combo, data_sel,
x_start_up_edit, x_end_up_edit, x_start_dw_edit, x_end_dw_edit,
params_edit, function_edit, logger, plot_state, draw_plot,
output_box, window, option="hc"):
'''
Parameters
----------
file_combo : QComboBox
Combo box to select source data file.
x_up_combo : QComboBox
Combo box to select X column for Up branch.
y_up_combo : QComboBox
Combo box to select Y column for Up branch.
x_down_combo : QComboBox
Combo box to select X column for Down branch.
y_down_combo : QComboBox
Combo box to select Y column for Down branch.
data_sel : QComboBox
Combo box to select data type (corrected or original).
x_start_up_edit : QLineEdit
Line edit for Up branch coercive fit start limit.
x_end_up_edit : QLineEdit
Line edit for Up branch coercive fit end limit.
x_start_dw_edit : QLineEdit
Line edit for Down branch coercive fit start limit.
x_end_dw_edit : QLineEdit
Line edit for Down branch coercive fit end limit.
params_edit : QLineEdit
Line edit for coercive fit parameter names.
function_edit : QLineEdit
Line edit for coercive fit function.
logger : logging.Logger
Logger instance used to record spline computation details.
plot_state : dict
Dictionary storing the current plotting state.
draw_plot : callable
Callback function responsible for redrawing the plot.
output_box : QTextEdit
Text edit for displaying output results.
window : QWidget
Parent widget used to display error message boxes.
option : str
Option to specify the type of fit (e.g., "hc" for coercivity, "rm" for remanence).
'''
try :
idx_src = file_combo.currentIndex()
selected = data_sel.currentText()
# Read x/y column names
x_up_col = x_up_combo.currentText()
y_up_col = y_up_combo.currentText()
x_dw_col = x_down_combo.currentText()
y_dw_col = y_down_combo.currentText()
#Read data
if selected == "Corrected":
x_up = plot_state["x_up_corr"]
y_up = plot_state["y_up_corr"]
x_dw = plot_state["x_dw_corr"]
y_dw = plot_state["y_dw_corr"]
e_up = plot_state["e_up"]
e_dw = plot_state["e_dw"]
else:
try :
x_up = plot_state["x_up"]
y_up = plot_state["y_up"]
x_dw = plot_state["x_dw"]
y_dw = plot_state["y_dw"]
tail = np.concatenate((y_up[0:25], y_dw[-25:],
y_up[-25:], y_dw[0:25]))
dy_data_err = np.std(tail)
dy_err = (2*np.random.random(x_up.size) - 1) * dy_data_err
e_up = dy_err
e_dw = dy_err
except Exception as e:
QMessageBox.critical(window, "Error", f"Ensure of data selection:\n{e}")
return
if e_up is None:
QMessageBox.critical(window, "Error", f"You need to correct data first")
return
results_text_lines = []
try:
param_names = [p.strip() for p in params_edit.text().split(",") if p.strip() != ""]
func_code_hc = f"lambda x, {', '.join(param_names)}: {function_edit.text()}"
g_func = eval(func_code_hc, {"np": np, "__builtins__": {}})
except Exception as e:
QMessageBox.critical(window, "Error", f"Invalid function for fit:\n{e}")
return
# Fit
try:
x_n_start = float(x_start_dw_edit.text())
x_n_end = float(x_end_dw_edit.text())
x_p_start = float(x_start_up_edit.text())
x_p_end = float(x_end_up_edit.text())
except Exception as e:
QMessageBox.critical(window, "Error", f"Invalid value for range:\n{e}")
return
mask_n = (x_dw >= x_n_start) & (x_dw <= x_n_end)
mask_p = (x_up >= x_p_start) & (x_up <= x_p_end)
if mask_n.sum() < 2 or mask_p.sum() < 2:
QMessageBox.critical(window, "Error", "Not enough points in fit ranges.")
return
else :
# Perform weighted fits
popt_n, covm_n = curve_fit(g_func, x_dw[mask_n], y_dw[mask_n], sigma=e_dw[mask_n])
popt_p, covm_p = curve_fit(g_func, x_up[mask_p], y_up[mask_p], sigma=e_up[mask_p])
t1 = np.linspace(x_n_start, x_n_end, 400)
t2 = np.linspace(x_p_start, x_p_end, 400)
if option == "hc":
plot_state.update({
"fit_hc_p" : (t1, g_func(t1, *popt_n)),
"fit_hc_n" : (t2, g_func(t2, *popt_p))
})
draw_plot()
if option == "rm":
plot_state.update({
"fit_rm_p" : (t1, g_func(t1, *popt_n)),
"fit_rm_n" : (t2, g_func(t2, *popt_p))
})
draw_plot()
# Store numerical results
results_text_lines.append("Coercive fit results:")
for p, val, err in zip(param_names, popt_n, np.sqrt(np.diag(covm_n))):
try :
results_text_lines.append(f"{p} = {format_value_error(val, err)}")
except Exception as e:
results_text_lines.append(f"{p} = {val:.6f} ± {err:.6f}")
for i , pi in zip(range(len(popt_n)), param_names):
for j , pj in zip(range(i+1, len(popt_n)), param_names[i+1:]):
corr_ij = covm_n[i, j]/np.sqrt(covm_n[i, i]*covm_n[j, j])
results_text_lines.append(f"corr({pi}, {pj}) = {corr_ij:.3f}")
for p, val, err in zip(param_names, popt_p, np.sqrt(np.diag(covm_p))):
try :
results_text_lines.append(f"{p} = {format_value_error(val, err)}")
except Exception as e:
results_text_lines.append(f"{p} = {val:.6f} ± {err:.6f}")
for i , pi in zip(range(len(popt_p)), param_names):
for j , pj in zip(range(i+1, len(popt_p)), param_names[i+1:]):
corr_ij = covm_p[i, j]/np.sqrt(covm_p[i, i]*covm_p[j, j])
results_text_lines.append(f"corr({pi}, {pj}) = {corr_ij:.3f}")
# Show textual results
output_box.setPlainText("\n".join(results_text_lines))
# Summary of results
log_results_lines = []
log_results_lines.append(f"Summary of fit for data in file {idx_src + 1}, columns {x_up_col}/{y_up_col} and {x_dw_col}/{y_dw_col}:")
log_results_lines.append(f"Using fit function: {function_edit.text()}")
log_results_lines.append(f"Using fit ranges (down): {x_n_start} to {x_n_end}")
log_results_lines.append(f"Using fit ranges (up): {x_p_start} to {x_p_end}\n")
logger.info("Fit completed. Summary:\n" + "\n".join(log_results_lines) +"\n".join(results_text_lines))
except Exception as e:
QMessageBox.critical(window, "Error", f"Error during fitting:\n{e}")
return