Source code for hyloa.data.anisotropy

# 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/>.

import numpy as np
from scipy.interpolate import splrep, BSpline
from scipy.interpolate import InterpolatedUnivariateSpline

from PyQt5.QtWidgets import QMessageBox

from hyloa.utils.err_format import format_value_error


[docs] def compute_b_spline(file_combo, x_up_combo, y_up_combo, x_down_combo, y_down_combo, data_sel, smooth_up_edit, smooth_dw_edit, plot_state, logger, window, draw_plot): ''' Compute cubic B-spline interpolations for the corrected hysteresis loop branches. This function applies a cubic B-spline interpolation separately to the corrected up and down branches of a hysteresis loop. The spline is computed using the corrected data stored in plot_state and is intended as a smooth representation of the loop after tilt/drift correction. The spline can only be computed after the correction step has been performed, since it operates on the corrected arrays (x_up_corr, y_up_corr, x_dw_corr, y_dw_corr). The resulting splines are stored in plot_state and immediately visualized via the provided plotting callback. For numerical stability and consistency with SciPy requirements, the data are internally sorted to ensure a strictly monotonic x-axis. Duplicate x values are explicitly checked and rejected, as they would invalidate the spline computation. Parameters ---------- file_combo : QComboBox Combo box used to select the source data file (used for logging purposes). x_up_combo, y_up_combo : QComboBox Combo boxes selecting the x and y columns corresponding to the up branch. x_down_combo, y_down_combo : QComboBox Combo boxes selecting the x and y columns corresponding to the down branch. data_sel : QComboBox Combo box to select data type (corrected or original). smooth_up_edit : QLineEdit Input field specifying the smoothing parameter s for the up branch spline. Must be a non-negative float. smooth_dw_edit : QLineEdit Input field specifying the smoothing parameter s for the down branch spline. Must be a non-negative float. plot_state : dict Dictionary storing the current plotting state. logger : logging.Logger Logger instance used to record spline computation details. window : QWidget Parent widget used to display error message boxes. draw_plot : callable Callback function responsible for redrawing the plot after the spline computation. Notes ----- - The spline is computed using scipy.interpolate.splrep and evaluated on a dense, uniformly spaced grid spanning the corrected x-range. - The spline represents the full corrected curve. ''' 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() s_up = float(smooth_up_edit.text()) s_dw = float(smooth_dw_edit.text()) if s_up < 0 or s_dw < 0: QMessageBox.critical(window, "Error", "Smoothing parameter must be non-negative.") return #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"] e_up = None e_dw = None if x_up is None: QMessageBox.critical(window, "Error", "Spline must be applied on corrected data.") return x_min = max(x_up.min(), x_dw.min()) x_max = min(x_up.max(), x_dw.max()) #=========================================================# # Up branch spline # #=========================================================# # Ensure monotonic x idx = np.argsort(x_up) x_up, y_up = x_up[idx], y_up[idx] # Check duplicates if np.any(np.diff(x_up) == 0): QMessageBox.critical(window, "Error", "Duplicate x values detected in up branch.") return # Compute spline try: tck_up = splrep(x_up, y_up, s=s_up) except Exception as e: QMessageBox.critical(window, "Error", f"Spline fit up branch failed: {e}.") return try : x_dense_up = np.linspace(x_min, x_max, 5000) y_dense_up = BSpline(*tck_up)(x_dense_up) except Exception as e: QMessageBox.critical(window, "Error", f"Error in spline conputation for up branch: {e}.") return #=========================================================# # Dw branch spline # #=========================================================# # Ensure monotonic x idx = np.argsort(x_dw) x_dw, y_dw = x_dw[idx], y_dw[idx] # Check duplicates if np.any(np.diff(x_dw) == 0): QMessageBox.critical(window, "Error", "Duplicate x values detected in dw branch.") return # Compute spline try: tck_dw = splrep(x_dw, y_dw, s=s_dw) except Exception as e: QMessageBox.critical(window, "Error", f"Spline fit dw branch failed: {e}.") return try : x_dense_dw = np.linspace(x_min, x_max, 5000) y_dense_dw = BSpline(*tck_dw)(x_dense_dw) except Exception as e: QMessageBox.critical(window, "Error", f"Error in spline conputation for dw branch: {e}.") return plot_state.update({ "smooth" : [s_up, s_dw], "fit_hc_p" : None, "fit_hc_n" : None, "done_spl3" : True, "spline_up" : (x_dense_up, y_dense_up), "spline_dw" : (x_dense_dw, y_dense_dw) }) draw_plot() log_lines = [] log_lines.append(f"Computed Bspline for data in file {idx_src + 1}, columns {x_up_col}/{y_up_col} and {x_dw_col}/{y_dw_col}.") log_lines.append(f"With smoting parameter {s_up} for up branch and {s_dw} for down branch.\n") logger.info(f"Bspline results" + "\n".join(log_lines)) except Exception as e: QMessageBox.critical(window, "Error", f"Error during spline interpolation:\n{e}") return
[docs] def compute_Hk(file_combo, x_up_combo, y_up_combo, x_down_combo, y_down_combo, hk_thr_edit, plot_state, logger, window, output_box): ''' Compute the anisotropy field Hk. This function estimates the anisotropy field using the cubic spline representations of the up and down branches previously computed. The method is based on the relative difference between the two branches: regions where the difference falls below a given threshold are used to identify the negative and positive anisotropy fields. The final anisotropy field is computed as the mean value between the negative and positive branches, with an associated uncertainty. Parameters ---------- file_combo : QComboBox Combo box used to select the source data file. x_up_combo : QComboBox Combo box selecting the X column for the up branch. y_up_combo : QComboBox Combo box selecting the Y column for the up branch. x_down_combo : QComboBox Combo box selecting the X column for the down branch. y_down_combo : QComboBox Combo box selecting the Y column for the down branch. hk_thr_edit : QComboBox or QLineEdit Widget containing the threshold value used to select the region where the relative difference between up and down branches is below the chosen limit. plot_state : dict Dictionary storing the current plot state. logger : logging.Logger Logger instance used to record the computation results. window : QWidget Parent widget used to display error message boxes. output_box : QTextEdit Text box where the computed anisotropy field and related information are displayed. Notes ----- This function requires that the cubic splines for both branches have already been computed. If the spline data are not available, the computation will fail. ''' try : idx_src = file_combo.currentIndex() 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() x_up, y_up = plot_state["spline_up"] x_dw, y_dw = plot_state["spline_dw"] diff = abs( (y_up - y_dw)/y_up ) field = x_up thr_y = np.where(diff == np.max(diff))[0][0] thr = float(hk_thr_edit.text()) pts = np.where(diff < thr)[0] field_a = field[pts] field_n = field_a[field_a < field[thr_y]] field_p = field_a[field_a > field[thr_y]] field_p = np.sort(field_p) field_n = np.sort(field_n) Hk_1 = field_n.max() Hk_2 = field_p.min() Hk = 0.5*(-Hk_1 + Hk_2) dH = abs(0.5*( Hk_1 + Hk_2)) results_text_lines = [] results_text_lines.append(f"Negative anisotropy field: {Hk_1}") results_text_lines.append(f"Positive anisotropy field: {Hk_2}") results_text_lines.append(f"Mean: {format_value_error(Hk, dH)}") output_box.setPlainText("\n".join(results_text_lines)) log_lines = [] log_lines.append(f"Computed anisotropy field for data 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) +"\n".join(results_text_lines)) except Exception as e: QMessageBox.critical(window, "Error", f"Error during anisotropy field calculation:\n{e}")
[docs] def symmetrize(file_combo, x_up_combo, y_up_combo, x_down_combo, y_down_combo, data_sel, logger, plot_state, draw_plot, window): ''' Symmetrize a hysteresis loop using spline-interpolated up and down branches. This function performs a symmetrization of the hysteresis loop starting from the cubic spline representations of the corrected up and down branches. The symmetrized loop is constructed by averaging the field values and antisymmetrizing the magnetization, enforcing physical symmetry conditions. A linear spline is built from the averaged data and evaluated on the original corrected field arrays. A small random noise, estimated from the data, is added to mimic experimental uncertainty. Optionally, the symmetrized data can be saved into a user-selected destination data file. Parameters ---------- file_combo : QComboBox Combo box used to select the source data file. x_up_combo : QComboBox Combo box selecting the X column for the up branch. y_up_combo : QComboBox Combo box selecting the Y column for the up branch. x_down_combo : QComboBox Combo box selecting the X column for the down branch. y_down_combo : QComboBox Combo box selecting the Y column for the down branch. data_sel : QComboBox Combo box to select data type (corrected or original). x_up_dest : QComboBox Combo box selecting the destination X column for the symmetrized up branch. y_up_dest : QComboBox Combo box selecting the destination Y column for the symmetrized up branch. x_dw_dest : QComboBox Combo box selecting the destination X column for the symmetrized down branch. y_dw_dest : QComboBox Combo box selecting the destination Y column for the symmetrized down branch. 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. window : QWidget Parent widget used to display error message boxes. Notes ----- This function requires that both the drift correction and spline interpolation steps have already been completed. ''' 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() x_up, y_up = plot_state["spline_up"] x_dw, y_dw = plot_state["spline_dw"] if selected == "Corrected": x_data_up = plot_state["x_up_corr"] x_data_dw = plot_state["x_dw_corr"] y_data_up = plot_state["y_up_corr"] else: x_data_up = plot_state["x_up"] x_data_dw = plot_state["x_dw"] y_data_up = plot_state["y_up"] s_up, s_dw = plot_state["smooth"] x_mean = (x_up + x_dw)/2 y_mean_l = (y_up - y_dw[::-1])/2 y_mean_q = (y_up + y_dw[::-1])/2 spl_l = InterpolatedUnivariateSpline(x_mean, y_mean_l, k=1) spl_q = InterpolatedUnivariateSpline(x_mean, y_mean_q, k=1) dy_data_err = np.std(y_data_up[0:20]) if (s_up != 0 and s_dw != 0) else 0 dy_err = (2*np.random.random(x_data_up.size) - 1) * dy_data_err x_new_up, x_new_dw = x_data_up, -x_data_dw y_new_up, y_new_dw = spl_l(x_data_up) + dy_err, -spl_l(x_data_dw) + dy_err y_q_up, y_q_dw = spl_q(x_data_up)+ dy_err, spl_q(x_data_dw) + dy_err plot_state.update({ "s_data_up" : (x_new_up, y_new_up), "s_data_dw" : (x_new_dw, y_new_dw), "q_data_up" : (x_new_up, y_q_up), "q_data_dw" : (x_new_dw, y_q_dw) }) draw_plot() log_lines = [] log_lines.append(f"Symmetrizied loop 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 symmetrization:\n{e}")